biang崽学java (1)—— 并发安全问题

1.概念

1.进程

一个程序运行的基本单位。包括计算机全部资源 + 程序。【cpu、内存、外设】

2.线程

实现cpu的虚拟化。同一个进程下的线程共享内存,但是每个线程有独立的栈 <=> 看起来好像有一个自己的cpu。【会将cpu的上下文内容储存在栈中】

【之所以出现线程是因为:一个程序可能需要实现不同的功能,或者需要同时处理多个任务,但是又需要这些功能共享内存,如果创建多个进程,开销会很大,而且进程通信的代价高,因此轻便的多线程由民间走进官方视野】

3.并发

是指同时开始,但是每一个时刻做的只有一个,每一个做一个小时间片。【单核多线程就可以】

4.并行

同时运行。在一个时刻会有多个在同时执行。【这个需要多核来实现】

进程中的多个线程就是并发执行的,但是由于他们共享资源,就产生了一些安全问题。

image一个具体场景:

假设内存中有一个变量num = 0;线程AB对其做同样的工作,对num++线程A将A拿出++之后,时间片用尽;开始执行线程B,也取出num++;这时应当发现问题因为线程A操作完之后没有讲数据还回内存,线程B就打断了它,这是线程B看到并操作的num = 0;相当于A、B都执行的是将num 从0 变成1。之后可以判断,A将1写回,B也将1写回内存。最终num = 1。

5.线程安全问题/临界区问题

1)问题产生原因剖析:

线程对资源产生争夺。在争夺过程中被别的线程打断了执行。修改值包含取出——修改——写回3步。取出、修改执行完被打断,还没写回更新就被打断了,这个时候内存的值已经不是最新的了,但是其他线程并不知道。

=>需要保证争夺资源的这部分代码变成原子化,eg:取出——修改——写回一块执行完而不能被其他线程打断。

2)解决线程安全问题的方法

加锁的思想 —— 将操作原子化

     (1)同步代码块 synchronized

     (2)同步方法

     (3)lock变量【c++直接用std::mutex】

纯纯将操作原子化

     (1)CAS操作




2.锁机制

1.实现方法:

同步代码块

synchronized (监视器) {

    被锁起来的代码

}

1.监视器一定是唯一的,可以被所有线程看到的,否则不能实现执行这段代码时只有一个线程在执行不被打断(因为其他线程可能看不到锁)

2.任何一个对象都可以做监听器

同步方法

provide synchronized void 函数名(){

    函数体

}

1.可以在implement Runable接口中重写run实现使用

手动加锁,释放锁

privateLocklock=newReentrantLock();//创建一个lock对象,因为Lock是一个接口 注意new的是ReentrantLock,而不是Lock

……

lock.lock();

被锁起来的代码

lock.unlock()

1.lock效率比较低

2.手动释放锁的时候很有可能会忘记释放建议使用try{ 上锁代码 }finall{ lock.unlock}

try{

    lock.lock();

    ……

}

finally{

    lock.unlock();

}

3.如果忘记释放锁会导致上锁之后部分的代码一直处于单线程状态,别的线程无法执行,效率低下。


2.上锁机制

在之前的文章中有总结过一部分较为详细。接下来我完善一些细节。

之前的总结


首先我们清楚了 synchronized 的实现历程从无锁 <—— 偏向锁 <—— 轻量锁 <—— 重量级锁。以及产生的大致缘由:下面详细解释这几种锁以及此时当一个线程来争夺锁的流程细节。

讲锁之前先要熟悉一个概念 —— CAS操作

CAS:【compare and swap 比较交换】。实现给某个内存原子化换值【之前都是取出——修改——写回,不安全】。

该操作保存:一个内存地址addr,之前保留的这个地址存储的值old_value,要修改成的新值new_value。

操作:比较addr这个地址内存储的值和我之前存的old_value值是否还保持一致,如果一样就把它换成new_value。不一样就会放弃操作(不一样可能是因为有线程争夺到并修改了值)。这下只写new_value是原子的。


