3.并发 共享变量
并发
并发导致共享变量问题
@Slf4j(topic = "c.share")
public class TestShareVar {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int j = 0; j< 5000; j++){
i++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for(int j = 0; j< 5000; j++){
i--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("i = {}", i);
}
}15:31:26.186 c.share [main] - i = -37问题
以上的结果可能是正数、负数、零。因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i而对应i--也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
时序图
单线程模式下
sequenceDiagram participant t1 as 线程1 participant i as static i i ->> t1: getstatic i 读取 0 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: iadd 加法,线程内 i = 1 t1 ->> i: putstatic i 写入 1 i ->> t1: getstatic i 获取 1 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: isub 减法,线程内 i = 0 t1 ->> i: putstatic i 写入 0sequenceDiagram participant t1 as 线程1 participant i as static i i ->> t1: getstatic i 读取 0 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: iadd 加法,线程内 i = 1 t1 ->> i: putstatic i 写入 1 i ->> t1: getstatic i 获取 1 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: isub 减法,线程内 i = 0 t1 ->> i: putstatic i 写入 0
多线程模式下-出现负数的情况
sequenceDiagram participant t1 as 线程1 participant t2 as 线程2 participant i as static i i ->> t2: getstatic i 读取 0 t2 ->> t2: iconst_1 准备常数 1 t2 ->> t2: isub 减法 线程内 i=-1 t2 -->> t1: 上下文切换 i->> t1: getstatic i 读取 0 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: iadd 加法 线程内 i= 1 t1 ->> i: putstatic i 写入 1 t1 -->> t2: 上下文切换 t2 ->> i : putstatic i 写入 -1sequenceDiagram participant t1 as 线程1 participant t2 as 线程2 participant i as static i i ->> t2: getstatic i 读取 0 t2 ->> t2: iconst_1 准备常数 1 t2 ->> t2: isub 减法 线程内 i=-1 t2 -->> t1: 上下文切换 i->> t1: getstatic i 读取 0 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: iadd 加法 线程内 i= 1 t1 ->> i: putstatic i 写入 1 t1 -->> t2: 上下文切换 t2 ->> i : putstatic i 写入 -1
多线程模式下-出现正数的情况
sequenceDiagram participant t1 as 线程1 participant t2 as 线程2 participant i as static i i ->> t1: getstatic i 读取 0 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: iadd 加法 线程内 i= 1 t1 -->> t2: 上下文切换 i->> t2: getstatic i 读取 0 t2 ->> t2: iconst_1 准备常数 1 t2 ->> t2: isub 减法 线程内 i=-1 t2 ->> i : putstatic i 写入 -1 t2 -->> t1: 上下文切换 t1 ->> i: putstatic i 写入 1sequenceDiagram participant t1 as 线程1 participant t2 as 线程2 participant i as static i i ->> t1: getstatic i 读取 0 t1 ->> t1: iconst_1 准备常数 1 t1 ->> t1: iadd 加法 线程内 i= 1 t1 -->> t2: 上下文切换 i->> t2: getstatic i 读取 0 t2 ->> t2: iconst_1 准备常数 1 t2 ->> t2: isub 减法 线程内 i=-1 t2 ->> i : putstatic i 写入 -1 t2 -->> t1: 上下文切换 t1 ->> i: putstatic i 写入 1
临界区 Critical Section
-
一个程序运行多个线程本身是没有问题的
-
问题出在多个线程访问共享资源
-
多个线程读共享资源其实也没有问题
-
在多个线程对共享资源读写操作时发生指令交错,就会出现问题
-
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int j = 0; j< 5000; j++)
// 临界区
{
i++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for(int j = 0; j< 5000; j++)
// 临界区
{
i--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("i = {}", i);
}竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
synchronized 应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
synchronized 俗称【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
synchronized 语法
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}采用synchronized改造
@Slf4j(topic = "c.share")
public class TestShareVar {
static int i = 0;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
test2();
}
public static void test1() throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int j = 0; j< 5000; j++)
// 临界区
{
i++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for(int j = 0; j< 5000; j++)
// 临界区
{
i--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("i = {}", i);
}
public static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
for(int j = 0; j< 5000; j++){
synchronized (lock){
i++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for(int j = 0; j< 5000; j++){
synchronized (lock){
i--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("i = {}", i);
}
}19:03:44.568 c.share [main] - i = 0时序图如下:
sequenceDiagram participant t1 as 线程1 participant t2 as 线程2 participant i as static i participant lock as 对象锁(lock) t2 ->> lock: 线程2尝试获取锁 note over t2,lock: 拥有锁 i ->> t2: getstatic i 读取 0 t2 ->> t2: iconst_1 准备常数 1 t2 ->> t2: isub 减法 i = -1 t2 -->> t1: 上下文切换 t1 -x lock: 尝试获取锁,被阻塞(blocked) t1 -->> t2: 上下文切换 t2 ->> i: putstatic i 写入 -1 note over t2,lock : 拥有锁 t2 ->> lock: 释放锁并唤醒阻塞线程 note over t1,lock: 拥有锁 i ->> t1: getstatic i 读取 -1 t1 ->> t1: iconst_1 准备常量 1 t1 ->> t1: iadd 加法 i = 0 t1 ->> i: putstatic i 写入 0 note over t1, lock: 拥有锁 t1 ->> lock: 释放锁并唤醒阻塞线程sequenceDiagram participant t1 as 线程1 participant t2 as 线程2 participant i as static i participant lock as 对象锁(lock) t2 ->> lock: 线程2尝试获取锁 note over t2,lock: 拥有锁 i ->> t2: getstatic i 读取 0 t2 ->> t2: iconst_1 准备常数 1 t2 ->> t2: isub 减法 i = -1 t2 -->> t1: 上下文切换 t1 -x lock: 尝试获取锁,被阻塞(blocked) t1 -->> t2: 上下文切换 t2 ->> i: putstatic i 写入 -1 note over t2,lock : 拥有锁 t2 ->> lock: 释放锁并唤醒阻塞线程 note over t1,lock: 拥有锁 i ->> t1: getstatic i 读取 -1 t1 ->> t1: iconst_1 准备常量 1 t1 ->> t1: iadd 加法 i = 0 t1 ->> i: putstatic i 写入 0 note over t1, lock: 拥有锁 t1 ->> lock: 释放锁并唤醒阻塞线程
面向对象的方式改造代码
@Slf4j(topic = "c.test8")
public class Test8 {
public static void main(String[] args) throws InterruptedException {
TestLock lock = new TestLock();
Thread t1 = new Thread(() -> {
for(int j = 0; j< 5000; j++){
lock.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for(int j = 0; j< 5000; j++){
lock.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("i = {}", lock.getI());
}
}
class TestLock {
private Integer i = 0;
public void increment(){
synchronized (this){
i++;
}
}
public void decrement(){
synchronized (this){
i--;
}
}
public Integer getI(){
synchronized (this){
return i;
}
}
}方法上的synchronized
class Test{
public synchronized void test() {
}
}
等价于
class Test{
public void test() {
synchronized(this) {
}
}
}贴在方法上等于锁住的this对象
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}贴在静态方法上等价于锁住的是类对象
线程八锁问题
其实就是考察 synchronized 锁住的是哪个对象
情况1:12 或 21
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}情况2:1s后12,或 2 1s后 1
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}情况3:3 1s 12 或 23 1s 1 或 32 1s 1
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
public void c() {
log.debug("3");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
new Thread(()->{ n1.c(); }).start();
}情况4:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}情况5:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}情况6:1s 后12, 或 2 1s后 1
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}情况7:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}情况8:1s 后12, 或 2 1s后 1
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}变量的线程安全分析
成员变量和静态变量是否线程安全?
- 如果他们都没有被共享则线程安全
- 如果他们被共享了,则看他们的状态是否可改变:
- 如果只有读操作,则线程安全
- 如果有读写操作,者这段代码是临界区,需要考虑线程安全问题
局部变量是否线程安全
- 局部变量是线程安全的
- 但局部变量引用的对象就不一定
- 如果该对象没有脱离方法的作用范围,它是线程安全的
- 如果该对象逃离了方法的作用范围,需要考虑线程安全问题
常见线程安全类
- String
- Integer
- StringBuffffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();- 它们的每个方法是原子的
- 但注意它们多个方法的组合不是原子的
Monitor 对象头
普通对象
Mark Word:主要用来存储对象自身的运行时数据Klass Word: 指向Class对象
数组对象
相对于普通对象多了记录数组长度
Mark Word 结构
32位虚拟机

