共享带来的问题

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

  1. static int counter = 0;
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread t1 = new Thread(() -> {
  4. for (int i = 0; i < 5000; i++) {
  5. counter++;
  6. }
  7. }, "t1");
  8. Thread t2 = new Thread(() -> {
  9. for (int i = 0; i < 5000; i++) {
  10. counter--;
  11. }
  12. }, "t2");
  13. t1.start();
  14. t2.start();
  15. t1.join();
  16. t2.join();
  17. log.debug("{}",counter);
  18. }

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

  1. getstatic i // 获取静态变量i的值
  2. iconst_1 // 准备常量1
  3. iadd // 自增
  4. putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存(元空间)和私有内存中进行数据交换:
image.png
这样导致线程上下文切换时,另一个线程读取的时主内存的数据,而不是另外一个线程处理后的数据。

临界区 Critical Section

临界区:一段代码块如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。

为了避免临界区的竞态条件发生,有多种手段可以达到目的。
阻塞时的解决方案:synchronized ,Lock
非阻塞式的解决方案: 原子变量

ps:同步和互斥的区别:
互斥是保证在临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步是由于线程执行的先后丶顺序不同,需要一个线程等待其它线程运行到某个点

synchronized(对象锁):它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
注意:不要错误理解为线程锁住了对象就能一直执行下去,如果cpu时间片用完,得等分配cpu时间片,但这时还是锁住的,别的线程进不来。

方法上synchronized的区别

  1. //成员方法锁住this对象
  2. class Test{
  3. public synchronized void test() {
  4. }
  5. }
  6. 等价于
  7. class Test{
  8. public void test() {
  9. synchronized(this) {
  10. ....
  11. }
  12. }
  13. }
  1. //静态方法锁住类对象
  2. class Test{
  3. public synchronized static void test() {
  4. }
  5. }
  6. 等价于
  7. class Test{
  8. public static void test() {
  9. synchronized(Test.class) {
  10. }
  11. }
  12. }

“线程8锁”,考察锁的对象

  1. class Number{
  2. public synchronized void a() {
  3. sleep(1);
  4. log.debug("1");
  5. }
  6. public synchronized void b() {
  7. log.debug("2");
  8. }
  9. }
  10. public static void main(String[] args) {
  11. Number n1 = new Number();
  12. Number n2 = new Number();
  13. new Thread(()->{ n1.a(); }).start();
  14. new Thread(()->{ n2.b(); }).start();
  15. }

由于n1和n2是两个不同的对象,对应堆中的实体对象是不同的,堆中实体对象不同,则指向方法区的对象实体数据不同(this指向运行时常量池的某块数据)。所以不影响两个线程同时运行。

  1. class Number{
  2. public static synchronized void a() {
  3. sleep(1);
  4. log.debug("1");
  5. }
  6. public synchronized void b() {
  7. log.debug("2");
  8. }
  9. }
  10. public static void main(String[] args) {
  11. Number n1 = new Number();
  12. new Thread(()->{ n1.a(); }).start();
  13. new Thread(()->{ n1.b(); }).start();
  14. }

线程1锁的是类对象,线程2锁的是this对象。所以不影响两个线程同时运行。

  1. class Number{
  2. public static synchronized void a() {
  3. sleep(1);
  4. log.debug("1");
  5. }
  6. public static synchronized void b() {
  7. log.debug("2");
  8. }
  9. }
  10. public static void main(String[] args) {
  11. Number n1 = new Number();
  12. Number n2 = new Number();
  13. new Thread(()->{ n1.a(); }).start();
  14. new Thread(()->{ n2.b(); }).start();
  15. }

方法a和b都锁住Number.class对象,所以线程1和2互斥。

变量的线程安全分析