重量级锁

我们熟悉的锁操作,当线程争夺资源时,发现资源被其他线程上了锁,就要被阻塞等待,直到当前线程释放锁被唤醒重新争夺锁。

其底层通过监视器锁(monitor) —— OS互斥锁(mutex)实现。

轻量级锁

如果一争夺就要去睡,然后被唤醒这样需要切换到kernel去执行,频繁地切换会消耗大量资源(浪费时间、CPU等),如果是很快就释放了锁岂不是代价太大了,因此提出轻量级锁。当我争夺资源的时候,如果发现被锁了,我先等等看,一定时间后再查看一下是否释放了锁,如果释放了则参与争夺;如果这个等待时间短于我阻塞切换的时间,就赢了。

偏向锁

这样等等,等不到再去阻塞还不够,当只有一个线程反复争夺锁的时候还可以继续优化,反正只有一个线程争夺锁,就不要再加锁机制了,就记录下线程id,下一次来再获取的时候,对比看看是不是还是当 前线程线程 上次拥有的资源,如果是就直接执行,如果不是说明这个资源已经被争夺了。那就立刻升级成为轻量级锁。

无锁

当一个线程没有要争夺的资源时是处于无锁状态的,此时所有线程都可以获取这个资源。

于是当一个进程被执行的时候就会走下面的流程:

1.一个进程上锁会在线程的栈帧里创建lockRecord,在lockRecord里和锁对象的MarkWord里存储线程a的线程id。一个线程执行:CAS操作测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,成功就是当前线程上锁(偏向锁)【可能情况是:当前线程号为null,或者线程号一致】;否则就是另一个线程锁着,检查这个原来持有该对象锁的线程是否依然存活,挂了,则可以将对象变为无锁状态,然后重新偏向新的线程;存活则锁升级到轻量级锁。

2.在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果,完成自旋策略还是发现线程没有释放锁,或者让别的线程占用,则线程试图将轻量级锁升级为重量级锁。

ps:升级为重量级锁有两种情况,

一个是自旋到了一定次数;

另一个是第一个线程上锁,第二个线程自旋,第三个线程来的时候就转化为重量级锁。

3.自旋的线程由用户态切换到内核态进行阻塞,等待之前线程执行完成,内核唤醒等待在这个锁上的线程进行竞争。

有一个大佬博主画了十分详细的流程图,

大佬总结流程图

还有《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》中的简化版流程图:


简化流程图

3.其他锁机制

锁剔除:一定不会发生线程安全问题,其他线程不会操作数据。——不需要锁

锁粗化:对同一个对象反复加锁、释放锁。——将锁的范围扩展到整个操作序列外部。


3.CAS操作

前面大概讲解了CAS操作的实现机制,我们发现通过比较-交换就很好地实现了原子化操作,那不用锁机制,直接使用CAS操作不就可以了吗?

循环CAS操作:每次先取出旧值,然后比较取出的值和当前内存的值是否一致,一致操作;不一致循环检测。

而且通过CAS比较交换可以扩展实现一群原子化操作:

1.自增/自减 :新值变为旧值+1 / 旧值-1 

2.交换:新值变为要交换的值。

3.也可以不相等则……

但是cas真的可以这么完美吗?

cas存在的问题:

1.ABA:CAS根本在于想要查看值是否发生了变化来决定是否执行这次操作。检查变化时直接用比较实现的。可是值仍旧相等并不代表没有发生过变化:当当前线程比较之前,有一个线程将这个值改回去了,然后当前这个线程以为这个值没有修改过就继续执行了后续操作。

解决这问题的方法:是为每一次操作引入一个version号,操作一次++,以此来区别不同操作。

2.循环检测CAS是否可以执行,如果一直不能实现操作就会造成CPU资源开销浪费。

3.一次循环是检测一个共享变量的,如果是多个共享变量就难以支持。【时间拉的太长?】



