线程同步(锁)

Java的线程安全性问题主要关注点有3个:可见性、有序性和原子性

  • voliate 是由于本身语义禁止了指令重排语义
  • synchronized 加重量锁

Java内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题。

0. 线程实现

  1. 内核线程实现
    内核态内进行,需要不断的系统调用(用户态<-->内核态切换)
    轻量级线程与内核线程关系 1:1
  2. 用户线程实现(控制过于复杂基本不再使用)
    用户态内进行,不需要切态
    进程与用户线程关系1:N
  3. 混合实现
    轻量级线程与用户线程关系M:N
  4. Java线程实现
    根据操作系统线程模型决定(JVM最新版本使用 轻量级线程LWT实现)

0.1 Java线程调度

  1. 协同式线程调度(线程自己控制),容易导致一个线程长期block
  2. 抢占式线程调度(系统控制 - Java方式),可以通过线程优先级来控制

0.2 Java线程状态与操作系统线程状态

Java线程状态 关系 操作系统线程状态
New 创建未启动 New
Runable 执行/等待CPU Running/Ready
Waiting 不分配CPU,等待其它线程显示唤醒 .
Timed Waiting 不分配CPU,自动超时/等待其它线程唤醒 .
Blocked 进入同步区,等待阻塞锁 Blocked
Teminaled 终止线程 Teminaled
线程状态转换图.png

0.3 线程安全实现

互斥是因,同步是果。互斥是方法,同步是目的

  1. 互斥同步(synchronized/reentranLock)
  2. 非阻塞同步(CAS)
  3. 无同步(可重入代码,线程本地存储 -> 静态方法一般都是可重入)
  • 变量被多线程访问,使用volatile声明“异变”
  • 变量线程独享,使用ThreadLocal

1. Happens-Before工作内存->主内存交互方式

  1. 需要顺序,但不需要连续,必须成对出现
    1.1 read(主内存读入工作内存) -> load(载入副本)
    1.2 store(工作内存存储到主内存) -> write(写入主内存)
  2. assign(赋值)给工作内存赋值后必须同步回主内存,不允许线程无故同步回主主内存
  3. 初始化一个对象过程 read->load->assign->use->store->write
  4. lock(线程锁定)只能被一个线程操作,但是可以被同一个线程操作多次,同时要执行同样多次的unlock(线程解锁)
  5. unlock/lock 需要成对存在
  6. unlock需要将工作内存同步回主内存(store->write)
  7. lock变量,会清空此变量的工作内存信息(read->load)

2. synchronized(内置锁)- 监控器monitor实现

synchronized代码块在Java会被编译成为monitorenter和monitorexit字节码指令(方法常量池method_info结构体里放入标识ACC_SYNCHRONIZED),需要一个reference类型参数来指明锁定和解锁对象

  1. 有重入锁的特性在执行monitorenter指令时,首先要尝试获取对象的锁->进入monitor,如果这个对象没有被锁定(monitor为空),或者当前线程已经拥有了那个对象的锁(重入monitor),把锁的计数器加1,相应地,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就会被释放
  2. 如果获取对象锁失败,那当前线程就要加入同步队列,阻塞等待,直到对象锁被另外一个线程释放,线程争取到信号量
  3. 静态方法同步方式采用的是 Class Lock,非静态方法同步方式采用的是Object Lock
通过对象引用找到同步对象,然后获取对象上的监视器锁
- 进入sync 块后, 清空工作内存, 从主内存加载数据
- 退出 sync 块前, 将工作内存回写主内存

monitorenter和monitorexit 特点是Java内存模型中的lock/unlock更高层的字节码指令:

  1. 同一对象实例 是可重入的,不会锁自身

  2. sync块前后保证有序性(块内部不保证有序性),因此在 DLC(double-checked locking)sync(Objec.class) { o=new Object();} 时代码块内部存在指令重排,对象初始化分为三步:

    a. 为o分配内空间
    b. 执行Object构造函数初始化对象, 在中开辟空间
    c. 栈空间的值存储堆空间的地址

