Java并发编程艺术学习笔记(一)

Java并发编程艺术学习笔记(一)

学习了JVM后,在最后引出了内存模型以及并发编程,所以希望接着通过学习《Java并发编程艺术》以及研究jdk8中的源码来搞清楚Java并发的奥秘。这个系列主要会总结下Java并发编程艺术这本书以及粘贴些源码来加深理解。

一.并发编程的挑战

并发编程的好处很多,但是同样的面临着挑战也很多,例如上下文切换、死锁以及软硬件限制等等。
1.上下文切换
单核处理器也可以实现多线程,通过分配时间片的方式,由于时间片十分小,CPU需要不断地切换线程,这里会产生许多上下文切换的损失。因此百万次以下的计算可能会存在并发比串行效率更低的情况,就是因为上下文切换的损失。
减少上下文切换的方法:
1️⃣无锁并发编程。例如可以将数据的ID通过Hash算法取模分段,不同的线程处理不同的区域(concurrentHashMap就是用的这种方式)。
2️⃣CAS算法。
3️⃣使用最少线程。任务少就不需要很多线程。
4️⃣协程。单线程中维持多个任务间的切换。
2.死锁
锁非常好用,但是可能会造成死锁,例如线程t1和t2都等着对方释放锁就会造成死锁。
避免死锁的几个常用方法:
1️⃣避免一个线程同时获得多个锁。
2️⃣避免一个线程在锁内同时占用多个资源。
3️⃣尝试使用定时锁来替代内部锁。
4️⃣对于数据库锁,加锁和解锁必须在一个数据库连接中。
3.资源限制

二.Java并发机制的底层实现原理

JAVA代码编译后会形成字节码,再加载进JVM中运行,最后转成汇编语言在CPU中运行。因此Java的并发依赖JVM的实现和CPU的指令。
1.volatile的应用
volatile是轻量级的synchronized,执行成本更低,不会引起线程上下文的切换和调度,主要可以保证变量的“可见性”。接下来主要分析Intel处理器如何处理volatile变量。
1️⃣与实现有关的几种CPU术语
①内存屏障:用来实现对内存操作的顺序限制。
②缓冲行:CPU高速缓冲中可以分配的最小储存单位。填写缓存行时需要执行几百次缓冲行。
③原子操作:不可中断的一个或者一系列的操作。
④缓存行填充:当识别到内存中读取操作数是可缓存的,处理器读取整个高速缓存行到适当的缓存。
⑤缓存命中:如果进行缓冲行填充操作的内存位置是下次处理器访问的地址,处理器将从缓存中读取操作数,而不是从内存中读取。
⑥写命中。
⑦写缺失。
2️⃣volatile如何保证可见性:
带有volatile的变量编译后会多出lock开头的汇编代码,Lock前缀的指令在多核处理器会引发两件事情:
①将当前处理器缓存行的数据写回系统内存:
在多处理器的环境中,LOCK#信号确保了在这个期间处理器可以独占任何共享内存。但是在最近的处理器中,一般不锁总线,因为总线锁定的消耗较大,现在通过“内存锁定”,锁定这块内存区域并且写回内存。
②使CPU中其他缓存了该内存地址的数据无效:
现在使用的是嗅探技术,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效。
3️⃣volatile的优化
JDK7中新增了一个队列集合类LinkedTransferQueue,这个集合中通过追加字节的方式来优化了集合中的volatile变量。
将共享变量追加到64字节,由于现在很多处理器都是64字节的,意味着处理器的高速缓存行也是64字节的,如果不追加字节很可能把head以及tail缓存在一个高速缓存行中,这样volatile进行内存锁定时可能将head和tail一起锁定,而我们往往只需要锁定head或者tail,另一个可以被其他线程所使用。因此追加到64字节保证了head和tail肯定是在不同的缓存行中,修改起来不会相互锁定。
在两种情况下可以不将volatile变量追加到64字节:
①缓存行不是64字节宽。
②共享变量不会被频繁地写。

2.synchronized的实现原理与应用
synchronized一般被称作重量级锁,但是SE1.6之后对于synchronized进行优化后,synchronized已经变得不那么的重。
Java的每一个对象都可以作为锁,具体可以体现为三方面:
①对于普通同步方法,锁是当前实例对象。
②对于静态同步方法,锁是当前类的Class对象。
③对于同步方法块,锁是synchronized中配置的对象。
synchronized在JVM中实现的方式,代码块同步和方法同步的方式不同。代码块同步是使用monitorenter和monitorexit实现的,方法的同步是使用另一种方式但是规范中没说,而方法的同步用这两个指令也是可以实现的。
1️⃣Java对象头
synchronized用的锁是存在Java对象头里的,如果是数组类型,则虚拟机用3个字宽来存储对象头;如果是非数组类型,虚拟机用2个字宽来存储对象头。
非数组类型,第一个字宽存储着MarkWord,第二个字宽存储着到对象类型数据的指针。
MarkWord中存储的是对象的HashCode、分代年龄和锁标记位。
Java并发编程艺术学习笔记(一)_第1张图片
上图是32位处理器下的MarkWord情况。