目前各类语言提供的线程安全的变量:对其增减都是原子化的:

std::atomic【c++】

AtomicInteger【java】


4.volatile关键字

并发编程的3大特性

1)原子性:某些操作必须是一个整体,不可以被打断。

2)可见性:一个线程改变了值要保证其他线程都可以看见(知晓)。

3)有序性:优化进行指令重排,对于单线程是串行,对于多线程就变得无序。


1.volatile原理

volatile实现的就是可见性和有序性。

实现机制

主线程有一个主存,其他线程都将主存中的变量拷贝一份到本地线程,互不干扰,当有一个线程修改volatile修饰的变量时,就会将该值强制刷新到主存,并导致其他线程的缓存无效。当其他线程想要使用该变量时,发现值无效就从主存中重新获取最新变量。


实现的底层原理

volatile作为关键字修饰变量。在这个变量的读写操作前后,加一些屏障控制,可以实现:

当volatile变量被读取前,读取操作执行完毕;这个volatile变量读取操作完成之后才可以进行其它读写操作;

这次写操作执行之前的写操作都被线程看到;当前写操作之后的所有读操作都看到了这次写过程。

在jvm上实现了4种屏障:

LoadLoad屏障: 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障: 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。


硬件层面提供的屏障

sfence:即写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见

lfence:即读屏障(Load Barrier),在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据,以保证读取的是最新的数据。

mfence:即全能屏障(modify/mix Barrier ),兼具sfence和lfence的功能

lock 前缀:lock不是内存屏障,而是一种锁。执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。


volatile的屏障:

LoadLoadBarrier
volatile 读操作
LoadStoreBarrier

StoreStoreBarrier
volatile 写操作
StoreLoadBarrier

源码——jdk8:写操作

在底层源码实现的时候,可以看到首先判断了这个变量的类型是否为volatile,如果是则在赋值操作前后添加屏障,否则就直接赋值。


2.volatile实现了什么

上面已经讲了volatile实现的是可见性和有序性,通过屏障机制严格限制某些指令执行的顺序,并且在变量发生变化之后及时使其他线程本地变量无效,实现了可见性。

可是他并不能保证原子性操作,即获取——修改——写回之间还是可以被打断的,因此无法保证线程安全。

详细分析:

还是两个线程A、B想要实现对主存中的num++,num被volatile修饰。

A、B均保存了当前变量到本地线程,当线程A加载 num到寄存器中,时间片用完被打断,此时还没有写回到本地变量,没有触发写屏障;

线程B 加载 num到寄存器实现++,并将其写回到本地变量,然后触发写屏障,强制刷新到主存;

回到线程A, 这时虽然之前B线程触发了写屏障,但是A线程在此之前已经load了num到寄存器,没有对其的访问了,因此并不会再次从主存中获取最新数据,而是将寄存器的数据++之后写回到变量,也触发写屏障,强制写回。

因此线程仍旧不安全。

大佬文章:

分析锁原理以及变化路径↓

浅谈偏向锁、轻量级锁、重量级锁 - 简书


讲CAS操作 & CAS如何实现线程安全↓

深入理解CAS操作 - 知乎

CAS操作_姑娘加油的博客-CSDN博客_cas操作


volatile底层原理 扒了源码 & 详细分析了volatile为啥还会造成线程不安全↓

volatile底层原理详解 - 知乎

为什么volatile也无法保证线程安全_IT_农厂的博客-CSDN博客_volatile为什么不能保证线程安全

volatile关键字无法保证线程安全的讨论_Simon铭少的博客-CSDN博客_volatile关键字不能保证线程安全


哦哦哦现在应该对于线程安全这个问题有了一定程度的了解了,有很多细节还需要扣扣源码和细节,

例如

原子化关键字操作是否要配合CAS才能实现线程安全?

原子化关键字是如何实现线程安全的?

……

共勉呀~

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

推荐阅读更多精彩内容