synchronized

一、定义

        如果某一个资源被多个线程共享,为了避免因为资源抢占导致资源数据错乱,需要对线程进行同步,也就是让他们阻塞等待。synchronized就是实现线程同步的关键字。也被称为同步锁。

        synchronized的作用是保证在同一时刻,被其修饰的代码块或方法只会有一个线程执行,已达到并发安全的效果。

二、特性

  • 原子性:所谓原子性就是多个操作要么全部执行成功,要么都不执行成功。
  • 可见性:可见性是指多个线程访问同一个资源的时候,该资源的状态,值等信息其他线程是可见的。(通过“在执行unlock之前,必须先把变量同步回主内存”实现)
  • 有序性:保证代码块和方法体执行顺序和出现顺序一致。(通过“一个变量在同一时刻只允许一个线程对其进行lock操作”实现)

三、用法

        从语法上看,synchronized可以将任何非null对象作为“锁”。在HotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor)。

        三种用法:(1)修饰静态方法(2)修饰成员函数(3)直接修饰一个代码块

    // (1)修饰静态方法
    public synchronized static void syncTest(){
        System.out.println("hello synchronized");
    }

    // (2)修饰成员函数
    public synchronized void syncTest(){
        System.out.println("hello synchronized");
    }

    // (3)修饰代码块
    public  void syncTest(){
        synchronized(this) {
            System.out.println("hello synchronized");
        }
    }

 四、锁的实现

        synchronized上锁有两种形式,一是在方法上上锁,二是在代码块上上锁。他们的上锁策略都是一样的,都是在同步代码执行前获取锁,获取成功后在计数器上+1,同步代码执行完后计数器-1,获取失败则阻塞等待锁释放。只是在同步代码块的识别方式不一样,从class字节码文件可以看出来,一个是通过方法flags标志,一个是通过monitorenter和monitorexit指令操作的。

同步代码块:

 

关于monitorenter:

        每个对象都有一个监视器锁(monitor)。当monitor被占用就会处于锁定状态,线程执行monitorenter指令的时候会尝试获取monitor的所有权,过程如下:

         1、如果monitor的进入数为0,则该线程进入monitor,然后进入数设置为1,该线程即为monitor所有者。

        2、如果线程占有monitor,只是重新进入,则进入数+1。(重入)

        3、如果其他线程占用了monitor,则该线程进入阻塞等待,知道monitor的进入数为0,再重新获取monitor的所有权。

关于monitorexit:

        执行monitorexit指令的线程必须是objectref对应的monitor所有者。

        执行指令时monitor的进入数-1,如果-1后monitor的进入数为0,那么线程退出monitor,不在所有monitor。此时被阻塞的线程可以尝试去获取这个monitor的所有权了。

synchronized的寓意底层是通过一个monitor对象来完成的,其实wait/notify等方法也依赖于monitor对象。这也是为什么只有在被synchronized修饰的方法内或者代码块内才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

同步方法:

 

         从字节码文件上看出当synchronized修饰方法的时候,会使用一个ACC_SYNCHRONIZED常量标识。JVM就是通过这个标识符实现方法同步的:

        当方法调用时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,线程会先获取相应的monitor,获取成功则执行方法,执行完后则释放monitor,获取失败的话,怎进行阻塞等待。

        两种方式在本质上都是基于monitor实现同步控制的,只是方法的同步是基于标识符ACC_SYNCHRONIZED隐式控制,无需字节码来完成。两个monitor指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

        synchronized在修饰静态方法的时候锁对象就是类的class对象。

五、锁的原理和Monitor

(一)、对象内存布局

  1. 当一个对象的大小不足8*n的时候,会对其进行内存填充以达到8的整数倍字节。
  2. 类型指针占4字节是因为默认开启了指针压缩,如果不开启指针压缩则占8字节。

        synchronized用的锁就是存放在Java对象头里面的,什么是Java对象头呢?HotSpot虚拟机的对象头主要包括两部分:mark word(标记字段)、class pointer(类型指针)。其中class pointer是对象指向他的类元数据的指针,通过这个指针确定对象属于哪个类的实例,mark word用于存储对象自身的运行时数据,是实现轻量级锁和偏向锁的关键。

         mark word用于存储对象自身的运行时数据,如:hashCode,GC分带年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间戳等。

         mark word的后三位被设定成了一个跟锁相关的标志位,其中两位是锁标志位,一位是偏向锁标志位。

(二)、监视器(Monitor)

        任何一个对象都有一个Monitor与之相关联,当该对象的Monitor被持有后,它将处于锁定状态。synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体细节不一样,但是通过成对的monitorenter和monitorexit指令来实现。

        monitorenter: 插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取monitor的所有权,即尝试获得该对象的锁。

        monitorexit:插入在同步代码块结束处和异常处,JVM八正每个monitorenter必须有对应的monitorexit。

        每一个Java对象在被创建出来就有一个monitor对象,通常称为内部锁或者monitor锁

        通常说的synchronized的对象锁也就是说的monitor锁,在C++源码实现中,Monitor是由ObjectMonitor实现的。

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 ;
}

        在这个构造函数中,主要关注两个变量:_WaitSet 和 _EntryList,这两个容器,一个用来存放wait的线程,一个用来存放阻塞中的线程。当多个线程访问同一个同步代码的时候:

        1、首先会进入_EntryList集合,当线程获取到对象的Monitor对象的时候,将_owner 设置为当前线程,计数器_count+1。

        2、若线程调用了wait()方法,将释放当前持有的Monitor,_owner = NULL,_count-1,同时将线程put到_WaitSet 中。

        3、若当前线程执行完毕,也将释放Monitor锁,复位_count的值,以便其他线程进入获取。

         Monitor对象存在于每一个对象的对象头mark word中,也就是任意对象都能作为锁的原因,勇士notify/notifyAll/wait等方法会使用到Monitor对象,所以必须在同步代码中使用。

