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 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

时序图

单线程模式下

多线程模式下-出现负数的情况

多线程模式下-出现正数的情况

临界区 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

时序图如下:

面向对象的方式改造代码

@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释放锁:

整体流程如下:

  1. 刚开始Monitorownernull
  2. 当Thread-1执行到 synchronized(obj) 就会将Monitorowner置为:Thread-1Monitor中只能有一个Owner
  3. Thread-1上锁的过程中,如果Thread-2Thread-3也执行到synchronized(obj),就会进入EntryList进入Blocked状态
  4. 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.class
Compiled 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 替换 ObjectMark 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 退出同步块解锁时,使用 casMark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Ownernull,唤醒 EntryListBLOCKED 线程

自旋优化

重量级锁竞争的时候,还可以使用自旋(循环尝试获取重量级锁)来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。 (进入阻塞再恢复,会发生上下文切换,比较耗费性能)

自旋重试成功的情况

线程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(重量锁)重量锁指针 阻塞
0%