引入的小程序

  1. 多个线程访问了同一个资源产生了竞争===>race condition
  2. 产生竞争有可能产生数据不一致(unconsistency)的问题,并发访问下出现不期望出现的结果
  3. 如何保证数据一致性?===>线程同步(线程执行的顺序要安排好)
    1. 具体:保证操作的原子性(Atomicity)
    2. 悲观的认为这个操作会被别的线程打断(悲观锁) synchronized
    3. 乐观的认为这个做不会被别的线程打断(乐观锁) cas操作
      1. CAS = Compare And Set/Swap/Exchange
  4. 为什么不一致?
    1. 因为++操作要将变量从内存读到寄存器中
    2. 但这时又有另一个线程读取这个值
    3. 就是说正在执行的线程被别的线程打断了===>保证正在执行(正在操作数据)的线程不被其他线程所打断
  5. 不能被打断的操作称为原子操作,不能被打断只能称为一个整体,不能被其他线程拉过去一起执行,不能并发执行!


程序代码

  1. 每次执行结果不一样,并且不能得到想要的值一百万
  1. package com.mashibing.juc.c_001_sync_basics;
  2. import java.util.concurrent.CountDownLatch;
  3. import java.util.concurrent.locks.Lock;
  4. import java.util.concurrent.locks.ReentrantLock;
  5. public class T00_00_IPlusPlus {
  6. private static long n = 0L;
  7. public static void main(String[] args) throws Exception {
  8. //Lock lock = new ReentrantLock();
  9. Thread[] threads = new Thread[100];
  10. CountDownLatch latch = new CountDownLatch(threads.length);
  11. for (int i = 0; i < threads.length; i++) {
  12. threads[i] = new Thread(() -> {
  13. for (int j = 0; j < 10000; j++) {
  14. synchronized (T00_00_IPlusPlus.class) {
  15. //lock.lock();
  16. n++;
  17. //lock.unlock();
  18. }
  19. }
  20. latch.countDown();
  21. });
  22. }
  23. for (Thread t : threads) {
  24. t.start();
  25. }
  26. latch.await();
  27. System.out.println(n);
  28. }
  29. }

什么样的语句不是原子性,什么样的语句是原子性

java中哪些语句是原子性的?即打断不了的?

  1. 不管什么语言都要最终翻译成汇编语言、机器语言;即便是汇编语言也不是原子性的,汇编语言执行的任何一条指令,都有可能被其他线程打断
  2. 汇编语言需要查汇编手册
  3. java语言中的八大原子操作:(读jvm规范的那本书!)
    1. lock:主内存,标识变量为线程独占===>系统内存中的内容
    2. unlock:主内存,解锁线程独占变量
    3. read:主内存,读取内存到线程缓存(工作内存)
    4. load:工作内存,read后的值放入线程本地变量副本
    5. use:工作内存,传值给执行引擎===>线程本地的缓存
    6. assign:工作内存,执行引擎结果赋值给线程本地变量
    7. store:工作内存,存值到主内存给write备用
    8. write:主内存,写变量值
  4. 以上java中为虚拟机级别的操作,而不是语句级别的操作
  5. 不用背,只要是正常情况判断不了是不是原子性操作的时候,只要给他上锁就行了(不管三七二十一)

n++为什么不是原子性的?

  1. jvm课中讲过
  2. jmm模型、线程执行的每一条汇编取执行
  3. n++会被编译成好几条汇编指令
  4. 微观上的n++

image.png

  1. 字节码还会翻译,翻译成更多条指令本地的汇编语言,本地汇编还会翻译成机器语言(机器指令)
  2. 不同cpu有不同的汇编语言===>汇编语言中是不是原子操作要去查cpu的汇编手册
  3. 不能确定某一个操作是原子性的时候有需要对数据进行同步时,需要一种机制保证操作是原子性的(机制保证原子性)(原子性的概念)