2️⃣锁的升级与对比
Java SE1.6中引入了“偏向锁”以及“轻量级锁”,那么根据锁的状态级别从低到高可以分为:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但是不能降级。
①偏向锁
大多数情况下锁并不存在多线程竞争状态,而是由一个线程多次获得,为了获得锁的代价的减少引入了偏向锁。当一个线程获得锁的时候会在对象头和栈帧中的锁记录中存储锁偏向的线程ID,以后该线程进出同步块不需要CAS,只需要测试下对象头中是否有当前对象的偏向锁。如果测试失败,需要先验证锁的标志是否设置成1(偏向锁):如果没有设置,则用CAS竞争锁,如果设置了,就尝试用CAS将偏向锁指向当前线程。
(1)偏向锁的撤销
偏向锁只有当其他线程尝试竞争偏向锁的时候,持有偏向锁的线程才会释放锁。偏向锁的撤销需要等待全局安全点,首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态就将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,MarkWord要么偏向于其他线程,要么恢复到无锁状态或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
(2)关闭偏向锁
可以通过-XX:-UseBiasedLocking=false来默认程序直接进入轻量级锁的状态。

②轻量级锁
(1)轻量级锁的加锁
首先在当前线程的栈帧中创建用来存储锁记录的空间,将对象头的MarkWord复制到锁记录,称为DisplacedMarkWord。然后线程尝试用CAS将对象头中的MarkWord替换成指向锁记录的指针,如果成功,就获得锁;不成功就会自旋来获取锁。
(2)轻量级锁解锁
会使用原子CAS将DisplacedMarkWord替换回对象头,如果成功表示没有竞争发生,如果失败就会膨胀成重量级锁。
Java并发编程艺术学习笔记(一)_第2张图片
**关于锁膨胀的理解(个人理解):
一个线程需要去竞争的时候肯定实现查看对象头中的MarkWord标记符,只有是01(无锁)的状态下才会进入,因此如果对象已经被锁定,即标记位是00(轻量级锁)时这时候线程是不会进入的,两个线程交替进入时就不会发生膨胀的情况。因此发生膨胀只会在2个线程或者2个以上的线程一起进入时(注意这个时候它们都检测到对象头是无锁的状态),这两个线程先把对象头中的MarkWord拷贝到线程的栈帧中形成DisplacedMarkWord,接着CAS更新对象头,将对象头除了标志位以外的区域更新为指向栈帧中锁记录的,这时候只有一个线程可以更新成功(会把标志位改成00),另外一个线程只能不断CAS自旋来获得锁,当自旋到阈值,会把锁膨胀到重量级锁(修改标志位为10,并且停止自旋改成阻塞)。因此当第一个线程想要释放锁的时候,会CAS对象头,对象头的标志位是00会更新成功,10则更新不成功,说明此时锁已经膨胀,它就会唤醒那些阻塞线程,转成重量级锁来处理。

3️⃣原子操作的实现原理
原子操作就是“不可被中断的一个或者一系列操作”。
①CPU一些术语:
(1)比较并交换(CAS):如果值是预计的值则把它替换成新值,如果不是则返回false。
(2)缓存行:缓存的最小操作单位。
(3)CPU流水线:由5-6个不同功能的电路单元组成一条指令处理流水线,实现在一个CPU时钟周期完成一条指令,提高CPU的运算速度。
(4)内存顺序冲突:假共享引起的,多个CPU同时修改同一个缓存行中不同部分引起的一个CPU操作无效,当出现冲突的时候,CPU必须清空流水线。
②处理器如何实现原子操作
一般最新的处理器都可以自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的。而对于内存的操作处理器不能保证原子性,可以通过以下两个机制来保证:
(1)通过总线锁保证原子性:必须保证CPU1读改写共享变量时,CPU2不能操作缓存了该共享内存地址的缓存。当一个处理器在总线上输出此信号时,其他线程将被阻塞住。
(2)通过缓存锁保证原子性:保证在对某个内存地址的操作是原子性的,CPU1修改缓存行中的i时使用了缓存锁定,那么CPU2就不能同时缓存i缓存行。
有两种情况不会使用缓存锁定:1.操作的数据无法缓存在处理器的内部,或者操作的数据跨多个缓存行时,处理器会调用总线锁定。
2.有些处理器不支持内存锁定。
③Java如何实现原子操作
Java中可以通过锁和循环CAS来实现原子操作
(1)使用循环CAS来实现原子操作
(2)CAS实现原子操作的三大问题:1.ABA问题 2.循环时间开销大 3.只能保证一个共享变量的原子操作。
(3)使用锁机制来实现原子操作。

你可能感兴趣的:(多线程)