成员变量和静态变量是否线程安全:

  1. 如果它们没有共享,则线程安全<br /> 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况<br /> 如果只有读操作,则线程安全<br /> 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全:

  1. 一般情况下局部变量是线程安全的<br /> 但局部变量引用的对象则未必:
  1. 如果对象仅在方法内创建、使用、消亡,则是线程安全的;
  2. 如果一个对象由外部传入,或者传出外部,则需要考虑线程安全问题(外部仅读,线程安全;外部有读写—如果不考虑同步机制的话,会存在线程安全问题)

    1. class Number{
    2. public void method1(int loopNumber) {
    3. ArrayList<String> list = new ArrayList<>();
    4. for (int i = 0; i < loopNumber; i++) {
    5. method2(list);
    6. method3(list);
    7. System.out.println("父类执行完后的list:"+list);
    8. }
    9. }
    10. private void method2(ArrayList<String> list) {
    11. list.add("1");
    12. }
    13. void method3(ArrayList<String> list) {
    14. System.out.println("原来");
    15. list.remove(0);
    16. }
    17. }
    1. public class ThreadSafeSubClass extends Number{
    2. public static void main(String[] args) {
    3. new ThreadSafeSubClass().method1(1);
    4. }
    5. public void method1(int loopNum){
    6. super.method1(loopNum);
    7. }
    8. @Override
    9. public void method3(ArrayList<String> list) {
    10. System.out.println(list);
    11. new Thread(() -> {
    12. try {
    13. Thread.sleep(1000);
    14. } catch (InterruptedException e) {
    15. e.printStackTrace();
    16. }
    17. list.add("1");
    18. System.out.println("执行线程");
    19. System.out.println(list+"重写后");
    20. }).start();
    21. }
    22. }

    在子类调用父类的method1()方法,那么父类的method1()方法创建一个list对象,传给了父类的method2()方法使用,还传给了子类的method3()使用,子类的方法创建了一个新线程使用这个list对象(局部变量),那么线程安全问题就出现了。
    image.png
    其实可以在父类的method3()创建线程来使用list对象,同样的效果。
    子类重写父类方法是想说明, private 或 final 提供【安全】的意义所在。

1.父类中的方法被private修饰,子类中也定义了一个跟父类一样的方法image.png
2.父类中的方法被final修饰,子类中重写了这个方法image.png
3.父类中的方法同时被private和final修饰,子类中也定义了一个跟父类一样的方法image.png

针对于上面三种不同的情况,所产生不同的结果,在此进行总结:
①父类中被private修饰的方法表示仅在该类可见,所以子类没有继承到父类的private方法,因此,若子类定义了一个与父类的private方法相同的方法名和参数列表也是没问题的,相当于子类自己定义了一个新的方法;

③需要注意的点:若父类中的方法是既被private修饰也被final修饰了,那么说明该方法是不会被子类继承,此时子类定义相同的方法也没有问题,不再产生重写与final的矛盾,而是在子类中定义了新的方法。

补充:

关于 子类局部变量 和 父类局部变量 的关系

  1. class Fu {
  2. int i = 2;
  3. public int getI() {
  4. return i;
  5. }
  6. }
  7. class Zi extends Fu {
  8. int i = 4;
  9. }
  10. public class Jicheng {
  11. public static void main(String[] args) {
  12. System.out.println(new Zi().getI());
  13. }
  14. }

输出结果为2?
子类不是应该 也继承了父类的getI()方法么,
怎么get到的是父类的值?

  1. 子类继承父类,会继承父类的所有属性(properties)和方法(methods),包括private修饰的属性和方法,但是子类只能访问和使用非private的,所以可以这么理解: 子类对象的内部 包涵了一个完整父类对象。

  2. new Zi()就是创建一个子类对象,而子类对象内部包涵了父类对象,所以又要先new Fu(), 也就是说创建子类对象 = 创建父类对象 + 其他

  3. 子类对象没有重写(Overriding)父类的方法,那么这个方法就还”包涵”在父类对象里,子类对象用getI()方法,其实质调用的是 子类对象”肚子里的”那个父类对象的方法。

  4. JAVA规定,变量前面没有特别说明是谁的变量,那么就适用”就近原则”,显然父类对象的属性int i是最近的

线程安全类

  1. String
  2. Integer
  3. StringBuffer
  4. Random
  5. Vector
  6. Hashtable
  7. java.util.concurrent包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。
注意它们多个方法的组合不是原子的。

  1. Hashtable table = new Hashtable();
  2. // 线程1,线程2
  3. if( table.get("key") == null) {
  4. table.put("key", value);
  5. }

image.png
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?

以下是String的substring方法源码

  1. public String substring(int beginIndex) {
  2. if (beginIndex < 0) {
  3. throw new StringIndexOutOfBoundsException(beginIndex);
  4. } else {
  5. int subLen = this.length() - beginIndex;
  6. if (subLen < 0) {
  7. throw new StringIndexOutOfBoundsException(subLen);
  8. } else if (beginIndex == 0) {
  9. return this;
  10. } else {
  11. return this.isLatin1() ? StringLatin1.newString(this.value, beginIndex, subLen) : StringUTF16.newString(this.value, beginIndex, subLen);
  12. }
  13. }
  14. }

通过new出来新的字符串这个操作来保证不可变性。

