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

一、共享带来的问题

问题:

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

问题分析:

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

 

1. 临界区

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

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

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

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

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

2. 竞态条件 Race Condition

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

二、Synchronized 解决方案

1. 应用之互斥

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

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

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

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

注意:

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

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

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

2. synchronized

 

思考:

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

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

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

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

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

3. 面向对象改进

三、方法上的 synchronized

四、变量的线程安全分析

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

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

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

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

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

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

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

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

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

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

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

 

成员对象分析:

分析: 

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

局部变量分析:

 分析:

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

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

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

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

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

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

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

4. 常见线程安全类

String

包装类(Integer 等)

StringBuffer

Random

Vector

Hashtable

java.util.concurrent 包下的类

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

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

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

4.1 线程安全类方法的组合

4.2 不可变类线程安全性

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

五、Monitor 概念

1. Java 对象头

  

2. Monitor(锁)原理

Monitor 被翻译为监视器管程

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

(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 原理(字节码角度)

 P77

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

5. synchronized 原理进阶

5.1 轻量级锁

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

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

 

 

  

5.2 锁膨胀

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

5.3 自旋优化

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

  

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

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

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

5.4 偏向锁

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

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

(1)偏向状态

 一个对象创建时:

①如果开启了偏向锁(默认开启),那么对象创建后,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)撤销 - 其他线程使用对象

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

(4)撤销 - 调用 wait/notify

(5)批量重偏向

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

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

(6)批量撤销

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

5.5 锁消除

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值