2.1 线程安全问题

2.1.1 自增运算不是线程安全的

2.1.2 临界区资源与临界区代码段

2.2 synchronized关键字

2.2.1 synchronized同步方法

synchronized关键字是Java的保留字,当使用synchronized关键字修饰一个方法的时候,该方法被声明为同步方法

  1. //同步方法
  2. public synchronized void selfPlus()
  3. {
  4. amount++;
  5. }

2.2.2 synchronized同步块

对于小的临界区,我们直接在方法声明中设置synchronized同步关键字,可以避免竞态条件的问题。但是对于较大的临界区代码段,为了执行效率,最好将同步方法分为小的临界区代码段

  1. public class TwoPlus {
  2. private int sum1 = 0;
  3. private int sum2 = 0;
  4. //同步方法
  5. public synchronized void plus(int val1, int val2){
  6. //临界区代码段
  7. this.sum1 += val1;
  8. this.sum2 += val2;
  9. }
  10. }
  1. 一旦线程进入,当线程在操作sum1而没有操作sum2时,也将sum2的操作权白白占用,其他的线程由于没有进入临界区,只能看着sum2被闲置而不能去执行操作。<br />synchronized同步块的写法是:
  1. synchronized(syncObject) //同步块而不是方法
  2. {
  3. //临界区代码段的代码块
  4. }
  1. public class TwoPlus{
  2. private int sum1 = 0;
  3. private int sum2 = 0;
  4. private Integer sum1Lock = new Integer(1); // 同步锁一
  5. private Integer sum2Lock = new Integer(2); // 同步锁二
  6. public void plus(int val1, int val2){
  7. //同步块1
  8. synchronized(this.sum1Lock){
  9. this.sum1 += val1;
  10. }
  11. //同步块2
  12. synchronized(this.sum2Lock){
  13. this.sum2 += val2;
  14. }
  15. }
  16. }

例如,下面两种实现多线程同步的plus方法版本编译成JVM内部字节码后结果是一样的。
版本一,使用synchronized代码块对方法内部全部代码进行保护,具体代码如下:

  1. public void plus() {
  2. synchronized(this){ //对方法内部全部代码进行保护
  3. amount++;
  4. }
  5. }

版本二,使用synchronized方法对方法内部全部代码进行保护,具体代码如下:

  1. public synchronized void plus() {
  2. amount++;
  3. }

2.2.3 静态的同步方法

下面展示一个使用synchronized关键字修饰static方法的例子,具体如下:

  1. package com.crazymakercircle.plus;
  2. // 省略import
  3. public class SafeStaticMethodPlus
  4. { //静态的临界区资源
  5. private static Integer amount = 0;
  6. //使用synchronized关键字修饰 static方法
  7. public static synchronizedvoid selfPlus()
  8. {
  9. amount++;
  10. }
  11. }

2.3 生产者-消费者问题

2.3.1 生产者-消费者模式

image.png