练习:

  1. public class 卖票练习 {
  2. public static void main(String[] args) throws InterruptedException {
  3. //模拟多人买票
  4. TicketWindow ticketWindow = new TicketWindow(1000000000);
  5. List<Integer> amountList = new ArrayList<>();
  6. List<Thread> threadList=new ArrayList<>();
  7. for (int i = 0; i < 20000; i++) {
  8. Thread thread = new Thread(()->{
  9. int amout=ticketWindow.sell(randomAmount());//买票
  10. amountList.add(amout);
  11. });
  12. threadList.add(thread);
  13. thread.start();
  14. }
  15. for (Thread thread:threadList
  16. ) {thread.join(); //让每个线程都排到main线程前面
  17. }
  18. //统计卖出的票数和剩余票数相加是否等于总票数
  19. System.out.println(ticketWindow.getCount()+amountList.stream().mapToInt(i-> i).sum());
  20. }
  21. static Random random = new Random();
  22. public static int randomAmount(){return random.nextInt(5)+1;}
  23. }
  24. class TicketWindow {
  25. private int count;
  26. public TicketWindow(int count) {
  27. this.count = count;
  28. }
  29. public int getCount() {
  30. return count;
  31. }
  32. public int sell(int amount) {
  33. if (this.count >= amount) {
  34. this.count -= amount;
  35. return amount;
  36. } else {
  37. return 0;
  38. }
  39. }
  40. }

特别留意多线程执行的代码,9~10行,有两个竞争条件。

  1. amountList
  2. randomAmount() —->买号票

这两个条件的竞争是线程不安全的。

正确修改:

  1. public synchronized int sell(int amount) {
  2. if (this.count >= amount) {
  3. this.count -= amount;
  4. return amount;
  5. } else {
  6. return 0;
  7. }
  8. }
  1. List<Integer> amountList = new Vector<>();

Monitor 概念


Java对象头

首先补充以下对象的内存布局(具体在jvm篇有讲)
Java多线程编程-2 - 图7
举例

  1. public class Customer{
  2. int id = 1001;
  3. String name;
  4. Account acct;
  5. {
  6. name = "匿名客户";
  7. }
  8. public Customer() {
  9. acct = new Account();
  10. }
  11. }
  12. public class CustomerTest{
  13. public static void main(string[] args){
  14. Customer cust=new Customer();
  15. }
  16. }

图示

17d4fda3-067a-47bd-a259-3a3860081ae2.png

以 32 位虚拟机为例
image.png
image.png
image.png
说明:若与Monitor关联成功则 mark word变为62位指针(指向 Monitor)+2 位00/10

Monitor 被翻译为监视器或管程(操作系统叫管程)
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针(+两位来表示锁类型)

Monitor 结构如下:
image.png

  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析

注意:
synchronized 必须是进入同一个对象的 monitor 才有上述的效果
不加 synchronized 的对象不会关联监视器,不遵从以上规则

synchronized 原理

  1. public class test {
  2. static final Object lock = new Object();
  3. static int counter = 0;
  4. public static void main(String[] args) {
  5. synchronized (lock) {
  6. counter++;
  7. }
  8. }
  9. }

对应的字节码:
image.png
image.png
说明:如果6-16行执行没有发生异常,那么直接到24,即return。
如果发生异常,则到19行,进行异常处理,注意的是异常后还是会主动将锁释放

synchronized原理进阶


