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(重量锁)重量锁指针 | 阻塞 | 
| … | 