这时候b和c是可以指令重排,为了保证不被指令重排,应该对o设置为volatile,否则并发的情况下,b还没有执行完就分配了堆地址, 内部数据是错误的.

  1. 可以使用lazy initialization holder解决初始化问题,使用延迟加载,而线程安全使用静态类的加载和初始化保证
private static class ResourceHolder {
  public static Resource resource = new Resource();
}
public static Resource getResource() {
  return ResourceHolder .resource;
}

尽可能的使用synchronized,易于优化, JVM 保证释放锁
不足之处:

  1. 不能中断阻塞, 没有timeout机制,不能停止等待,必须到成功
  2. 不能跨越多个对象
  3. 只能是非公平锁

3. ReentrantLock(显式锁)

优点:

  1. 等待可中断(等待超时)
  2. 公平/非公平锁
  3. 可以绑定多个condition条件
  4. 可以使用非块结构的锁

解决写写写读,保证数据一致性的加锁约束,同时也限制了读读

  • 公平锁: 严格按照队列, 等待前续节点
  • 非公平锁: 使用抢占式, 只要state 变为可用, 抢占到即可(头插法变成head)

state标识状态,AQS在判断状态时,通过用waitStatus>0表示取消状态,而waitStatus<0表示有效状态
采用前驱设置状态为SIGNAL通知下一个节点pred准备好了的方式,此时后继可以park等待唤醒. 插入使用尾插法

3.1 acquire(ReentrantLock使用1个信号量来控制lock/unlock)

  1. 先尝试获acquire取信号量
  2. 获取信号量失败,等待队列使用使用尾插法队列,此Node是一个双向链表插入一个waiter node(使用CAS+loop保证线程安全)
  3. 使线程在等待队列一直自旋到获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false(这个自旋只是等待等待队列中被被标记为SIGNAL同步节点park后被unpark,直到所有等待队列中的节点被唤醒并执行)
  4. 获取资源后中出现中断,再进行自我中断selfInterrupt()补偿
acquire流程.png
acquire流程.png

acquire-release.png

3.2 公平锁/非公平锁

使用队列来实现初始化公平

独占模式:
acquire 使用队列来在不可获取信号量后,入队列等待。信号量为1的互斥区间

共享模式:
acquireShared 信号量为n的的区间,判断是否还有资源,返回-1无资源,否则类似独占模式处理等待队列,在唤醒下一节点的时候有区别独占模式(只有一个资源)只会修改节点,而共享模式有多个资源,在当前节点修改后还有资源,会循环主动唤醒有效的资源waiter节点,直到等待队列中所有的waiter都被唤醒一次,至于唤醒后能不能获取资源由自己去争抢

3.3 Condition -> await

reentrantLock->内置有同步队列condition->内置有等待队列

等待队列(FIFO): 当我们将等待队列中的线程节点加入到同步队列之后,才会唤醒线程。

3.4 等待队列是单向队列、同步队列是双向队列的一些思考

同步队列要设计成双向的,是因为在等待队列中,节点唤醒是接力式的,由前一个节点唤醒它的后一个节点,如果是由next指针获取下一个节点,是有可能获取失败的,因为虚拟队列每添加一个节点,是先用CAS把tail设置为新节点,然后才修改原tail的next指针到新节点的。因此用next向后遍历是不安全的,但是如果在设置新节点为tail前,为新节点设置prev,则可以保证从tail往前遍历是安全的。因此要安全的获取一个节点Node的下一个节点,先要看next是不是null,如果是null,还要从tail往前遍历看看能不能遍历到Node

等待队列单向,等待的线程就是等待者,只负责等待,唤醒的线程就是唤醒者,只负责唤醒,因此每次要执行唤醒操作的时候,直接唤醒同步队列的首节点就行了。等待队列的实现中不需要遍历队列,因此也不需要prev指针