1.轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以jvm会使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized

  1. static final Object obj = new Object();
  2. public static void method1() {
  3. synchronized( obj ) {
  4. // 同步块 A
  5. method2();
  6. }
  7. }
  8. public static void method2() {
  9. synchronized( obj ) {
  10. // 同步块 B
  11. }
  • 创建锁记录(Lock Record)对象(属于栈帧中的附加信息),每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

image.png

  • 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

image.png

  • 如果 cas 替换成功,对象头中存储了 锁记录地址和状态00(代表轻量级锁),表示由该线程给对象加锁,这时图示如下

image.png

  • 如果 cas 失败,有两种情况
    • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

image.png

  • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

image.png

  • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
    • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

image.png

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 然后自己进入 Monitor 的 EntryList BLOCKED

image.png

  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁 流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程 。(cas失败后对象的mark word变成有关锁的记录,cas到Lock Record的Mark Word随着栈帧消失,但这并不影响,这些信息都是与对象自身定义无关的数据,在运行期间 Mark Word 里存储的数据会随着锁标志位的变化而变化。)

接下来Thread-0释放锁,唤醒EntryList中阻塞的线程,这些线程开始竞争锁。
但有一个优化细节,重量级锁竞争的时候,还可以使用自旋来进行优化(其实在轻量级锁进行cas对象的mark word时如果其它线程正在使用,也会进行一定次数的自旋尝试),如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
image.png
在java 6之后自旋锁时自适应的 ,比如对象刚刚的一次自旋操作成功后,那么认为这次自旋成功的可能性较高,就会多自旋几次;反之,就少自旋甚至不自旋,比较智能(默认开启)

2.偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。
image.png
如果在运行过程中,遇到了其他线程抢占锁,等待原持有偏向锁的线程到安全点,持有偏向锁的线程会被挂起,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,撤销偏向锁,设置为无锁(标志位为01)或轻量级锁(标志位为00)的状态。(偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。)
如果为活动状态,即还在同步代码块中,则设置为轻量级锁状态,然后互相cas对象mark word到Lock Record 地址,Lock Record指针指向对象锁记录(同轻量级锁一样),然后将挂起 状态的线程(原持有偏向锁的线程到安全点,所以可以挂起后从安全点继续运行)继续运行,后面流程就跟轻量级锁一样了。

更新一下原有的对象头结构:
image.png

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的 thread、epoch、age 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -

XX:BiasedLockingStartupDelay=0 来禁用延迟

  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、 age 都为 0,第一次用到 hashcode 时才会赋值

测试偏向锁

  1. // 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0
  2. public class test {
  3. public static void main(String[] args) {
  4. Cat cat = new Cat();
  5. ClassLayout classLayout = ClassLayout.parseInstance(cat);
  6. new Thread(() -> {
  7. System.out.println("synchronized 前");
  8. System.out.println(classLayout.toPrintable());
  9. synchronized (cat) {
  10. System.out.println("synchronized 中");
  11. System.out.println(classLayout.toPrintable());
  12. }
  13. System.out.println("synchronized 后");
  14. System.out.println(classLayout.toPrintable());
  15. }, "t1").start();
  16. }
  17. }
  18. public class Cat {
  19. }

image.png
我这也没使用synchronized关键字呀,那不也应该是无锁么?怎么会是偏向锁呢?仔细看一下偏向锁的组成,对照输出结果红色划线位置,你会发现占用 thread 和 epoch 的 位置的均为0,说明当前偏向锁并没有偏向任何线程。此时这个偏向锁正处于可偏向状态,准备好进行偏向了!你也可以理解为此时的偏向锁是一个特殊状态的无锁。

当使用了synchronized关键字:
image.png
对象头内容有了明显的变化,当前偏向锁偏向主线程。

当退出了synchronized关键字代码块,偏向锁还是锁着主线程,说明线程不会主动释放偏向锁的。
image.png

但有些情况线程会主动释放偏向锁,现在讲下偏向锁的撤销。

偏向锁的撤销

撤销 - 调用对象 hashCode

  • 当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;
  • 重量锁的实现中, Monitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。或者简单说就是重量锁/轻量锁 可以存下identity hash code。

请一定要注意,这里讨论的hash code都只针对identity hash code。用户自定义的hashCode()方法所返回的值跟这里讨论的不是一回事。

还是上面的代码

  1. {
  2. ...
  3. cat.hashCode();
  4. System.out.println("synchronized 后");
  5. System.out.println(classLayout.toPrintable());
  6. }, "t1").start();
  7. }

线程主动释放偏向锁变无锁状态了
image.png

撤销 - 其它线程使用对象

当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

  1. public class test {
  2. public static void main(String[] args) {
  3. Cat cat = new Cat();
  4. ClassLayout classLayout = ClassLayout.parseInstance(cat);
  5. Thread t2 = new Thread(()->{
  6. try {
  7. TimeUnit.SECONDS.sleep(5);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. synchronized (cat){
  12. System.out.println("我来锁住cat对象了");
  13. }
  14. });
  15. t2.start();
  16. new Thread(() -> {
  17. System.out.println("synchronized 前");
  18. System.out.println(classLayout.toPrintable());
  19. synchronized (cat) {
  20. System.out.println("synchronized 中");
  21. System.out.println(classLayout.toPrintable());
  22. }
  23. try {
  24. t2.join(); //等t2锁住对象后再执行
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. System.out.println("等t2 线程 synchronized 后,t1线程的偏向锁状态:");
  29. System.out.println(classLayout.toPrintable());
  30. }, "t1").start();
  31. }
  32. }

前中还是跟之前一样
image.png

撤销 - 调用 wait/notify

线程1将cat对象锁住并进行wait(),线程2将cat对象锁住并进行notify()
image.png

批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的Thread ID 。当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

  1. public class test {
  2. public static void main(String[] args) {
  3. Vector<Dog> list = new Vector<>();
  4. Thread t1 = new Thread(() -> {
  5. for (int i = 0; i < 30; i++) {
  6. Dog d = new Dog();
  7. list.add(d);
  8. synchronized (d) {
  9. System.out.println(ClassLayout.parseInstance(d).toPrintable());
  10. }
  11. }
  12. synchronized (list) {
  13. list.notify(); //防止t2线程对list集合进行影响
  14. }
  15. }, "t1");
  16. t1.start();
  17. Thread t2 = new Thread(() -> {
  18. synchronized (list) {
  19. try {
  20. list.wait();
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. System.out.println("=============================================================");
  26. for (int i = 0; i < 30; i++) {
  27. if(i==19){
  28. System.out.println("---------------------开始变化,剩下的偏向锁都指向t2线程---------------------");
  29. }
  30. Dog d = list.get(i);
  31. System.out.println("t2线程加锁前: "+ClassLayout.parseInstance(d).toPrintable());
  32. synchronized (d) {
  33. System.out.println("t2线程加锁后: "+ClassLayout.parseInstance(d).toPrintable());
  34. }
  35. }
  36. }, "t2");
  37. t2.start();
  38. }
  39. }

t1线程将30个dog对象都加上偏向锁—->指向t1线程
image.png

当t2线程将前20个dog对象加锁时,t1线程放弃偏向锁,偏向锁变为轻量级锁指向t2线程
image.png

当t2线程将后20个dog对象加锁时,原本指向t1线程的偏向锁,指向了t2
image.png

批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象 都会变为不可偏向的,新建的对象也是不可偏向的。

3.锁消除

锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。如果逃逸分析发现对象是非逃逸的,编译器就可以自行消除同步。

偏向锁-轻量锁-重量锁变化过程:
20190717184330658.jpg

wait/notify

image.png

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争

常用API

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待 (放弃锁)
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

注意:它们都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法。
关键:waiting的线程被调用 notify()唤醒后仍需要 进入entrylist重新竞争

补充:sleep(long n) 和 wait(long n) 的区别

1) sleep 是 Thread 方法,而 wait 是 Object 的方法
2) sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用
3) sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
4) 它们状态都是 TIMED_WAITING

