引入的小程序
- 多个线程访问了同一个资源产生了竞争===>race condition
- 产生竞争有可能产生数据不一致(unconsistency)的问题,并发访问下出现不期望出现的结果
- 如何保证数据一致性?===>线程同步(线程执行的顺序要安排好)
- 具体:保证操作的原子性(Atomicity)
- 悲观的认为这个操作会被别的线程打断(悲观锁) synchronized
- 乐观的认为这个做不会被别的线程打断(乐观锁) cas操作
- CAS = Compare And Set/Swap/Exchange
- 为什么不一致?
- 因为++操作要将变量从内存读到寄存器中
- 但这时又有另一个线程读取这个值
- 就是说正在执行的线程被别的线程打断了===>保证正在执行(正在操作数据)的线程不被其他线程所打断
- 不能被打断的操作称为原子操作,不能被打断只能称为一个整体,不能被其他线程拉过去一起执行,不能并发执行!
程序代码
- 每次执行结果不一样,并且不能得到想要的值一百万
package com.mashibing.juc.c_001_sync_basics;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T00_00_IPlusPlus {
private static long n = 0L;
public static void main(String[] args) throws Exception {
//Lock lock = new ReentrantLock();
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length);
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
synchronized (T00_00_IPlusPlus.class) {
//lock.lock();
n++;
//lock.unlock();
}
}
latch.countDown();
});
}
for (Thread t : threads) {
t.start();
}
latch.await();
System.out.println(n);
}
}
什么样的语句不是原子性,什么样的语句是原子性
java中哪些语句是原子性的?即打断不了的?
- 不管什么语言都要最终翻译成汇编语言、机器语言;即便是汇编语言也不是原子性的,汇编语言执行的任何一条指令,都有可能被其他线程打断
- 汇编语言需要查汇编手册
- java语言中的八大原子操作:(读jvm规范的那本书!)
- lock:主内存,标识变量为线程独占===>系统内存中的内容
- unlock:主内存,解锁线程独占变量
- read:主内存,读取内存到线程缓存(工作内存)
- load:工作内存,read后的值放入线程本地变量副本
- use:工作内存,传值给执行引擎===>线程本地的缓存
- assign:工作内存,执行引擎结果赋值给线程本地变量
- store:工作内存,存值到主内存给write备用
- write:主内存,写变量值
- 以上java中为虚拟机级别的操作,而不是语句级别的操作
- 不用背,只要是正常情况判断不了是不是原子性操作的时候,只要给他上锁就行了(不管三七二十一)
n++为什么不是原子性的?
- jvm课中讲过
- jmm模型、线程执行的每一条汇编取执行
- n++会被编译成好几条汇编指令
- 微观上的n++
- 字节码还会翻译,翻译成更多条指令本地的汇编语言,本地汇编还会翻译成机器语言(机器指令)
- 不同cpu有不同的汇编语言===>汇编语言中是不是原子操作要去查cpu的汇编手册
- 不能确定某一个操作是原子性的时候有需要对数据进行同步时,需要一种机制保证操作是原子性的(机制保证原子性)(原子性的概念)
如何保证原子性操作
- 上锁(悲观、乐观)===>CAS也是一种锁
- 也有特殊的锁,比如只允许两个线程同时执行这样的也可以
- 可以用AQS框架写出自己想要的锁(可以实现2中的需求)
- 用synchronized代码块,可以锁new出来的对象,也可以锁类(class)对象
- 代码块中的操作被当成一个整体,不可打断
synchronized(T00_00_IPlusPlus.class){ n++; }
- 数组也是对象!
上锁的本质
- 上锁的机制、上锁的本质、上锁是如何做到的?
- 上锁的本质是把并发编程序列化===>并发—->序列化
- 并发执行===>一起开始,一起结束
- 序列化操作===>串行
- 效率一定会变低===>因为要竞争锁
- 要保证线程同步,必须要保证上的是同一把锁
- 线程同步===>给线程安排前后的顺序,这个结束了,另一个才能开始
- 原子操作===>一个线程开始了,必须要等到该线程执行完才可以继续向下
- 上的是synchronized锁时,是可以**保证可见性的**===>这块结束了一定会跟主内存去同步,会刷新缓存,主内存中永远是最新的
- synchronized是不可以保证有序性,有序性只要但线程中保证数据的最终一致性,与锁没有关系
- synchronized代码块中的代码不能保证有序性
锁的细节
- 基本概念
基本概念
- Monitor(管程、监视器)
- 简单说就是我们要上的那把锁Lock;
- 比如synchronized后面括号中跟着的对象或者类对象
- Critical Section
- 临界区:当持有这把锁(可以是乐观锁,可以是悲观锁)时要执行的那些代码
- 不能够两个线程同时执行的代码,必须顺序按顺序执行
- 比如synchronized代码块中大括号{ }括住的部分代码
- 如果临界区执行事件(代码执行时间)比较长,那么说锁的粒度比较粗;反之,就是锁的粒度比较细
- 不是锁的粒度越粗越好,也不是越细越好,锁的粒度要合适才是最好的(与线程的数量、线程的执行时间、线程所进行的操作有关)
锁定了某段代码===>锁定了某个对象,锁定了某一把锁;只有持有这锁时才能执行这些代码(不管多长,也解决了共享同一变量或者同一代码的问题,变为顺序执行)
- race condition
- 多个线程访问了同一个资源产生了竞争===>race condition
- unconsistency
- 产生竞争有可能产生数据不一致(unconsistency)的问题,并发访问下出现不期望出现的结果
- Atomicity
- 保障操作的原子性(两种方式)
- 悲观锁
- 乐观锁
- 保障操作的原子性(两种方式)
悲观锁与乐观锁
悲观锁
- 持有悲观态度,不管有没有人和我一起访问,都把这块代码锁上
- synchronized是悲观锁
synchronized如何保障可见性===>这也是使用sout语句(其中有synchronized代码块)能够强制刷新缓存的原因!!!
- synchronized本身可以保证可见性
- 保证可见性与synchronized的lock语句的执行有关
- 多线程原来是并发性的,但是加了锁之后就变成了顺序执行(序列化操作)===>加了锁之后必须解开锁之后才能让其他线程继续运行。
- 保证了程序执行的数据一致性
- 保障可见性这件事实际上unlock本身就能保障的(记住即可)
- 原因是在解锁之后,会所有的内存中的状态跟本地的缓存状态进行刷新、对比、同步===>一定要保证一致性,然后下一个线程才能继续
- 深入一点:最底层(?synchronized中)有一个lock语句,lock有内存屏障的作用,前后语句都不能越过他;他**本身就是要做内存同步的**,synchronized可以保证可见性的,一个线程运行完了另一个线程立马可见
- synchronized唯一不能保障的就是有序性,synchronized代码块中有多条语句时,可能会改变顺序
乐观锁(自旋锁、CAS、无锁、轻量级锁)
- 上厕所时,大胆上,乐观地认为不会有人来
- 无锁在是乐观锁时是一把锁
- 乐观地认为线程执行的过程不会被别人所打断
- 万一有人来了怎么办
- 乐观锁通过CAS操作实现
CAS的概念
- CAS = Compare And Set/Swap/Exchange
- 乐观锁、自旋锁
- 以n++为例
- 将n读过来并加上1,此时值被改为1
- 在往回写的时候,检查一下是不是还是刚才读到的值0
- 如果还是就写进去
- 如果不是,例如检查n的时候变成8了,这时就再来一遍
- 即把8读出来,对其加1变成9,把9往回写的时候再检查一下n是不是刚才的8,如果是就往回写(说明刚才厕所没人来过)
- 加入又有人打断了,再重复上面的过程
- 乐观锁一直在那循环读,看是不是===>是的话就写进去;不是的话就再度再判断
CAS的ABA问题
- 存在此0非彼0的问题===>ABA问题
- 再往回写的时候依然为0,但是这个0不是刚才看到的0了
- 中间被别的线程改成8了,又被另外的线程从8改回0了===>0->8->0
- 刚才的n++程序不存在这个问题,但是理论上确实有这种问题
- 不在乎时、无所谓时,可以略过不解决这个问题,尤其是对简单数据类型,这个值和原来的值是没有区别的,不解决ABA问题
- 但是有些情况必须要解决:
- 引用:对该引用所指向的对象(比如其中的字段)进行了更改;这就造成了虽然引用又变回来了,但是引用所指向对象的内容发生了改变
- 解决方案:
- 加个version版本号,每经过一个操作就将版本号加一(看是哪个版本的问题)
- 带时间戳类型、带数字
- boolean类型
- 通过额外的每次操作都会改变的版本号判断是否变化了
- AtomicStampedReference类型(自带版本号?)
- java中(Atomic×××类)CAS底层用Unsafe类中提供的原子性操作方法实现
CAS的数据不一致问题
- 写回去时,判断+写入是分两步来做的
- 在写回去进行判断的时候,假如判断的那一刻值还没有变化,但是判断完接下来就要写入的时候,这个原来的值发生了变化===>这样数据就还会出问题,导致最终结果不是逻辑上想要的结果(数据依然不一致)
- CAS原理确实能产生作用,但是由于上面的问题,必须还保证CAS操作本身是原子性的(操作系统中的CAS也是原子性的原语)
如何保证CAS操作的原子性!
- cpu在汇编指令级别上支持一条原子指令,即cas指令(cpu底层直接支持)
- 但cmpxchgl不是原子的(汇编语言操作手册),读过来再写进去的时候很可能被其他线程修改了
- 在cmpxchgl指令前面添加lock指令(简单粗暴):执行后面这条指令的时候上锁,后面这条指令执行结束的时候才解锁
乐观锁与悲观锁的效率比较
- CAS不一定效率比悲观锁效率高
- 悲观锁会有与之对应的一个队列,队列中存放的是等待着这把锁的线程(等待队列===>系统调用),最重要的是在队列中的线程是不使用cpu资源的(阻塞or挂起)
- 而乐观锁不会坐在那里安静地等待,而是在那转圈等待,这些线程依然是活着的,是可以运行的,依然会占用cpu资源===>cpu一边要进行cas的while循环一边要进行线程的切换,消耗cpu资源
- 而等待队列中的线程是不占用cpu资源的,状态是parking、waiting或者是blocked阻塞状态,等cpu说轮到你了,才会让他占用cpu资源并且让cpu进行调度
- 乐观锁要消耗cpu资源,消耗的资源比悲观锁多一点
什么时候用悲观锁,什么时候用乐观锁
- 小明的肠胃不好,拉一次屎的时间很长,等待的人很多(等待的线程特别多),当然用悲观
- 临界区执行的时间比较长,锁的粒度比较粗(等的人比较多)
- 小明肠胃好,拉一次屎的时间很短,等待的人很少(等待的线程特别少),当然用乐观锁(效果好,效率高)
- 量化的概念:自己去做压测,实现两种锁来看看哪种更合适;压测的结果支持哪一种就用哪一种
- 4偏面试,而实战中就用synchronized===>因为synchronized现在做了一系列的优化,在他内部既有自旋锁,又有偏向锁,又有重量级锁(自适应锁),进行自适应地升级过程===>效率已经被调教得很不错了
- synchronized锁自动完成锁升级!!!
保证n++多线程安全更新还有一种方法Atomic×××类
- 是一种CAS的操作(乐观锁)
- AtomicInteger中incrementAndGet()方法替代了n++的操作
- 不需要再用synchronized额外上锁了===>CAS产生的作用
- 原子类型的自增操作是自带原子性的
- incrementAndGet()方法调用了unsafe的getAndAddInt()方法
- getAndAddInt()方法调用了compareAndSwapInt()方法
- 没有用synchronized锁,而用的是CAS乐观锁
- 老师笔记中的incrementAndGet()方法可能是因为版本不同而调用流程和方法不一样
UnSafe类中compareAndSwapInt方法的底层C语言源码
- 学c底层,jni注册机制
- c底层调用了Atomic类的cmpxchg方法
- 代码中的mp指multi-processors,表示是不是多核的cpu,如果是的话,前面加一条lock汇编指令,后面是cmpxchgl汇编指令===>cpu在底层就支持这样一条汇编指令(原语):cmpxchgl
- 如果cpu不是多核的,就不要加lock了;如果是多核的就要加lock(并发维度不同!!!)
- 单核的时候,同一时刻必然不会有多余1条的指令执行===>每一时刻只有一条指令,不会出现同时进入if的情况
- 多核的时候,同一时刻会有多余1条指令一起执行,这样就会造成可能会同时进入if的情况
- 一个线程不可能打断自己,就一个人一个线程没必要上锁,打断不了的
- 虽然单核也可以并发,但是这条指令依然不会从中间将自己的腿打折;cpu核发出指令,是不能打断一条指令的;假如两条指令时,会单核并发就有可能会打断了
- 并发的维度不一样
- 在单核看来,他本身就是原子,是打断不了的;而多核中,它是由很多细小的操作构成的,读过来,写回去,因此其他核会将它打断
- ❓单核并发要在一个时间片中执行完一条指令?机器周期、指令周期、……**(计算机组成原理)**
- 六核十二**线程视为12个核**(不要钻牛角尖)
- 加上lock时,一颗cpu执行时会把总线锁住,等这颗cpu执行完了(改完了),才会把总线锁放开;这样其他cpu才能去访问这块内存
- lock指令在执行的时候可以是总线锁也可以是缓存锁,视具体情况而定
硬件:
- lock指令在执行后面指令的时候锁定一个北桥信号(不采用锁总线的方式)
- CAS在宏观上是自旋锁、乐观锁,而在底层上仍然是悲观锁(把总线锁住或者把数据所在的缓存行给锁住)===>乐观锁在底层还是有一把锁lock
- 缓存行是指一次从内存中读出来的数据量(块、页之类的),读出来放进三级缓存中(所以叫缓存**行**)