如何保证原子性操作

  1. 上锁(悲观、乐观)===>CAS也是一种锁
  2. 也有特殊的锁,比如只允许两个线程同时执行这样的也可以
  3. 可以用AQS框架写出自己想要的锁(可以实现2中的需求)
  4. 用synchronized代码块,可以锁new出来的对象,也可以锁类(class)对象
  5. 代码块中的操作被当成一个整体,不可打断

synchronized(T00_00_IPlusPlus.class){ n++; }

  1. 数组也是对象!

上锁的本质

  1. 上锁的机制、上锁的本质、上锁是如何做到的?
  2. 上锁的本质是把并发编程序列化===>并发—->序列化
  3. 并发执行===>一起开始,一起结束
  4. 序列化操作===>串行
  5. 效率一定会变低===>因为要竞争锁
  6. 要保证线程同步,必须要保证上的是同一把锁
  7. 线程同步===>给线程安排前后的顺序,这个结束了,另一个才能开始
  8. 原子操作===>一个线程开始了,必须要等到该线程执行完才可以继续向下
  9. 上的是synchronized锁时,是可以**保证可见性的**===>这块结束了一定会跟主内存去同步,会刷新缓存,主内存中永远是最新的
  10. synchronized是不可以保证有序性,有序性只要但线程中保证数据的最终一致性,与锁没有关系
  11. synchronized代码块中的代码不能保证有序性

锁的细节

  1. 基本概念

