并发二-锁

并发二-锁

4. 共享模型之管程

4.1 共享带来的问题

  • 老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快

  • 并发二-锁_第1张图片

  • 小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用

  • 但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有 时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞)

  • 并发二-锁_第2张图片

  • 在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算

  • 另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平

  • 于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]

  • 这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然

  • 最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了 一个笔记本(主存),把一些中间结果先记在本上

  • 计算流程是这样的

  • 并发二-锁_第3张图片

  • 但是由于分时系统,有一天还是发生了事故

  • 小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果

  • 老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1…] 不甘心地 到一边待着去了(上下文切换)

  • 老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本

  • 这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写 入了笔记本

  • 并发二-锁_第4张图片

  • 小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0

4.1.1 Java的体现
  • package com.sunyang.concurrentstudy;
    
    import lombok.extern.slf4j.Slf4j;
    
    /**
     * @Author: sunyang
     * @Date: 2021/7/30
     * @Description:
     */
    @Slf4j(topic = "c.Demo")
    public class ConcurrentProDemo {
        static int counter = 0;
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                for(int i = 0 ; i < 5000; i++ ){
                    counter++;
                }
            }, "t1");
            Thread t2 = new Thread(() -> {
                for(int i = 0 ; i < 5000; i++ ){
                    counter--;
                }
            }, "t2");
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            log.debug("{}", counter);
        }
    }
    
  • 问题分析

  • 以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理 解,必须从字节码来进行分析

  • 例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

    • getstatic i // 获取静态变量i的值iconst_1 // 准备常量1iadd // 自增putstatic i // 将修改后的值存入静态变量i
      
  • 而对应 i-- 也是类似:

    • getstatic i // 获取静态变量i的值iconst_1 // 准备常量1isub // 自减putstatic i // 将修改后的值存入静态变量i
      
  • 而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

    • 并发二-锁_第5张图片
  • 如果是单线程就不会有问题

  • 并发二-锁_第6张图片

  • 如果是多线程

    • 并发二-锁_第7张图片
    • 并发二-锁_第8张图片
4.1.2 临界区
  • 共享变量+读写操作+多线程=临界区
  • 一个程序运行多个线程本身没问题
  • 问题出在多个线程访问共享资源
    • 多个线程度共享资源也没问题
    • 在多个线程对共享资源读写操作时发生指令交错(违背原子性)就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,成这段代码块为临界区
4.1.3 静态条件
  • 多个线程在临界区内执行,由于代码的执行序列不同而导致的结果无法预测,称之为发生了竞态条件

4.2 synchronized

应用之互斥
  • 为了避免临界区竞态条件发生,有多种手段可以达到目的

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

    • 俗称对象锁 它采用互斥的方式让同一时刻之多只有一个线程能持有**【对象锁】**
    • 其他线程再想获取这个锁对象时就会阻塞,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
  • 注意:

    • 虽然Java中互斥和同步都可以采用synchronized关键字来完成,但他们还是有区别的
      • 互斥是保证临界区的竞态条件不发生,同一时刻只能有一个线程执行临界区代码。
      • 同步是由于线程执行的先后,顺序不同,需要一个线程等待其他线程运行到某个点
  • package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;/** * @Author: sunyang * @Date: 2021/7/30 * @Description: */@Slf4j(topic = "c.Demo")public class ConcurrentProDemo {    static int counter = 0;    static final Object lock = new Object();    public static void main(String[] args) throws InterruptedException {        Thread t1 = new Thread(() -> {            for(int i = 0 ; i < 5000; i++ ){                synchronized (lock) {                    counter++;                }            }        }, "t1");        Thread t2 = new Thread(() -> {            for(int i = 0 ; i < 5000; i++ ){                synchronized (lock) {                    counter--;                }            }        }, "t2");        t1.start();        t2.start();        t1.join();        t2.join();        log.debug("{}", counter);    }}
    
  • 并发二-锁_第9张图片

  • 可以做这样的类比:

    • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人 进行计算,线程 t1,t2 想象成两个人
    • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
    • 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切 换,阻塞住了
    • 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦), 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才 能开门进入
    • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥 匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码
  • 代码

    • 并发二-锁_第10张图片
  • synchronized 实际使用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

  • 不加锁的方法就像是翻窗进入的小偷(隔壁老王,没有钥匙进入你家)。

