JUC

JUC

1、如何解决可见性、有序性、原子性问题

”并发问题产生的三大根源”文中我们已经了解了,并发问题产生的三大根源分别是

1、缓存导致的可见性问题。

2、CPU指令和编译器优化导致的指令重排序问题。

3、CPU切换线程执行任务导致的原子性问题。

可见性和有序性的解决方案

咱们再来回顾下因为多个CPU都有各自的缓存,然后在一定时间内都操作者自己的缓存,最后导致在并发的情况下,每个线程访问着当前CPU的缓存信息,所以由于缓存可见性问题导致程序的BUG。

有序性也是一种可见性问题

如果你看了”并发问题:造成缓存可见性问题的底层原因”这篇文章,我想你也已经知道了,指令重排序问题最终导致的结果其实也还是多个线程之间的缓存不可见,其他线程读取不到最新的缓存数据而造成并发问题,那么其实可以说解决了缓存可见性问题也同时可以解决掉有序性问题。

硬件工程师留给软件工程师的解决方案

其实解决缓存不可见问题最直接的办法就是禁用缓存,但是我们不可能一刀切的禁用所有缓存,因为缓存能极大的提高我们程序的性能,所以我们只能针对不同的场景“有选择性”的禁用缓存。从”并发问题:造成缓存可见性问题的底层原因”文章中我们也知道硬件工程师其实已经做了很多的工作解决了一大部分的缓存可见性问题,因为他们无法预知未知的程序逻辑场景所以一些问题还是遗留给了软件工程师,但是他们给我们提供了一套对应场景的解决方案就是**“内存屏障指令”,我们的软件工程师可以同内存屏障来针对不同场景来选择性的“禁用缓存”。**

JAVA内存模型

由于不同的硬件系统提供给的“内存屏障”指令都可能不一样,所以我们的JAVA语言把不同系统内存屏障指令统一进行了封装,让我们的程序员不需要关心到系统的底层,只需要关心他们的自己的程序逻辑开发,而封装这套解决方案的模型名称就是我们常说的JMM(Java Memory Model) ,JAVA 不仅提供了一些现成的产品来让我们使用还用几个hapens before 规则来告诉我们在哪些地方做了缓存可见性的处理。

Volatile、synchronized、final解决缓存可见性

刚才我们已经知道了JMM已经提供了一套解决缓存可见性问题的产品,它们分别就是volatile、synchronized、final, 而这几个关键字的原理也就是通过内存屏障指令来禁用缓存,当我们对一个变量使用volatile后,那么其实就是告诉我们的程序,在修改变量时强制把最新的数据同步到内存中,读取变量值时强制从内存中读取。而synchronized 也是一样,在线程解锁前,必须把共享变量的值刷新到内存中,而线程加锁前,清空缓存区的共享变量值,这样当读取共享变量时候就必须从内存中读取。

Hapens before规则

**1、代码执行顺序原则:**前面的代码的操作缓存对后面的代码是可见的。

**2、Volatile 变量规则:**一个对于volatile变量的写操作对于后续对这个volatile变量的读操作是可见的。

**3、传递性规则:**如果A操作的缓存对于B是可见的, 而B操作的缓存对于C又是可见的,那么A操作缓存对于C操作是可见的。

**4、锁的规则:**一个线程的解锁操作之前操作的内容对于另外一个线程的加锁是可见的。

**5、线程start规则:**如果线程A启动线程B ,那么在A线程调用B.start()之前操作的缓存内容对于B线程是可见的。

6、线程join规则:如果A线程调用B线程的join方法,那么在A线程调用B.join线程返回后,B线程操作缓存的内容对于A线程是可见的。

原子性问题的解决方案

同样咱们也来回顾下原子性问题存在的根本原因,这个问题是在于因为CPU的切换线程执行指令,导致两个线程都在操作同一个变量时,一个线程使用了另外一个线程的“半成品”数据,从而导致程序BUG的产生。

就像线程A、B 同时运行这一串代码:

Int number=0;

number=number+1;

执行过程就可能如下图,从而导致问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p5E4tASX-1618927638587)(JUC.assets/v2-61a50aab2da597e9a97f19689ab3519d_1440w.jpg)]

能否通过禁止线程切换保证原子性?

那么既然是因为线程切换导致的原子性丢失的问题,那么我们是否也可以通过禁止线程切换从而达到避免原子性丢失的问题呢?答案是不行的, 这种方式只有在单核CPU的情况下行得通,但是多核CPU的话就行不通了。

因为多核CPU的情况下,同一个语句的指令可能同时被多个CPU在不同的线程上执行,你只能禁用当前CPU切换线程执行,而没办法阻止其他CPU执行,回到上面的图,假如线程A 和线程B 是在两个不同的CPU上同时执行的, 当他们同时执行算出各自的结果number+1=1,最后同时把number=1写到内存里面,这也不是我们想要的结果。