wait/notify的 正确使用姿势

  1. synchronized(lock) {
  2. while(条件不成立) {
  3. lock.wait();
  4. }
  5. // 干活
  6. }
  7. //另一个线程
  8. synchronized(lock) {
  9. lock.notifyAll();
  10. }


保护性暂停模式(同步):

即 Guarded Suspension,用在一个线程等待另一个线程的执行结果。
有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
JDK 中,join 的实现、Future 的实现,采用的就是此模式。因为要等待另一方的结果,因此归类到同步模式。

  1. public class 同步模式之保护性暂停 {
  2. //线程1等下线程2下载结果
  3. public static void main(String[] args) {
  4. GuardedObject guardedObject = new GuardedObject();
  5. new Thread(()->{
  6. //等待结果
  7. try {
  8. System.out.println("等待结果");
  9. int o =(int) guardedObject.get();
  10. System.out.println("结果是:"+o);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. },"t1").start();
  15. new Thread(()->{
  16. System.out.println("执行下载");
  17. guardedObject.complete(1);
  18. },"t2").start();
  19. }
  20. }
  21. class GuardedObject{
  22. //结果
  23. private Object response;
  24. //获取结果
  25. public Object get() throws InterruptedException {
  26. synchronized (this){
  27. while (response==null){
  28. this.wait();
  29. }
  30. return response;
  31. }
  32. }
  33. //产生结果
  34. public void complete(Object response){
  35. synchronized (this){
  36. this.response=response;
  37. this.notifyAll();
  38. }
  39. }
  40. }

带超时版的get

  1. public Object get(long millis) {
  2. synchronized (lock) {
  3. // 1) 记录最初时间
  4. long begin = System.currentTimeMillis();
  5. // 2) 已经经历的时间
  6. long timePassed = 0;
  7. while (response == null) {
  8. // 4) 假设 millis 是 1000,结果在 400 时唤醒了,那么还有 600 要等
  9. long waitTime = millis - timePassed;
  10. log.debug("waitTime: {}", waitTime);
  11. if (waitTime <= 0) {
  12. log.debug("break...");
  13. break; }
  14. try {
  15. lock.wait(waitTime);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. // 3) 如果提前被唤醒,这时已经经历的时间假设为 400 (关键)
  20. timePassed = System.currentTimeMillis() - begin;
  21. log.debug("timePassed: {}, object is null {}",
  22. timePassed, response == null);
  23. }
  24. return response; }
  25. }

join原理其实就是保护性暂停模式的实现

  1. public final synchronized void join(long millis)
  2. throws InterruptedException {
  3. long base = System.currentTimeMillis();
  4. long now = 0;
  5. if (millis < 0) {
  6. throw new IllegalArgumentException("timeout value is negative");
  7. }
  8. if (millis == 0) {
  9. while (isAlive()) {
  10. wait(0);
  11. }
  12. } else {
  13. while (isAlive()) {
  14. long delay = millis - now;
  15. if (delay <= 0) {
  16. break;
  17. }
  18. wait(delay);
  19. now = System.currentTimeMillis() - base;
  20. }
  21. }
  22. }

这是最终走进的方法,当isAlive,就wait。注意当前锁住是整个对象。我们在A线程中调用B线程的Join方法,也就是B线程充当了这把锁,但调用者是A线程,也就是说挂起来的是A线程(谁调用wait谁休眠),当B线程还活着,就一直wait,只有当B线程执行完了,才会被唤醒,所以易推测出当B线程执行完毕会有一个收尾工作:使用notify方法,不然A线程就会一直挂着了,此代码可以在JVM源码中看到。

生产者/消费者(异步)


  • 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK 中各种阻塞队列,采用的就是这种模式

image.png

  1. public class 消息队列 {
  2. public static void main(String[] args) {
  3. MessageQueue messageQueue = new MessageQueue(2);
  4. for (int i = 0; i < 3; i++) {
  5. int id=i;
  6. new Thread(()->{
  7. try {
  8. messageQueue.put(new Message(id,"值"+id));
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. },"线程"+i).start();
  13. }
  14. new Thread(()->{
  15. try {
  16. while (true){
  17. Thread.sleep(1000);
  18. Message take = messageQueue.take();
  19. }
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. },"消费者").start();
  24. }
  25. }
  26. class MessageQueue{
  27. private LinkedList<Message> list=new LinkedList<>();
  28. private int capcity;//容量
  29. public MessageQueue(int capcity) {
  30. this.capcity = capcity;
  31. }
  32. public Message take() throws InterruptedException {
  33. synchronized (list){
  34. while (list.isEmpty()){
  35. {
  36. System.out.println("队列为空等待生产");
  37. list.wait();
  38. }
  39. }
  40. Message message = list.removeFirst();
  41. System.out.println("已消费消息 内容:"+message.getValue());
  42. list.notifyAll();//告诉put有信息消费了可以生产信息了
  43. return message;//返回消息并删除
  44. }
  45. }
  46. public void put(Message message) throws InterruptedException {
  47. synchronized (list){
  48. while (list.size()==capcity){
  49. System.out.println("队列已满等待消费");
  50. list.wait();
  51. }
  52. list.add(message);
  53. System.out.println("已生产消息 内容"+message.getValue());
  54. list.notifyAll();//告诉take有信息进来了可以消费信息了
  55. }
  56. }
  57. }


Park & Unpark

它们是 LockSupport 类中的方法

  1. // 暂停当前线程
  2. LockSupport.park();
  3. // 恢复某个线程的运行
  4. LockSupport.unpark(暂停线程对象)
  1. Thread t1 = new Thread(() -> {
  2. log.debug("start...");
  3. sleep(2);
  4. log.debug("park...");
  5. LockSupport.park();
  6. log.debug("resume...");
  7. }, "t1");
  8. t1.start();
  9. sleep(1);
  10. log.debug("unpark...");
  11. LockSupport.unpark(t1);

先执行unpark,再执行park,不了解原理的会很容易认为park住线程了,但并不会。

  1. 18:43:50.765 c.TestParkUnpark [t1] - start...
  2. 18:43:51.764 c.TestParkUnpark [main] - unpark...
  3. 18:43:52.769 c.TestParkUnpark [t1] - park...
  4. 18:43:52.769 c.TestParkUnpark [t1] - resume..

先讲一下LockSupport 的 park(底层用Unsafe类) 与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify。

原理之 park & unpark

每个线程都有自己的一个 Parker 对象,由三部分组成 _counter, _cond 和 _mutex 打个比喻。
线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)

调用 park 就是要看需不需要停下来歇息

  • 如果备用干粮耗尽,那么钻进帐篷歇息
  • 如果备用干粮充足,那么不需停留,继续前进

调用 unpark,就好比令干粮充足

  • 如果这时线程还在帐篷,就唤醒让他继续前进
  • 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进

因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮

image.png

  1. 当前线程调用 Unsafe.park() 方法
    2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
    3. 线程进入 _cond 条件变量阻塞 Thread-0线程
    4. 设置 _counter = 0

image.png
1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2. 唤醒 _cond 条件变量中的 Thread_0
3. Thread_0 恢复运行
4. 设置 _counter 为 0

多把锁造成的 死锁,活锁,饥饿 问题(了解)

死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁。
t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁 t2 线程 获得 B对象锁,接下来想获取 A对象的锁

  1. public class test {
  2. public static void main(String[] args) {
  3. Object A = new Object();
  4. Object B = new Object();
  5. Thread t1 = new Thread(() -> {
  6. synchronized (A) {
  7. System.out.println("lock A");
  8. try {
  9. TimeUnit.SECONDS.sleep(1);
  10. synchronized (B) {
  11. System.out.println("lock B");
  12. }
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. }, "t1");
  18. Thread t2 = new Thread(() -> {
  19. synchronized (B) {
  20. System.out.println("lock B");
  21. synchronized (A) {
  22. System.out.println("lock A");
  23. }
  24. }
  25. }, "t2");
  26. t1.start();
  27. t2.start();
  28. }
  29. }

定位死锁
检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁
image.png
另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查。

哲学家就餐问题
image.png
image.png
image.png
image.png
执行了一会就执行不下去,发生了死锁。
最优解决方法在后面ReentrantLock马上会讲。

活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如

  1. public static void main(String[] args) {
  2. new Thread(() -> {
  3. // 期望减到 0 退出循环
  4. while (count > 0) {
  5. sleep(0.2);
  6. count--;
  7. log.debug("count: {}", count);
  8. }
  9. }, "t1").start();
  10. new Thread(() -> {
  11. // 期望超过 20 退出循环
  12. while (count < 20) {
  13. sleep(0.2);
  14. count++;
  15. log.debug("count: {}", count);
  16. }
  17. }, "t2").start();
  18. }

饥饿

如果线程优先级“不均”,在 CPU 繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生线程“饥饿”;持有锁的线程,如果执行的时间过长,也可能导致“饥饿”问题。解决“饥饿”问题的方案很简单,有三种方案:一是保证资源充足,二是公平地分配资源,三就是避免持有锁的线程长时间执行。这三个方案中,方案一和方案三的适用场景比较有限,因为很多场景下,资源的稀缺性是没办法解决的,持有锁的线程执行的时间也很难缩短。倒是方案二的适用场景相对来说更多一些。那如何公平地分配资源呢?在并发编程里,主要是使用公平锁。所谓公平锁,是一种先来后到的方案,线程的等待是有顺序的,排在等待队列前面的线程会优先获得资源。

ReentrantLock

相对于 synchronized 它具备如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁 (防止饥饿)
  • 支持多个条件变量 (可叫醒 因某条件处于waiting set的线程,不同synchronized叫醒全部处于waitig set线程)
  • 与 synchronized 一样,都支持可重入(重复获得锁,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住)

语法跟Unsafe.park()一样

  1. // 获取锁
  2. reentrantLock.lock();
  3. try {
  4. // 临界区
  5. } finally {
  6. // 释放锁
  7. reentrantLock.unlock();
  8. }

可打断(被动)

  1. @Slf4j
  2. public class test {
  3. ReentrantLock lock = new ReentrantLock();
  4. Thread t1 = new Thread(() -> {
  5. log.debug("启动...");
  6. try {
  7. lock.lockInterruptibly();
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. log.debug("等锁的过程中被打断");
  11. return;
  12. }
  13. try {
  14. log.debug("获得了锁");
  15. } finally {
  16. lock.unlock();
  17. }
  18. }, "t1");
  19. lock.lock(); //main线程获取锁
  20. log.debug("获得了锁");
  21. t1.start();
  22. try {
  23. sleep(1);
  24. t1.interrupt();
  25. log.debug("执行打断");
  26. } finally {
  27. lock.unlock();
  28. }
  29. }

结果:

  1. 18:02:40.520 [main] c.TestInterrupt - 获得了锁
  2. 18:02:40.524 [t1] c.TestInterrupt - 启动...
  3. 18:02:41.530 [main] c.TestInterrupt - 执行打断
  4. java.lang.InterruptedException
  5. at
  6. java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchr
  7. onizer.java:898)
  8. at
  9. java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchron
  10. izer.java:1222)
  11. at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
  12. at cn.itcast.n4.reentrant.TestInterrupt.lambda$main$0(TestInterrupt.java:17)
  13. at java.lang.Thread.run(Thread.java:748)
  14. 18:02:41.532 [t1] c.TestInterrupt - 等锁的过程中被打断

注意,这里的ReentrantLock设置的是可中断锁 lock.lockInterruptibly()

锁超时(主动)

立刻失败:

  1. ....
  2. Thread t1 = new Thread(() -> {
  3. log.debug("启动...");
  4. if (!lock.tryLock()) {
  5. log.debug("获取立刻失败,返回");
  6. return;
  7. }
  8. try {
  9. log.debug("获得了锁");
  10. } finally {
  11. lock.unlock();
  12. }
  13. }, "t1");
  14. ....

线程尝试获取锁,失败则立刻返回。lock.tryLock()

还可以设置超时失败,在tryLock()上设置参数(单位+时间)

  1. public boolean tryLock(long timeout, TimeUnit unit)
  2. throws InterruptedException {
  3. return sync.tryAcquireNanos(1, unit.toNanos(timeout));
  4. }

公平锁
ReentrantLock 默认是不公平的,每个线程抢占锁的顺序不定,谁运气好,谁就获取到锁,和调用lock方法的先后顺序无关。
公平锁一般没有必要,会降低并发度,后面分析原理时会讲解

条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待 。
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
synchronized 是那些不满足条件的线程都在一间休息室等消息 ,而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行

同步模式之顺序控制


1.固定运行顺序

比如,必须先 2 后 1 打印

  1. Thread t1 = new Thread(() -> {
  2. try { Thread.sleep(1000); } catch (InterruptedException e) { }
  3. // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行
  4. LockSupport.park();
  5. System.out.println("1");
  6. });
  7. Thread t2 = new Thread(() -> {
  8. System.out.println("2");
  9. // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
  10. LockSupport.unpark(t1);
  11. });
  12. t1.start();
  13. t2.start();

2.交替输出

比如,交替输出abc

  1. public class test {
  2. public static void main(String[] args) {
  3. AwaitSignal as = new AwaitSignal(5);
  4. Condition aWaitSet = as.newCondition();
  5. Condition bWaitSet = as.newCondition();
  6. Condition cWaitSet = as.newCondition();
  7. as.start(aWaitSet);
  8. new Thread(() -> {
  9. as.print("a", aWaitSet, bWaitSet);
  10. }).start();
  11. new Thread(() -> {
  12. as.print("b", bWaitSet, cWaitSet);
  13. }).start();
  14. new Thread(() -> {
  15. as.print("c", cWaitSet, aWaitSet);
  16. }).start();
  17. }
  18. }
  19. class AwaitSignal extends ReentrantLock {
  20. public void start(Condition first) {
  21. this.lock();
  22. try {
  23. System.out.println("start");
  24. first.signal();
  25. } finally {
  26. this.unlock();
  27. }
  28. }
  29. public void print(String str, Condition current, Condition next) {
  30. for (int i = 0; i < loopNumber; i++) {
  31. this.lock();
  32. try {
  33. current.await(); //a休息室等待,经过start方法后a休息室被呼叫
  34. System.out.println(str);
  35. next.signal(); //呼叫b休息室
  36. } catch (InterruptedException e) {
  37. e.printStackTrace();
  38. } finally {
  39. this.unlock();
  40. }
  41. }
  42. }
  43. // 循环次数
  44. private int loopNumber;
  45. public AwaitSignal(int loopNumber) {
  46. this.loopNumber = loopNumber;
  47. }
  48. }

解决哲学家就餐问题

其实很简单,就是使用ReentrantLock的tryLock()方法

  1. public class 哲学家就餐问题 {
  2. public static void main(String[] args) {
  3. Chopstick c1 = new Chopstick("1");
  4. Chopstick c2 = new Chopstick("2");
  5. Chopstick c3 = new Chopstick("3");
  6. Chopstick c4 = new Chopstick("4");
  7. Chopstick c5 = new Chopstick("5");
  8. new Philosopher("苏格拉底", c1, c2).start();
  9. new Philosopher("柏拉图", c2, c3).start();
  10. new Philosopher("亚里士多德", c3, c4).start();
  11. new Philosopher("赫拉克利特", c4, c5).start();
  12. new Philosopher("阿基米德", c5, c1).start();
  13. }
  14. }
  15. class Philosopher extends Thread{
  16. Chopstick left;
  17. Chopstick right;
  18. public Philosopher(String name, Chopstick left, Chopstick right) {
  19. super(name);
  20. this.left = left;
  21. this.right = right;
  22. }
  23. private void eat() throws InterruptedException {
  24. System.out.println(super.getName()+"eating...");
  25. Thread.sleep(1000);
  26. }
  27. @Override
  28. public void run() {
  29. while (true) {
  30. // 获得左手筷子 如果拿不到左手筷子 那就先放下来,等待下一次循环再尝试拿
  31. if(left.tryLock()){
  32. // 获得右手筷子 如果拿不到右手筷子 那就先放下来,等待下一次循环再尝试拿
  33. try {
  34. if(right.tryLock()){
  35. try {
  36. eat(); //都拿到了
  37. } catch (InterruptedException e) {
  38. e.printStackTrace();
  39. } finally {
  40. right.unlock();
  41. }
  42. }
  43. }finally {
  44. left.unlock();
  45. }
  46. }
  47. }
  48. }
  49. }
  50. class Chopstick extends ReentrantLock {
  51. String name;
  52. public Chopstick(String name) {
  53. this.name = name;
  54. }
  55. @Override
  56. public String toString() {
  57. return "筷子{" + name + '}';
  58. }
  59. }