老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快
小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用
但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有 时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞)
在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算
另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平
于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]
这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然
最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了 一个笔记本(主存),把一些中间结果先记在本上
计算流程是这样的
但是由于分时系统,有一天还是发生了事故
小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果
老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1…] 不甘心地 到一边待着去了(上下文切换)
老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本
这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写 入了笔记本
小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0
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 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
如果是单线程就不会有问题
如果是多线程
为了避免临界区的竞态条件发生,有多种手段可以达到目的
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 实际使用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
不加锁的方法就像是翻窗进入的小偷(隔壁老王,没有钥匙进入你家)。
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){} }}
局部变量如果是基本数据类型则是安全的
如果局部变量是引用类型则未必(要进行逃逸分析)
分析
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执行。并且是共享变量,就会发生线程安全问题
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关键字
他们的每个方法是原子的
但是它们的组合方法不是线程安全的
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);}
他是新建了一个字符串
概括起来分为对象头、对象体和对齐字节。
对象的几个部分的作用:
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
Monitor 被翻译为监视器或管程(操作系统相关就翻译成管程)
每个对象都可以关联一个Monitor对象(操作系统提供的对象,非java对象,我们不可见),如果使用synchronized给对象上锁(重量级锁)后,该对象头的Mark word 中就被设置指向Monitor对象的指针,
只有synchronized(重量级锁)才会产生Monitor
一个对象共用一个Monitor对象
Owner
刚开始时Owner为Null
当Thread2执行**synchronized(obj)**时就会将Monitor的Owner(所有者)指向Thread2,Monitor中只能有一个Owner
_cxq:
EntryList
WaitSet
EntryList跟cxq的区别
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"
轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。
轻量级锁对使用者是透明的,即语法仍然是 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失败,有两种情况
当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有锁重入,这时重置锁记录,表示重入计数减一
当线程退出 synchronized 代码块(解锁)的时候,如果获取的锁记录取值不为 null,那么使用 cas 将 Mark Word 的值恢复给对象
如果在尝试添加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
当Thread1进行轻量级加锁时,Thread0已经对该对象加了轻量级锁
这时Thread1加轻量级锁失败,进入锁膨胀流程
当 Thread-0 退出 synchronized 同步块时,使用 cas 将 Mark Word 的值恢复给对象头时,发现lock已经变成了10,所以会失败,那么会进入重量级锁的解锁过程,即按照 Monitor 的地址找到 Monitor 对象,将 Owner 设置为 null ,唤醒 EntryList 中的 BLOCKED 线程
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退 出了同步块,释放了锁),这时当前线程就可以避免阻塞。 自选成功的情况
自选失败的情况
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势
在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能 性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
JDK1.7之后不能控制是否开启自旋功能
偏向锁撤销会导致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 }}
对象头
一个对象的创建过程
撤销偏向
以下几种情况会使对象的偏向锁失效
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++; } }}
由于条件不满足,小南不能继续进行计算
但小南如果一直占用着锁,其它人就得一直阻塞,效率太低
于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开, 其它人可以由老王随机安排进屋
直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)
小南于是可以离开休息室,重新进入竞争锁的队列
锁对象调用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 都不会分给他们时间片。但是有所区别:
注:只有当对象获得到锁以后,才能调用 wait 和 notify 方法
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);}
当线程不满足某些条件,需要暂停运行时,可以使用 wait 。这样会将对象的锁释放,让其他线程能够继续运行。如果此时使用 sleep,会导致所有线程都进入阻塞,导致所有线程都没法运行,直到当前线程 sleep 结束后,运行完毕,才能得到执行。
notify()会有虚假唤醒问题,因为他是随机唤醒一个wait()线程,如果多个线程都在等待条件满足,那么他唤醒的有可能是另一个线程(唤醒的是一个不满足条件的线程)这就是虚假唤醒
而notifyall()存在的问题就是,如果我将全部线程唤醒了,但是只有一个线程满足了继续运行的条件,那么其他没有满足组条件的线程,就会被白白唤醒,并且不能执行满足条件以后的任务。这是就需要一个while(加上条件)来限制当我被唤醒后,但是来的条件是别的线程的条件,不是我所需要的,那么我就要重新进入wait()等待下一轮唤醒
synchronized (lock) { while(//不满足条件,一直等待,避免虚假唤醒) { lock.wait(); } //满足条件后再运行}synchronized (lock) { //唤醒所有等待线程 lock.notifyAll();}
即 Guarded Suspension(保护性暂停),用在一个线程等待另一个线程的执行结果,要点:
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(); } }
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; } }}
和保护性暂停原理一样。
解耦等待和生产
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(); } }}
// 用来同步的对象 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(); }
首先,需要保证先 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 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』, 不需要『同步对象』和『运行标记』
要点:
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(); }}
执行监控执行监控执行监控
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(); }}
1.先调用 park
2.调用 upark
假设有线程 Thread t
当调用了 t.start() 方法时,由 NEW –> RUNNABLE
互斥,请求与保持,不可剥夺,循环等待
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
如: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(); }
有五位哲学家,围坐在圆桌旁。
筷子类
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)
这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情 况
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如
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(); }}
解决办法
具有以下特点
与synchronized一样,都支持重入锁(有不支持的)。
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
在等待获取锁的过程中,其他线程可以使用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 - 没有获得锁,返回
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 - 获得到锁
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); }}
synchronized也有条件变量,就是waitSet休息室(阻塞队列,等待队列),当条件不满足时进入waitSet等待。
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比 synchronized 是那些不满足条件的线程都在一间休息室等消息
而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤 醒
使用要点:
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 - 等到了它的烟
自旋锁是一种乐观锁,是一种无锁实现,是基于CAS(一种算法,CAS算法)实现的一种乐观锁,叫自旋锁。
悲观锁和乐观锁并不是一种锁实现方式,而是一种锁的思想;
synchronized关键字和LOCK类是悲观锁的具体实现,而CAS自旋锁(自旋锁)是乐观锁的一种实现。而自旋锁实现采用的机制是CAS算法。
悲观锁因为独占资源,所以比较在读多写少的情况下,这样比较影响性能,因为数据都不变嘛,这样的情况下还加了把锁,但是在写多的情况下,这个机制就非常适合。它适合写多读少的场景。
乐观锁因为不加锁,实现同步使用的是操作系统的CAS操作,CAS操作又是一个耗费资源的操作,所以在乐观锁碰到写多的时候就比较糟糕了。它适合读多写少的场景。
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;
}
}
}
}
获取变量时,为了保证该变量的可见性,需要使用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 才能读取到共享变量的最新值来实现【比较并交换】的效果
因为CAS 自旋是CPU指令集层面,不涉及到系统调用和上下文切换。
单核CPU肯定不行,因为别的线程在占用CPU运行你就不可能有CPU来供你自旋。
CAS只是减少了线程上下文切换的次数,并不是避免了线程上下文的切换。只要涉及到多线程,就会有上下文切换问题。
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时 候,发生上下文切换,进入阻塞。
打个比喻 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火, 等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑 道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还 是会导致上下文切换。
当线程数量较少,且线程任务执行时间也较短时,用CAS更好。
当线程数量较多,且线程任务执行时间也较长时,直接用synchronized关键字会更好
结合 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 才能读取到共享变量的最新值来实现【比较并交换】的效果
因为CAS 自旋是CPU指令集层面,不涉及到系统调用和上下文切换。
单核CPU肯定不行,因为别的线程在占用CPU运行你就不可能有CPU来供你自旋。
CAS只是减少了线程上下文切换的次数,并不是避免了线程上下文的切换。只要涉及到多线程,就会有上下文切换问题。
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时 候,发生上下文切换,进入阻塞。
打个比喻 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火, 等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑 道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还 是会导致上下文切换。
当线程数量较少,且线程任务执行时间也较短时,用CAS更好。
当线程数量较多,且线程任务执行时间也较长时,直接用synchronized关键字会更好
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再 重试呗。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想 改,我改完了解开锁,你们才有机会。
CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响