64位虚拟机

Monitor(锁)
Monitor 被翻译为监视器或管程
每个java对象都可以关联一个Monitor对象,如果使用synchronized给对象加锁(重量级)以后,该对象头的Mard word中就被设置指向Monitor对象的指针
当没有线程执行同步代码块内容时:

当线程1执行到同步代码块时,对象的mark word 指针会指向Monitor, 并且其他线程如果也访问到同步代码块的时候会进入阻塞队列中,等待Thread-1释放锁:

整体流程如下:
- 刚开始
Monitor的owner为null - 当Thread-1执行到
synchronized(obj)就会将Monitor的owner置为:Thread-1,Monitor中只能有一个Owner - 在
Thread-1上锁的过程中,如果Thread-2,Thread-3也执行到synchronized(obj),就会进入EntryList进入Blocked状态 - Thread-1执行完同步代码后,就会唤醒EntryList中的所有线程来竞争锁,竞争的时候是非公平的。
synchronized 必须是进入同一个对象的 monitor 才有上述的效果
不加 synchronized 的对象不会关联监视器,不遵从以上规则
synchronized 字节码
public class TestSynchronizedCode {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}打印字节码
javap -c TestSynchronizedCode.classCompiled from "TestSynchronizedCode.java"
public class cn.com.wuhm.juc.three.n4.TestSynchronizedCode {
static final java.lang.Object lock;
static int counter;
public cn.com.wuhm.juc.three.n4.TestSynchronizedCode();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // lock 的引用
3: lot
4: astore_1 // lock引用 slot 1
5: monitorenter // 将 lock对象的MarkWord置为Monitor指针
6: getstatic #3 // 获取常量 counter:0
9: iconst_1 // 准备常数 1
10: iadd // + 1
11: putstatic #3 // counter = 1
14: aload_1 // lock 引用
15: monitorexit // 将lock对象的MarkWord重置,并唤醒EntryList
16: goto 24 // 跳转24行
19: astore_2 // lock引用 slot 2
20: aload_1 // lock 引用
21: monitorexit // 将lock对象的MarkWord重置,并唤醒EntryList
22: aload_2
23: athrow // throw e
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
static {};
Code:
0: new #4 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: putstatic #2 // Field lock:Ljava/lang/Object;
10: iconst_0
11: putstatic #3 // Field counter:I
14: return
}==从上面的字节码中可以看出,同步代码块执行完后或有异常发生时会自动释放锁资源==
轻量级锁/偏向锁/重量级锁
轻量级锁
轻量级锁使用场景:如果一个对象虽然有多个线程访问,但是多线程访问的时间是错开的(也就是没有竞争),那么就可以使用轻量级锁来优化
轻量级锁对使用者是透明的,即语法仍然是 synchronized
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}- 创建 锁记录
(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

- 让锁记录中
Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录

-
如果 cas 替换成功,对象头中存储了
锁记录地址和状态 00,表示由该线程给对象加锁,这时图示如下
-
如果
cas失败,有两种情况-
如果是其它线程已经持有了该
Object的轻量级锁,这时表明有竞争,进入锁膨胀过程 -
如果是自己执行了
synchronized锁重入,那么再添加一条Lock Record作为重入的计数
-
-
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
-
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
-
成功,则解锁成功
-
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
-
(轻量级)锁膨胀(为重量级锁)
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}-
当
Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
-
这是Thread-1加轻量级锁失败,进入锁膨胀流程
- Object对象申请Monitor锁,让Object指向重量级锁地址
- 然后自己进入Monitor的EntryList

