并发编程(三)共享模型之管程(上)

一、共享带来的问题

问题:

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

并发编程(三)共享模型之管程(上)_第1张图片

问题分析:

以上的结果可能是正数、负数、零。因为 Java 中对静态变量的自增、自减并不是原子操作,要彻底理解,必须从字节码来进行分析。

并发编程(三)共享模型之管程(上)_第2张图片

并发编程(三)共享模型之管程(上)_第3张图片

并发编程(三)共享模型之管程(上)_第4张图片

 并发编程(三)共享模型之管程(上)_第5张图片

并发编程(三)共享模型之管程(上)_第6张图片

1. 临界区

(1)一个程序运行多个线程本身是没有问题的

(2)问题出在多个线程访问共享资源

        ①多个线程共享资源其实也没有问题

        ②在多个线程对共享资源读操作时发送指令交错,就会出现问题

(3)一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

并发编程(三)共享模型之管程(上)_第7张图片

2. 竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

二、Synchronized 解决方案

1. 应用之互斥

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

(1)阻塞式的解决方案:synchronized、Lock

(2)非阻塞式的解决方案:原子变量

synchronized,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

注意:

虽然 Java 中互斥和同步都可以采用 synchronized 关键字来完成,但有区别:

(1)互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码。

(2)同步是由于线程执行的先后顺序不同,需要一个线程等待其他线程运行到某个点。

2. synchronized

并发编程(三)共享模型之管程(上)_第8张图片

并发编程(三)共享模型之管程(上)_第9张图片

 并发编程(三)共享模型之管程(上)_第10张图片

思考:

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。

为了加深理解,请思考下面的问题(评论区交流)

(1)如果把 synchronized(obj) 放在 for 循环的外面,如何理解?——原子性

(2)如果 t1 synchronized(obj1),而 t2 synchronized(obj2) 会怎样运作?——锁对象

(2)如果 t1 synchronized(obj1),而 t2 没有加会怎么样?——锁对象

3. 面向对象改进

并发编程(三)共享模型之管程(上)_第11张图片

三、方法上的 synchronized

并发编程(三)共享模型之管程(上)_第12张图片

四、变量的线程安全分析

1. 成员变量和静态变量是否线程安全?

(1)如果它们没有共享,则线程安全

(2)如果它们被共享了,根据它们的状态是否能否改变,又分两种期刊

        ①如果只有读操作,则线程安全

        ②如果有读写操作,则这段代码是临界区,需要考虑线程安全

2. 局部变量是否线程安全?

(1)局部变量是线程安全的

(2)但局部变量引用的对象则未必

        ①如果该对象没有逃离方法的作用范围,它是线程安全的

        ②如果该对象逃离方法的作用范围,需要考虑线程安全

3. 局部变量线程安全分析

并发编程(三)共享模型之管程(上)_第13张图片

并发编程(三)共享模型之管程(上)_第14张图片

 并发编程(三)共享模型之管程(上)_第15张图片

成员对象分析:

并发编程(三)共享模型之管程(上)_第16张图片

并发编程(三)共享模型之管程(上)_第17张图片

分析: 

(1)无论哪个线程中的 method2、method3 引用的都是同一个对象中的 list 成员变量

并发编程(三)共享模型之管程(上)_第18张图片

局部变量分析:

并发编程(三)共享模型之管程(上)_第19张图片

 分析:

(1)list 是局部变量,每个线程调用时会创建其不同实例,没有共享

(2)而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象

(3)method3 的参数分析与 method2 相同

并发编程(三)共享模型之管程(上)_第20张图片

方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会带来线程安全问题?

(1)情况1:有其他线程调用 method2 和 method3

(2)情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法。

并发编程(三)共享模型之管程(上)_第21张图片

 从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】。

4. 常见线程安全类

String

包装类(Integer 等)

StringBuffer

Random

Vector

Hashtable

java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的方法时,是线程安全的。也可以理解为:

(1)它们的每个方法是原子的

(2)但注意它们多个方法的组合不是原子的

4.1 线程安全类方法的组合

并发编程(三)共享模型之管程(上)_第22张图片

4.2 不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。

五、Monitor 概念

1. Java 对象头

并发编程(三)共享模型之管程(上)_第23张图片

并发编程(三)共享模型之管程(上)_第24张图片  

2. Monitor(锁)原理

Monitor 被翻译为监视器管程

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针

并发编程(三)共享模型之管程(上)_第25张图片

(1)刚开始 Monitor 中 Owner 为 null。

(2)当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner。

(3)在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKER。

(4)Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的。

(5)图中 WaitSet 中的  Thread-0, Thread-1 是之前获得过锁的,当条件不满足加入 WAITING 状态的线程,后面涉及到 wait-notify 时会分析

注意:

(1)synchronized 必须是进入同一个对象的 Monitor 才有上述的效果。

(2)不加 synchronized 的对象不会关联监视器,不遵从以上规则。

3. synchronized 原理(字节码角度)

并发编程(三)共享模型之管程(上)_第26张图片

并发编程(三)共享模型之管程(上)_第27张图片 P77

4. 小故事(方便理解 Monitor、轻量级锁、偏向锁)

并发编程(三)共享模型之管程(上)_第28张图片

5. synchronized 原理进阶

5.1 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级来优化。

轻量级锁对使用者是透明的,即语法仍然是 synchronized。

并发编程(三)共享模型之管程(上)_第29张图片

 并发编程(三)共享模型之管程(上)_第30张图片

 并发编程(三)共享模型之管程(上)_第31张图片

并发编程(三)共享模型之管程(上)_第32张图片  

5.2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

并发编程(三)共享模型之管程(上)_第33张图片

5.3 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

并发编程(三)共享模型之管程(上)_第34张图片

并发编程(三)共享模型之管程(上)_第35张图片  

(1)自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

(2)在 Java6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

(3)Java7 之后不能控制是否开启自旋功能。

5.4 偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。

并发编程(三)共享模型之管程(上)_第36张图片

并发编程(三)共享模型之管程(上)_第37张图片

(1)偏向状态

并发编程(三)共享模型之管程(上)_第38张图片

 一个对象创建时:

①如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0*05 即最后 3 位为 101,这时它的 thread、epoch、age 都为0。

②偏向锁是默认延迟的,不会再程序启动时立即生效,如果想避免延迟,可以加 VM 参数【-XX:BiasedLockingStartupDelay=0】来禁用延迟。

③如果没有开启偏向锁,那么对象创建后,markword 值为 0*01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值。

④处于偏向锁的对象解锁后,线程 id 仍存储于对象头中。

⑤可以加 VM 参数【-XX:-UseBiasedLocking】来禁用偏向锁。

⑥正常状态对象一开始是没有 hashCode 的,第一次调用才生成。

(2)撤销 - 调用对象 hashCode

调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销。

        ①轻量级锁会在锁记录中记录 hashCode

        ②重量级锁会在 Monitor 中记录 hashCode

在调用 hashCode 后使用偏向锁,记得去掉【-XX:-UseBiasedLocking】

(3)撤销 - 其他线程使用对象

当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。

并发编程(三)共享模型之管程(上)_第39张图片

(4)撤销 - 调用 wait/notify

并发编程(三)共享模型之管程(上)_第40张图片

(5)批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID。

当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了,预示会再给这些对象加锁时重新偏向至锁线程。

并发编程(三)共享模型之管程(上)_第41张图片

(6)批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是这个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。

5.5 锁消除

锁消除是发生在编译器级别的一种锁优化方式。有时候我们写的代码完全不需要加锁,却执行了加锁操作。通过编译器将其优化,将锁消除。

你可能感兴趣的:(JUC并发编程,java,开发语言,并发编程)