• 共享模型之管程


    1、共享带来的问题

    线程出现问题的根本原因是因为线程上下文切换,导致线程里的指令没有执行完就切换执行其它线程了,下面举一个例子 Test13.java

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

    我将从字节码的层面进行分析:

    1583568350082

    1583568587168

    getstatic i // 获取静态变量i的值
    iconst_1 // 准备常量1
    iadd // 自增
    putstatic i // 将修改后的值存入静态变量i
        
    getstatic i // 获取静态变量i的值
    iconst_1 // 准备常量1
    isub // 自减
    putstatic i // 将修改后的值存入静态变量i
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以看到count++count-- 操作实际都是需要这个4个指令完成的,那么这里问题就来了!Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

    1583569253392

    如果代码是正常按顺序运行的,那么count的值不会计算错

    1583569326977

    出现负数的情况:

    1583569380639

    出现正数的情况:

    1583569416016

    问题的进一步描述

    (1)临界区 Critical Section
    • 一个程序运行多个线程本身是没有问题的
    • 问题出在多个线程访问共享资源
      • 多个线程读共享资源其实也没有问题
      • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
    • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
      例如,下面代码中的临界区
    static int counter = 0;
     
    static void increment() 
    // 临界区 
    {   
        counter++; 
    }
     
    static void decrement() 
    // 临界区 
    { 
        counter--; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    (2)竞态条件 Race Condition

    多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

    2、synchronized 解决方案

    (1)解决手段

    为了避免临界区的竞态条件发生,有多种手段可以达到目的。

    • 阻塞式的解决方案:synchronized,Lock
    • 非阻塞式的解决方案:原子变量

    本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的**【对象锁】**,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住(blocked)。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

    (2)synchronized语法

    synchronized(对象) {
    	//临界区
    }
    
    • 1
    • 2
    • 3

    例:

    static int counter = 0; 
    //创建一个公共对象,作为对象锁的对象
    static final Object room = new Object();
     
    public static void main(String[] args) throws InterruptedException {    
    	Thread t1 = new Thread(() -> {        
        for (int i = 0; i < 5000; i++) {            
            synchronized (room) {     
            counter++;            
           	 }       
     	   }    
        }, "t1");
     
        Thread t2 = new Thread(() -> {       
            for (int i = 0; i < 5000; i++) {         
                synchronized (room) {            
                counter--;          
                }    
            } 
        }, "t2");
     
        t1.start();    
        t2.start(); 
        t1.join();   
        t2.join();    
        log.debug("{}",counter); 
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    synchronized原理

    synchronized实际上利用对象保证了临界区代码的原子性,临界区内的代码在外界看来是不可分割的,不会被线程切换所打断

    思考

    如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象

    那么t2不会被阻塞可以直接运行

    1583571633729

    (3)synchronized加在方法上

    • 加在成员方法上

      锁住的是当前方法所在类

      public class Demo {
      	//在方法上加上synchronized关键字
      	public synchronized void test() {
      	
      	}
      	//等价于
      	public void test() {
      		synchronized(this) {
      		
      		}
      	}
      }Copy
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    • 加在静态方法上

      public class Demo {
      	//在静态方法上加上synchronized关键字
      	public synchronized static void test() {
      	
      	}
      	//等价于
      	public void test() {
      		synchronized(Demo.class) {
      		
      		}
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12

    3、变量的线程安全分析

    成员变量和静态变量是否线程安全?
    • 如果它们没有共享,则线程安全
    • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
      • 如果只有读操作,则线程安全
      • 如果有读写操作,则这段代码是临界区,需要考虑线程安全
    局部变量是否线程安全?
    • 局部变量是线程安全的

    • 但局部变量引用的对象则未必 (要看该对象

      是否被共享

      且被执行了读写操作)

      • 如果该对象没有逃离方法的作用范围,它是线程安全的
      • 如果该对象逃离方法的作用范围,需要考虑线程安全
    • 局部变量是线程安全的——每个方法都在对应线程的栈中创建栈帧,不会被其他线程共享

    img

    • 如果调用的对象被共享,且执行了读写操作,则线程不安全

    img

    • 如果是局部变量,则会在堆中创建对应的对象,不会存在线程安全问题。

    img

    局部变量线程安全分析
    public static void test1() {
    int i = 10;
    i++;
    }
    
    • 1
    • 2
    • 3
    • 4

    每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

    public static void test1();
    descriptor: ()V
        flags: ACC_PUBLIC, ACC_STATIC
    Code:
    stack=1, locals=1, args_size=0
    0: bipush 10
    2: istore_0
    3: iinc 0, 1
    6: return
    LineNumberTable:
    line 10: 0
    line 11: 3
    line 12: 6
    LocalVariableTable:
    Start Length Slot Name Signature
    3 4 0 i I
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    image-20231016230909129

    局部变量的引用稍有不同
    先看一个成员变量的例子

    class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
    for (int i = 0; i < loopNumber; i++) {
    // { 临界区, 会产生竞态条件
    method2();
    method3();
        // } 临界区
    }
    }
    private void method2() {
    list.add("1");
    }
    private void method3() {
    list.remove(0);
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    执行

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
    	ThreadUnsafe test = new ThreadUnsafe();
    	for (int i = 0; i < THREAD_NUMBER; i++) {
    	new Thread(() -> {
    			test.method1(LOOP_NUMBER);
    			}, "Thread" + i).start();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    其中一种情况是,如果线程2 还未 add,线程1 remove 就会报错:

    Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
    at java.util.ArrayList.rangeCheck(ArrayList.java:657)
    at java.util.ArrayList.remove(ArrayList.java:496)
    at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35)
    at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26)
    at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14)
    at java.lang.Thread.run(Thread.java:748)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    分析:
    无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
    method3 与 method2 分析相同

    image-20231016231520326

    将 list 修改为局部变量

    
    class ThreadSafe {
    public final void method1(int loopNumber) {
    ArrayList<String> list = new ArrayList<>();
    for (int i = 0; i < loopNumber; i++) {
    method2(list);
    method3(list);
    }
    }
    private void method2(ArrayList<String> list) {
    list.add("1");
    }
    private void method3(ArrayList<String> list) {
    list.remove(0);
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    那么就不会有上述问题了

    分析:

    • list 是局部变量,每个线程调用时会创建其不同实例,没有共享
    • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
    • method3 的参数分析与 method2 相同

    image-20231016231745629

    方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?

    • 情况1:有其它线程调用 method2 和 method3
    • 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
    class ThreadSafe {
    public final void method1(int loopNumber) {
    ArrayList<String> list = new ArrayList<>();
    for (int i = 0; i < loopNumber; i++) {
    	method2(list);
    	method3(list);
    	}
    }
    private void method2(ArrayList<String> list) {
    	list.add("1");	
        }
    private void method3(ArrayList<String> list) {
    		list.remove(0);
    		}
    	}
    class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
    new Thread(() -> {
    list.remove(0);
    }).start();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    常见线程安全类

    • String
    • Integer
    • StringBuffer
    • Random
    • Vector (List的线程安全实现类)
    • Hashtable (Hash的线程安全实现类)
    • java.util.concurrent 包下的类

    这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的

    • 它们的每个方法是原子的(都被加上了synchronized)
    • 但注意它们多个方法的组合不是原子的,所以可能会出现线程安全问题

    img

    不可变类线程安全性

    String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的

    有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安 全的呢?

    这是因为这些方法的返回值都创建了一个新的对象,而不是直接改变String、Integer对象本身。

    4、Monitor概念

    Java 对象头

    以 32 位虚拟机为例,普通对象的对象头结构如下,其中的Klass Word为指针,指向对应的Class对象;

    1583651065372

    数组对象

    1583651088663

    其中 Mark Word 结构为

    1583651590160

    所以一个对象的结构如下:

    1583678624634

    Monitor 原理

    Monitor被翻译为监视器或者说管程

    每个java对象都可以关联一个Monitor,如果使用synchronized给对象上锁(重量级),该对象头的Mark Word中就被设置为指向Monitor对象的指针

    1583652360228

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

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

    • 当线程执行到临界区代码时,如果使用了synchronized,会先查询synchronized中所指定的对象(obj)是否绑定了Monitor

      • 如果没有绑定,则会先去去与Monitor绑定,并且将Owner设为当前线程。

      • 如果

        已经绑定

        ,则会去查询该Monitor是否已经有了Owner

        • 如果没有,则Owner与将当前线程绑定
        • 如果有,则放入EntryList,进入阻塞状态(blocked)
    • 当Monitor的Owner将临界区中代码执行完毕后,Owner便会被清空,此时EntryList中处于阻塞状态的线程会被叫醒并竞争,此时的竞争是非公平的

    • 注意

      • 对象在使用了synchronized后与Monitor绑定时,会将对象头中的Mark Word置为Monitor指针。
      • 每个对象都会绑定一个唯一的Monitor,如果synchronized中所指定的对象(obj)不同,则会绑定不同的Monitor

    5. synchronized原理

    代码如下 Test17.java

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

    反编译后的部分字节码

     0 getstatic #2 
     # 取得lock的引用(synchronized开始了)
     3 dup    
     # 复制操作数栈栈顶的值放入栈顶,即复制了一份lock的引用
     4 astore_1
     # 操作数栈栈顶的值弹出,即将lock的引用存到局部变量表中
     5 monitorenter
     # 将lock对象的Mark Word置为指向Monitor指针
     6 getstatic #3 
     9 iconst_1
    # 准备常数1
    10 iadd
    11 putstatic #3 
     # ->i
    14 aload_1
    # 从局部变量表中取得lock的引用,放入操作数栈栈顶
    15 monitorexit
    # 将lock对象的Mark Word重置,唤醒EntryList
    16 goto 24 (+8)
    # 下面是异常处理指令,可以看到,如果出现异常,也能自动地释放锁
    19 astore_2
    20 aload_1
    21 monitorexit
    22 aload_2
    23 athrow
    24 return
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    注意:方法级别的 synchronized 不会在字节码指令中有所体现

    monitor是由操作系统提供的,所以耗费挺大的

    小故事

    故事角色

    • 老王 - JVM
    • 小南 - 线程
    • 小女 - 线程
    • 房间 - 对象
    • 房间门上 - 防盗锁 - Monitor
    • 房间门上 - 小南书包 - 轻量级锁
    • 房间门上 - 刻上小南大名 - 偏向锁
    • 批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
    • 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向

    小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样,
    即使他离开了,别人也进不了门,他的工作就是安全的。
    但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女
    晚上用。每次上锁太麻烦了,有没有更简单的办法呢?
    小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因
    此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是
    自己的,那么就在门外等,并通知对方下次用锁门的方式。
    后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍
    然觉得麻烦。
    于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那
    么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦
    掉,升级为挂书包的方式。
    同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老
    家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老
    王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字
    后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包

    synchronized 原理进阶

    1.轻量级锁

    轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用者是透明的,即语法仍然是synchronized,假设有两个方法同步块,利用同一个对象加锁

    static final Object obj = new Object();
    public static void method1() {
         synchronized( obj ) {
             // 同步块 A
             method2();
         }
    }
    public static void method2() {
         synchronized( obj ) {
             // 同步块 B
         }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    1. 每次指向到synchronized代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的Mark Word和对象引用reference

      1583755737580

    2. 让锁记录中的Object reference指向对象,并且尝试用cas(compare and sweep)替换Object对象的Mark Word ,将Mark Word 的值存入锁记录中

      1583755888236

    3. 如果cas替换成功,那么对象的对象头储存的就是锁记录的地址和状态00,如下所示

      1583755964276

    4. 如果cas失败,有两种情况

      1. 如果是其它线程已经持有了该Object的轻量级锁,那么表示有竞争,将进入锁膨胀阶段

      2. 如果是自己的线程已经执行了synchronized进行加锁,那么那么再添加一条 Lock Record 作为重入的计数

        1583756190177

    5. 当线程退出synchronized代码块的时候,

      如果获取的是取值为 null 的锁记录

      ,表示有重入,这时重置锁记录,表示重入计数减一

      1583756357835

    6. 当线程退出synchronized代码块的时候,如果获取的锁记录取值不为 null,那么使用cas将Mark Word的值恢复给对象

      1. 成功则解锁成功
      2. 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
    2.锁膨胀

    如果在尝试加轻量级锁的过程中,cas操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。

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

      1583757433691

    2. 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程

      1. 即为对象申请Monitor锁,让Object指向重量级锁地址,然后自己进入Monitor 的EntryList 变成BLOCKED状态

    1583757586447

    1. 当Thread-0 推出synchronized同步块时,使用cas将Mark Word的值恢复给对象头,失败,那么会进入重量级锁的解锁过程,即按照Monitor的地址找到Monitor对象,将Owner设置为null,唤醒EntryList 中的Thread-1线程
    3.自旋优化

    重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换就获得了锁

    1. 自旋重试成功的情况

      1583758113724

    2. 自旋重试失败的情况,自旋了一定次数还是没有等到持锁的线程释放锁

      1583758136650

    自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

    在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

    Java 7 之后不能控制是否开启自旋功能

    4.偏向锁

    在轻量级的锁中,我们可以发现,如果同一个线程对同一个2对象进行重入锁时,也需要执行CAS操作(把对象头换为自己的锁记录),这是有点耗时滴,那么java6开始引入了偏向锁的东东,只有第一次使用CAS时将对象的Mark Word头设置为入锁线程ID,之后这个入锁线程再进行重入锁时,发现线程ID是自己的,那么就不用再进行CAS了

    1583760728806

    偏向状态

    第一行那个表示是否启用了偏向锁

    1583762169169

    一个对象的创建过程

    1. 如果开启了偏向锁(默认是开启的),那么对象刚创建之后,Mark Word 最后三位的值101,并且这是它的Thread,epoch,age都是0,在加锁的时候进行设置这些的值.
    2. 偏向锁默认是延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:-XX:BiasedLockingStartupDelay=0来禁用延迟
    3. 注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中

    image-20231017212930403

    初始情况 前面全为0 后面是101

    这里上锁后对象头信息变为锁记录

    释放锁后对象头信息还是不变,只有其他线程获得这个锁才会变

    1. 实验Test18.java,加上虚拟机参数-XX:BiasedLockingStartupDelay=0进行测试

      1. public static void main(String[] args) throws InterruptedException {
                Test1 t = new Test1();
                test.parseObjectHeader(getObjectHeader(t))synchronized (t){
                    test.parseObjectHeader(getObjectHeader(t));
                }
                test.parseObjectHeader(getObjectHeader(t));
            }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        1. 输出结果如下,三次输出的状态码都为101
        biasedLockFlag (1bit): 1
        	LockFlag (2bit): 01
        biasedLockFlag (1bit): 1
        	LockFlag (2bit): 01
        biasedLockFlag (1bit): 1
        	LockFlag (2bit): 01
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6

    测试禁用:如果没有开启偏向锁,那么对象创建后最后三位的值为001,这时候它的hashcode,age都为0,hashcode是第一次用到hashcode时才赋值的。在上面测试代码运行时在添加 VM 参数-XX:-UseBiasedLocking禁用偏向锁(禁用偏向锁则优先使用轻量级锁),退出synchronized状态变回001

    1. 测试代码Test18.java 虚拟机参数-XX:-UseBiasedLocking

    2. 输出结果如下,最开始状态为001,然后加轻量级锁变成00,最后恢复成001

      image-20231017213353282

      biasedLockFlag (1bit): 0
      	LockFlag (2bit): 01
      LockFlag (2bit): 00
      biasedLockFlag (1bit): 0
      	LockFlag (2bit): 01
      
      • 1
      • 2
      • 3
      • 4
      • 5
    撤销偏向锁-hashcode方法

    测试 hashCode:当调用对象的hashcode方法的时候就会撤销这个对象的偏向锁(线程ID啊什么的),因为使用偏向锁时没有位置存hashcode的值了

    而轻量级锁的hash码存在线程栈帧的锁记录里面,重量级锁的hash码会存在monitor对象,最后还会换元回来

    1. 测试代码如下,使用虚拟机参数

      -XX:BiasedLockingStartupDelay=0
      
      • 1

      ,确保我们的程序最开始使用了偏向锁!但是结果显示程序还是使用了轻量级锁。 Test20.java

          public static void main(String[] args) throws InterruptedException {
              Test1 t = new Test1();
              t.hashCode();
              test.parseObjectHeader(getObjectHeader(t));
       
              synchronized (t){
                  test.parseObjectHeader(getObjectHeader(t));
              }
              test.parseObjectHeader(getObjectHeader(t));
          }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
    2. 输出结果

      biasedLockFlag (1bit): 0
      	LockFlag (2bit): 01
      LockFlag (2bit): 00
      biasedLockFlag (1bit): 0
      	LockFlag (2bit): 01
      
      • 1
      • 2
      • 3
      • 4
      • 5
    撤销偏向锁-其它线程使用对象

    这里我们演示的是偏向锁撤销变成轻量级锁的过程,那么就得满足轻量级锁的使用条件,就是没有线程对同一个对象进行锁竞争,我们使用waitnotify 来辅助实现

    1. 代码 Test19.java,虚拟机参数-XX:BiasedLockingStartupDelay=0确保我们的程序最开始使用了偏向锁!

    2. 输出结果,最开始使用的是偏向锁,但是第二个线程尝试获取对象锁时,发现本来对象偏向的是线程一,那么偏向锁就会失效,加的就是轻量级锁

      biasedLockFlag (1bit): 1
      	LockFlag (2bit): 01
      biasedLockFlag (1bit): 1
      	LockFlag (2bit): 01
      biasedLockFlag (1bit): 1
      	LockFlag (2bit): 01
      biasedLockFlag (1bit): 1
      	LockFlag (2bit): 01
      LockFlag (2bit): 00
      biasedLockFlag (1bit): 0
      	LockFlag (2bit): 01
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
    撤销 - 调用 wait/notify

    会使对象的锁变成重量级锁,因为wait/notify方法之后重量级锁才支持

    批量重偏向

    如果对象被多个线程访问,但是没有竞争,这时候偏向了线程一的对象又有机会重新偏向线程二,即可以不用升级为轻量级锁,可这和我们之前做的实验矛盾了呀,其实要实现重新偏向是要有条件的:就是超过20对象对同一个线程如线程一撤销偏向时,那么第20个及以后的对象才可以将撤销对线程一的偏向这个动作变为将第20个及以后的对象偏向线程二。

    5)批量重偏向

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

    当撤销偏向锁的阈值超过40以后,就会将整个类的对象都改为不可偏向的

    package cn.itcast.test;
    
    import lombok.extern.slf4j.Slf4j;
    import org.openjdk.jol.info.ClassLayout;
    
    import java.util.Vector;
    import java.util.concurrent.locks.LockSupport;
    
    @Slf4j(topic = "c.TestBiased")
    public class TestBiased {
    
        static Thread t1,t2,t3;
    
        public static void main(String[] args) throws InterruptedException {
            test4();
        }
        private static void test4() throws InterruptedException {
            Vector<Dog> list = new Vector<>();
    
            int loopNumber = 38;
            t1 = new Thread(() -> {
                for (int i = 0; i < loopNumber; i++) {
                    Dog d = new Dog();
                    list.add(d);
                    synchronized (d) {
                        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                    }
                }
                LockSupport.unpark(t2);
            }, "t1");
            t1.start();
    
            t2 = new Thread(() -> {
                LockSupport.park();
                log.debug("===============> ");
                for (int i = 0; i < loopNumber; i++) {
                    Dog d = list.get(i);
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                    synchronized (d) {
                        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                    }
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
                LockSupport.unpark(t3);
            }, "t2");
            t2.start();
    
            t3 = new Thread(() -> {
                LockSupport.park();
                log.debug("===============> ");
                for (int i = 0; i < loopNumber; i++) {
                    Dog d = list.get(i);
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                    synchronized (d) {
                        log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                    }
                    log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true));
                }
            }, "t3");
            t3.start();
    
            t3.join();
            log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true));
        }
    }
    
    class Dog {
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    image-20231017230513916

    6. 锁优化

    6、Wait/Notify

    小故事 - 为什么需要 wait

    由于条件不满足,小南不能继续进行计算

    但小南如果一直占用着锁,其它人就得一直阻塞,效率太低

    image-20231017233241552

    于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开,
    其它人可以由老王随机安排进屋
    直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)

    image-20231017233255009

    小南于是可以离开休息室,重新进入竞争锁的队列

    image-20231017233328953

    (1)原理

    img

    • 锁对象调用wait方法(obj.wait),就会使当前线程进入WaitSet中,变为WAITING状态。

    • 处于BLOCKED和WAITING状态的线程都为

      阻塞

      状态,CPU都不会分给他们时间片。但是有所区别:

      • BLOCKED状态的线程是在竞争对象时,发现Monitor的Owner已经是别的线程了,此时就会进入EntryList中,并处于BLOCKED状态
      • WAITING状态的线程是获得了对象的锁,但是自身因为某些原因需要进入阻塞状态时,锁对象调用了wait方法而进入了WaitSet中,处于WAITING状态
    • BLOCKED状态的线程会在锁被释放的时候被唤醒,但是处于WAITING状态的线程只有被锁对象调用了notify方法(obj.notify/obj.notifyAll),才会被唤醒。

    注:只有当对象被锁以后,才能调用wait和notify方法

    public class Test1 {
    	final static Object LOCK = new Object();
    	public static void main(String[] args) throws InterruptedException {
            //只有在对象被锁住后才能调用wait方法
    		synchronized (LOCK) {
    			LOCK.wait();
    		}
    	}
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

    它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法

    final static Object obj = new Object();
    public static void main(String[] args)
    {
        new Thread(() - >
        {
            synchronized(obj)
            {
                log.debug("执行....");
                try
                {
                    obj.wait(); // 让线程在obj上一直等待下去
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        }).start();
        new Thread(() - >
        {
            synchronized(obj)
            {
                log.debug("执行....");
                try
                {
                    obj.wait(); // 让线程在obj上一直等待下去
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        }).start();
        // 主线程两秒后执行
        sleep(2);
        log.debug("唤醒 obj 上其它线程");
        synchronized(obj)
        {
            obj.notify(); // 唤醒obj上一个线程
            // obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    notify 的一种结果

    20:00:53.096 [Thread-0] c.TestWaitNotify - 执行....
    20:00:53.099 [Thread-1] c.TestWaitNotify - 执行....
    20:00:55.096 [main] c.TestWaitNotify - 唤醒 obj 上其它线程
    20:00:55.096 [Thread-0] c.TestWaitNotify - 其它代码....
    
    • 1
    • 2
    • 3
    • 4

    notifyAll 的结果

    19:58:15.457 [Thread-0] c.TestWaitNotify - 执行....
    19:58:15.460 [Thread-1] c.TestWaitNotify - 执行....
    19:58:17.456 [main] c.TestWaitNotify - 唤醒 obj 上其它线程
    19:58:17.456 [Thread-1] c.TestWaitNotify - 其它代码....
    19:58:17.456 [Thread-0] c.TestWaitNotify - 其它代码....
    
    • 1
    • 2
    • 3
    • 4
    • 5

    wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到
    notify 为止
    wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify

    (2)Wait与Sleep的区别

    不同点

    • Sleep是Thread类的静态方法,Wait是Object的方法,Object又是所有类的父类,所以所有类都有Wait方法。
    • Sleep在阻塞的时候不会释放锁,而Wait在阻塞的时候会释放锁
    • Sleep不需要与synchronized一起使用,而Wait需要与synchronized一起使用(对象被锁以后才能使用)

    相同点

    • 阻塞状态都为TIMED_WAITING

    (3)优雅地使用wait/notify

    什么时候适合使用wait

    • 当线程不满足某些条件,需要暂停运行时,可以使用wait。这样会将对象的锁释放,让其他线程能够继续运行。如果此时使用sleep,会导致所有线程都进入阻塞,导致所有线程都没法运行,直到当前线程sleep结束后,运行完毕,才能得到执行。

    使用wait/notify需要注意什么

    • 当有多个线程在运行时,对象调用了wait方法,此时这些线程都会进入WaitSet中等待。如果这时使用了notify方法,可能会造成虚假唤醒(唤醒的不是满足条件的等待线程),这时就需要使用notifyAll方法
    synchronized (LOCK) {
    	while(//不满足条件,一直等待,避免虚假唤醒) {
    		LOCK.wait();
    	}
    	//满足条件后再运行
    }
    
    synchronized (LOCK) {
    	//唤醒所有等待线程
    	LOCK.notifyAll();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    然后其他线程用while

    7、模式之保护性暂停

    (1)定义

    img

    (2)举例

    public class Test2 {
    	public static void main(String[] args) {
    		String hello = "hello thread!";
    		Guarded guarded = new Guarded();
    		new Thread(()->{
    			System.out.println("想要得到结果");
    			synchronized (guarded) {
    				System.out.println("结果是:"+guarded.getResponse());
    			}
    			System.out.println("得到结果");
    		}).start();
    
    		new Thread(()->{
    			System.out.println("设置结果");
    			synchronized (guarded) {
    				guarded.setResponse(hello);
    			}
    		}).start();
    	}
    }
    
    class Guarded {
    	/**
    	 * 要返回的结果
    	 */
    	private Object response;
    	
        //优雅地使用wait/notify
    	public Object getResponse() {
    		//如果返回结果为空就一直等待,避免虚假唤醒
    		while(response == null) {
    			synchronized (this) {
    				try {
    					this.wait();
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    			}
    		}
    		return response;
    	}
    
    	public void setResponse(Object response) {
    		this.response = response;
    		synchronized (this) {
    			//唤醒休眠的线程
    			this.notifyAll();
    		}
    	}
    
    	@Override
    	public String toString() {
    		return "Guarded{" +
    				"response=" + response +
    				'}';
    	}
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    带超时判断的暂停

    public Object getResponse(long time) {
    		synchronized (this) {
    			//获取开始时间
    			long currentTime = System.currentTimeMillis();
    			//用于保存已经等待了的时间
    			long passedTime = 0;
    			while(response == null) {
    				//看经过的时间-开始时间是否超过了指定时间
    				long waitTime = time -passedTime;
    				if(waitTime <= 0) {
    					break;
    				}
    				try {
                       	//等待剩余时间
    					this.wait(waitTime);
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    				//获取当前时间
    				passedTime = System.currentTimeMillis()-currentTime		
                }
    		}
    		return response;
    	}Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    (3)join源码——使用保护性暂停模式

    image-20231018090342661

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

    wait():注意,前述提及wait操作对象一定是持有锁的对象,而join方法在方法头中含有Syschronized关键字

    拓展

    图中Futures就好比居民楼一层的信箱(每个信箱有房间编号),左侧的t0,t2,t4就好比等待邮件的居民,右侧的t1,t3,t5就好比邮递员
    如果需要在多个类之间使用GuardedObject对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理

    邮递员和收信者就不需要互相传递GuardedObject对象

    image-20231018090804764

    package cn.itcast.test;
    
    import cn.itcast.n2.util.Sleeper;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Hashtable;
    import java.util.Map;
    import java.util.Set;
    
    @Slf4j(topic = "c.Test20")
    public class Test20 {
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 3; i++) {
                new People().start();
            }
            Sleeper.sleep(1);
            for (Integer id : Mailboxes.getIds()) {
                new Postman(id, "内容" + id).start();
            }
        }
    }
    
    @Slf4j(topic = "c.People")
    class People extends Thread{
        @Override
        public void run() {
            // 收信
            GuardedObject guardedObject = Mailboxes.createGuardedObject();
            log.debug("开始收信 id:{}", guardedObject.getId());
            Object mail = guardedObject.get(5000);
            log.debug("收到信 id:{}, 内容:{}", guardedObject.getId(), mail);
        }
    }
    
    @Slf4j(topic = "c.Postman")
    class Postman extends Thread {
        private int id;
        private String mail;
    
        public Postman(int id, String mail) {
            this.id = id;
            this.mail = mail;
        }
    
        @Override
        public void run() {
            GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
            log.debug("送信 id:{}, 内容:{}", id, mail);
            guardedObject.complete(mail);
        }
    }
    
    class Mailboxes {
        private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
    
        private static int id = 1;
        // 产生唯一 id
        private static synchronized int generateId() {
            return id++;
        }
    
        public static GuardedObject getGuardedObject(int id) {
            return boxes.remove(id);
        }
    
        public static GuardedObject createGuardedObject() {
            GuardedObject go = new GuardedObject(generateId());
            boxes.put(go.getId(), go);
            return go;
        }
    
        public static Set<Integer> getIds() {
            return boxes.keySet();
        }
    }
    
    // 增加超时效果
    class GuardedObject {
    
        // 标识 Guarded Object
        private int id;
    
        public GuardedObject(int id) {
            this.id = id;
        }
    
        public int getId() {
            return id;
        }
    
        // 结果
        private Object response;
    
        // 获取结果
        // timeout 表示要等待多久 2000
        public Object get(long timeout) {
            synchronized (this) {
                // 开始时间 15:00:00
                long begin = System.currentTimeMillis();
                // 经历的时间
                long passedTime = 0;
                while (response == null) {
                    // 这一轮循环应该等待的时间
                    long waitTime = timeout - passedTime;
                    // 经历的时间超过了最大等待时间时,退出循环
                    if (timeout - passedTime <= 0) {
                        break;
                    }
                    try {
                        this.wait(waitTime); // 虚假唤醒 15:00:01
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 求得经历时间
                    passedTime = System.currentTimeMillis() - begin; // 15:00:02  1s
                }
                return response;
            }
        }
    
        // 产生结果
        public void complete(Object response) {
            synchronized (this) {
                // 给结果成员变量赋值
                this.response = response;
                this.notifyAll();
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130

    异步模式之生产者消费者

    • 与前面的保护性暂停中的GuardObjecl不同,不需要产生结果和消费结果的线程一一对应

    • 消费队列可以用来平衡生产和消费的线程资源

    • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果

    • 数据消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据

    • JDK中各种阻塞队列,采用的就是这种模式

    image-20231018154609064

    java之间线程进行通信,rabbitmq是进程之间进行通信

    
    package cn.itcast.test;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.LinkedList;
    
    import static cn.itcast.n2.util.Sleeper.sleep;
    
    @Slf4j(topic = "c.Test21")
    public class Test21 {
    
        public static void main(String[] args) {
            MessageQueue queue = new MessageQueue(2);
    
            for (int i = 0; i < 3; i++) {
                int id = i;
                new Thread(() -> {
                    queue.put(new Message(id , "值"+id));
                }, "生产者" + i).start();
            }
    
            new Thread(() -> {
                while(true) {
                    sleep(1);
                    Message message = queue.take();
                }
            }, "消费者").start();
        }
    
    }
    
    // 消息队列类 , java 线程之间通信
    @Slf4j(topic = "c.MessageQueue")
    class MessageQueue {
        // 消息的队列集合
        private LinkedList<Message> list = new LinkedList<>();
        // 队列容量
        private int capcity;
    
        public MessageQueue(int capcity) {
            this.capcity = capcity;
        }
    
        // 获取消息
        public Message take() {
            // 检查队列是否为空
            synchronized (list) {
                while(list.isEmpty()) {
                    try {
                        log.debug("队列为空, 消费者线程等待");
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 从队列头部获取消息并返回
                Message message = list.removeFirst();
                log.debug("已消费消息 {}", message);
                list.notifyAll();
                return message;
            }
        }
    
        // 存入消息
        public void put(Message message) {
            synchronized (list) {
                // 检查对象是否已满
                while(list.size() == capcity) {
                    try {
                        log.debug("队列已满, 生产者线程等待");
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 将消息加入队列尾部
                list.addLast(message);
                log.debug("已生产消息 {}", message);
                list.notifyAll();
            }
        }
    }
    
    final class Message {
        private int id;
        private Object value;
    
        public Message(int id, Object value) {
            this.id = id;
            this.value = value;
        }
    
        public int getId() {
            return id;
        }
    
        public Object getValue() {
            return value;
        }
    
        @Override
        public String toString() {
            return "Message{" +
                    "id=" + id +
                    ", value=" + value +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109

    8、park/unpark

    (1)基本使用

    park/unpark都是LockSupport类中的的方法

    //暂停线程运行
    LockSupport.park;
    
    //恢复线程运行
    LockSupport.unpark(thread);Copy
    public static void main(String[] args) throws InterruptedException {
    		Thread thread = new Thread(()-> {
    			System.out.println("park");
                //暂停线程运行
    			LockSupport.park();
    			System.out.println("resume");
    		}, "t1");
    		thread.start();
    
    		Thread.sleep(1000);
    		System.out.println("unpark");
        	//恢复线程运行
    		LockSupport.unpark(thread);
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    image-20231018163235322

    主线程可以先进行unpark ,

    (2)特点

    与wait/notify的区别

    • wait,notify 和 notifyAll 必须配合Object Monitor一起使用==(先获得锁)==,而park,unpark不必
    • park ,unpark 是以线程为单位阻塞唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么精确
    • park & unpark 可以先 unpark,而 wait & notify 不能先 notify
    • park不会释放锁,而wait会释放锁

    (3)原理

    每个线程都有一个自己的Park对象,并且该对象**_counter, _cond,__mutex**组成

    • 先调用park再调用unpark时

      • 先调用park

        • 线程运行时,会将Park对象中的**_counter的值设为0**;
        • 调用park时,会先查看counter的值是否为0,如果为0,则将线程放入阻塞队列cond中
        • 放入阻塞队列中后,会再次将counter设置为0
      • 然后调用unpark

        • 调用unpark方法后,会将counter的值设置为1

        • 去唤醒阻塞队列cond中的线程

        • 线程继续运行并将counter的值设为0

          img

    img

    • 先调用unpark,再调用park
      • 调用unpark
        • 会将counter设置为1(运行时0)
      • 调用park方法
        • 查看counter是否为0
        • 因为unpark已经把counter设置为1,所以此时将counter设置为0,但不放入阻塞队列cond中

    img

    9、线程中的状态转换

    img

    情况一:NEW –> RUNNABLE

    • 当调用了t.start()方法时,由 NEW –> RUNNABLE

    情况二: RUNNABLE <–> WAITING

    • 当调用了t 线程用 synchronized(obj) 获取了对象锁后
      • 调用 obj.wait() 方法时,t 线程从 RUNNABLE –> WAITING
      • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
        • 竞争锁成功,t 线程从 WAITING –> RUNNABLE
        • 竞争锁失败,t 线程从 WAITING –> BLOCKED

    情况三:RUNNABLE <–> WAITING

    • 当前线程

      调用 t.join() 方法时,当前线程从 RUNNABLE –> WAITING

      • 注意是当前线程在t 线程对象的监视器上等待
    • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING –> RUNNABLE

    情况四: RUNNABLE <–> WAITING

    • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE –> WAITING
    • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING –> RUNNABLE

    情况五: RUNNABLE <–> TIMED_WAITING

    t 线程用 synchronized(obj) 获取了对象锁后

    • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE –> TIMED_WAITING
    • t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
      • 竞争锁成功,t 线程从 TIMED_WAITING –> RUNNABLE
      • 竞争锁失败,t 线程从 TIMED_WAITING –> BLOCKED

    情况六:RUNNABLE <–> TIMED_WAITING

    • 当前线程调用 t.join

      (long n

      ) 方法时,当前线程从 RUNNABLE –> TIMED_WAITING

      • 注意是当前线程在t 线程对象的监视器上等待
    • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING –> RUNNABLE

    情况七:RUNNABLE <–> TIMED_WAITING

    • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE –> TIMED_WAITING
    • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE

    情况八:RUNNABLE <–> TIMED_WAITING

    • 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE –> TIMED_WAITING
    • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE

    情况九:RUNNABLE <–> BLOCKED

    • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE –> BLOCKED
    • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED –> RUNNABLE ,其它失败的线程仍然 BLOCKED

    情况十: RUNNABLE <–> TERMINATED

    当前线程所有代码运行完毕,进入 TERMINATED

    10、多把锁

    将锁的粒度细分

    image-20231018172428635

    class BigRoom {
        //额外创建对象来作为锁
    	private final Object studyRoom = new Object();
    	private final Object bedRoom = new Object();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 好处,可以增强并发度
    • 坏处,如果一个线程需要获得多把锁,就容易发生死锁

    11、活跃性

    (1)定义

    因为某种原因,使得代码一直无法执行完毕,这样的现象叫做活跃性

    (2)死锁

    有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

    如:t1线程获得A对象 锁,接下来想获取B对象的锁t2线程获得B对象锁,接下来想获取A对象的锁

    public static void main(String[] args) {
    		final Object A = new Object();
    		final Object B = new Object();
    		new Thread(()->{
    			synchronized (A) {
    				try {
    					Thread.sleep(2000);
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    				synchronized (B) {
    
    				}
    			}
    		}).start();
    
    		new Thread(()->{
    			synchronized (B) {
    				try {
    					Thread.sleep(1000);
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    				synchronized (A) {
    
    				}
    			}
    		}).start();
    	}Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    发生死锁的必要条件
    • 互斥条件
      • 在一段时间内,一种资源只能被一个进程所使用
    • 请求和保持条件
      • 进程已经拥有了至少一种资源,同时又去申请其他资源。因为其他资源被别的进程所使用,该进程进入阻塞状态,并且不释放自己已有的资源
    • 不可抢占条件
      • 进程对已获得的资源在未使用完成前不能被强占,只能在进程使用完后自己释放
    • 循环等待条件
      • 发生死锁时,必然存在一个进程——资源的循环链。
    定位死锁的方法
    • 检测死锁可以使用jconsole工具

    • jps+jstack ThreadID

      • 在JAVA控制台中的Terminal中输入jps指令可以查看运行中的线程ID,使用jstack ThreadID可以查看线程状态。

        img

      F:\Thread_study>jps
      20672 RemoteMavenServer36
      22880 Jps
      4432 Launcher
      5316 Test5
      20184 KotlinCompileDaemon
      11132
      
      F:\Thread_study>jstack 5316Copy
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    • 打印的结果

      //找到一个java级别的死锁
      Found one Java-level deadlock:
      =============================
      "Thread-1":
        waiting to lock monitor 0x0000000017f40de8 (object 0x00000000d6188880, a java.lang.Object),
        which is held by "Thread-0"
      "Thread-0":
        waiting to lock monitor 0x0000000017f43678 (object 0x00000000d6188890, a java.lang.Object),
        which is held by "Thread-1"Copy
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
    • jconsole检测死锁

      img

      img

    哲学家就餐问题

    img

    避免死锁的方法

    在线程使用锁对象时**,顺序加锁**即可避免死锁

    img

    (3)活锁

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

    避免活锁的方法

    在线程执行时,中途给予不同的间隔时间即可。

    死锁与活锁的区别
    • 死锁是因为线程互相持有对象想要的锁,并且都不释放,最后到时线程阻塞停止运行的现象。
    • 活锁是因为线程间修改了对方的结束条件,而导致代码一直在运行,却一直运行不完的现象。

    (4)饥饿

    某些线程因为优先级太低,导致一直无法获得资源的现象。

    在使用顺序加锁时,可能会出现饥饿现象

    12、ReentrantLock

    和synchronized相比具有的的特点

    • 可中断
    • 可以设置超时时间
    • 可以设置为公平锁 (先到先得防止饥饿)
    • 支持多个条件变量( 具有多个waitset)

    和synchronized一样,都支持可重入自己加的锁下一次自己也可以直接进去

    基本语法

    //获取ReentrantLock对象
    private ReentrantLock lock = new ReentrantLock();
    //加锁
    lock.lock();
    try {
    	//需要执行的代码
    }finally {
    	//释放
    	lock.unlock();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    可重入
    • 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
    • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args)
    {
        method1();
    }
    public static void method1()
    {
        lock.lock();
        try
        {
            log.debug("execute method1");
            method2();
        }
        finally
        {
            lock.unlock();
        }
    }
    public static void method2()
    {
        lock.lock();
        try
        {
            log.debug("execute method2");
            method3();
        }
        finally
        {
            lock.unlock();
        }
    }
    public static void method3()
    {
        lock.lock();
        try
        {
            log.debug("execute method3");
        }
        finally
        {
            lock.unlock();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    结果:

    17:59:11.862 [main] c.TestReentrant - execute method1
    17:59:11.865 [main] c.TestReentrant - execute method2
    17:59:11.865 [main] c.TestReentrant - execute method3
    
    • 1
    • 2
    • 3

    三个被锁住的方法都可以运行

    可打断

    如果某个线程处于阻塞状态,可以调用其interrupt方法让其停止阻塞,获得锁失败

    简而言之就是:处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行

    public static void main(String[] args) {
    		ReentrantLock lock = new ReentrantLock();
    		Thread t1 = new Thread(()-> {
    			try {
    				//加锁,可打断锁
    				//如果没有竞争那么此方法就会获取lock对象锁
    				//如果有竞争就进入阻塞队列,可以被其他线程用interrupt打断
    				lock.lockInterruptibly();
    			} catch (InterruptedException e) {
    				e.printStackTrace();
                    //被打断,返回,不再向下执行
    				return;
    			}finally {
    				//释放锁
    				lock.unlock();
    			}
    
    		});
    
    		lock.lock();
    		try {
    			t1.start();
    			Thread.sleep(1000);
    			//打断
    			t1.interrupt();
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		} finally {
    			lock.unlock();
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    image-20231018192410097

    主要防止线程死等

    锁超时

    使用lock.tryLock方法会返回获取锁是否成功。如果成功则返回true,反之则返回false。

    并且tryLock方法可以指定等待时间,参数为:tryLock(long timeout, TimeUnit unit), 其中timeout为最长等待时间,TimeUnit为时间单位

    简而言之就是:获取失败了、获取超时了或者被打断了,不再阻塞,直接停止运行

    不设置等待时间

    public static void main(String[] args) {
    		ReentrantLock lock = new ReentrantLock();
    		Thread t1 = new Thread(()-> {
                //未设置等待时间,一旦获取失败,直接返回false
    			if(!lock.tryLock()) {
    				System.out.println("获取失败");
                    //获取失败,不再向下执行,返回
    				return;
    			}
    			System.out.println("得到了锁");
    			lock.unlock();
    		});
    
    
    		lock.lock();
    		try{
    			t1.start();
    			Thread.sleep(3000);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		} finally {
    			lock.unlock();
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    设置等待时间

    public static void main(String[] args) {
    		ReentrantLock lock = new ReentrantLock();
    		Thread t1 = new Thread(()-> {
    			try {
    				//判断获取锁是否成功,最多等待1秒
    				if(!lock.tryLock(1, TimeUnit.SECONDS)) {
    					System.out.println("获取失败");
    					//获取失败,不再向下执行,直接返回
    					return;
    				}
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    				//被打断,不再向下执行,直接返回
    				return;
    			}
    			System.out.println("得到了锁");
    			//释放锁
    			lock.unlock();
    		});
    
    
    		lock.lock();
    		try{
    			t1.start();
    			//打断等待
    			t1.interrupt();
    			Thread.sleep(3000);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		} finally {
    			lock.unlock();
    		}
    	}Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    公平锁

    在线程获取锁失败,进入阻塞队列时,先进入的会在锁被释放后先获得锁。这样的获取方式就是公平的。

    //默认是不公平锁,需要在创建时指定为公平锁
    ReentrantLock lock = new ReentrantLock(true);
    
    • 1
    • 2
    条件变量

    synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入waitSet 等待

    ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

    • synchronized 是那些不满足条件的线程都在一间休息室等消息
    • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤 醒

    使用要点:

    • await 前需要获得锁
    • await 执行后,会释放锁,进入 conditionObject 等待
    • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
    • 竞争 lock 锁成功后,从 await 后继续执
    static Boolean judge = false;
    public static void main(String[] args) throws InterruptedException {
    	ReentrantLock lock = new ReentrantLock();
    	//获得条件变量
    	Condition condition = lock.newCondition();
    	new Thread(()->{
    		lock.lock();
    		try{
    			while(!judge) {
    				System.out.println("不满足条件,等待...");
    				//等待
    				condition.await();
    			}
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		} finally {
    			System.out.println("执行完毕!");
    			lock.unlock();
    		}
    	}).start();
    
    	new Thread(()->{
    		lock.lock();
    		try {
    			Thread.sleep(1);
    			judge = true;
    			//释放
    			condition.signal();
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		} finally {
    			lock.unlock();
    		}
    
    	}).start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    通过Lock与AQS实现可重入锁
    public class MyLock implements Lock {
       private static class Sync extends AbstractQueuedSynchronizer {
          @Override
          protected boolean tryAcquire(int arg) {
             if (getExclusiveOwnerThread() == null) {
                if (compareAndSetState(0, 1)) {
                   setExclusiveOwnerThread(Thread.currentThread());
                   return true;
                }
                return false;
             }
    
             if (getExclusiveOwnerThread() == Thread.currentThread()) {
                int state = getState();
                compareAndSetState(state, state + 1);
                return true;
             }
    
             return false;
          }
    
          @Override
          protected boolean tryRelease(int arg) {
             if (getState() <= 0) {
                throw new IllegalMonitorStateException();
             }
    
             if (getExclusiveOwnerThread() != Thread.currentThread()) {
                throw new IllegalMonitorStateException();
             }
    
             int state = getState();
             if (state == 1) {
                setExclusiveOwnerThread(null);
                compareAndSetState(state, 0);
             } else {
                compareAndSetState(state, state - 1);
             }
             return true;
          }
    
          @Override
          protected boolean isHeldExclusively() {
             return getState() >= 1;
          }
    
          public Condition newCondition() {
             return new ConditionObject();
          }
    
       }
    
       Sync sync = new Sync();
    
       @Override
       public void lock() {
          sync.acquire(1);
       }
    
       @Override
       public void lockInterruptibly() throws InterruptedException {
          sync.acquireInterruptibly(1);
       }
    
       @Override
       public boolean tryLock() {
          return sync.tryAcquire(1);
       }
    
       @Override
       public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
          return sync.tryAcquireNanos(1, time);
       }
    
       @Override
       public void unlock() {
          sync.release(1);
       }
    
       @Override
       public Condition newCondition() {
          return sync.newCondition();
       }
    }
    
    class Main {
       static int num = 0;
       public static void main(String[] args) throws InterruptedException, IOException {
          MyLock lock = new MyLock();
    
          Object syncLock = new Object();
    
          Thread t1 = new Thread(() -> {
             for (int i = 0; i < 10000; i++) {
                lock.lock();
                try {
                   lock.lock();
                   try {
                      lock.lock();
                      try {
                         num++;
                      } finally {
                         lock.unlock();
                      }
                   } finally {
                      lock.unlock();
                   }
                } finally {
                   lock.unlock();
                }
             }
          });
    
          Thread t2 = new Thread(() -> {
             for (int i = 0; i < 10000; i++) {
                lock.lock();
                try {
                   lock.lock();
                   try {
                      lock.lock();
                      try {
                         num--;
                      } finally {
                         lock.unlock();
                      }
                   } finally {
                      lock.unlock();
                   }
                } finally {
                   lock.unlock();
                }
             }
          });
    
          t1.start();
          t2.start();
          t1.join();
          t2.join();
    
          int x = 0;
       }
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142

    13、同步模式之顺序控制

    Wait/Notify版本

    static final Object LOCK = new Object();
    //判断先执行的内容是否执行完毕
    static Boolean judge = false;
    public static void main(String[] args) {
    	new Thread(()->{
    		synchronized (LOCK) {
    			while (!judge) {
    				try {
    					LOCK.wait();
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    			}
    			System.out.println("2");
    		}
    	}).start();
    
    	new Thread(()->{
    		synchronized (LOCK) {
    			System.out.println("1");
    			judge = true;
                   //执行完毕,唤醒所有等待线程
    			LOCK.notifyAll();
    		}
    	}).start();
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    使用park往背包放东西

    image-20231018204210651

    交替输出

    wait/notify版本

    public class Test4 {
    	static Symbol symbol = new Symbol();
    	public static void main(String[] args) {
    		new Thread(()->{
    			symbol.run("a", 1, 2);
    		}).start();
    
    		new Thread(()->{
    			symbol.run("b", 2, 3);
    
    		}).start();
    		symbol.run("c", 3, 1);
    		new Thread(()->{
    
    		}).start();
    	}
    }
    
    class Symbol {
    	public synchronized void run(String str, int flag, int nextFlag) {
    		for(int i=0; i<loopNumber; i++) {
    			while(flag != this.flag) {
    				try {
    					this.wait();
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    			}
    			System.out.println(str);
    			//设置下一个运行的线程标记
    			this.flag = nextFlag;
    			//唤醒所有线程
    			this.notifyAll();
    		}
    	}
    
    	/**
    	 * 线程的执行标记, 1->a 2->b 3->c
    	 */
    	private int flag = 1;
    	private int loopNumber = 5;
    
    	public int getFlag() {
    		return flag;
    	}
    
    	public void setFlag(int flag) {
    		this.flag = flag;
    	}
    
    	public int getLoopNumber() {
    		return loopNumber;
    	}
    
    	public void setLoopNumber(int loopNumber) {
    		this.loopNumber = loopNumber;
    	}
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    await/signal版本

    public class Test5 {
    	static AwaitSignal awaitSignal = new AwaitSignal();
    	static Condition conditionA = awaitSignal.newCondition();
    	static Condition conditionB = awaitSignal.newCondition();
    	static Condition conditionC = awaitSignal.newCondition();
    	public static void main(String[] args) {
    		new Thread(()->{
    			awaitSignal.run("a", conditionA, conditionB);
    		}).start();
    
    		new Thread(()->{
    			awaitSignal.run("b", conditionB, conditionC);
    		}).start();
    
    		new Thread(()->{
    			awaitSignal.run("c", conditionC, conditionA);
    		}).start();
    
    
    		try {
    			Thread.sleep(1000);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		awaitSignal.lock();
    		try {
                //唤醒一个等待的线程
    			conditionA.signal();
    		}finally {
    			awaitSignal.unlock();
    		}
    	}
    }
    
    class AwaitSignal extends ReentrantLock{
    	public void run(String str, Condition thisCondition, Condition nextCondition) {
    		for(int i=0; i<loopNumber; i++) {
    			lock();
    			try {
                    //全部进入等待状态
    				thisCondition.await();
    				System.out.print(str);
    				nextCondition.signal();
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			} finally {
    				unlock();
    			}
    		}
    	}
    
    	private int loopNumber=5;
    
    	public int getLoopNumber() {
    		return loopNumber;
    	}
    
    	public void setLoopNumber(int loopNumber) {
    		this.loopNumber = loopNumber;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    14、ThreadLocal

    简介

    ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题

    使用

    public class ThreadLocalStudy {
       public static void main(String[] args) {
          // 创建ThreadLocal变量
          ThreadLocal stringThreadLocal = new ThreadLocal<>();
          ThreadLocal userThreadLocal = new ThreadLocal<>();
    
          // 创建两个线程,分别使用上面的两个ThreadLocal变量
          Thread thread1 = new Thread(()->{
             // stringThreadLocal第一次赋值
             stringThreadLocal.set("thread1 stringThreadLocal first");
             // stringThreadLocal第二次赋值
             stringThreadLocal.set("thread1 stringThreadLocal second");
             // userThreadLocal赋值
             userThreadLocal.set(new User("Nyima", 20));
    
             // 取值
             System.out.println(stringThreadLocal.get());
             System.out.println(userThreadLocal.get());
              
              // 移除
    		 userThreadLocal.remove();
    		 System.out.println(userThreadLocal.get());
          });
    
          Thread thread2 = new Thread(()->{
             // stringThreadLocal第一次赋值
             stringThreadLocal.set("thread2 stringThreadLocal first");
             // stringThreadLocal第二次赋值
             stringThreadLocal.set("thread2 stringThreadLocal second");
             // userThreadLocal赋值
             userThreadLocal.set(new User("Hulu", 20));
    
             // 取值
             System.out.println(stringThreadLocal.get());
             System.out.println(userThreadLocal.get());
          });
    
          // 启动线程
          thread1.start();
          thread2.start();
       }
    }
    
    class User {
       String name;
       int age;
    
       public User(String name, int age) {
          this.name = name;
          this.age = age;
       }
    
       @Override
       public String toString() {
          return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
       }
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    运行结果

    thread1 stringThreadLocal second
    thread2 stringThreadLocal second
    User{name='Nyima', age=20}
    User{name='Hulu', age=20}
    nullCopy
    
    • 1
    • 2
    • 3
    • 4
    • 5

    从运行结果可以看出

    • 每个线程中的ThreadLocal变量是每个线程私有的,而不是共享的
      • 从线程1和线程2的打印结果可以看出
    • ThreadLocal其实就相当于其泛型类型的一个变量,只不过是每个线程私有的
      • stringThreadLocal被赋值了两次,保存的是最后一次赋值的结果
    • ThreadLocal可以进行以下几个操作
      • set 设置值
      • get 取出值
      • remove 移除值

    原理

    Thread中的threadLocals
    public class Thread implements Runnable {
     ...
    
     ThreadLocal.ThreadLocalMap threadLocals = null;
    
     // 放在后面说
     ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
     ...
    }Copy
    static class ThreadLocalMap {
        static class Entry extends WeakReference> {
            /** The value associated with this ThreadLocal. */
            Object value;
    
            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    可以看出Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap。在默认情况下,每个线程中的这两个变量都为null。此处先讨论threadLocals,inheritableThreadLocals放在后面讨论

    ThreadLocal中的方法

    set方法

    public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread();
        
        // 获得ThreadLocalMap对象 
        // 这里的get会返回Thread类中的threadLocals
        ThreadLocalMap map = getMap(t);
        
        // 判断map是否已经创建,没创建就创建并放入值,创建了就直接放入
        if (map != null)
            // ThreadLocal自生的引用作为key,传入的值作为value
            map.set(this, value);
        else
            createMap(t, value);
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    如果未创建

    void createMap(Thread t, T firstValue) {
        // 创建的同时设置想放入的值
        // hreadLocal自生的引用作为key,传入的值作为value
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5

    get方法

    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
    	// 获取当前线程的threadLocals变量
        ThreadLocalMap map = getMap(t);
        
        // 判断threadLocals是否被初始化了
        if (map != null) {
            // 已经初始化则直接返回
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 否则就创建threadLocals
        return setInitialValue();
    }Copy
    private T setInitialValue() {
        // 这个方法返回是null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        
        // 无论map创建与否,最终value的值都为null
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }Copy
    protected T initialValue() {
        return null;
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    remove方法

    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            // 如果threadLocals已经被初始化,则移除
            m.remove(this);
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    总结

    在每个线程内部都有一个名为threadLocals的成员变量,该变量的类型为HashMap,其中key为我们定义的ThreadLocal变量的this引用,value则为我们使用set方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals中

    只有当前线程第一次调用ThreadLocal的set或者get方法时才会创建threadLocals(inheritableThreadLocals也是一样)。其实每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面

    15、InheritableThreadLocal

    简介

    从ThreadLocal的源码可以看出,无论是set、get、还是remove,都是相对于当前线程操作的

    Thread.currentThread()Copy
    
    • 1

    所以ThreadLocal无法从父线程传向子线程,所以InheritableThreadLocal出现了,它能够让父线程中ThreadLocal的值传给子线程。

    也就是从main所在的线程,传给thread1或thread2

    使用

    public class Demo1 {
       public static void main(String[] args) {
          ThreadLocal stringThreadLocal = new ThreadLocal<>();
          InheritableThreadLocal stringInheritable = new InheritableThreadLocal<>();
    
          // 主线程赋对上面两个变量进行赋值
          stringThreadLocal.set("this is threadLocal");
          stringInheritable.set("this is inheritableThreadLocal");
    
          // 创建线程
          Thread thread1 = new Thread(()->{
             // 获得ThreadLocal中存放的值
             System.out.println(stringThreadLocal.get());
    
             // 获得InheritableThreadLocal存放的值
             System.out.println(stringInheritable.get());
          });
    
          thread1.start();
       }
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    运行结果

    null
    this is inheritableThreadLocalCopy
    
    • 1
    • 2

    可以看出InheritableThreadLocal的值成功从主线程传入了子线程,而ThreadLocal则没有

    原理

    InheritableThreadLocal
    public class InheritableThreadLocal extends ThreadLocal {
        // 传入父线程中的一个值,然后直接返回
        protected T childValue(T parentValue) {
            return parentValue;
        }
    
      	// 返回传入线程的inheritableThreadLocals
        // Thread中有一个inheritableThreadLocals变量
        // ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
        ThreadLocalMap getMap(Thread t) {
           return t.inheritableThreadLocals;
        }
    
     	// 创建一个inheritableThreadLocals
        void createMap(Thread t, T firstValue) {
            t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
        }
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    由如上代码可知,InheritableThreadLocal继承了ThreadLocal,并重写了三个方法。InheritableThreadLocal重写了createMap方法,那么现在当第一次调用set方法时,创建的是当前线程的inheritableThreadLocals变量的实例而不再是threadLocals。当调用getMap方法获取当前线程内部的map变量时,获取的是inheritableThreadLocals而不再是threadLocals

    childValue(T parentValue)方法的调用

    在主函数运行时,会调用Thread的默认构造函数(创建主线程,也就是父线程),所以我们先看看Thread的默认构造函数

    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }Copy
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
       	...
            
    	// 获得当前线程的,在这里是主线程
        Thread parent = currentThread();
       
        ...
        
        // 如果父线程的inheritableThreadLocals存在
        // 我们在主线程中调用set和get时,会创建inheritableThreadLocals
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            // 设置子线程的inheritableThreadLocals
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;
    
        /* Set thread ID */
        tid = nextThreadID();
    }Copy
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在createInheritedMap内部使用父线程的inheritableThreadLocals变量作为构造函数创建了一个新的ThreadLocalMap变量,然后赋值给了子线程的inheritableThreadLocals变量

    private ThreadLocalMap(ThreadLocalMap parentMap) {
        Entry[] parentTable = parentMap.table;
        int len = parentTable.length;
        setThreshold(len);
        table = new Entry[len];
    
        for (int j = 0; j < len; j++) {
            Entry e = parentTable[j];
            if (e != null) {
                @SuppressWarnings("unchecked")
                ThreadLocal key = (ThreadLocal) e.get();
                if (key != null) {
                    // 这里调用了 childValue 方法
                    // 该方法会返回parent的值
                    Object value = key.childValue(e.value);
                    
                    Entry c = new Entry(key, value);
                    int h = key.threadLocalHashCode & (len - 1);
                    while (table[h] != null)
                        h = nextIndex(h, len);
                    table[h] = c;
                    size++;
                }
            }
        }
    }Copy
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在该构造函数内部把父线程的inheritableThreadLocals成员变量的值复制到新的ThreadLocalMap对象中

    总结

    InheritableThreadLocal类通过重写getMap和createMap,让本地变量保存到了具体线程的inheritableThreadLocals变量里面,那么线程在通过InheritableThreadLocal类实例的set或者get方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。

    当父线程创建子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量里面。

    本章小结

    本章我们需要重点掌握的是
    分析多线程访问共享资源时,哪些代码片段属于临界区
    使用 synchronized 互斥解决临界区的线程安全问题
    掌握 synchronized 锁对象语法
    掌握 synchronzied 加载成员方法和静态方法语法
    掌握 wait/notify 同步方法
    使用 lock 互斥解决临界区的线程安全问题
    掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
    学会分析变量的线程安全性、掌握常见线程安全类的使用
    了解线程活跃性问题:死锁、活锁、饥饿
    应用方面
    互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
    同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果
    原理方面
    monitor、synchronized 、wait/notify 原理
    synchronized 进阶原理
    park & unpark 原理
    模式方面
    同步模式之保护性暂停
    异步模式之生产者消费者
    `
    public Thread() {
    init(null, null, “Thread-” + nextThreadNum(), 0);
    }Copy
    private void init(ThreadGroup g, Runnable target, String name,
    long stackSize, AccessControlContext acc,
    boolean inheritThreadLocals) {

    // 获得当前线程的,在这里是主线程
    Thread parent = currentThread();
    
    ...
    
    // 如果父线程的inheritableThreadLocals存在
    // 我们在主线程中调用set和get时,会创建inheritableThreadLocals
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        // 设置子线程的inheritableThreadLocals
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    
    /* Stash the specified stack size in case the VM cares */
    this.stackSize = stackSize;
    
    /* Set thread ID */
    tid = nextThreadID();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    }Copy
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
    }Copy

    
    在createInheritedMap内部使用父线程的inheritableThreadLocals变量作为构造函数创建了一个新的ThreadLocalMap变量,然后赋值给了子线程的inheritableThreadLocals变量
    
    
    • 1
    • 2
    • 3

    private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal key = (ThreadLocal) e.get();
            if (key != null) {
                // 这里调用了 childValue 方法
                // 该方法会返回parent的值
                Object value = key.childValue(e.value);
                
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    }Copy

    
    在该构造函数内部把父线程的inheritableThreadLocals成员变量的值复制到新的ThreadLocalMap对象中
    
    #### 总结
    
    InheritableThreadLocal类通过重写getMap和createMap,让本地变量保存到了具体线程的inheritableThreadLocals变量里面,那么线程在通过InheritableThreadLocal类实例的set或者get方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。
    
    **当父线程创建子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量里面。**
    
    
    
    
    
    # 本章小结
    
    本章我们需要重点掌握的是
    分析多线程访问共享资源时,哪些代码片段属于临界区
    使用 synchronized 互斥解决临界区的线程安全问题
    掌握 synchronized 锁对象语法
    掌握 synchronzied 加载成员方法和静态方法语法
    掌握 wait/notify 同步方法
    使用 lock 互斥解决临界区的线程安全问题
    掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
    学会分析变量的线程安全性、掌握常见线程安全类的使用
    了解线程活跃性问题:死锁、活锁、饥饿
    应用方面
    互斥:使用 synchronized 或 Lock 达到共享资源互斥效果
    同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果
    原理方面
    monitor、synchronized 、wait/notify 原理
    synchronized 进阶原理
    park & unpark 原理
    模式方面
    同步模式之保护性暂停
    异步模式之生产者消费者
    同步模式之顺序控制
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
  • 相关阅读:
    [Document]VectoreStoreToDocument开发
    网络套接字编程(二)
    maven报orace,jdbc错误,并出现大量红色波浪线Unresolved dependency
    MySQL - UNION 与 UNION ALL
    Debezium系列之:修改源码支持unix_timestamp() as DEFAULT value
    k8s--基础--08--环境搭建--安装kubernetes的web ui界面
    点餐小程序实战教程04-用户注册
    4年工作经验,多线程间的5种通信方式都说不出来,你信吗?
    C++ STL 【stack&queue】模拟实现
    让历史文化“活”起来,北京河图“万象中轴”助力打造北京城市金名片
  • 原文地址:https://blog.csdn.net/qq_37427142/article/details/133915022