4. volatile

volatile在Java会被编译成0x01a3de24:lock addl $0x0,(%esp) ;...f0830424 00指令, 来指示缓存一致原则

此指令相当于一个内存屏障,在多CPU访问同一块内存屏障(Memory Barrier)的时候,有 总线锁/缓存锁 两种方式来保证只有一个处理器可以修改, 在修改后缓存上的数据后写回内存, 通过缓存一致性原则, 让其他处理器上的数据缓存无效. 强制读取内存来达到 可见性. 而 barrier 的存在禁止了对其进行指令重拍.
不要做 volatile int count=0; count++;操作,这个不保证原子性

  • 保障可见性/有序性
  • 不保障原子性

使用场景:

  1. 写入变量不依赖变量当前值
  2. 不与其他状态变量共同参与
  3. 访问变量不需要加其他锁

读有monitorenter语义
写有monitorexit语义

5. (关卡barrier)CyclicBarrier

CyclicBarrier(集结点)设置集中总数. 当一个线程运行barrier.await()时, 如果集中点没有足够多线程达到, 会等待,直到所有的线程都到达了这个点,所有线程才重新运行
CyclicBarrier重用,使用reset()

6. (闭锁latch)CountDownLatch / FuntureTask

CountDownLatch(计数器)某线程运行到某个点上之后,只是给某个数值减1而已,该线程继续运行
CountDownLatch.await() 可以有多个线程监听, 不可重用,无法重置

FutureTask 多线程处理,在 run() 的过程中,线程使用get(),会一致阻塞到获得run()的结果
闭锁不阻塞线程本身,等待的是事件

7. AtomicXXX 原子变量

CAS (Compareand-Swap) 原子操作
这是由硬件提供原子操作指令实现的

CAS采用自旋等待的方式来解决同步问题,绝大多数的并发都是瞬时的,等待片刻即可
阻塞的,需要挂起线程,稍后需要唤醒进程,大量的中断等待
CAS在非激烈竞争的情况下,开销更小,速度更
CAS在激烈竞争的情况下,和锁的性能类似或者更

java.util.concurrent中实现的原子操作类包括:AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference

Lock-Free算法(乐观锁):Lock-Free 是指能够确保执行它的所有线程中至少有一个能够继续往下执行。由于每个线程不是 starvation-free 的,即有些线程可能会被任意地延迟,然而在每一步都至少有一个线程能够往下执行,因此系统作为一个整体是在持续执行的,可以认为是 system-wide的。所有 Wait-free 的算法都是 Lock-Free 的。
Mutex操作系统实现,而atomic包中的原子操作则由底层硬件直接提供支持。在 CPU 实现的指令集里,有一些指令被封装进了atomic包,这些指令在执行的过程中是不允许中断(interrupt)的,因此原子操作可以在lock-free的情况下保证并发安全,并且它的性能也能做到随 CPU 个数的增多而线性扩展

8. 原子性

一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为原子性(atomicity), CPU 不可能不中断的执行一系列操作,但如果我们在执行多个操作时,能让他们的中间状态对外不可见,那我们就可以宣称他们拥有了”不可分割”的原子性下(我怎么觉得这个更像一致性)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 11,244评论 4 56
  • 1. 死锁的产生条件 计算机系统中同时具备下面四个必要条件时,那么会发生死锁 互斥条件。即某个资源在一段时间内只能...
    酱油和醋阅读 1,514评论 0 4
  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,696评论 0 11
  • 一、多线程 说明下线程的状态 java中的线程一共有 5 种状态。 NEW:这种情况指的是,通过 New 关键字创...
    Java旅行者阅读 4,665评论 0 44
  • 仲夏夜微凉, 台灯映板床。 微风拂面来, 长夜铃铛响 二O一八 七月七日
    太油阅读 190评论 0 1