Java多线程与并发2-Synchronized

Synchronized

synchronized是Java中实现线程安全的基本方式,其底层就是通过加锁的方式控制

基本用法

2个维度来看,

  • 修饰位置
    • 代码块,修饰对象分为变量、实例或者class对象;
    • 方法,分为静态方法和普通方法。
  • 加锁对象
    • 对象锁,作用于调用方法的对象实例上;
    • 类锁,作用于对象类上。

修饰普通方法

/**
 * 对象锁, 方法级别, 同一对象争用该锁,普通(非静态)方法, synchronized的锁绑定在调用该方法的对象上
 */
public synchronized void methodSync() {
    System.out.println(System.currentTimeMillis() + " ==> " + Thread.currentThread().getName()
            + " method synchronized start");

    try {
        TimeUnit.MILLISECONDS.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println(System.currentTimeMillis() + " ==> " + Thread.currentThread().getName()
            + " method synchronized end");
}
/**
 * 对象锁, 代码级别, 同一对象争用该锁, this为当前对象实例, synchronized的锁绑定在this对象上
 */
public void methodSyncThis() {
    synchronized (this) {
        System.out.println(System.currentTimeMillis() + " ==> " + Thread.currentThread().getName()
                + " method synchronized-this start");
        try {
            TimeUnit.MILLISECONDS.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(System.currentTimeMillis() + " ==> " + Thread.currentThread().getName()
                + " method synchronized-this end");
    }
}
/**
 * 不含synchronized修饰作为对比.
 */
public void method() {
    System.out.println(System.currentTimeMillis() + " ==> " + Thread.currentThread().getName() + " method start");

    try {
        TimeUnit.MILLISECONDS.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println(System.currentTimeMillis() + " ==> " + Thread.currentThread().getName() + " method end");
}
public static void main(String[] args) {
    C1Synchronized c1 = new C1Synchronized();
    new Thread(c1::methodSync, "thread-sync-1").start();
    new Thread(c1::methodSync, "thread-sync-2").start();
    new Thread(c1::method, "thread-3").start();
    new Thread(c1::method, "thread-4").start();
}

运行结果

1588169925896 ==> thread-sync-1 method synchronized start
1588169925897 ==> thread-3 method start
1588169925897 ==> thread-4 method start
1588169928897 ==> thread-sync-1 method synchronized end
1588169928897 ==> thread-sync-this-2 method synchronized-this start
1588169928902 ==> thread-4 method end
1588169928902 ==> thread-3 method end
1588169931902 ==> thread-sync-this-2 method synchronized-this end
1588169931902 ==> thread-sync-this-1 method synchronized-this start
1588169934905 ==> thread-sync-this-1 method synchronized-this end
1588169934905 ==> thread-sync-2 method synchronized start
1588169937909 ==> thread-sync-2 method synchronized end

修饰对象

private Obejct obj = new Object();
private int count = 10;
public void methodSyncObject() {
    synchronized (obj) {
        System.out.println(System.currentTimeMillis() + " ==> " + Thread.currentThread().getName() + " start");
        count--;
        try {
            TimeUnit.MILLISECONDS.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(System.currentTimeMillis() + " ==> " + Thread.currentThread().getName()
                + " count=" + count);
    }
}

运行结果

1588170268344 ==> thread-sync-object-1 start
1588170271344 ==> thread-sync-object-1 count=9
1588170271345 ==> thread-sync-object-2 start
1588170274350 ==> thread-sync-object-2 count=8

修饰静态方法

/**
 * 类锁, 方法级别, 同一类争用该锁, synchronized的锁绑定在C1Synchronized.class上
 */
public static synchronized void methodSyncStatic() {
    System.out.println(System.currentTimeMillis() + " ==> " + Thread.currentThread().getName()
            + " static method synchronized start");

    try {
        TimeUnit.MILLISECONDS.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println(System.currentTimeMillis() + " ==> " + Thread.currentThread().getName()
            + " static method synchronized end");
}
/**
 * 类锁, 代码级别, 同一类争用该锁
 */
public synchronized void methodSyncClass() {
    synchronized (C1Synchronized.class) {
        System.out.println(System.currentTimeMillis() + " ==> " + Thread.currentThread().getName()
                + " class synchronized start");

        try {
            TimeUnit.MILLISECONDS.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(System.currentTimeMillis() + " ==> " + Thread.currentThread().getName()
                + " class synchronized end");
    }
}
public static void main(String[] args) {
    new Thread(C1Synchronized::methodSyncStatic, "thread-static-1").start();
    new Thread(C1Synchronized::methodSyncStatic, "thread-static-2").start();
}

运行结果

1588170487278 ==> thread-static-1 static method synchronized start
1588170490282 ==> thread-static-1 static method synchronized end
1588170490282 ==> thread-class-1 class synchronized start
1588170493286 ==> thread-class-1 class synchronized end
1588170493287 ==> thread-static-2 static method synchronized start
1588170496289 ==> thread-static-2 static method synchronized end
1588170496289 ==> thread-class-2 class synchronized start
1588170499289 ==> thread-class-2 class synchronized end

实现原理

本文暂时通过字节码的形式分析synchronized的实现原理,

public class C2Synchronized {
    private int count = 100;
    private Object obj = new Object();

    public static void main(String[] args) {
        C2Synchronized c2 = new C2Synchronized();
        new Thread(c2::method, "Thread-1").start();
        new Thread(c2::method, "Thread-2").start();
    }

    public void method() {
        synchronized (obj) {
            System.out.println(Thread.currentThread().getName() + " start");
            count++;
            System.out.println(count);
        }
    }
}

通过编译与反编译命令(IDEA中可以通过 jclasslib 插件查看)

javac C2Synchronized.java
# javap是jdk自带的反解析工具。
# 根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
javap -c C2Synchronized

解析结果

public class pri.ljw.java.concurrent.p2.lock.C2Synchronized {
  public pri.ljw.java.concurrent.p2.lock.C2Synchronized();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        100
       7: putfield      #7                  // Field count:I
      10: aload_0
      11: new           #2                  // class java/lang/Object
      14: dup
      15: invokespecial #1                  // Method java/lang/Object."<init>":()V
      18: putfield      #13                 // Field obj:Ljava/lang/Object;
      21: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #8                  // class pri/ljw/java/concurrent/p2/lock/C2Synchronized
       3: dup
       4: invokespecial #17                 // Method "<init>":()V
       7: astore_1
       8: new           #18                 // class java/lang/Thread
      11: dup
      12: aload_1
      13: dup
      14: invokestatic  #20                 // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
      17: pop
      18: invokedynamic #26,  0             // InvokeDynamic #0:run:(Lpri/ljw/java/concurrent/p2/lock/C2Synchronized;)Ljava/lang/Runnable;
      23: ldc           #30                 // String Thread-1
      25: invokespecial #32                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;Ljava/lang/String;)V
      28: invokevirtual #35                 // Method java/lang/Thread.start:()V
      31: new           #18                 // class java/lang/Thread
      34: dup
      35: aload_1
      36: dup
      37: invokestatic  #20                 // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
      40: pop
      41: invokedynamic #26,  0             // InvokeDynamic #0:run:(Lpri/ljw/java/concurrent/p2/lock/C2Synchronized;)Ljava/lang/Runnable;
      46: ldc           #38                 // String Thread-2
      48: invokespecial #32                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;Ljava/lang/String;)V
      51: invokevirtual #35                 // Method java/lang/Thread.start:()V
      54: return

  public void method();
    Code:
       0: aload_0
       1: getfield      #13                 // Field obj:Ljava/lang/Object;
       4: dup
       5: astore_1
       6: monitorenter
       7: getstatic     #40                 // Field java/lang/System.out:Ljava/io/PrintStream;
      10: invokestatic  #46                 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
      13: invokevirtual #50                 // Method java/lang/Thread.getName:()Ljava/lang/String;
      16: invokedynamic #54,  0             // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
      21: invokevirtual #58                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      24: aload_0
      25: dup
      26: getfield      #7                  // Field count:I
      29: iconst_1
      30: iadd
      31: putfield      #7                  // Field count:I
      34: getstatic     #40                 // Field java/lang/System.out:Ljava/io/PrintStream;
      37: aload_0
      38: getfield      #7                  // Field count:I
      41: invokevirtual #64                 // Method java/io/PrintStream.println:(I)V
      44: aload_1
      45: monitorexit
      46: goto          54
      49: astore_2
      50: aload_1
      51: monitorexit
      52: aload_2
      53: athrow
      54: return
    Exception table:
       from    to  target type
           7    46    49   any
          49    52    49   any
}

在JVM规范中,任何一个对象都有一个Monitor与之关联,当一个Monitor被持有时,就会被锁定。synchronize底层就是通过 monitorenter
monitorexit实现。

  • monitorenter指令插入在同步代码块的起始位置,当执行到此时,就会尝试获取该Monitor的所有权,即加锁;
  • monitorexit指令插入在同步代码块的结束位置和异常处,当执行到此时,就会释放锁。

synchronized细节

先修知识

Java对象头

  • 字宽(Word): 内存大小的单位概念,对于32位处理器 1 Word = 4 Bytes,64位处理器 1 Word = 8 Bytes;
  • 每一个Java对象都至少占用2个字宽的内存(数组类型占用3个字宽);
  • 第1个字宽是markword,包括hashcode、GC分代、 锁状态标志 、线程持有的锁、偏向线程ID、偏向时间戳等信息;
    markword
  • 第2个字宽是指向定义该对象类信息(class metadata)的指针;
  • 说明:
    • MarkWord中包含对象hashCode的那种无锁状态是偏向机制被禁用时,分配出来的无锁对象MarkWord起始状态;
    • 偏向机制被启用时,分配出来的对象状态是 ThreadId|Epoch|age|1|01
      • ThreadId为空时标识对象尚未偏向于任何一个线程
      • ThreadId不为空时,对象既可能处于偏向特定线程的状态,也有可能处于已经被特定线程占用完毕释放的状态,需结合 Epoch 和其他信息判断对象是否允许再偏向(rebias)。

CAS指令

  • CAS(Compare And Swap)指令是一个CPU层级的原子性操作指令。在Intel处理器中,其汇编指令为cmpxchg。
  • 该指令概念上存在3个参数,第一个参数【目标地址】,第二个参数【值1】,第三个参数【值2】,指令会比较【目标地址存储的内容和【值1】是否一致,
    如果一致,则将【值2】填写到【目标地址】,其语义可以用如下的伪代码表示。
function cas(p , old , new) returns bool {
    if *p ≠ old { // *p 表示指针p所指向的内存地址
        return false
    }
    *p ← new
    return true
}
  • 该指令是是原子性的,也就是说CPU执行该指令时,是不会被中断执行其他指令的。但是如果CAS操作放在应用层面来实现,则需要自行保证其原子性,如ABA问题。
  • ABA的问题并不在于多次修改,而是语义发生改变。假如目标地址的内容被多次修改以后,虽然从二进制上来看是依旧是A,但是其语义已经不是A。
    例如,发生了整数溢出,内存回收等等。

锁细节

在jdk1.6之前,synchronized是 重量级锁 ,jdk1.6开始引入了 偏向锁轻量级锁 概念,极大优化了synchronized的性能。
即从jdk1.6开始,锁有4种状态,分别是:无锁状态、偏向锁、轻量级锁、重量级锁,随着竞争情况逐渐升级。

偏向锁

偏向锁,即偏向某个线程获取锁,通过在对象头和线程栈帧中的LockRecord中存储锁偏向的线程ID,具体工作原理是:

  1. MarkWord中的偏向锁标识是否为1,锁标记为是否为01,若都是,则是偏向锁状态;
  2. 若是偏向锁状态,比较在LockRecord中的线程ID是否是当前线程ID,若是,获取偏向锁,执行同步代码块;
  3. 若是偏向锁状态,但线程ID不一致,通过CAS竞争获取锁,竞争成功,则将LR中的锁线程ID修改为当前线程,执行同步代码块;
  4. 若通过CAS竞争锁失败,且达到safepoint,LR中记录的偏向锁线程被挂起,锁升级为轻量级锁,由阻塞在safepoint的线程获取锁。

偏向锁的撤销,在第4步中,在全局的safepoint,首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,
撤销偏向锁后恢复到未锁定(标志位为"01")或轻量级锁(标志位为"00")的状态。

引入偏向锁的原因,是由于在大多数情况下,锁不存在多个线程竞争,甚至总是由同一个线程获取,因此为了让线程获取锁的代价更低引入偏向锁。

轻量级锁

加锁过程
  1. 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(001),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,
    用于存储锁对象目前的Mark Word的拷贝,官方称之为Displaced Mark Word;
  2. 拷贝对象头中的Mark Word复制到锁记录中;
  3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。
    如果更新成功,则执行步骤4,否则执行步骤5;
  4. 若更新成功,那么当前线程获取该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。
  5. 若更新失败,JVM首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。
    否则说明多个线程竞争锁,轻量级锁就要升级为重量级锁,锁标志的状态值变为"10",当前线程尝试通过自旋获取锁。
解锁过程
  1. 通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word;
  2. 替换成功,整个同步过程就完成了,即解锁完成;
  3. 替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

重量级锁

重量级锁依赖于操作系统的互斥量(mutex)实现,其具体的详细机制此处暂不展开,只需要了解该操作会导致进程从用户态与内核态之间的切换,是一个开销较大的操作。

参考文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值