并发二-锁
4. 共享模型之管程
4.1 共享带来的问题
-
老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快
-
-
小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用
-
但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有 时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞)
-
-
在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算
-
另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平
-
于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]
-
这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然
-
最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了 一个笔记本(主存),把一些中间结果先记在本上
-
计算流程是这样的
-
-
但是由于分时系统,有一天还是发生了事故
-
小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果
-
老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1…] 不甘心地 到一边待着去了(上下文切换)
-
老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本
-
这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写 入了笔记本
-
-
小南和小女都觉得自己没做错,但笔记本里的结果是 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 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
-
如果是单线程就不会有问题
-
-
如果是多线程
4.1.2 临界区
- 共享变量+读写操作+多线程=临界区
- 一个程序运行多个线程本身没问题
- 问题出在多个线程访问共享资源
- 多个线程度共享资源也没问题
- 在多个线程对共享资源读写操作时发生指令交错(违背原子性)就会出现问题
- 一段代码块内如果存在对共享资源的多线程读写操作,成这段代码块为临界区
4.1.3 静态条件
- 多个线程在临界区内执行,由于代码的执行序列不同而导致的结果无法预测,称之为发生了竞态条件
4.2 synchronized
应用之互斥
-
为了避免临界区的竞态条件发生,有多种手段可以达到目的
- 阻塞式的解决方案:synchronized, Lock
- 非阻塞式的解决方案:原子变量
-
synchronized
- 俗称对象锁 它采用互斥的方式让同一时刻之多只有一个线程能持有**【对象锁】**
- 其他线程再想获取这个锁对象时就会阻塞,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
-
注意:
- 虽然Java中互斥和同步都可以采用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); }}
-
-
可以做这样的类比:
- synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人 进行计算,线程 t1,t2 想象成两个人
- 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
- 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切 换,阻塞住了
- 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦), 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才 能开门进入
- 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥 匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码
-
代码
-
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);}
-
-
-
不可变类线程安全性
-
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位
-
-
概括起来分为对象头、对象体和对齐字节。
-
对象的几个部分的作用:
1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;
2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;
3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;
4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;
5.对齐字是为了减少堆内存的碎片空间(不一定准确)
-
-
以上是Java对象处于5种不同状态时,Mark Word中64个位的表现形式,上面每一行代表对象处于某种状态时的样子。其中各部分的含义如下:
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个Mark Word表示的含义不同。biased_lock和lock一起,表达的锁状态含义如下:
-
-
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位)
-
-
-
mark word和类指针
5.1.3 数组对象(32位)
- mark word 和类指针 和数组长度
5.1.4 Mark Word 结构(32位)
-
-
hashcode(25位): 每个对象都有自己的哈希值
-
age(4位): 分代年龄,用于垃圾回收
-
biased_lock(1位): 偏向锁
-
01(2位):加锁状态 01:表示没有与任何锁对象关联
5.2 Monitor(锁)
-
Monitor 被翻译为监视器或管程(操作系统相关就翻译成管程)
-
每个对象都可以关联一个Monitor对象(操作系统提供的对象,非java对象,我们不可见),如果使用synchronized给对象上锁(重量级锁)后,该对象头的Mark word 中就被设置指向Monitor对象的指针,
-
只有synchronized(重量级锁)才会产生Monitor
-
一个对象共用一个Monitor对象
-
-
Owner
-
刚开始时Owner为Null
-
当Thread2执行**synchronized(obj)**时就会将Monitor的Owner(所有者)指向Thread2,Monitor中只能有一个Owner
-
-
_cxq:
- 竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。
-
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."<init>":()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 <init> #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 <clinit> #26 = Utf8 SourceFile #27 = Utf8 SynchronizedDemo.java #28 = NameAndType #10:#11 // "<init>":()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."<init>":()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."<init>":()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
-
让锁记录中Object reference指向锁对象,并尝试用CAS替换Object的MarkWord,将Mark Word的值存入锁记录 的原来锁地址的位置
-
-
如果CAS替换成功了,将锁地址和MarkWord做交换,对象头中的Mark Word就存储了锁地址记录和状态00,表示该线程给对象加锁了
-
如果CAS失败,有两种情况
- 如果是其它线程已经持有了该 Object 的轻量级锁,那么表示有竞争,首先会进行自旋锁,自旋一定次数后,如果还是失败就进入锁膨胀阶段。
- 如果是自己的线程已经执行了 synchronized 进行加锁,那么再添加一条 Lock Record 作为重入的计数(轻量级锁中通过查询线程中Lock Record(锁记录)的数量来进行计数),就是锁重入
-
当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有锁重入,这时重置锁记录,表示重入计数减一
-
-
当线程退出 synchronized 代码块(解锁)的时候,如果获取的锁记录取值不为 null,那么使用 cas 将 Mark Word 的值恢复给对象
-
成功,则解锁成功
-
失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
-
5.2.4锁膨胀
-
如果在尝试添加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
-
当Thread1进行轻量级加锁时,Thread0已经对该对象加了轻量级锁
-
这时Thread1加轻量级锁失败,进入锁膨胀流程
- 即为Object对象申请Monitor锁,让Object指向重量级锁(Monitor)地址
- 会先进入_cxq竞争队列进行自旋尝试加锁,失败一定次数后会进入EntryList
- 然后自己进入Monitor的EntryList的BLOCKED状态
-
当 Thread-0 退出 synchronized 同步块时,使用 cas 将 Mark Word 的值恢复给对象头时,发现lock已经变成了10,所以会失败,那么会进入重量级锁的解锁过程,即按照 Monitor 的地址找到 Monitor 对象,将 Owner 设置为 null ,唤醒 EntryList 中的 BLOCKED 线程
5.2.5 自旋优化
-
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退 出了同步块,释放了锁),这时当前线程就可以避免阻塞。 自选成功的情况
-
自选失败的情况
-
自旋会占用 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 }}
-
偏向状态
-
对象头
-
-
一个对象的创建过程
- 如果开启了偏向锁(默认是开启的),那么对象刚创建之后,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()
-
由于条件不满足,小南不能继续进行计算
-
但小南如果一直占用着锁,其它人就得一直阻塞,效率太低
-
-
于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开, 其它人可以由老王随机安排进屋
-
直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)
-
-
小南于是可以离开休息室,重新进入竞争锁的队列
-
wait()原理
-
-
锁对象调用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的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
代码示例
-
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<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 { 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 异步模式之生产者/消费者
定义
要点:
- 与前面的保护性暂停中的 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
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁(mutex对象有个等待队列 _cond)
- 线程进入 _cond 条件变量阻塞
- 设置 _counter = 0
2.调用 upark
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 唤醒 _cond 条件变量中的 Thread_0
- Thread_0 恢复运行
- 设置 _counter 为 0
先调用upark再调用park的过程
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
- 设置 _counter 为 0
8. 重新理解线程状态转换
假设有线程 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 死锁-哲学家就餐问题
-
-
有五位哲学家,围坐在圆桌旁。
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
- 吃饭时要用两根筷子吃,桌上共有 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 调度执行,也不能够结束,饥饿的情况不 易演示,在读写锁时会涉及饥饿问题
-
先来看看使用顺序加锁的方式解决之前的死锁问题
-
顺序加锁解决方案
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,所以线程不会陷入阻塞,这是效率提升的因素之一
-
但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
-