锁在方法上
  • class Room {    // 加在成员方法(普通方法上)就是在为实例对象加锁,锁的是调用这个方法的实例对象    public synchronized void test() {    }    public void test2() {        synchronized (this){}    }    // 加在竞态方法上,锁住的就是类对象Room.class 因为静态方法在是由类对象调用的,    public synchronized static void test3(){    }    public static void test4() {        synchronized (Room.class){}    }}
    
线程八锁

4.3 变量的线程安全问题分析

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

  • 如果局部变量是引用类型则未必(要进行逃逸分析)

    • 如果该对象没有逃离方法的作用域被访问,他是线程安全的
    • 如果该对象逃离了方法的作用域,则需要考虑线程安全问题(例如:用return 将这个应用对象返回给了其他方法)
  • 分析

    • package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.ArrayList;import java.util.List;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-01 09:14 **/@Slf4j(topic = "c.Demo")public class ThreadUnsafeDemo {    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 +1)).start();        }    }}class ThreadUnsafe {    List<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);}}class Threadsafe {    public 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 Threadsafe {    public void method1 (int loopNumber){        ArrayList<String> list = new ArrayList<>();        for (int i = 0; i <loopNumber; i++){            method2(list);            method3(list);        }    }    public void method2(ArrayList<String> list){list.add("1");}    public void method3(ArrayList<String> list){list.remove(0);}}class ThreadSafeSubClass extends Threadsafe {    @Override    public void method3(ArrayList<String> list) {        new Thread(() -> {            list.remove(0);        });    }}
      
    • 线程不安全,因为局部变量已经逃逸出了该方法的作用域,布置在方法内部,如果在方法内部,在同一个线程中由于为了遵守as-if-serial语义所以method2和method 3不会被指令重排序,但是因为子类重写了method3又起了一个线程,这时就会繁盛指令重排序,子类的method3就由可能先于method2执行。并且是共享变量,就会发生线程安全问题

4.4 常用的线程安全类

  • String

  • Integer

  • StringBuffer

  • Random

  • Vector

  • Hashtable

  • java.until.concurrent包下的类

  • 这里说他们是线程安全的是指,多个线程调用他们同一个实例的某个方法时,是线程安全的,也可以理解为

    • Hashtable = table = new Hashtable();new Thread(() -> {	table.put("key", "value1");}).start();new Thread(() -> {	table.put("key", "value2");}).start();
      
    • public synchronized V put(K key, V value) {} // 因为加了synchronized关键字
      
    • 他们的每个方法是原子的

    • 但是它们的组合方法不是线程安全的

      • Hashtable table = new Hashtable();// 线程1,线程2if( table.get("key") == null) { table.put("key", value);}
        
      • 并发二-锁_第11张图片

不可变类线程安全性
  • String Integer 嗾使不可变类,因为其内部的状态不可以改变,因此他们的方法都是线程安全的(就是只能读,不能写)

  • String的replace和substring方法可以改变值,是怎么保证线程安全的

    • public String substring(int beginIndex, int endIndex) {    if (beginIndex < 0) {        throw new StringIndexOutOfBoundsException(beginIndex);    }    if (endIndex > value.length) {        throw new StringIndexOutOfBoundsException(endIndex);    }    int subLen = endIndex - beginIndex;    if (subLen < 0) {        throw new StringIndexOutOfBoundsException(subLen);    }    return ((beginIndex == 0) && (endIndex == value.length)) ? this            : new String(value, beginIndex, subLen);}
      
    • public String(char value[], int offset, int count) {    if (offset < 0) {        throw new StringIndexOutOfBoundsException(offset);    }    if (count <= 0) {        if (count < 0) {            throw new StringIndexOutOfBoundsException(count);        }        if (offset <= value.length) {            this.value = "".value;            return;        }    }    // Note: offset or count might be near -1>>>1.    if (offset > value.length - count) {        throw new StringIndexOutOfBoundsException(offset + count);    }    this.value = Arrays.copyOfRange(value, offset, offset+count);}
      
    • 他是新建了一个字符串

5. Monitor 概念

5.1 Java对象头

5.1.1 64位
  • 并发二-锁_第12张图片

  • 概括起来分为对象头、对象体和对齐字节。

  • 对象的几个部分的作用:

    1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;

    2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;

    3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;

    4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;

    5.对齐字是为了减少堆内存的碎片空间(不一定准确)

  • 并发二-锁_第13张图片

  • 以上是Java对象处于5种不同状态时,Mark Word中64个位的表现形式,上面每一行代表对象处于某种状态时的样子。其中各部分的含义如下:

    lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。biased_lock和lock一起,表达的锁状态含义如下:

  • 并发二-锁_第14张图片

  • biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。lock和biased_lock共同表示对象处于什么锁状态。

  • age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

  • identity_hashcode:31位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。

  • thread:持有偏向锁的线程ID。

  • epoch:偏向锁的时间戳。

  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。

  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。

  • 详细见博客:https://blog.csdn.net/baidu_35751704/article/details/107334577

5.1.2 普通对象(32位)
  • 并发二-锁_第15张图片

  • 并发二-锁_第16张图片

  • mark word和类指针

5.1.3 数组对象(32位)
  • 并发二-锁_第17张图片
  • mark word 和类指针 和数组长度
5.1.4 Mark Word 结构(32位)
  • 并发二-锁_第18张图片

  • hashcode(25位): 每个对象都有自己的哈希值

  • age(4位): 分代年龄,用于垃圾回收

  • biased_lock(1位): 偏向锁

  • 01(2位):加锁状态 01:表示没有与任何锁对象关联

5.2 Monitor(锁)

  • Monitor 被翻译为监视器或管程(操作系统相关就翻译成管程)

  • 每个对象都可以关联一个Monitor对象(操作系统提供的对象,非java对象,我们不可见),如果使用synchronized给对象上锁(重量级锁)后,该对象头的Mark word 中就被设置指向Monitor对象的指针,

  • 只有synchronized(重量级锁)才会产生Monitor

  • 一个对象共用一个Monitor对象

  • 并发二-锁_第19张图片

  • Owner

    • 刚开始时Owner为Null

    • 当Thread2执行**synchronized(obj)**时就会将Monitor的Owner(所有者)指向Thread2,Monitor中只能有一个Owner

  • _cxq:

    • 竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。
    • 并发二-锁_第20张图片
  • EntryList

    • 当Thread2上锁的过程中,如果其他线程也来执行**synchronized(obj)**就会进入EntryList()阻塞(BLOCKED)状态
    • _cxq队列中有资格成为候选资源的线程会被移动到该队列中
  • WaitSet

    • Thread0 和1是之前获得过锁,但条件不满足然后进入WAITING状态的线程
    • 因为调用wait方法而被阻塞的线程会被放在该队列中。
  • EntryList跟cxq的区别

    • 在cxq中的队列可以继续自旋CAS等待锁,若达到自旋的阈值仍未获取到锁则会调用park方法挂起。而EntryList中的线程都是被挂起的线程。
    • 在EntryList中的线程是不能直接通过自旋获得锁的,也就是不能主动获得锁了,只能等待被唤醒之后才有机会获得锁。
5.2.1 原理之Synchronized
  • package com.sunyang.concurrentstudy;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-01 16:49 **/public class SynchronizedDemo {    static final Object lock = new Object();    static int counter = 0;    public static void main(String[] args) {        synchronized (lock){            counter++;        }    }}
    
  • Classfile /C:/ideaworkspace/ConcurrentStudy/target/classes/com/sunyang/concurrentstudy/SynchronizedDemo.class  Last modified 2021-8-1; size 719 bytes  MD5 checksum 2942c965af5ab11b6f53bc496812369f  Compiled from "SynchronizedDemo.java"public class com.sunyang.concurrentstudy.SynchronizedDemo  minor version: 0  major version: 52  flags: ACC_PUBLIC, ACC_SUPERConstant pool:   #1 = Methodref          #4.#28         // java/lang/Object."":()V   #2 = Fieldref           #5.#29         // com/sunyang/concurrentstudy/SynchronizedDemo.lock:Ljava/lang/Object;   #3 = Fieldref           #5.#30         // com/sunyang/concurrentstudy/SynchronizedDemo.counter:I   #4 = Class              #31            // java/lang/Object   #5 = Class              #32            // com/sunyang/concurrentstudy/SynchronizedDemo   #6 = Utf8               lock   #7 = Utf8               Ljava/lang/Object;   #8 = Utf8               counter   #9 = Utf8               I  #10 = Utf8                 #11 = Utf8               ()V  #12 = Utf8               Code  #13 = Utf8               LineNumberTable  #14 = Utf8               LocalVariableTable  #15 = Utf8               this  #16 = Utf8               Lcom/sunyang/concurrentstudy/SynchronizedDemo;  #17 = Utf8               main  #18 = Utf8               ([Ljava/lang/String;)V  #19 = Utf8               args  #20 = Utf8               [Ljava/lang/String;  #21 = Utf8               StackMapTable  #22 = Class              #20            // "[Ljava/lang/String;"  #23 = Class              #31            // java/lang/Object  #24 = Class              #33            // java/lang/Throwable  #25 = Utf8                 #26 = Utf8               SourceFile  #27 = Utf8               SynchronizedDemo.java  #28 = NameAndType        #10:#11        // "":()V  #29 = NameAndType        #6:#7          // lock:Ljava/lang/Object;  #30 = NameAndType        #8:#9          // counter:I  #31 = Utf8               java/lang/Object  #32 = Utf8               com/sunyang/concurrentstudy/SynchronizedDemo  #33 = Utf8               java/lang/Throwable{  static final java.lang.Object lock;    descriptor: Ljava/lang/Object;    flags: ACC_STATIC, ACC_FINAL  static int counter;    descriptor: I    flags: ACC_STATIC  public com.sunyang.concurrentstudy.SynchronizedDemo();    descriptor: ()V    flags: ACC_PUBLIC    Code:      stack=1, locals=1, args_size=1         0: aload_0         1: invokespecial #1                  // Method java/lang/Object."":()V         4: return      LineNumberTable:        line 9: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature            0       5     0  this   Lcom/sunyang/concurrentstudy/SynchronizedDemo;  public static void main(java.lang.String[]);    descriptor: ([Ljava/lang/String;)V    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=2, locals=3, args_size=1         0: getstatic     #2                  // Field lock:Ljava/lang/Object;         3: dup         4: astore_1         5: monitorenter         6: getstatic     #3                  // Field counter:I         9: iconst_1        10: iadd        11: putstatic     #3                  // Field counter:I        14: aload_1        15: monitorexit        16: goto          24        19: astore_2        20: aload_1        21: monitorexit        22: aload_2        23: athrow        24: return      Exception table:         from    to  target type             6    16    19   any            19    22    19   any      LineNumberTable:        line 14: 0        line 15: 6        line 16: 14        line 17: 24      LocalVariableTable:        Start  Length  Slot  Name   Signature            0      25     0  args   [Ljava/lang/String;      StackMapTable: number_of_entries = 2        frame_type = 255 /* full_frame */          offset_delta = 19          locals = [ class "[Ljava/lang/String;", class java/lang/Object ]          stack = [ class java/lang/Throwable ]        frame_type = 250 /* chop */          offset_delta = 4  static {};    descriptor: ()V    flags: ACC_STATIC    Code:      stack=2, locals=0, args_size=0         0: new           #4                  // class java/lang/Object         3: dup         4: invokespecial #1                  // Method java/lang/Object."":()V         7: putstatic     #2                  // Field lock:Ljava/lang/Object;        10: iconst_0        11: putstatic     #3                  // Field counter:I        14: return      LineNumberTable:        line 10: 0        line 11: 10}SourceFile: "SynchronizedDemo.java"
    
5.2.2 Synchronized小故事(锁升级)
  • 故事角色 老王 - JVM
  • 小南 - 线程
  • 小女 - 线程
  • 房间 - 对象
  • 房间门上 - 防盗锁 - Monitor
  • 房间门上 - 小南书包 - 轻量级锁
  • 房间门上 - 刻上小南大名 - 偏向锁
  • 批量重刻名 - 一个类的偏向锁撤销到达 20 阈值
  • 不能刻名字 - 批量撤销该类对象的偏向锁,设置该类不可偏向
  • 小南要使用房间保证计算不被其它人干扰(原子性),最初,他用的是防盗锁,当上下文切换时,锁住门。这样, 即使他离开了,别人也进不了门,他的工作就是安全的。
  • 但是,很多情况下没人跟他来竞争房间的使用权。小女是要用房间,但使用的时间上是错开的,小南白天用,小女 晚上用。每次上锁太麻烦了,有没有更简单的办法呢?
  • 小南和小女商量了一下,约定不锁门了,而是谁用房间,谁把自己的书包挂在门口,但他们的书包样式都一样,因 此每次进门前得翻翻书包,看课本是谁的,如果是自己的,那么就可以进门,这样省的上锁解锁了。万一书包不是 自己的,那么就在门外等,并通知对方下次用锁门的方式。
  • 后来,小女回老家了,很长一段时间都不会用这个房间。小南每次还是挂书包,翻书包,虽然比锁门省事了,但仍 然觉得麻烦。
  • 于是,小南干脆在门上刻上了自己的名字:【小南专属房间,其它人勿用】,下次来用房间时,只要名字还在,那 么说明没人打扰,还是可以安全地使用房间。如果这期间有其它人要用这个房间,那么由使用者将小南刻的名字擦 掉,升级为挂书包的方式。
  • 同学们都放假回老家了,小南就膨胀了,在 20 个房间刻上了自己的名字,想进哪个进哪个。后来他自己放假回老 家了,这时小女回来了(她也要用这些房间),结果就是得一个个地擦掉小南刻的名字,升级为挂书包的方式。老 王觉得这成本有点高,提出了一种批量重刻名的方法,他让小女不用挂书包了,可以直接在门上刻上自己的名字
  • 后来,刻名的现象越来越频繁,老王受不了了:算了,这些房间都不能刻名了,只能挂书包
5.2.3 轻量级锁
  • 轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。

  • 轻量级锁对使用者是透明的,即语法仍然是 synchronized ,

  • 假设有两个方法同步块,利用同一个对象加锁

  • static final Object obj = new Object();public static void method1() {     synchronized( obj ) {         // 同步块 A         method2();     }}public static void method2() {     synchronized( obj ) {         // 同步块 B     }}
    
  • 每次指向到 synchronized 代码块时,都会在栈帧中创建锁记录(Lock Record)对象,每个线程的栈帧都包括一个锁记录的结构,

  • 锁记录中包括Object reference指向锁对象,和锁地址 + 00

    • 并发二-锁_第21张图片
  • 让锁记录中Object reference指向锁对象,并尝试用CAS替换Object的MarkWord,将Mark Word的值存入锁记录 的原来锁地址的位置

  • 并发二-锁_第22张图片

  • 如果CAS替换成功了,将锁地址和MarkWord做交换,对象头中的Mark Word就存储了锁地址记录和状态00,表示该线程给对象加锁了

    • 并发二-锁_第23张图片
    • 并发二-锁_第24张图片
  • 如果CAS失败,有两种情况

    • 如果是其它线程已经持有了该 Object 的轻量级锁,那么表示有竞争,首先会进行自旋锁,自旋一定次数后,如果还是失败就进入锁膨胀阶段。
    • 如果是自己的线程已经执行了 synchronized 进行加锁,那么再添加一条 Lock Record 作为重入的计数(轻量级锁中通过查询线程中Lock Record(锁记录)的数量来进行计数),就是锁重入
    • 并发二-锁_第25张图片
  • 当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有锁重入,这时重置锁记录,表示重入计数减一

  • 并发二-锁_第26张图片

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

    • 成功,则解锁成功

      • 并发二-锁_第27张图片
      • 并发二-锁_第28张图片
    • 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

      • 并发二-锁_第29张图片
5.2.4锁膨胀
  • 如果在尝试添加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

  • 当Thread1进行轻量级加锁时,Thread0已经对该对象加了轻量级锁

    • 并发二-锁_第30张图片
  • 这时Thread1加轻量级锁失败,进入锁膨胀流程

    • 即为Object对象申请Monitor锁,让Object指向重量级锁(Monitor)地址
    • 会先进入_cxq竞争队列进行自旋尝试加锁,失败一定次数后会进入EntryList
    • 然后自己进入Monitor的EntryList的BLOCKED状态
    • 并发二-锁_第31张图片
    • 并发二-锁_第32张图片
  • 当 Thread-0 退出 synchronized 同步块时,使用 cas 将 Mark Word 的值恢复给对象头时,发现lock已经变成了10,所以会失败,那么会进入重量级锁的解锁过程,即按照 Monitor 的地址找到 Monitor 对象,将 Owner 设置为 null ,唤醒 EntryList 中的 BLOCKED 线程

5.2.5 自旋优化
  • 重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退 出了同步块,释放了锁),这时当前线程就可以避免阻塞。 自选成功的情况

    • 并发二-锁_第33张图片
  • 自选失败的情况

    • 并发二-锁_第34张图片
  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势

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

  • JDK1.7之后不能控制是否开启自旋功能

5.2.6 偏向锁(JDK15已消除)
  • 偏向锁撤销会导致STW,因为为什么频繁的偏向锁撤销会导致STW时间增加呢?阅读偏向锁源码可以知道:偏向锁的撤销需要等待全局安全点(safe point),暂停持有偏向锁的线程,检查持有偏向锁的线程状态。首先遍历当前JVM的所有线程,如果能找到偏向线程,则说明偏向的线程还存活,此时检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁。可以看出撤销偏向锁的时候会导致stop the word。

  • 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作

  • java6 开始引入了偏向锁来进行进一步优化,只有第一次使用 CAS 时将线程 ID设置到对象的MarkWord, 之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有。

  • static final Object obj = new Object();public static void m1() {	synchronized(obj) {		// 同步块 A		m2();	}}public static void m2() {	synchronized(obj) {		// 同步块 B		m3();	}}public static void m3() {	synchronized(obj) {		// 同步块 C	}}
    
  • 并发二-锁_第35张图片

偏向状态
  • 对象头

  • 并发二-锁_第36张图片

  • 一个对象的创建过程

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

  • 以下几种情况会使对象的偏向锁失效

    • 调用对象的 hashCode 方法(因为MarkWord中存储的是线程ID,如果调用hashCode (也是存储在MarkWord)会撤销偏向锁)
    • 多个线程使用该对象(发生锁竞争但是线程之间的时间是错开的 )
    • 调用了 wait/notify 方法(调用wait方法会导致锁膨胀而使用重量级锁)因为这两个方法只有重量级锁有
批量重偏向
  • 如果对象虽然被多个线程访问,但是线程间不存在竞争,这时偏向 t1 的对象仍有机会重新偏向 t2
    • 重偏向会重置Thread ID
  • 当撤销超过20次后(超过阈值),JVM 会觉得是不是偏向错了,这时会在给对象加锁时,重新偏向至加锁线程。
批量撤销
  • 当撤销偏向锁的阈值超过 40 以后,就会将整个类的对象都改为不可偏向的
锁粗化
  • 对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于细分锁的粒度。
锁消除
  • package com.sunyang.concurrentstudy;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-01 22:46 **/public class SuoXiaoChuJITDemo {    static int x = 0;    public  void  teset() {        x++;    }    public void test() {        Object o = new Object();  // o这个对象并不会被别的线程所共享,        synchronized (o){  // 所以这个加锁完全是无意义的,所以就会被JIT即时编译器优化消除            x++;        }    }}
    
5.2.7 wait()
为什么需要wait()
  • 由于条件不满足,小南不能继续进行计算

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

  • 并发二-锁_第37张图片

  • 于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开, 其它人可以由老王随机安排进屋

  • 直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)

  • 并发二-锁_第38张图片

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

  • 并发二-锁_第39张图片

wait()原理
  • 并发二-锁_第40张图片

  • 锁对象调用wait方法(obj.wait),就会使当前线程进入 WaitSet 中,变为 WAITING 状态,如果调用的是wait(10)则是TIMED_WAITING,如果调用的是wait()无参的方法则是WAITING状态

    • package com.sunyang.concurrentstudy;import java.util.concurrent.TimeUnit;/** * @Author: sunyang * @Date: 2021/8/2 * @Description: */public class WaitDemo {    static final Object obj = new Object();    public static void main(String[] args) {       Thread thread = new Thread(() -> {            synchronized (obj){                try {                    System.out.println("33");                    obj.wait(10000);                    System.out.println("11");                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }, "t1");       thread.start();        try {            TimeUnit.SECONDS.sleep(1);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println(thread.getState());    }}
      
    • 33TIMED_WAITING11
      
    • package com.sunyang.concurrentstudy;import java.util.concurrent.TimeUnit;/** * @Author: sunyang * @Date: 2021/8/2 * @Description: */public class WaitDemo {    static final Object obj = new Object();    public static void main(String[] args) {       Thread thread = new Thread(() -> {            synchronized (obj){                try {                    System.out.println("33");                    obj.wait();                    System.out.println("11");                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }, "t1");       thread.start();        try {            TimeUnit.SECONDS.sleep(1);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println(thread.getState());    }}
      
    • 33WAITING
      
  • 处于BLOCKED和 WAITING 状态的线程都为阻塞状态,CPU 都不会分给他们时间片。但是有所区别:

    • BLOCKED 状态的线程是在竞争对象时,发现 Monitor 的 Owner 已经是别的线程了,此时就会进入 EntryList 中,并处于 BLOCKED 状态
    • WAITING 状态的线程是获得了对象的锁,但是自身因为某些原因需要进入WAITING阻塞状态时,锁对象调用了 wait() 方法而进入了 WaitSet 中,处于 WAITING 状态
    • BLOCKED 状态的线程会在锁被释放的时候被唤醒,但是处于 WAITING 状态的线程只有被锁对象调用了 notify 方法(obj.notify/obj.notifyAll),才会被唤醒。
    • 如果是调用wait(10)导致的TIMED_WAITING状态,其他线程是可以获取到锁的,但是sleep(10)导致的TIMED_WAITING状态,其他线程是不可以获取到锁的,以为就是说sleep不会释放锁(抱着锁睡觉)wait()方法会释放锁资源
    • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争
  • 注:只有当对象获得到锁以后,才能调用 wait 和 notify 方法

wait()和sleep()的区别
  • sleep() 是线程Thread类的方法,wait是Object的方法,Object又是所有类的父类,所以所有类都有wait()方法

  • 在调用sleep方法阻塞时是不会释放锁的,而调用wait() 方法导致的阻塞的时候会释放锁,但是他们都是阻塞状态,所以他们都会释放CPU资源。

  • sleep不需要与synchronized一起使用,而wait()需要与synchronized一起使用,(因为只有对象获得锁以后才能调用wait()方法,不然会报异常。)

  • 使用wait一般需要搭配notify或者notifyall来使用,不然会让线程一直等待。

  • wait()的纳秒方法实际上是将毫秒加1,假的。

    • // 源码public final void wait(long timeout, int nanos) throws InterruptedException {    if (timeout < 0) {        throw new IllegalArgumentException("timeout value is negative");    }    if (nanos < 0 || nanos > 999999) {        throw new IllegalArgumentException(                            "nanosecond timeout value out of range");    }    if (nanos > 0) {        timeout++;    }    wait(timeout);}
      
API介绍
  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
5.2.8 notify()和notifyall()
  • 当线程不满足某些条件,需要暂停运行时,可以使用 wait 。这样会将对象的锁释放,让其他线程能够继续运行。如果此时使用 sleep,会导致所有线程都进入阻塞,导致所有线程都没法运行,直到当前线程 sleep 结束后,运行完毕,才能得到执行。

  • notify()会有虚假唤醒问题,因为他是随机唤醒一个wait()线程,如果多个线程都在等待条件满足,那么他唤醒的有可能是另一个线程(唤醒的是一个不满足条件的线程)这就是虚假唤醒

  • 而notifyall()存在的问题就是,如果我将全部线程唤醒了,但是只有一个线程满足了继续运行的条件,那么其他没有满足组条件的线程,就会被白白唤醒,并且不能执行满足条件以后的任务。这是就需要一个while(加上条件)来限制当我被唤醒后,但是来的条件是别的线程的条件,不是我所需要的,那么我就要重新进入wait()等待下一轮唤醒

  • synchronized (lock) {	while(//不满足条件,一直等待,避免虚假唤醒) {		lock.wait();	}	//满足条件后再运行}synchronized (lock) {	//唤醒所有等待线程	lock.notifyAll();}
    

6. 设计模式

6.1 同步模式之保护性暂停

定义

即 Guarded Suspension(保护性暂停),用在一个线程等待另一个线程的执行结果,要点:

  • 有一个结果需要需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject(保护对象)
  • 如果有结果不断地从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK中,join的实现,Future的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式
  • 并发二-锁_第41张图片
代码示例
  • package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;/** * @Author: sunyang * @Date: 2021/8/2 * @Description: */@Slf4j(topic = "c.Demo")public class GuardedObjectDemo {    public static void main(String[] args) {        GuardedObject guardedObject = new GuardedObject();        new Thread(() -> {            log.debug("等待结果");            String str = (String) guardedObject.get();            log.debug("{}", str);        }, "t1").start();        new Thread(() -> {            log.debug("生产结果");            try {                TimeUnit.SECONDS.sleep(1);            } catch (InterruptedException e) {                e.printStackTrace();            }            String str = "aaaa";            guardedObject.complete(str);        }, "t2").start();    }}class GuardedObject {    private Object response;    // 获取结果    public Object get() {        synchronized (this){            // 没有结果进入wait()            while (response == null) {                try {                    this.wait();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }            return response;        }    }    // 产生结果    public void complete(Object response) {        synchronized (this){            this.response = response;            this.notifyAll();        }    }}
    
  • 17:19:23 [t1] c.Demo - 等待结果17:19:23 [t2] c.Demo - 生产结果17:19:24 [t1] c.Demo - aaaa
    
  • join必须等待生产线程结束,而这个方法在生产结果结束后(guardedObject.complete(str);)还可以执行其他操作。且等待结果变量必须是全局的,这个可以是局部的。

超时扩展
  • package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;/** * @Author: sunyang * @Date: 2021/8/2 * @Description: */@Slf4j(topic = "c.Demo")public class GuardedObjectDemo {    public static void main(String[] args) {        GuardedObject guardedObject = new GuardedObject();        new Thread(() -> {            log.debug("等待结果");            String str = (String) guardedObject.get(20);            log.debug("{}", str);        }, "t1").start();        new Thread(() -> {            log.debug("生产结果");            try {                TimeUnit.SECONDS.sleep(1);            } catch (InterruptedException e) {                e.printStackTrace();            }            String str = "aaaa";            guardedObject.complete(str);        }, "t2").start();    }}class GuardedObject {    private Object response;    // 获取结果    // timeout表示最大等多久    public Object get(long timeout) {        synchronized (this) {            // 没有结果进入wait()            long begin = System.currentTimeMillis();            long passedTime = 0;            while (response == null) {                long waitTime = timeout - passedTime;                if (waitTime <= 0) {                    break;                }                try {                    this.wait(waitTime);                } catch (InterruptedException e) {                    e.printStackTrace();                }                passedTime = System.currentTimeMillis() - begin;            }            return response;        }    }    // 产生结果    public void complete(Object response) {        synchronized (this) {            this.response = response;            this.notifyAll();        }    }
    
join原理
  • 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;        }    }}
    
  • 和保护性暂停原理一样。

多任务版 GuardedObject
  • 解耦等待和生产

  • package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.Hashtable;import java.util.Map;import java.util.Set;import java.util.concurrent.TimeUnit;/** * @Author: sunyang * @Date: 2021/8/2 * @Description: */@Slf4j(topic = "c.Demo")public class GuardedObjectDemo {        public static void main(String[] args) throws InterruptedException {            for (int i = 0; i < 3; i++) {                new People().start();            }            TimeUnit.SECONDS.sleep(1);            for (Integer id : Mailboxes.getIds()) {                new Postman(id, "内容" + id).start();            }        }}@Slf4j(topic = "c.Demo")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.Demo")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 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 getIds() {        return boxes.keySet();    }}class GuardedObject {    private Object response;    // 标识Guarded Object    private int id;    public GuardedObject(int id){        this.id = id;    }    public int getId() {        return id;    }    public void setId(int id) {        this.id = id;    }    // 获取结果    // timeout表示最大等多久    public Object get(long timeout) {        synchronized (this) {            // 没有结果进入wait()            long begin = System.currentTimeMillis();            long passedTime = 0;            while (response == null) {                long waitTime = timeout - passedTime;                if (waitTime <= 0) {                    break;                }                try {                    this.wait(waitTime);                } catch (InterruptedException e) {                    e.printStackTrace();                }                passedTime = System.currentTimeMillis() - begin;            }            return response;        }    }    // 产生结果    public void complete(Object response) {        synchronized (this) {            this.response = response;            this.notifyAll();        }    }}
    

6.2 同步模式之顺序控制

6.2.1 固定运行顺序
wait notify 版
  • // 用来同步的对象    static Object obj = new Object();    // t2 运行标记, 代表 t2 是否执行过    static boolean t2runed = false;    public static void main(String[] args) {        Thread t1 = new Thread(() -> {            synchronized (obj) {                // 如果 t2 没有执行过                while (!t2runed) {                    try {                        // t1 先等一会                        obj.wait();                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }            System.out.println(1);        });        Thread t2 = new Thread(() -> {            System.out.println(2);            synchronized (obj) {                // 修改运行标记                t2runed = true;                // 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)                obj.notifyAll();            }        });        t1.start();        t2.start();    }
    
Park Unpark 版
  • 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该 wait

  • 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决 此问题

  • 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个

  • Thread t1 = new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { } // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行 LockSupport.park(); System.out.println("1");});Thread t2 = new Thread(() -> { System.out.println("2"); // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』) LockSupport.unpark(t1);});t1.start();t2.start();
    
  • park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』, 不需要『同步对象』和『运行标记』

6.3 交替输出

6.4 异步模式之生产者/消费者

定义

要点:

  • 并发二-锁_第42张图片
  • 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK 中各种阻塞队列,采用的就是这种模式
  • “异步”的意思就是生产者产生消息之后消息没有被立刻消费,而“同步模式”中,消息在产生之后被立刻消费了。

6.5 终止模式之两阶段终止模式

  • package com.sunyang.concurrentstudy;import java.sql.Time;import java.util.concurrent.TimeUnit;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-05 22:24 **/public class VolatileDemo {    private volatile static boolean stop = false;    private static Thread t1;    public static void main(String[] args) throws InterruptedException {        start();        TimeUnit.SECONDS.sleep(3);        stop();    }    public static void  start() {        t1 = new Thread(() -> {            while (true) {                if (stop) {                    break;                }                try {                    TimeUnit.SECONDS.sleep(1);                    System.out.println("执行监控");                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        t1.start();    }    public static void stop () {        stop = true;        t1.interrupt();    }}
    
  • 执行监控执行监控java.lang.InterruptedException: sleep interrupted	at java.lang.Thread.sleep(Native Method)	at java.lang.Thread.sleep(Thread.java:340)	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)	at com.sunyang.concurrentstudy.VolatileDemo.lambda$start$0(VolatileDemo.java:28)	at java.lang.Thread.run(Thread.java:748)
    
  • package com.sunyang.concurrentstudy;import java.sql.Time;import java.util.concurrent.TimeUnit;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-05 22:24 **/public class VolatileDemo {    private volatile static boolean stop = false;    private static Thread t1;    public static void main(String[] args) throws InterruptedException {        start();        TimeUnit.SECONDS.sleep(3);        stop();    }    public static void  start() {        t1 = new Thread(() -> {            while (true) {                if (stop) {                    break;                }                try {                    TimeUnit.SECONDS.sleep(1);                    System.out.println("执行监控");                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        t1.start();    }    public static void stop () {        stop = true;//        t1.interrupt();    }}
    
  • 执行监控执行监控执行监控
    

6.6 同步模式之Balking(犹豫模式)

  • package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.sql.Time;import java.util.concurrent.TimeUnit;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-05 22:24 **/@Slf4j(topic = "c.Demo")public class VolatileDemo {    public static void main(String[] args) throws InterruptedException {        TwoPhaseInterrupted twoPhaseInterrupted = new TwoPhaseInterrupted();        twoPhaseInterrupted.start();        twoPhaseInterrupted.start();        TimeUnit.SECONDS.sleep(1);        twoPhaseInterrupted.stop();    }}@Slf4j(topic = "c.Demo")class TwoPhaseInterrupted {    private volatile boolean stop = false;    private Thread t1;    private volatile boolean starting = false;    public void start() {        // 锁类是因为防止多例  双重检查锁  单例就锁this        if (!starting) {            synchronized (TwoPhaseInterrupted.class) {                if (starting) {                    return;                }                starting = true;            }        }        t1 = new Thread(() -> {            while (true) {                if (stop) {                    log.debug("停止");                    break;                }                try {                    TimeUnit.SECONDS.sleep(1);                    log.debug("执行监控");                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        t1.start();    }    public void stop() {        stop = true;        t1.interrupt();    }}
    

7. park和unpark

  • 个人理解:疫情期间你想通过某一个管控严密的门岗,你就是线程,
  • 走到门岗时,他需要先检查你有没有通行许可证这时就是park,你需要停住接受通行许可证(counter)检查,
  • (counter)如果_counter为0,那么代表你没有许可证,那么不允许你通过,什么时候有人(unpark)给你送来了许可证(counter=1)你才能通过
    • 如果为0,那么你需要停住(阻塞等待),然后等待门岗去请示上级(LockSupport),获取一个许可通知(unpark)后你才可以通过(也就是继续运行)在申请期间你需要进入阻塞等待一个通过许可通知(unpark),才能继续运行,并且运行之后,你这个许可证就作废了,下次你还需要一个许可证才能通过。
  • 如果为1 则允许你通过,
  • 而提前unpark就是,你贿赂了上级,说这一次我没有通行证,你想办法让我这次通过,走后门,上级(LockSupport)提前给门岗打电话告诉他一会你要从这里通过,但是上级只收了这一次的钱,只能保证你这次通过,下一次不管了,让门岗放行,但是门岗并不认识你,你需要一个标记让他知道是你,那就还可以用_counter做标记,等到门岗检查时,他一看你的counter为1,就知道是和他打过招呼的,就放你过去,但是只能这一次有效。
  • 而多次Unpark也只能生效一次,就是上级怕门岗忘记,多次打电话提醒他,一会有一个人过去,你要放行,但是无论提醒多少次,都是放行你这一回。

原理

  • 每个线程都有自己的一个 Parker 对象(C代码实现的),由三部分组成 _counter, _cond 和 mutex
  • _打个比喻线程就像一个旅人,Parker 就像他随身携带的背包,条件变量 _ cond 就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)
  • 调用 park 就是要看需不需要停下来歇息
    • 如果备用干粮耗尽,那么钻进帐篷歇息
    • 如果备用干粮充足,那么不需停留,继续前进
  • 调用 unpark,就好比令干粮充足
    • 如果这时线程还在帐篷,就唤醒让他继续前进
    • 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
    • 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮

先调用park再调用upark的过程

1.先调用 park

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

并发二-锁_第43张图片

2.调用 upark

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

并发二-锁_第44张图片

先调用upark再调用park的过程

  1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
  2. 当前线程调用 Unsafe.park() 方法
  3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
  4. 设置 _counter 为 0

并发二-锁_第45张图片

8. 重新理解线程状态转换

并发二-锁_第46张图片

假设有线程 Thread t

情况一:NEW –> RUNNABLE

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

情况二:RUNNABLE <–> WAITING

  • 当调用了t 线程用 synchronized(obj) 获取了对象锁后,调用 obj.wait() 方法时,t 线程从 RUNNABLE –> WAITING
  • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时,会在 WaitSet 等待队列中出现锁竞争,非公平竞争
    • WAITING的线程被唤醒后都是直接进入到EntryList就绪队列中排队BLOCKED去竞争锁
    • 竞争锁成功,t 线程从 WAITING –>BLOCKED–> 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 –> –> BLOCKED—> 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

9. 活跃性

9.1 死锁

9.1.1 案例
  • 互斥,请求与保持,不可剥夺,循环等待

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

  • 如: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();	}
      
9.1.2 定位死锁的方法
  • 检测死锁可以使用 jconsole工具;或者使用 jps 定位进程 id,再用 jstack 根据进程 id 定位死锁。
9.1.3 发生死锁的必要条件
  • 互斥条件
    • 在一段时间内,一种资源只能被一个进程所使用
  • 请求和保持条件
    • 进程已经拥有了至少一种资源,同时又去申请其他资源。因为其他资源被别的进程所使用,该进程进入阻塞状态,并且不释放自己已有的资源
  • 不可抢占条件
    • 进程对已获得的资源在未使用完成前不能被强占,只能在进程使用完后自己释放
  • 循环等待条件
    • 发生死锁时,必然存在一个进程——资源的循环链。
9.1.4 死锁-哲学家就餐问题
  • 并发二-锁_第47张图片

  • 有五位哲学家,围坐在圆桌旁。

    • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
    • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
    • 如果筷子被身边的人拿着,自己就得等待
  • 筷子类

    • class Chopstick { String name; public Chopstick(String name) { this.name = name; } @Override public String toString() { return "筷子{" + name + '}'; }}
      
  • 哲学家类

    • class Philosopher extends Thread {    Chopstick left;    Chopstick right;    public Philosopher(String name, Chopstick left, Chopstick right) {        super(name);        this.left = left;        this.right = right;    }    private void eat() {        log.debug("eating...");        Sleeper.sleep(1);    }    @Override    public void run() {        while (true) {            // 获得左手筷子            synchronized (left) {                // 获得右手筷子                synchronized (right) {                    // 吃饭                    eat();                }                // 放下右手筷子            }            // 放下左手筷子        }    }}
      
  • 就餐

    • public static void main(String[] args) {        Chopstick c1 = new Chopstick("1");        Chopstick c2 = new Chopstick("2");        Chopstick c3 = new Chopstick("3");        Chopstick c4 = new Chopstick("4");        Chopstick c5 = new Chopstick("5");        new Philosopher("苏格拉底", c1, c2).start();        new Philosopher("柏拉图", c2, c3).start();        new Philosopher("亚里士多德", c3, c4).start();        new Philosopher("赫拉克利特", c4, c5).start();        new Philosopher("阿基米德", c5, c1).start();    }
      
  • 12:33:15.575 [苏格拉底] c.Philosopher - eating...12:33:15.575 [亚里士多德] c.Philosopher - eating...12:33:16.580 [阿基米德] c.Philosopher - eating...12:33:17.580 [阿基米德] c.Philosopher - eating...// 卡在这里, 不向下运行
    
  • -------------------------------------------------------------------------名称: 阿基米德状态: cn.itcast.Chopstick@1540e19d (筷子1) 上的BLOCKED, 拥有者: 苏格拉底总阻止数: 2, 总等待数: 1堆栈跟踪:cn.itcast.Philosopher.run(TestDinner.java:48) - 已锁定 cn.itcast.Chopstick@6d6f6e28 (筷子5)-------------------------------------------------------------------------名称: 苏格拉底状态: cn.itcast.Chopstick@677327b6 (筷子2) 上的BLOCKED, 拥有者: 柏拉图总阻止数: 2, 总等待数: 1堆栈跟踪:cn.itcast.Philosopher.run(TestDinner.java:48) - 已锁定 cn.itcast.Chopstick@1540e19d (筷子1)-------------------------------------------------------------------------名称: 柏拉图状态: cn.itcast.Chopstick@14ae5a5 (筷子3) 上的BLOCKED, 拥有者: 亚里士多德总阻止数: 2, 总等待数: 0堆栈跟踪:cn.itcast.Philosopher.run(TestDinner.java:48) - 已锁定 cn.itcast.Chopstick@677327b6 (筷子2)-------------------------------------------------------------------------名称: 亚里士多德状态: cn.itcast.Chopstick@7f31245a (筷子4) 上的BLOCKED, 拥有者: 赫拉克利特总阻止数: 1, 总等待数: 1堆栈跟踪:cn.itcast.Philosopher.run(TestDinner.java:48) - 已锁定 cn.itcast.Chopstick@14ae5a5 (筷子3)-------------------------------------------------------------------------名称: 赫拉克利特状态: cn.itcast.Chopstick@6d6f6e28 (筷子5) 上的BLOCKED, 拥有者: 阿基米德总阻止数: 2, 总等待数: 0堆栈跟踪:cn.itcast.Philosopher.run(TestDinner.java:48) - 已锁定 cn.itcast.Chopstick@7f31245a (筷子4)
    
  • 这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情 况

9.2 活锁

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

  • package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-07-28 22:05 **/@Slf4j(topic = "c.Demo")public class Demo {    static int r = 0;    static volatile int count = 10;    static final Object lock = new Object();    public static void main(String[] args) {        new Thread(() -> {            // 期望减到 0 退出循环            while (count > 0) {                sleep(0.2);                count--;                log.debug("count: {}", count);            }        }, "t1").start();        new Thread(() -> {            // 期望超过 20 退出循环            while (count < 20) {                sleep(0.2);                count++;                log.debug("count: {}", count);            }        }, "t2").start();    }}
    
  • 解决办法

    • 让两个线程错开随机时间执行,这样一方可能就执行完了,另一方就不能改变他的结束条件了

9.3 饥饿

  • 把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不 易演示,在读写锁时会涉及饥饿问题

  • 先来看看使用顺序加锁的方式解决之前的死锁问题

    • image-20210803204946609
  • 顺序加锁解决方案

    • 并发二-锁_第48张图片

10. ReentrantLock(可重入锁)

10.1 与synchronized相比

  • 具有以下特点

    • 可被其他线程中断当前锁状态
    • 可以设置超时时间(超时则不去争抢锁,去执行其他逻辑)而synchronized是一只在EntryList阻塞队列等待,不能放弃争抢锁。
    • 设置公平锁,防止线程饥饿
    • 支持多个条件变量(synchronized中的wait set队列,如果条件不满足可以到wait set中等待)而ReentranLock可以有多个条件,那个不满足进入拿个wait set也就是有多个wait set,以为就是可以精准唤醒,避免了之前的虚假唤醒
    • synchronized关键字级别 ReentrantLock 对象级别来保护临界区
  • 与synchronized一样,都支持重入锁(有不支持的)。

10.2 ReentrantLock特性

10.2.1 可重入性示例
  • package com.sunyang.concurrentstudy;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * @program: ConcurrentStudy
     * @description: Demo
     * @author: SunYang
     * @create: 2021-08-03 21:42
     **/
    @Slf4j(topic = "c.Demo")
    public class ReentrantLockDemo {
        public static ReentrantLock lock = new ReentrantLock();
        public static void main(String[] args) {
            lock.lock();
            try {
                log.debug("进入 main");
                m2();
            } finally {
                lock.unlock();
            }
    
        }
    
        public static void m2() {
            lock.lock();
            try {
                log.debug("进入 m2");
                m3();
            } finally {
                lock.unlock();
            }
    
        }
    
        public static void m3() {
            lock.lock();
            try {
                log.debug("进入 m3");
            } finally {
                lock.unlock();
            }
    
        }
    }
    
    // 输出
    // 21:50:37 [main] c.Demo - 进入 main
    // 21:50:37 [main] c.Demo - 进入 m2
    // 21:50:37 [main] c.Demo - 进入 m3
    
10.2.2 可打断性示例
  • 在等待获取锁的过程中,其他线程可以使用interrupt来打断,synchronized和ReentrantLock.lock()都不可以

  • package com.sunyang.concurrentstudy;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * @program: ConcurrentStudy
     * @description: Demo
     * @author: SunYang
     * @create: 2021-08-03 22:09
     **/
    @Slf4j(topic = "c.Demo")
    public class ReentrantLockInterruptDemo {
        public static ReentrantLock lock = new ReentrantLock();
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() -> {
    
                try {
                    log.debug("尝试获取锁");
                    lock.lockInterruptibly();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    log.debug("没有获得锁,返回");
                    return;
                }
    
                try {
                    log.debug("获得到锁");
                } finally {
                    lock.unlock();
                }
            },"t1");
    
            lock.lock();
            thread.start();
    
            TimeUnit.SECONDS.sleep(1);
            log.debug("打断");
            thread.interrupt();
    
        }
    
    }
    
  • 22:15:34 [t1] c.Demo - 尝试获取锁
    22:15:35 [main] c.Demo - 打断
    java.lang.InterruptedException
    	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
    	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
    	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    	at com.sunyang.concurrentstudy.ReentrantLockInterruptDemo.lambda$main$0(ReentrantLockInterruptDemo.java:22)
    	at java.lang.Thread.run(Thread.java:748)
    22:15:35 [t1] c.Demo - 没有获得锁,返回
    
10.2.3 锁超时
  • package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.ReentrantLock;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-03 22:17 **/@Slf4j(topic = "c.Demo")public class ReentrantLockTimeOut {    public static ReentrantLock lock = new ReentrantLock();    public static void main(String[] args) {        Thread thread = new Thread(() -> {            log.debug("尝试获得锁");            try {                if (!lock.tryLock()) {// 无参数 没有等待时间,获取不到锁,立即返回失败,并且不会抛出被打断异常                   log.debug("获取锁失败!");                   return;                }            } catch (InterruptedException e) {                e.printStackTrace();                log.debug("获取锁失败");                return;            }            try {                log.debug("获得到锁");            } finally {                lock.unlock();            }        }, "t1");        lock.lock();        log.debug("main 获得锁");        thread.start();    }}
    
  • 22:32:40 [main] c.Demo - main 获得锁22:32:40 [t1] c.Demo - 尝试获得锁22:32:42 [t1] c.Demo - 获取锁失败!
    
  • package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.ReentrantLock;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-03 22:17 **/@Slf4j(topic = "c.Demo")public class ReentrantLockTimeOut {    public static ReentrantLock lock = new ReentrantLock();    public static void main(String[] args) throws InterruptedException {        Thread thread = new Thread(() -> {            log.debug("尝试获得锁");            try {                if (!lock.tryLock(2, TimeUnit.SECONDS)) {// 无参数 没有等待时间,获取不到锁,立即返回失败,并且不会抛出被打断异常                   log.debug("获取锁失败!");                   return;                }            } catch (InterruptedException e) {                e.printStackTrace();                log.debug("被打断,获取锁失败");                return;            }            try {                log.debug("获得到锁");            } finally {                lock.unlock();            }        }, "t1");        lock.lock();        log.debug("main 获得锁");        thread.start();        TimeUnit.SECONDS.sleep(1);        thread.interrupt();            }}// 22:35:01 [main] c.Demo - main 获得锁// 22:35:01 [t1] c.Demo - 尝试获得锁// 22:35:02 [t1] c.Demo - 被打断,获取锁失败
    
  • package com.sunyang.concurrentstudy;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.ReentrantLock;/** * @program: ConcurrentStudy * @description: Demo * @author: SunYang * @create: 2021-08-03 22:17 **/@Slf4j(topic = "c.Demo")public class ReentrantLockTimeOut {    public static ReentrantLock lock = new ReentrantLock();    public static void main(String[] args) throws InterruptedException {        Thread thread = new Thread(() -> {            log.debug("尝试获得锁");            try {                if (!lock.tryLock(2, TimeUnit.SECONDS)) {// 无参数 没有等待时间,获取不到锁,立即返回失败,并且不会抛出被打断异常                   log.debug("获取锁失败!");                   return;                }            } catch (InterruptedException e) {                e.printStackTrace();                log.debug("被打断,获取锁失败");                return;            }            try {                log.debug("获得到锁");            } finally {                lock.unlock();            }        }, "t1");        lock.lock();        log.debug("main 获得锁");        thread.start();        TimeUnit.SECONDS.sleep(1);        log.debug("mian 释放了锁");        lock.unlock();    }}
    
  • 22:38:19 [main] c.Demo - main 获得锁22:38:19 [t1] c.Demo - 尝试获得锁22:38:20 [main] c.Demo - mian 释放了锁22:38:20 [t1] c.Demo - 获得到锁
    
10.2.4 解决哲学家就餐问题
  • class Chopstick extends ReentrantLock {    String name;    public Chopstick(String name) {        this.name = name;    }    @Override    public String toString() {        return "筷子{" + name + '}';    }}class Philosopher extends Thread {    Chopstick left;    Chopstick right;    public Philosopher(String name, Chopstick left, Chopstick right) {        super(name);        this.left = left;        this.right = right;    }    @Override    public void run() {        while (true) {            // 尝试获得左手筷子            if (left.tryLock()) {                try {                    // 尝试获得右手筷子                    if (right.tryLock()) {                        try {                            eat();                        } finally {                            right.unlock();                        }                    }                } finally {                    left.unlock();                }            }        }    }    private void eat() {        log.debug("eating...");        Sleeper.sleep(1);    }}
    
10.2.5 公平锁
  • (ReetrantLock默认不公平)
  • 先进先出(一般没必要,会降低并发度,实验难以复现)
  • 只是用来解决饥饿问题,那还不如用trylock就可以解决问题,就没必要用公平锁来解决饥饿问题。
10.2.6 条件变量
  • synchronized也有条件变量,就是waitSet休息室(阻塞队列,等待队列),当条件不满足时进入waitSet等待。

  • ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比 synchronized 是那些不满足条件的线程都在一间休息室等消息

  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤 醒

  • 使用要点:

    • await 前需要获得锁
    • await 执行后,会释放锁,进入 conditionObject 等待
    • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
    • 竞争 lock 锁成功后,从 await 后继续执行
  • package com.sunyang.concurrentstudy;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.ReentrantLock;
    
    import static java.lang.Thread.sleep;
    
    /**
     * @program: ConcurrentStudy
     * @description: Demo
     * @author: SunYang
     * @create: 2021-08-04 20:17
     **/
    @Slf4j(topic = "c.Demo")
    public class ReentractLockAwaitDemo {
        static ReentrantLock lock = new ReentrantLock();
        static Condition waitCigaretteQueue = lock.newCondition();
        static Condition waitbreakfastQueue = lock.newCondition();
        static volatile boolean hasCigrette = false;
        static volatile boolean hasBreakfast = false;
    
    
        public static void main(String[] args) throws InterruptedException {
            new Thread(() -> {
                try {
                    lock.lock();
                    while (!hasCigrette) {
                        try {
                            waitCigaretteQueue.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    log.debug("等到了它的烟");
                } finally {
                    lock.unlock();
                }
            }).start();
    
            new Thread(() -> {
                try {
                    lock.lock();
                    while (!hasBreakfast) {
                        try {
                            waitbreakfastQueue.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    log.debug("等到了它的早餐");
                } finally {
                    lock.unlock();
                }
            }).start();
            TimeUnit.SECONDS.sleep(1);
            sendBreakfast();
            TimeUnit.SECONDS.sleep(1);
            sendCigarette();
        }
    
        private static void sendCigarette() {
            lock.lock();
            try {
                log.debug("送烟来了");
                hasCigrette = true;
                waitCigaretteQueue.signal();
            } finally {
                lock.unlock();
            }
        }
    
        private static void sendBreakfast() {
            lock.lock();
            try {
                log.debug("送早餐来了");
                hasBreakfast = true;
                waitbreakfastQueue.signal();
            } finally {
                lock.unlock();
            }
        }
    }
    
  • 20:26:51 [main] c.Demo - 送早餐来了
    20:26:51 [Thread-1] c.Demo - 等到了它的早餐
    20:26:52 [main] c.Demo - 送烟来了
    20:26:52 [Thread-0] c.Demo - 等到了它的烟
    

17. 无锁-乐观锁

  • 自旋锁是一种乐观锁,是一种无锁实现,是基于CAS(一种算法,CAS算法)实现的一种乐观锁,叫自旋锁。

  • 悲观锁和乐观锁并不是一种锁实现方式,而是一种锁的思想

  • synchronized关键字和LOCK类是悲观锁的具体实现,而CAS自旋锁(自旋锁)是乐观锁的一种实现。而自旋锁实现采用的机制是CAS算法。

  • 悲观锁因为独占资源,所以比较在读多写少的情况下,这样比较影响性能,因为数据都不变嘛,这样的情况下还加了把锁,但是在写多的情况下,这个机制就非常适合。它适合写多读少的场景。

  • 乐观锁因为不加锁,实现同步使用的是操作系统的CAS操作,CAS操作又是一个耗费资源的操作,所以在乐观锁碰到写多的时候就比较糟糕了。它适合读多写少的场景。

17.1 代码示例

  • package com.sunyang.concurrentstudy;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @program: ConcurrentStudy
     * @description: Dmeo
     * @author: SunYang
     * @create: 2021-08-07 16:40
     **/
    public interface Account {
    
        // 获取余额
        Integer getBalance();
    
        // 取款
        void withdraw(Integer amount);
    
        /**
         * 方法内会启动1000个线程,每个线程做-10操作
         * 如果初始余额为10000,那么正确的结果应该是0
         * **/
    
        static void demo (Account account) {
            List<Thread> ts = new ArrayList<>();
            long start = System.nanoTime();
            for (int i = 0; i < 1000; i++) {
                ts.add(new Thread(() -> {
                    account.withdraw(10);
                }));
            }
            ts.forEach(Thread::start);
            ts.forEach(t -> {
                try {
                    t.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            long end = System.nanoTime();
            System.out.println(account.getBalance() + "cost: " + (end -start)/1000_000 + "ms");
        }
    
    }
    
  • package com.sunyang.concurrentstudy;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * @program: ConcurrentStudy
     * @description: Demo
     * @author: SunYang
     * @create: 2021-08-07 16:36
     **/
    public class CASDemo {
        public static void main(String[] args) {
            Account account = new AccountCAS(10000);
            Account.demo(account);
        }
    }
    
    class AccountCAS implements Account {
        private AtomicInteger balance;
    
        public AccountCAS(int balance) {
            this.balance = new AtomicInteger(balance);
        }
    
        @Override
        public Integer getBalance() {
            return balance.get();
        }
    
        @Override
        public void withdraw(Integer amount) {
            while(true){
                int pre = balance.get();
                int next = pre - amount;
                // 比较并设置, CAS
                if (balance.compareAndSet(pre, next)) { 
                    break;
                }
            }
        }
    }
    

17.2 CAS与volatile

  • 关键字compareAndSet,简称CAS(Compare And Swap)的说法,他必须是原子操作。
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ipdEpzTs-1631354054444)(C:/Users/Administrator/Desktop/整理/并发8-5(1)].assets/image-20210807173340070.png)
17.2.1 底层实现
  • 因为**【比较-交换】**是两步,并不是一个原子操作,所以需要计算机底层去保证这个操作的原子性。
  • 其实CAS的底层是 lock cmpxchg 指令(X86架构),在单核CPU和多核CPU下都能保证【比较并交换】的原子性
  • 在多核状态下,某个核执行到带lock的指令时,CPU会让总线锁锁住消息总线,当这个核把此指令执行完毕,在开启总线,这个过程中不会被线程的调度机制打断,保证了多个线程对内存操作的准确性,是原子的。
17.2.2 Volatile
  • 获取变量时,为了保证该变量的可见性,需要使用volatile修饰,

    • public class AtomicInteger extends Number implements java.io.Serializable {
          private static final long serialVersionUID = 6214790243416807050L;
      
          // setup to use Unsafe.compareAndSwapInt for updates
          private static final Unsafe unsafe = Unsafe.getUnsafe();
          private static final long valueOffset;
      
          static {
              try {
                  valueOffset = unsafe.objectFieldOffset
                      (AtomicInteger.class.getDeclaredField("value"));
              } catch (Exception ex) { throw new Error(ex); }
          }
      
          private volatile int value;
      
  • 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取 它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。

  • 注意 volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原 子性)

  • CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

17.2.3 效率问题
  • 因为CAS 自旋是CPU指令集层面,不涉及到系统调用和上下文切换。

  • 单核CPU肯定不行,因为别的线程在占用CPU运行你就不可能有CPU来供你自旋。

  • CAS只是减少了线程上下文切换的次数,并不是避免了线程上下文的切换。只要涉及到多线程,就会有上下文切换问题。

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时 候,发生上下文切换,进入阻塞。

  • 打个比喻 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火, 等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大

  • 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑 道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还 是会导致上下文切换。

  • 当线程数量较少,且线程任务执行时间也较短时,用CAS更好。

  • 当线程数量较多,且线程任务执行时间也较长时,直接用synchronized关键字会更好

17.2.4 CAS 的特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再 重试呗。

  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想 改,我改完了解开锁,你们才有机会。

  • CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思

    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一

    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField(“value”));
} catch (Exception ex) { throw new Error(ex); }
}

    private volatile int value;
```
  • 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取 它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。

  • 注意 volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原 子性)

  • CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

17.2.3 效率问题
  • 因为CAS 自旋是CPU指令集层面,不涉及到系统调用和上下文切换。

  • 单核CPU肯定不行,因为别的线程在占用CPU运行你就不可能有CPU来供你自旋。

  • CAS只是减少了线程上下文切换的次数,并不是避免了线程上下文的切换。只要涉及到多线程,就会有上下文切换问题。

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时 候,发生上下文切换,进入阻塞。

  • 打个比喻 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火, 等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大

  • 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑 道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还 是会导致上下文切换。

  • 当线程数量较少,且线程任务执行时间也较短时,用CAS更好。

  • 当线程数量较多,且线程任务执行时间也较长时,直接用synchronized关键字会更好

17.2.4 CAS 的特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再 重试呗。

  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想 改,我改完了解开锁,你们才有机会。

  • CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思

    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一

    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

你可能感兴趣的:(多线程,J.U.C,Java,java,juc,并发编程,并发,锁)