(三)、锁优化

        之前有说过synchronized在字节码层面是monitorenter和monitorexit来实现的,但是真正实现互斥的锁其实是依赖于操作系统底层原语Mutex Lock 来实现的,这是一个重量级锁,由操作系统主导,要想使用它,必须将线程挂起并从用户态切换到内核态来执行,这种代价非常昂贵。

 

在JDK1.6以前,每次获取到的都是重量级锁,在很多场景下性能很低,所以JDK1.6对synchronized锁进行了优化,目的就是减少重量级锁的使用。

 整体的锁升级过程如下:

锁升级(锁膨胀) 

锁有四种状态,其中锁会由实际情况发生膨胀,膨胀方向:无锁-->偏向锁-->轻量级锁-->重量级锁,其中锁的膨胀是不可逆的。

偏向锁

在大多数情况下,锁不存在竞争,总是由同一个线程获得,此时就是偏向锁。

轻量级锁

轻量级锁是由偏向锁升级来的,当存在两个线程同时申请同一个锁的时候,偏向锁会立即升级为轻量级锁,这个时候两个线程并不竞争线程,而是前后交替执行同步代码。(或者理解为少量线程竞争时升级为轻量级锁)

重量级锁

重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁会升级为重量级锁,此时申请锁的开销增大。

重量级锁一般在追求吞吐量,同步块或者同步方法执行时间较长的场景。

六、面试题

        记录几个关于synchronized的面试题。

        单例模式了解吗?给我解释一下双重检验锁方式实现单例模式?

        单例模式分为饿汉式和懒汉式,其中双重检测锁是对懒汉式单例模式的优化。

public class Singleton {
  private volatile static Singleton uniqueInstance;
  private Singleton() {}
  public static Singleton getUniqueInstance() {
     //先判断对象是否已经实例过,没有实例化过才进入加锁代码
	if (uniqueInstance == null) {
      //类对象加锁
		synchronized (Singleton.class) {
        	if (uniqueInstance == null) {
          	uniqueInstance = new Singleton();
        	}
      	}
    }
   	return uniqueInstance;
 }
}
// 其中 volatile 关键保证变量uniqueInstance的可见性,主要是禁止JVM指令重排,保证多线程下的正常运行。

         说一下synchronized底层实现原理?

        1.synchronized的底层是就一个Monitor对象(监视器锁)来实现的。

        2.每个对象都有一个监视器锁(Monitor)。每个被synchronized修饰的代码块或者方法在被执行的时候都会预先去获取锁对象的Monitor,如果能够获取到则执行同步代码,如果失败则进入阻塞等待,具体过程:

                1.如果Monitor的进入数为0,则线程进入Monitor,计数器+1,线程获得Monitor所有权。

                2.如果线程已经占有Monitor,只是重新进入,那么计数器+1 ,

                 3.如果monitor被其他线程占有,那么该线程进入阻塞状态,知道monitor进入数为0,再次重新获取monitor的所有权。

        synchronized的可重入原理?

        重入锁是指一个线程获取到该锁后,该线程可以继续获取该锁。底层原理就是monitor维护了一个计数器,每当线程获取到monitor后,计数器+1,同一线程再次获取后,计数器+1,释放一次,计数器-1。计数器为0时,表示线程不被占有,其他线程可以竞争。

        什么是自旋?

        我们知道线程阻塞会涉及到用户态和内核态切换问题,那么我们考虑在线程试图获取一个已经被占有的monitor的时候,让其在synchronized之前做忙循环,执行一个空循环,让他循环尝试获取,这时候不让出CPU,这样也就避免了线程切换带来的性能损耗。

        synchronized的锁升级原理是什么?

        原理:每个锁对象的对象头里面有一个threadid字段,在第一次访问的时候,threadid为空,此时JVM让其持有偏向锁,并将threadid设置为线程id,在此进入的时候会先判断threadid与线程id是否一致,如果一致则可以使用这个锁,如果不一致,就将锁升级为轻量级锁,通过自旋循环一定次数来获取锁,如果执行一定次数的自旋后,还是没有获取到锁,那么锁升级为重量级锁。

        当线程T1进入到对象O的一个使用synchronized修饰的方法A,那么线程T2能不能进入到线程O中同样用synchronized修饰的方法B呢?

        当然不行。当synchronized修饰对象方法的时候,锁对象的提供者都为this,是通的monitor都是同一个monitor,那么显然线程B是无法获取到monitor对象的,也就无法调用同步方法了。

  • 27
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值