Java多线程:深入理解synchronized

一、概述
synchronized关键字是java应用中解决线程安全必不可少的,线程安全是并发编程中的重要关注点,造成线程不安全的诱因实质就是共享数据,以及多线程操作共享数据,为了解决多线程操作共享数据的问题,需要保证在同一时刻只有一个线程可以操作共享数据,其它线程处于等待状态,只有操作共享数据的线程执行结束,其他线程才可以进行,这种关系就是互斥锁,需要用到synchronized关键字, synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能)

二、synchronized的使用

  • 修饰实例方法,对当前实例对象加锁。
  • 修饰静态方法,对当前类的class对象加锁。
  • 修饰代码块,对当前代码块的对象加锁。

2.1、synchronized的作用于实例方法

/**
 * @author: hs
 * @Date: 2019/12/25 09:52
 * @Description: 
 */
public class SyncInstance implements Runnable {
	//共享资源
    static int i = 0;

    public synchronized void add() {
        for (int j = 0; j < 1000000000; j++) {
            i++;
        }
    }
    @Override
    public void run() {
        add();
    }
    public static void main(String[] args) throws InterruptedException {
    	//此处切记不可以new两个SyncInstance对象分别给到两个线程,
    	//这样的话就是两个不同的对象锁,依然存在线程安全问题。
        SyncInstance syncInstance = new SyncInstance();
        
        Thread t1 = new Thread(syncInstance);
        Thread t2 = new Thread(syncInstance);

        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println(i);
        /*
		 *结果:2000000000
		 */
    }
}

2.2、synchronized的作用于静态方法

public class SyncInstance implements Runnable {

    static int i = 0;

    public synchronized static void add() {
        for (int j = 0; j < 1000000000; j++) {
            i++;
        }
    }


    @Override
    public void run() {
        add();
    }


    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new SyncInstance());
        Thread t2 = new Thread(new SyncInstance());

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(i);
        /*
		 *结果:2000000000
		 */
    }
}

2.3、synchronized的作用于代码块

public class SyncInstance implements Runnable {

    static int i = 0;


    @Override
    public void run() {
        synchronized (SyncInstance.class){
            for (int j = 0; j < 1000000000; j++) {
                i++;
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new SyncInstance());
        Thread t2 = new Thread(new SyncInstance());

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(i);
        /*
		 *结果:2000000000
		 */
    }
}

三、synchronized锁的实现

3.1、同步代码块

public class BySync {

    private    int i;

    public void test() {
        synchronized (this) {
            i++;
        }
    }
}

编译上述代码并使用javap反编译后得到字节码如下:

public class com.staryea.interactive.oom.BySync {
  public int i;

  public com.staryea.interactive.oom.BySync();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public void test();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter   //进入同步方法
       4: aload_0
       5: dup
       6: getfield      #2                  // Field i:I
       9: iconst_1
      10: iadd
      11: putfield      #2                  // Field i:I
      14: aload_1
      15: monitorexit   //退出同步方法
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit   //发生异常时,退出同步方法
      22: aload_2
      23: athrow
      24: return
    Exception table:
       from    to  target type
           4    16    19   any
          19    22    19   any
}

如上字节码可知,锁的实现是通过进入和退出monitor对象完成,monitorenter和monitorexit分别代表了同步代码块的开始和结束位置。当jvm执行monitorenter指令时,当前线程则会试图获取对象锁所对应的monitor对象的所有权。当monitor对象进入计数器为0时,则表示获取monitor成功,此时计数器加1,如果当前线程获取了monitor的持有权时,那此线程可以重入这个monitor。执行线程执行完毕,monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。
3.1、同步方法

public class SyncMethod {

   public int i;

   public synchronized void syncTask(){
           i++;
   }
}

编译上述代码并使用javap反编译后得到字节码如下:

Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/hs/concurrencys/SyncMethod.class
  Last modified 2017-6-2; size 308 bytes
  MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
  Compiled from "SyncMethod.java"
public class com.zejian.concurrencys.SyncMethod
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool;

   //省略没必要的字节码
  //==================syncTask方法======================
  public synchronized void syncTask();
    descriptor: ()V
    //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
}
SourceFile: "SyncMethod.java"

从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。同时我们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期(jdk6之前)的synchronized效率低的原因。

四、synchronized锁底层实现

4.1、对象头
HotSpot虚拟机中,对象在堆内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
Java多线程:深入理解synchronized_第1张图片
在对象头中分为两个部分,第一部分是类型指针,用于表示是哪一个类的对象。第二部分存储了关于对象运行时的数据,比如GC年龄,hashcode,锁状态标志等,这一部分也被称为Mark Word。

Java多线程:深入理解synchronized_第2张图片
这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

在ObjectMonitor类中我们需要关注四个属性_count、_owner、_WaitSet、_EntryList,多个线程同时访问一个同步代码块或者同步方法时,先将线程加入到_EntryList队列中,当线程获取到monitor对象的持有权时,并把当前线程赋值给_owner对象,并且计数器_count加1,当调用wait()方法时,则释放此线程当前持有的monitor对象,_owner属性赋值为null,_count减一,并将此线程放入到_WaitSet队列中等待下次被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位属性的值,以便其他线程进入获取monitor。

参考文章:
https://blog.csdn.net/javazejian/article/details/72828483#synchronized

https://blog.csdn.net/gentlezuo/article/details/91410716

你可能感兴趣的:(JAVA)