基本概念

  1. Monitor(管程、监视器)
    1. 简单说就是我们要上的那把锁Lock
    2. 比如synchronized后面括号中跟着的对象或者类对象
  2. Critical Section
    1. 临界区:当持有这把锁(可以是乐观锁,可以是悲观锁)时要执行的那些代码
    2. 不能够两个线程同时执行的代码,必须顺序按顺序执行
    3. 比如synchronized代码块中大括号{ }括住的部分代码
    4. 如果临界区执行事件(代码执行时间)比较长,那么说锁的粒度比较粗;反之,就是锁的粒度比较细
      1. 不是锁的粒度越粗越好,也不是越细越好,锁的粒度要合适才是最好的(与线程的数量、线程的执行时间、线程所进行的操作有关
    5. 锁定了某段代码===>锁定了某个对象,锁定了某一把锁;只有持有这锁时才能执行这些代码(不管多长,也解决了共享同一变量或者同一代码的问题,变为顺序执行)
  3. race condition
    1. 多个线程访问了同一个资源产生了竞争===>race condition
  4. unconsistency
    1. 产生竞争有可能产生数据不一致(unconsistency)的问题,并发访问下出现不期望出现的结果
  5. Atomicity
    1. 保障操作的原子性(两种方式)
      1. 悲观锁
      2. 乐观锁

悲观锁与乐观锁

悲观锁

  1. 持有悲观态度,不管有没有人和我一起访问,都把这块代码锁上
    1. synchronized是悲观锁

synchronized如何保障可见性===>这也是使用sout语句(其中有synchronized代码块)能够强制刷新缓存的原因!!!

  1. synchronized本身可以保证可见性
  2. 保证可见性与synchronized的lock语句的执行有关
  3. 多线程原来是并发性的,但是加了锁之后就变成了顺序执行(序列化操作)===>加了锁之后必须解开锁之后才能让其他线程继续运行。
  4. 保证了程序执行的数据一致性
  5. 保障可见性这件事实际上unlock本身就能保障的(记住即可)
  6. 原因是在解锁之后,会所有的内存中的状态本地的缓存状态进行刷新、对比、同步===>一定要保证一致性,然后下一个线程才能继续
  7. 深入一点:最底层(?synchronized中)有一个lock语句,lock有内存屏障的作用,前后语句都不能越过他;他**本身就是要做内存同步的**,synchronized可以保证可见性的,一个线程运行完了另一个线程立马可见
  8. synchronized唯一不能保障的就是有序性,synchronized代码块中有多条语句时,可能会改变顺序

image.png

乐观锁(自旋锁、CAS、无锁、轻量级锁)

  1. 上厕所时,大胆上,乐观地认为不会有人来
  2. 无锁在是乐观锁时是一把锁
  3. 乐观地认为线程执行的过程不会被别人所打断
  4. 万一有人来了怎么办
  5. 乐观锁通过CAS操作实现

CAS的概念

  1. CAS = Compare And Set/Swap/Exchange
  2. 乐观锁、自旋锁
  3. 以n++为例
    1. 将n读过来并加上1,此时值被改为1
    2. 在往回写的时候,检查一下是不是还是刚才读到的值0
    3. 如果还是就写进去
    4. 如果不是,例如检查n的时候变成8了,这时就再来一遍
    5. 即把8读出来,对其加1变成9,把9往回写的时候再检查一下n是不是刚才的8,如果是就往回写(说明刚才厕所没人来过)
    6. 加入又有人打断了,再重复上面的过程

image.png

  1. 乐观锁一直在那循环读,看是不是===>是的话就写进去;不是的话就再度再判断

CAS的ABA问题

  1. 存在此0非彼0的问题===>ABA问题
  2. 再往回写的时候依然为0,但是这个0不是刚才看到的0了
  3. 中间被别的线程改成8了,又被另外的线程从8改回0了===>0->8->0
  4. 刚才的n++程序不存在这个问题,但是理论上确实有这种问题
  5. 不在乎时、无所谓时,可以略过不解决这个问题,尤其是对简单数据类型,这个值和原来的值是没有区别的,不解决ABA问题
  6. 但是有些情况必须要解决:
    1. 引用:对该引用所指向的对象(比如其中的字段)进行了更改;这就造成了虽然引用又变回来了,但是引用所指向对象的内容发生了改变
    2. 解决方案:
      1. 加个version版本号,每经过一个操作就将版本号加一(看是哪个版本的问题)
        1. 带时间戳类型、带数字
        2. boolean类型
      2. 通过额外的每次操作都会改变的版本号判断是否变化了
      3. AtomicStampedReference类型(自带版本号?)
  7. java中(Atomic×××类)CAS底层用Unsafe类中提供的原子性操作方法实现

CAS的数据不一致问题

  1. 写回去时,判断+写入是分两步来做的
  2. 在写回去进行判断的时候,假如判断的那一刻值还没有变化,但是判断完接下来就要写入的时候,这个原来的值发生了变化===>这样数据就还会出问题,导致最终结果不是逻辑上想要的结果(数据依然不一致)
  3. CAS原理确实能产生作用,但是由于上面的问题,必须还保证CAS操作本身是原子性的(操作系统中的CAS也是原子性的原语)

如何保证CAS操作的原子性!

  1. cpu在汇编指令级别上支持一条原子指令,即cas指令(cpu底层直接支持)
  2. 但cmpxchgl不是原子的(汇编语言操作手册),读过来再写进去的时候很可能被其他线程修改了
  3. 在cmpxchgl指令前面添加lock指令(简单粗暴):执行后面这条指令的时候上锁,后面这条指令执行结束的时候才解锁

乐观锁与悲观锁的效率比较

  1. CAS不一定效率比悲观锁效率高
  2. 悲观锁会有与之对应的一个队列,队列中存放的是等待着这把锁的线程(等待队列===>系统调用),最重要的是在队列中的线程是不使用cpu资源的(阻塞or挂起)
  3. 而乐观锁不会坐在那里安静地等待,而是在那转圈等待,这些线程依然是活着的,是可以运行的,依然会占用cpu资源===>cpu一边要进行cas的while循环一边要进行线程的切换,消耗cpu资源
  4. 而等待队列中的线程是不占用cpu资源的,状态是parking、waiting或者是blocked阻塞状态,等cpu说轮到你了,才会让他占用cpu资源并且让cpu进行调度
  5. 乐观锁要消耗cpu资源,消耗的资源比悲观锁多一点

什么时候用悲观锁,什么时候用乐观锁

  1. 小明的肠胃不好,拉一次屎的时间很长,等待的人很多(等待的线程特别多),当然用悲观
  2. 临界区执行的时间比较长,锁的粒度比较粗(等的人比较多)
  3. 小明肠胃好,拉一次屎的时间很短,等待的人很少(等待的线程特别少),当然用乐观锁(效果好,效率高)
  4. 量化的概念:自己去做压测,实现两种锁来看看哪种更合适;压测的结果支持哪一种就用哪一种
  5. 4偏面试,而实战中就用synchronized===>因为synchronized现在做了一系列的优化,在他内部既有自旋锁,又有偏向锁,又有重量级锁(自适应锁),进行自适应地升级过程===>效率已经被调教得很不错了
  6. synchronized锁自动完成锁升级!!!

保证n++多线程安全更新还有一种方法Atomic×××类

  1. 是一种CAS的操作(乐观锁)
  2. AtomicInteger中incrementAndGet()方法替代了n++的操作
  3. 不需要再用synchronized额外上锁了===>CAS产生的作用
  4. 原子类型的自增操作是自带原子性的
  5. incrementAndGet()方法调用了unsafe的getAndAddInt()方法
  6. getAndAddInt()方法调用了compareAndSwapInt()方法
  7. 没有用synchronized锁,而用的是CAS乐观锁
  8. 老师笔记中的incrementAndGet()方法可能是因为版本不同而调用流程和方法不一样

image.png

UnSafe类中compareAndSwapInt方法的底层C语言源码

  1. 学c底层,jni注册机制
  2. c底层调用了Atomic类的cmpxchg方法

image.png image.png image.png

  1. 代码中的mp指multi-processors,表示是不是多核的cpu,如果是的话,前面加一条lock汇编指令,后面是cmpxchgl汇编指令===>cpu在底层就支持这样一条汇编指令(原语):cmpxchgl
  2. 如果cpu不是多核的,就不要加lock了;如果是多核的就要加lock(并发维度不同!!!)
    1. 单核的时候,同一时刻必然不会有多余1条的指令执行===>每一时刻只有一条指令,不会出现同时进入if的情况
    2. 多核的时候,同一时刻会有多余1条指令一起执行,这样就会造成可能会同时进入if的情况
    3. 一个线程不可能打断自己,就一个人一个线程没必要上锁,打断不了的
    4. 虽然单核也可以并发,但是这条指令依然不会从中间将自己的腿打折;cpu核发出指令,是不能打断一条指令的;假如两条指令时,会单核并发就有可能会打断了
    5. 并发的维度不一样
    6. 在单核看来,他本身就是原子,是打断不了的;而多核中,它是由很多细小的操作构成的,读过来,写回去,因此其他核会将它打断
    7. ❓单核并发要在一个时间片中执行完一条指令?机器周期、指令周期、……**(计算机组成原理)**
    8. 六核十二**线程视为12个**(不要钻牛角尖)
  3. 加上lock时,一颗cpu执行时会把总线锁住,等这颗cpu执行完了(改完了),才会把总线锁放开;这样其他cpu才能去访问这块内存
  4. lock指令在执行的时候可以是总线锁也可以是缓存锁,视具体情况而定

image.png 硬件:

  1. lock指令在执行后面指令的时候锁定一个北桥信号(不采用锁总线的方式)

image.png

  1. CAS在宏观上是自旋锁、乐观锁,而在底层上仍然是悲观锁(把总线锁住或者把数据所在的缓存行给锁住)===>乐观锁在底层还是有一把锁lock
  2. 缓存行是指一次从内存中读出来的数据量(块、页之类的),读出来放进三级缓存中(所以叫缓存****)