使用互斥锁解决原子性问题

所以在多核CPU的情况下无法通过禁止CPU线程切换是无法达到效果的,而我们需要做的是,在同一个时间内永远只有一个线程执行对应的代码,前一个线程执行完后的结果对后面的线程又是可见的,这样最后的结果才符合我们的常识,因为可见性问题我们在上面已经解决了,那么这里我们需要考虑的是如何让线程之间产生“互斥”从而达到在当前线程没有执行完之前,永远不会有下线程执行该代码,JAVA就是通过synchronized达线程互斥的效果的。

当我们对某个共享资源加锁之后,如果线程想要访问共享资源,那么它首先要拿到这个对象的锁,当某一个线程获取到锁时,它便可以访问共享资源, 没有获取到锁的线程只能等待,直到上一个线程执行完毕之后释放锁再进行下轮锁的竞争,因为只有一把锁,所以永远只会有一个线程操作该资源。加了锁之后那么最后执行的流程就如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q9P9L42m-1618927638589)(JUC.assets/v2-c284cb58f16f6e79702e5bd0bc1cf08e_1440w.jpg)]

2、synchronized锁和锁优化

锁的逻辑结构

我们把锁要锁定的资源可以理解为一个临界区,一旦一个线程要访问这个临界区首先就要对这个临界区加锁,当然临界区只允许存在一把锁,如果有人已经在临界区上加了锁时,其他线程就无法对临界区再次加锁,当线程走出临界区时就需要对齐进行解锁。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DxO6l7GX-1618927638591)(JUC.assets/v2-04b586db6e6c61d8922d0a063ae2dc31_1440w.jpg)]

Java锁的实现 synchronized

Java中使用synchronized就可以实现锁的效果,synchronized主要有以下几种实现方式

public class SychronyzedDemo {

 //方式1:锁对象方法
 public synchronized  void demo1(){

    }

 //方式2:锁对象代码块
 public void demo2(){
 //TODO
 synchronized (this){

        }
 //TODO
 }


 //方式3:锁class的方法
 public synchronized  static  void demo3(){

    }

 //方式4:锁class的代码块
 public  void demo4(){
 //TODO
 synchronized (SychronyzedDemo.class){

        }
 //TODO
 }
}

从synchronized的使用中我们并没有发现它做任何的加锁和解锁操作,这个其实是因为编译器隐式的帮我们做了加锁和解锁操作,这样我们就可以放心大胆的加锁,要不然我们自己去做这个操作的话,一不小心忘记解锁了,那就会造成后面的线程迟迟不能加锁,造成死锁了。

锁的对象和范围

**对象锁:**如之前的代码,方式1和方式2锁的都是this 当前对象,只不过两种方式可控制的临界区范围有所不同,方式2可以灵活的控制锁住某一段代码块而不像方式一锁就锁住了整个方法。

**类锁:**方式3和方式4和方式1、方式2不同的是这两种方式锁住的是当前类的class,而锁住class和锁住this的不同之处在于,一个JVM可以创建N个this类型的对象,而永远只会保存一份类的class。

对象锁和类锁区别在于他们锁的范围不同,可以这么简单的理解,以 厕所为例,类锁相当于把锁加到了公共厕所上大门上,而对象锁是把锁加在了公共厕所的某间的门上。

锁是如何存储的?

我们经常说加锁加锁,那么这个锁到底是存在哪里的,那么下面我们现在就来翻开锁的神秘面纱。

首先需要确定的一点是,锁是存在了我们的对象信息里面的,至于是如何存储的那么我们就先了解下对象在内存的结构是怎样的,对象创建后在内存里面的结构包括以下几个部分的信息,对象头、类元信息、对象的实例信息、填充,我们先对每个部分功能做一个简单的了解。

对象头:这里面保存的对象的锁相关信息分代年龄(GC回收的那个年龄)。

类元信息:这保存着指向class信息的地址指针

**对象实例:**这里保存着对象的实例成员变量等信息。

填充: 这个主要是起补齐作用,如:当数据大小不足一个字节的时候会补齐为一个字节。

现在我们已经知道了锁的信息其实是保存在对象头里面了,下面我们以一张图来了解下对象头里面是如何存储锁数据的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rKvPl8Un-1618927638593)(JUC.assets/v2-272a484fd79189f23f6b8d01dfc41754_1440w.jpg)]

我们从图中可以看到对象头里面包括了 锁的状态和锁对应类型的标志位,同时我们也发现锁状态好像分为了无锁状态、轻量级锁、重量级锁、偏向锁好几种类型,那么这几个有什么区别我们下面继续了解。

锁的升级过程