2.3.2 一个线程不安全的实现版本

  1. 不是线程安全的数据缓冲区类 ```java package com.crazymakercircle.producerandcomsumer.store; // 省略import //数据缓冲区,不安全版本的类定义 class NotSafeDataBuffer {

    1. public static final int MAX_AMOUNT = 10;
    2. private List<T> dataList = new LinkedList<>();
    3. //保存数量
    4. private AtomicInteger amount = new AtomicInteger(0);
    5. //向数据区增加一个元素
    6. public void add(T element) throws Exception
    7. {
    8. if (amount.get() > MAX_AMOUNT)
    9. {
    10. Print.tcfo("队列已经满了!");
    11. return;
    12. }
    13. dataList.add(element);
    14. Print.tcfo(element + "");
    15. amount.incrementAndGet();
    16. //如果数据不一致,就抛出异常
    17. if (amount.get() != dataList.size())
    18. {
    19. throw new Exception(amount + "!=" + dataList.size());
    20. }
    21. }
    22. //从数据区取出一个元素
    23. public T fetch() throws Exception
    24. {
    25. if (amount.get() <= 0)
    26. {
    27. Print.tcfo("队列已经空了!");
    28. return null;
    29. }
    30. T element = dataList.remove(0);
    31. Print.tcfo(element + "");
    32. amount.decrementAndGet();
    33. //如果数据不一致,就抛出异常
    34. if (amount.get() != dataList.size())
    35. {
    36. throw new Exception(amount + "!=" + dataList.size());
    37. }
  1. 2. 生产者、消费者的逻辑与动作解耦
  2. 生产者、消费者逻辑与对应动作解耦后的类结构图<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/27178439/1650164864965-4fd04b69-0399-4859-b6d0-7b0a8d4388e8.png#clientId=u0d67bc05-45ec-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=178&id=u918ca972&margin=%5Bobject%20Object%5D&name=image.png&originHeight=222&originWidth=662&originalType=binary&ratio=1&rotation=0&showTitle=false&size=42980&status=done&style=none&taskId=u61a6bb23-d98f-4c0c-bcaa-e3a38fe270a&title=&width=529.6)
  3. 3. 通用的Producer类实现
  4. ```java
  5. package com.crazymakercircle.petstore.actor;
  6. // 省略import
  7. /**
  8. * 通用的生产者
  9. */
  10. public class Producer implements Runnable
  11. {
  12. //生产的时间间隔,生产一次等待的时间默认为200毫秒
  13. public static final int PRODUCE_GAP = 200;
  14. //总次数
  15. static final AtomicInteger TURN = new AtomicInteger(0);
  16. //生产者对象编号
  17. static final AtomicInteger PRODUCER_NO = new AtomicInteger(1);
  18. //生产者名称
  19. String name = null;
  20. //生产的动作
  21. Callable action = null;
  22. int gap = PRODUCE_GAP;
  23. public Producer(Callable action, int gap)
  24. {
  25. this.action = action;
  26. this.gap = gap;
  27. name = "生产者-" + PRODUCER_NO.incrementAndGet();
  28. }
  29. @Override
  30. public void run()
  31. {
  32. while (true)
  33. {
  34. try
  35. {
  36. //执行生产动作
  37. Object out = action.call();
  38. //输出生产的结果
  39. if (null != out)
  40. {
  41. Print.tcfo("第" + TURN.get() + "轮生产:" + out);
  42. }
  43. //每一轮生产之后,稍微等待一下
  44. sleepMilliSeconds(gap);
  45. //增加生产轮次
  46. TURN.incrementAndGet();
  47. } catch (Exception e)
  48. {
  49. e.printStackTrace();
  50. }
  51. }
  52. }
  53. }
  1. 通用的Consumer类实现 ```java package com.crazymakercircle.petstore.actor; // 省略import /**

    • 通用的消费者的定义 */ public class Consumer implements Runnable {

      //消费的时间间隔,默认等待100毫秒 public static final int CONSUME_GAP = 100; //消费总次数 static final AtomicInteger TURN = new AtomicInteger(0); //消费者对象编号 static final AtomicInteger CONSUMER_NO = new AtomicInteger(1); //消费者名称 String name; //消费的动作 Callable action = null;

      //消费一次等待的时间,默认为100毫秒 int gap = CONSUME_GAP;

      public Consumer(Callable action, int gap) {

      1. this.action = action;
      2. this.gap = gap;
      3. name = "消费者-" + CONSUMER_NO.incrementAndGet();

      }

      @Override public void run() {

      1. while (true)
      2. {
      3. //增加消费次数
      4. TURN.incrementAndGet();
      5. try
      6. {
      7. //执行消费动作
      8. Object out = action.call();
      9. if (null != out)
      10. {
      11. Print.tcfo("第" + TURN.get() + "轮消费:" + out);
      12. }
      13. //每一轮消费之后,稍微等待一下
      14. sleepMilliSeconds(gap);
      15. } catch (Exception e)
      16. {
      17. e.printStackTrace();
      18. }
      19. }

      } }

  1. 5. 数据区缓冲区实例、生产动作、消费动作的定义
  2. ```java
  3. package com.crazymakercircle.producerandcomsumer.store;
  4. // 省略import
  5. public class NotSafePetStore
  6. {
  7. //数据缓冲区静态实例
  8. private static NotSafeDataBuffer<IGoods> notSafeDataBuffer =
  9. new NotSafeDataBuffer();
  10. //生产者执行的动作
  11. static Callable<IGoods> produceAction = () ->
  12. {
  13. //首先生成一个随机的商品
  14. IGoods goods = Goods.produceOne();
  15. //将商品加上共享数据区
  16. try
  17. {
  18. notSafeDataBuffer.add(goods);
  19. } catch (Exception e)
  20. {
  21. e.printStackTrace();
  22. }
  23. return goods;
  1. 组装出一个生产者和消费者模式的简单实现版本 ```java package com.crazymakercircle.producerandcomsumer.store; // 省略import public class NotSafePetStore {

    1. public static void main(String[] args) throws InterruptedException
    2. {
    3. System.setErr(System.out);
    4. // 同时并发执行的线程数
    5. final int THREAD_TOTAL = 20;
    6. //线程池,用于多线程模拟测试
    7. ExecutorService threadPool =
    8. Executors.newFixedThreadPool(THREAD_TOTAL);
    9. for (int i = 0; i < 5; i++)
    10. {
    11. //生产者实例每生产一个商品,间隔500毫秒
    12. threadPool.submit(new Producer(produceAction, 500));
    13. //消费者实例每消费一个商品,间隔1500毫秒
    14. threadPool.submit(new Consumer(consumerAction, 1500));
    15. }
    16. }
    17. // 省略其他

    }

  1. <a name="yiglP"></a>
  2. ## 2.3.3 一个线程安全的实现版本
  3. 创建一个安全的数据缓存区类SafeDataBuffer,在其add(…)和fetch()两个实例方法的public声明后面加上synchronized关键字即可。其他的代码一行不动,与NotSafeDataBuffer的代码相同。SafeDataBuffer类的代码如下:
  4. ```java
  5. package com.crazymakercircle.producerandcomsumer.store;
  6. // 省略import
  7. //共享数据区,类定义
  8. class SafeDataBuffer<T>
  9. {
  10. public static final int MAX_AMOUNT = 10;
  11. private BlockingQueue<T> dataList = new LinkedBlockingQueue<>();
  12. //保存数量
  13. private AtomicInteger amount = new AtomicInteger(0);
  14. /**
  15. * 向数据区增加一个元素
  16. */
  17. public synchronized void add(T element) throws Exception
  18. {
  19. // 省略其他相同的代码
  20. dataList.add(element);
  21. Print.tcfo(element + "");w
  22. amount.incrementAndGet();
  23. }
  24. /**
  25. * 从数据区取出一个元素
  26. */
  27. public synchronized T fetch() throws Exception
  28. T element = dataList.remove(0);
  29. // 省略其他相同的代码
  30. return element;
  31. }

2.4 Java对象结构与内置锁

2.4.1 Java对象结构

Java对象(Object实例)结构包括三部分:对象头、对象体和对齐字节
image.png

  1. Java对象(Object实例)的三部分
    1. 对象头
    2. 对象体
    3. 对齐字节
  2. 对象结构中核心字段的作用
    1. Mark Word(标记字)字段主要用来表示对象的线程锁状态,另外还可以用来配合GC存放该对象的hashCode。
    2. Class Pointer(类对象指针)字段是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例。
    3. Array Length(数组长度)字段占用32位(在32位JVM中)字节,这是可选的,只有当本对象是一个数组对象时才会有这个部分。
    4. 对象体用于保存对象属性值,是对象的主体部分,占用的内存空间大小取决于对象的属性数量和类型。
    5. 对齐字节并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。当对象实例数据部分没有对齐(8字节的整数倍)时,就需要通过对齐填充来补全。
  3. 对象结构中的字段长度

    2.4.2 Mark Word的结构信息

  4. 不同锁状态下的Mark Word字段结构

不同锁状态下32位Mark Word的结构信息
image.png
不同锁状态下64位Mark Work的结构信息
image.png

  1. 64位Mark Word的构成
    1. lock:锁状态标记位,占两个二进制位,由于希望用尽可能少的二进制位表示尽可能多的信息,因此设置了lock标记。该标记的值不同,整个Mark Word表示的含义就不同。
    2. biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
    3. age:4位的Java对象分代年龄。在GC中,对象在Survivor区复制一次,年龄就增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,因此最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
    4. identity_hashcode:31位的对象标识HashCode(哈希码)采用延迟加载技术,当调用Object.hashCode()方法或者System.identityHashCode()方法计算对象的HashCode后,其结果将被写到该对象头中。当对象被锁定时,该值会移动到Monitor(监视器)中。
    5. thread:54位的线程ID值为持有偏向锁的线程ID。
    6. epoch:偏向时间戳。
    7. ptr_to_lock_record:占62位,在轻量级锁的状态下指向栈帧中锁记录的指针。
    8. ptr_to_heavyweight_monitor:占62位,在重量级锁的状态下指向对象监视器的指针。32位的Mark Word与64位的Mark Word结构相似,这里不再赘述。

2.4.3 使用JOL工具查看对象的布局

要使用JOL工具,先引入Maven的依赖坐标:

  1. <!--Java Object Layout -->
  2. <dependency>
  3. <groupId>org.openjdk.jol</groupId>
  4. <artifactId>jol-core</artifactId>
  5. <version>0.11</version>
  6. </dependency>
  1. 准备进行对象布局分析的ObjectLock类 ```java package com.crazymakercircle.innerlock; // 省略import public class ObjectLock {

    1. private Integer amount = 0; //整型字段占用4字节
    2. public void increase()
    3. {
    4. synchronized (this)
    5. {
    6. amount++;
    7. }
    8. }
    9. /**
    10. * 输出十六进制、小端模式的hashCode
    11. */
    12. public String hexHash()
    13. {
    14. //对象的原始 hashCode,Java默认为大端模式
    15. int hashCode = this.hashCode();
    16. //转成小端模式的字节数组
    17. byte[] hashCode_LE = ByteUtil.int2Bytes_LE(hashCode);
    18. //转成十六进制形式的字符串
    19. return ByteUtil.byteToHex(hashCode_LE);
    20. }
  1. 2. 编写对象布局分析的用例代码
  2. ```java
  3. package com.crazymakercircle.innerlock;
  4. // 省略import
  5. public class InnerLockTest
  6. {
  7. @org.junit.Test
  8. public void showNoLockObject() throws InterruptedException
  9. {
  10. //输出JVM的信息
  11. Print.fo(VM.current().details());
  12. //创建一个对象
  13. ObjectLock objectLock = new ObjectLock();
  14. Print.fo("object status: ");
  15. //输出对象的布局信息
  16. objectLock.printSelf();
  17. }
  18. // 省略其他用例
  19. }
  20. [InnerLockTest.showNoLockObject]:# Running 64-bit HotSpot VM.
  21. # Using compressed oop with 0-bit shift.
  22. # Using compressed klass with 3-bit shift.
  23. # Objects are 8 bytes aligned.
  24. # Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
  25. # Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
  26. [InnerLockTest.showNoLockObject]:object status:
  27. [ObjectLock.printSelf]:lock hexHash= 04 c0 97 0b
  28. [ObjectLock.printSelf]:lock binaryHash= 00000100 11000000 10010111 00001011
  29. [ObjectLock.printSelf]:lock = com.crazymakercircle.innerlock.ObjectLock object internals:
  30. OFFSET SIZE TYPE DESCRIPTION VALUE
  31. 0 4 (object header) 01 04 c0 97 (00000001 00000100 11000000 10010111) (-1749023743)
  32. 4 4 (object header) 0b 00 00 00 (00001011 00000000 00000000 00000000) (11)
  33. 8 4 (object header) 5d 0f 01 20 (01011101 00001111 00000001 00100000) (536940381)
  34. 12 4 java.lang.Integer ObjectLock.amount 0
  35. Instance size: 16 bytes
  36. Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
  1. JOL对象布局输出结果解读

    2.4.4 大小端问题

2.4.5 无锁、偏向锁、轻量级锁和重量级锁

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

    2.5 偏向锁的原理与实战

    2.5.1 偏向锁的核心原理

    偏向锁的核心原理是:如果不存在线程竞争的一个线程获得了锁,那么锁就进入偏向状态,此时Mark Word的结构变为偏向锁结构,锁对象的锁标志位(lock)被改为01,偏向标志位(biased_lock)被改为1,然后线程的ID记录在锁对象的Mark Word中(使用CAS操作完成)。以后该线程获取锁时判断一下线程ID和标志位,就可以直接进入同步块,连CAS操作都不需要,这样就省去了大量有关锁申请的操作,从而也就提升了程序的性能。

2.5.2 偏向锁的演示案例

  1. 偏向锁演示案例的代码 ```java package com.crazymakercircle.innerlock; // 省略import public class InnerLockTest {

    1. //偏向锁的案例演示
    2. @org.junit.Test
    3. public void showBiasedLock() throws InterruptedException
    4. {
    5. Print.tcfo(VM.current().details());
    6. //JVM延迟偏向锁
    7. sleepMilliSeconds(5000);
    8. ObjectLock counter = new ObjectLock();
    9. Print.tcfo("抢占锁前, lock的状态: ");
    10. lock.printObjectStruct();
    11. sleepMilliSeconds(5000);
    12. CountDownLatch latch = new CountDownLatch(1);
    13. Runnable runnable = () ->
    14. {
    15. for (int i = 0; i < MAX_TURN; i++)
    16. {
    17. synchronized (lock)
    18. {
    19. lock.increase();
    20. if (i == MAX_TURN / 2)
    21. {
    22. Print.tcfo("占有锁, lock的状态: ");
    23. lock.printObjectStruct();
    24. }
    25. }
    26. //每一次循环等待10毫秒
    27. sleepMilliSeconds(10);
    28. }
    29. latch.countDown();
    30. };
    31. new Thread(runnable, "biased-demo-thread").start();
    32. //等待加锁线程执行完成
    33. latch.await();
    34. sleepMilliSeconds(5000);
    35. Print.tcfo("释放锁后, lock的状态: ");
    36. lock.printObjectStruct();
    37. }
    38. // 省略不相干代码

    }

  1. <a name="sQQw3"></a>
  2. ## 2.5.3 偏向锁的膨胀和撤销
  3. 1. 偏向锁的撤销
  4. 1. 偏向锁的膨胀
  5. 1. 偏向锁的好处
  6. <a name="FCSI1"></a>
  7. ## 2.6 轻量级锁的原理与实战
  8. <a name="K36vB"></a>
  9. ## 2.6.1 轻量级锁的核心原理
  10. 轻量锁存在的目的是尽可能不动用操作系统层面的互斥锁,因为其性能比较差。线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁地阻塞和唤醒对CPU来说是一件负担很重的工作
  11. <a name="PRvjb"></a>
  12. ## 2.6.2 轻量级锁的演示案例
  13. ```java
  14. package com.crazymakercircle.innerlock;
  15. // 省略import
  16. public class InnerLockTest
  17. {
  18.   //偏向锁的案例演示
  19. @org.junit.Test
  20. public void showLightweightLock() throws InterruptedException
  21. {
  22. Print.tcfo(VM.current().details());
  23. //JVM延迟偏向锁
  24. sleepMilliSeconds(5000);
  25. ObjectLock lock = new ObjectLock();
  26. Print.tcfo("抢占锁前, lock的状态: ");
  27. lock.printObjectStruct();
  28. sleepMilliSeconds(5000);
  29. CountDownLatch latch = new CountDownLatch(2);
  30. Runnable runnable = () ->
  31. {
  32. for (int i = 0; i < MAX_TURN; i++)
  33. {
  34. synchronized (lock)
  35. {
  36. lock.increase();
  37. if (i == 1)
  38. {
  39. Print.tcfo("第一个线程占有锁, lock的状态: ");
  40. lock.printObjectStruct();
  41. }
  42. }
  43. }
  44. //循环完毕
  45. latch.countDown();
  46. //线程虽然释放锁,但是一直存在死循环
  47. for (int j = 0; ; j++)
  48. {
  49. //每一次循环等待1毫秒
  50. sleepMilliSeconds(1);
  51. }
  52. };
  53. new Thread(runnable).start();
  54. sleepMilliSeconds(1000); //等待1秒
  55. Runnable lightweightRunnable = () ->
  56. {
  57. for (int i = 0; i < MAX_TURN; i++)
  58. {
  59. synchronized (lock)
  60. {
  61. lock.increase();
  62. if (i == MAX_TURN / 2)
  63. [main|InnerLockTest.showLightweightLock]:抢占锁前, lock的状态:
  64. [ObjectLock.printObjectStruct]:lock = com.crazymakercircle.innerlock.ObjectLock object internals:
  65. OFFSET SIZE TYPE DESCRIPTION VALUE
  66. 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
  67. 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
  68. 8 4 (object header) a4 00 01 f8 (10100100 00000000 00000001 11111000) (-134152028)
  69. 12 4 java.lang.Integer ObjectLock.amount 0
  70. Instance size: 16 bytes
  71. Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
  72. [Thread-1|InnerLockTest.lambda$showLightweightLock$2]:第二个线程占有锁, lock的状态:
  73. [ObjectLock.printObjectStruct]:lock = com.crazymakercircle.innerlock.ObjectLock object internals:
  74. OFFSET SIZE TYPE DESCRIPTION VALUE
  75. 0 4 (object header) 88 ef d9 21 (10001000 11101111 11011001 00100001) (567930760)
  76. 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
  77. 8 4 (object header) a4 00 01 f8 (10100100 00000000 00000001 11111000) (-134152028)
  78. 12 4 java.lang.Integer ObjectLock.amount 1501
  79. Instance size: 16 bytes
  80. Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
  81. [ObjectLock.printObjectStruct]:lock = com.crazymakercircle.innerlock.ObjectLock object internals:
  82. OFFSET SIZE TYPE DESCRIPTION VALUE
  83. 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
  84. 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
  85. 8 4 (object header) a4 00 01 f8 (10100100 00000000 00000001 11111000) (-134152028)
  86. 12 4 java.lang.Integer ObjectLock.amount 2000
  87. Instance size: 16 bytes

2.6.3 轻量级锁的分类

  1. 普通自旋锁

所谓普通自旋锁,就是指当有线程来竞争锁时,抢锁线程会在原地循环等待,而不是被阻塞,直到那个占有锁的线程释放锁之后,这个抢锁线程才可以获得锁。

  1. 自适应自旋锁

所谓自适应自旋锁,就是等待线程空循环的自旋次数并非是固定的,而是会动态地根据实际情况来改变自旋等待的次数,自旋次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。自适应自旋锁的大概原理是:
a. 如果抢锁线程在同一个锁对象上之前成功获得过锁,JVM就会认为这次自旋很有可能再次成功,因此允许自旋等待持续相对更长的时间。
b. 如果对于某个锁,抢锁线程很少成功获得过,那么JVM将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。

轻量级锁也被称为非阻塞同步、乐观锁,因为这个过程并没有把线程阻塞挂起,而是让线程空循环等待。

2.6.4 轻量级锁的膨胀

轻量级锁的问题在哪里呢?虽然大部分临界区代码的执行时间都是很短的,但是也会存在执行得很慢的临界区代码。临界区代码执行耗时较长,在其执行期间,其他线程都在原地自旋等待,会空消耗CPU。因此,如果竞争这个同步锁的线程很多,就会有多个线程在原地等待继续空循环消耗CPU(空自旋),这会带来很大的性能损耗。

2.7 重量级锁的原理与实战

2.7.1 重量级锁的核心原理

2.7.2 重量级锁的开销

2.7.3 重量级锁的演示案例

  1. package com.crazymakercircle.innerlock;
  2. // 省略import
  3. public class InnerLockTest
  4. {
  5. @org.junit.Test
  6. public void showHeavyweightLock() throws InterruptedException
  7. {
  8. Print.tcfo(VM.current().details());
  9. //JVM延迟偏向锁
  10. sleepMilliSeconds(5000);
  11. ObjectLock counter = new ObjectLock();
  12. Print.tcfo("抢占锁前, lock的状态: ");
  13. lock.printObjectStruct();
  14. sleepMilliSeconds(5000);
  15. CountDownLatch latch = new CountDownLatch(3);
  16. Runnable runnable = () ->
  17. {
  18. for (int i = 0; i < MAX_TURN; i++)
  19. {
  20. synchronized (lock)
  21. {
  22. lock.increase();
  23. if (i == 0)
  24. {
  25. Print.tcfo("第一个线程占有锁, lock的状态: ");
  26. lock.printObjectStruct();
  27. }
  28. }
  29. }
  30. //循环完毕
  31. latch.countDown();
  32. //线程虽然释放锁,但是一直存在
  33. for (int j = 0; ; j++)
  34. {
  35. //每一次循环等待1毫秒
  36. sleepMilliSeconds(1);
  37. }
  38. };
  39. new Thread(runnable).start();
  40. sleepMilliSeconds(1000); //等待1秒
  41. Runnable lightweightRunnable = () ->
  42. {
  43. for (int i = 0; i < MAX_TURN; i++)
  44. {
  45. synchronized (lock)
  46. {
  47. lock.increase();
  48. if (i == 0)
  49. {
  50. Print.tcfo("占有锁, lock的状态: ");
  51. lock.printObjectStruct();
  52. }
  53. //每一次循环等待1毫秒
  54. sleepMilliSeconds(1);
  55. }
  56. }
  57. //循环完毕
  58. latch.countDown();
  59. };
  60. //启动两个线程,开始激烈地抢锁
  61. new Thread(lightweightRunnable,"抢锁线程1").start();
  62. sleepMilliSeconds(100); //等待100毫秒
  63. new Thread(lightweightRunnable,"抢锁线程2").start();
  64. //等待加锁线程执行完成
  65. latch.await();
  66. sleepMilliSeconds(2000); //等待2秒
  67. Print.tcfo("释放锁后, lock的状态: ");
  68. lock.printObjectStruct();
  69. }

2.8 偏向锁、轻量级锁与重量级锁的对比

2.9 线程间通信

2.9.1 线程间通信的定义

线程的通信可以被定义为:当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以避免无效的资源争夺。
线程间通信的方式可以有很多种:等待-通知、共享内存、管道流。每种方式用不同的方法来实现,这里首先介绍等待-通知的通信方式。

2.9.2 低效的线程轮询

每个Java对象都有wait()、notify()两类实例方法,并且wait()、notify()方法和对象的监视器是紧密相关的。

2.9.3 wait方法和notify方法的原理

  1. 对象的wait()方法 ```java synchronized(locko) { 
    1. //同步保护的代码块
    2. locko.wait();
    3. ...
    }
  1. 2. wait()方法的核心原理
  2. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/27178439/1650176753950-fe92f9a0-c68f-44a5-a849-4125cc2070b4.png#clientId=u0d67bc05-45ec-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=250&id=uec06cd0d&margin=%5Bobject%20Object%5D&name=image.png&originHeight=312&originWidth=623&originalType=binary&ratio=1&rotation=0&showTitle=false&size=51135&status=done&style=none&taskId=u3affacc8-08ca-4004-ab26-48e391f1f88&title=&width=498.4)
  3. 3. 对象的notify()方法
  4. ```java
  5. synchronized(locko)
  6. { 
  7. //同步保护的代码块
  8. locko.notify();
  9. ...
  10. }
  1. notify()方法的核心原理

image.png

2.9.4 “等待-通知”通信模式演示案例

  1. package com.crazymakercircle.multithread.basic.use;
  2. // 省略import
  3. public class WaitNotifyDemo
  4. {
  5. static Object locko = new Object();
  6. //等待线程的异步目标任务
  7. static class WaitTarget implements Runnable
  8. {
  9. public void run()
  10. {
  11. //加锁
  12. synchronized (locko)
  13. {
  14. try
  15. {
  16. //启动等待
  17. Print.tco("启动等待");
  18. //等待被通知,同时释放locko监视器的Owner权利
  19. locko.wait();
  20. //收到通知后,线程会进入locko监视器的EntryList
  21. } catch (InterruptedException e)
  22. {
  23. e.printStackTrace();
  24. }
  25. //获取到监视器的Owner权利
  26. Print.tco("收到通知,当前线程继续执行");
  27. }
  28. }
  29. }
  30. //通知线程的异步目标任务
  31. static class NotifyTarget implements Runnable
  32. {
  33. public void run()
  34. {
  35. //加锁
  36. synchronized (locko)
  37. {
  38. //从屏幕读取输入,目的是阻塞通知线程,方便使用jstack查看线程状态
  39. Print.consoleInput();
  40. //获取lock锁,然后进行发送
  41. // 此时不会立即释放locko的Monitor的Owner,需要执行完毕
  42. locko.notifyAll();
  43. Print.tco("发出通知了,但是线程还没有立马释放锁");
  44. }
  45. }
  46. }
  47. public static void main(String[] args) throws InterruptedException
  48. {
  49. //创建等待线程
  50. Thread waitThread = new Thread(new WaitTarget(), "WaitThread");
  51. //启动等待线程
  52. waitThread.start();
  53. sleepSeconds(1);
  54. //创建通知线程
  55. Thread notifyThread = new Thread(new NotifyTarget(), "NotifyThread");
  56. //启动通知线程
  57. notifyThread.start();
  58. }
  59. }

2.9.5 生产者-消费者之间的线程间通信

  1. package com.crazymakercircle.producerandcomsumer.store;
  2. // 省略import
  3. public class CommunicatePetStore
  4. {
  5. public static final int MAX_AMOUNT = 10; //数据缓冲区最大长度
  6. //数据缓冲区,类定义
  7. static class DataBuffer<T>
  8. {
  9. //保存数据
  10. private List<T> dataList = new LinkedList<>();
  11. //数据缓冲区长度
  12. private Integer amount = 0;
  13. private final Object LOCK_OBJECT = new Object();
  14. private final Object NOT_FULL = new Object();
  15. private final Object NOT_EMPTY = new Object();
  16. // 向数据区增加一个元素
  17. public void add(T element) throws Exception
  18. {
  19. while (amount > MAX_AMOUNT)
  20. {
  21. synchronized (NOT_FULL)
  22. {
  23. Print.tcfo("队列已经满了!");
  24. //等待未满通知
  25. NOT_FULL.wait();
  26. }
  27. }
  28. synchronized (LOCK_OBJECT)
  29. {
  30. dataList.add(element);
  31. amount++;
  32. }
  33. synchronized (NOT_EMPTY)
  34. {
  35. //发送未空通知
  36. NOT_EMPTY.notify();
  37. }
  38. }
  39. /**
  40. * 从数据区取出一个商品
  41. */
  42. public T fetch() throws Exception
  43. {
  44. while (amount <= 0)
  45. {
  46. synchronized (NOT_EMPTY)
  47. {
  48. Print.tcfo("队列已经空了!");
  49. //等待未空通知
  50. NOT_EMPTY.wait();
  51. }
  52. }

2.9.6 需要在synchronized同步块的内部使用wait和notify