-
当
Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程
自旋优化
重量级锁竞争的时候,还可以使用自旋(循环尝试获取重量级锁)来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。 (进入阻塞再恢复,会发生上下文切换,比较耗费性能)
自旋重试成功的情况
| 线程1(core1) | 对象Mark word | 线程2(core2) |
|---|---|---|
| - | 10(重量级锁) | - |
| 访问同步块获取Monitor | 10(重量锁)重量锁指针 | - |
| 成功(加锁) | 10(重量锁)重量锁指针 | - |
| 执行同步块代码 | 10(重量锁)重量锁指针 | - |
| 执行同步块代码 | 10(重量锁)重量锁指针 | 访问同步块获取Monitor |
| 执行同步块代码 | 10(重量锁)重量锁指针 | 自旋重试 |
| 执行完毕 | 10(重量锁)重量锁指针 | 自旋重试 |
| 成功(解锁) | 01(无锁) | 自旋重试 |
| - | 10(重量锁)重量锁指针 | 成功(加锁) |
| - | 10(重量锁)重量锁指针 | 执行同步块代码 |
| … | … |
自旋重试成功的情况
| 线程1(core1) | 对象Mark word | 线程2(core2) |
|---|---|---|
| - | 10(重量级锁) | - |
| 访问同步块获取Monitor | 10(重量锁)重量锁指针 | - |
| 成功(加锁) | 10(重量锁)重量锁指针 | - |
| 执行同步块代码 | 10(重量锁)重量锁指针 | - |
| 执行同步块代码 | 10(重量锁)重量锁指针 | 访问同步块获取Monitor |
| 执行同步块代码 | 10(重量锁)重量锁指针 | 自旋重试 |
| 执行同步块代码 | 10(重量锁)重量锁指针 | 自旋重试 |
| 执行同步块代码 | 01(无锁) | … |
| 执行同步块代码 | 10(重量锁)重量锁指针 | 自旋重试 |
| 执行同步块代码 | 10(重量锁)重量锁指针 | 阻塞 |
| … |