在JDK1.6以前,使用synchronized就只有一种方式即重量级锁,而在JDK1.6以后,引入了偏向锁,轻量级锁,重量级锁,来减少竞争带来的上下文切换。

从上面的图中我们已经知道了锁是存储在对象的对象头信息里面,并且锁还分了好几种类型,那么几种类型的锁分别有什么区别和联系呢,其实从名称上的定义我们也许就能看出一些逻辑,无锁、轻量级锁、重量级锁 这个好像就是一个锁的升级过程,而事实也的确如此。

为了更好的理解锁的升级过程,首先我们要联系到现实情况,然后根据实际情况来说明我们的锁是如何进行升级的,我们程序执行的场景可能是这样的:

场景1:程序不会有锁的竞争。

那么这种情况我们不需要加锁,所以这种情况下对象锁状态为无锁。

场景2:经常只有某一个线程来加锁。

加锁过程:也许获取锁的经常为同一个线程,这种情况下为了避免加锁造成的性能开销,所以并不会加实际意义上的锁,偏向锁的执行流程如下:

1、线程首先检查该对象头的线程ID是否为当前线程;

2、A:如果对象头的线程ID和当前线程ID一致,则直接执行代码;B:如果不是当前线程ID则使用CAS方式替换对象头中的线程ID,如果使用CAS替换不成功则说明有线程正在执行,存在锁的竞争,这时需要撤销偏向锁,升级为轻量级锁

3、如果CAS替换成功,则把对象头的线程ID改为自己的线程ID,然后执行代码。

4、执行代码完成之后释放锁,把对象头的线程ID修改为空

img

场景3:有线程来参与锁的竞争,但是获取锁的冲突时间很短。

当开始有锁的冲突了,那么偏向锁就会升级到轻量级锁;线程获取锁出现冲突时,线程必须做出决定是继续在这里等,还是回家等别人打电话通知,而轻量级锁的路基就是采用继续在这里等的方式,当发现有锁冲突,线程首先会使用自旋的方式循环在这里获取锁,因为使用自旋的方式非常消耗CPU当一定时间内通过自旋的方式无法获取到锁的话,那么锁就开始升级为重量级锁了

场景4:有大量的线程参与锁的竞争,冲突性很高。

我们知道当获取锁冲突多,时间越长的时候,我们的线程肯定无法继续在这里死等了,所以只好先休息,然后等前面获取锁的线程释放了锁之后再开启下一轮的锁竞争,而这种形式就是我们的重量级锁。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Np6AqiBB-1618927638594)(JUC.assets/v2-e0dcc1475a25eafea8dbc2a7d7359788_1440w.jpg)]

**总结:**偏向锁、轻量级锁、重量级锁其实分别是针对几种场景所做的优化机制,当线程获取锁的时候不存储竞争的时候,这时使用的是偏向锁,当线程之前有少量的竞争时,我们采用轻量级锁等一等的方式来获取锁,当锁竞争激烈等的时间太长那就没办法只能使用Monitor 基于操作系统的锁达到效果了。

Synchronyzed 重量级锁monitor 的实现

我们可以通过javac -p xxx.class来查看带有synchronyzed 关键字类的代码编译成的指令

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AhPprTNM-1618927638596)(JUC.assets/v2-5e13e83abff65288424e9ac57e5fa99e_1440w.jpg)]

我们发现编译成指令后每个synchronized修饰的代码块前后都会有加上一个monitorenter 和monitorexit指令, 这其实就对应了我们上面那种加锁逻辑图里的lock 和unlock操作,monitorexit 指令又两次是因为在出现异常的时候我们也需要解锁操作。

我们知道只有锁达到一定竞争情况才会使用重量级锁,而synchronized重量级锁的实现原理就是基于Monitor,monitor封装了一套多线程访问共享变量机制以达到互斥和同步的机,下面我们简单的了解下Monitor实现的流程。

这里我们把sychronized修饰的代码块当做一个临界区,当线程进入临界区内时首先要获得对应的锁,这里我们简把monitorEnter指令理解为获取锁的过程,获取锁成功则可以进入临界区,获取锁 失败时,会把当前线程放到一个等待队列里面去,线程置为wait状态;当前面的线进入临界区执行完之后,再释放锁,我们可以把monitorExit 指令理解为释放锁的过程,锁释放后再从等待队列里面唤醒线程,进行下一轮的锁竞争。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r7XN63r2-1618927638597)(JUC.assets/v2-106eaf19e1cc984d235e2b3182d6ee5a_1440w.jpg)]

https://zhuanlan.zhihu.com/p/92808298

https://www.cnblogs.com/shangxiaofei/p/5567776.html用户态和内核态

https://zhuanlan.zhihu.com/p/86607654 synchronized锁实现机制

面唤醒线程,进行下一轮的锁竞争。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值