synchronized关键字详解

1.synchronized简介:

​ synchronized是Java多线程中经常使用的一个关键字。synchronized可以保证原子性、可见性、有序性。它包括两种用法:synchronized 方法和 synchronized 代码块。它可以用来给对象、方法或代码块进行加锁。当它锁定一个方法或者一个代码块时,同一时刻最多只有一个线程可以执行这段代码,其他线程想在此时调用该方法只能排队等候。当它锁定一个对象时,同一时刻最多只有一个线程可以对这个类进行操作,没有获得锁的线程,在该类所有对象上的任何操作都不能进行。

锁的类型:

  • synchronized 是悲观锁的实现,因为 synchronized 修饰的代码,每次执行时都会进行加锁操作,同时只允许一个线程进行操作,所以它是悲观锁的实现。

  • synchronized 是非公平锁,并且是不可设置的。这是因为非公平锁的吞吐量大于公平锁,并且是主流操作系统线程调度的基本选择,所以这也是 synchronized 使用非公平锁原因。

  • 同时,synchronized是一个典型的可重入锁,可重入锁最大的作用是避免死锁。

2.实际应用:

例1:

public synchronized void synMethod(){
	//方法体
}

​这时,线程获得的是成员锁,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。

例2:

public Object synMethod(Object a1){
    synchronized(a1){
		//一次只能有一个线程进入
  }
}

​对某一代码块使用 synchronized后跟括号,括号里是变量。如上所示,此时,一次只有一个线程进入该代码块,此时,线程获得的是成员锁。

例3:

public classMyThread implements Runnable{
    public static void main(Stringargs[]){
        Thread t1=newThread(mt,"t1");
        Thread t2=newThread(mt,"t2");
        t1.start();
        t2.start();
}   
public void run(){
    synchronized(this){
      System.out.println(Thread.currentThread().getName());
   }
}

​如果synchronized后面括号里是一个对象,此时,线程获得的是对象锁。如果线程进入,则得到当前对象锁,那么其他没有获得锁的线程,在该类所有对象上的任何操作都不能进行。

例4:

class ArrayWithLockOrder{
 public ArrayWithLockOrder(int[]a){
    	synchronized(ArrayWithLockOrder.class){
          //代码逻辑
      }
 } 
}

​如果synchronized后面括号里是类,此时线程获得的是对象锁。如果其他线程进入,则线程在该类中所有操作不能进行,包括静态变量和静态方法。实际上,对于含有静态方法和静态变量的代码块的同步,我们通常选用例4来加锁。

3.实现原理

​ 为了解决线程安全问题,Java提供了同步机制、互斥锁机制,这个机制保证了在同一时刻只有一个线程能访问共享资源。这个机制的保障来源于监视锁Monitor。每个Object对象中都内置了一个monitor对象,monitor对象存在于每个Java对象的对象头中(存储的是指针)。monitor相当于一个许可证,线程拿到许可证即可以进行操作,没有拿到则需要阻塞等待。任何对象都有一个monitor与之相关联,当monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,会尝试获取对象所对应的monitor的所有权,即尝试获取对象的锁。

·ObjectMonitor中的关键属性:

_owner:指向持有ObjectMonitor对象的线程。

_WaitSet:存放处于wait状态的线程队列。

_EntryList:存放处于等待锁block状态的线程队列。

_recursions:锁的重入次数。

_count:用来记录该线程获取锁的次数。

解释说明:

·当多个线程同时访问,这些线程会被放进_EntryList队列,此时线程处于blocked状态。

·当一个线程获得了对象的monitor后,就可以进入running状态执行方法,此时,ObjectMonitor对象的 _ Owner指向当前线程,_count加1表示当前对象锁被一个线程获取。

·如果running状态的线程调用wait()方法时,当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的 _ owner变为null,_ count减1,同时线程进入 _ WaitSet队列。直到有线程调用notify()方法唤醒该线程,则该线程进入 _ EntryList队列,竞争到锁再进入 _Owner区。

·如果当前线程执行完毕,那么也释放monitor对象,ObjectMonitor对象的_ owner变为null,_count减1

·同步方法和同步代码块的实现原理:

同步代码块:
monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。执行monitorenter指令进入同步代码块,执行monitorexit指令退出同步代码块。但实际应用中,往往是有2个monitorexit指令,第一个是正常退出时执行的,第二个是发生异常时执行的。

同步方法:
synchronized方法的字节码中,并没有看到monitorenter和monitorexit指令。synchronized方法会被翻译成普通方法的调用和返回指令,如:invokevirtual、areturn指令。在JVM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中,会将该方法的access_flags字段中的synchronized标志为1,表示该方法是同步方法。并将调用该方法的对象或该方法所属的Class,在JVM的内部对象表示Klass做为锁对象。

4.三大特性:

synchronized保证原子性:
1.通过monitorenter和monitorexit指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。
2.即使在执行过程中,由于某种原因,比如CPU时间片用完,线程1放弃了CPU,但是它并没有进行解锁。而由于synchronized的锁是可重入的,这就保证下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完为止。

synchronized保证可见性:
对于一个被synchronized修饰的变量,在其解锁之前,必须先把此变量同步回主存当中。

synchronized保证有序性:
​ 尽管synchronized无法禁止指令重排和处理器优化,但是可以通过单线程机制来保证有序性。由于synchronized修饰的代码,在同一时刻只能被同一线程访问,从根本上避免了多线程的情况。而单线程环境下,在本线程内观察到的所有操作都是天然有序的,所以synchronized可以通过单线程的方式来保证程序的有序性。

5.锁升级过程:

synchronized锁优化:

​ 在Java的早期版本中,synchronized属于重量级锁,效率低下,因为操作系统实现线程之间的切换时,需要从用户态转换到核心态,这个状态之间的转换需要较长的时间,时间成本相对较高。在jdk1.6之后,Java官方从JVM层面对synchronized进行优化。为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁。其中,synchronized锁升级是锁优化的一种具体实现方式。

synchronized锁升级:

synchronized锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

无锁 → 偏向锁:
​ 当锁对象第一次被线程获取的时候,虚拟机会将锁对象的对象头中的锁标志位设置为01,并将偏向锁标志设置为1。线程通过CAS的方式将自己的ID值放置到对象头中(因为在这个过程中有可能会有其他线程来竞争锁,所以要通过CAS的方式,一旦有竞争就会升级为轻量级锁)。如果成功,线程就成功的获得到了该对象的偏向锁。这样每次再进入该锁对象的时候,就不用进行任何的同步操作,直接比较当前锁对象的对象头是不是该线程的ID,如果是就可以直接进入。

偏向锁 → 轻量级锁:
​ 偏向锁是一种无竞争锁,一旦出现了竞争,大多数情况下就会升级为轻量级锁。现在我们假设有线程1持有偏向锁,线程2来竞争偏向锁,会经历以下几个过程:

  1. 首先线程2会先检查偏向锁标记,如果是1,说明当前是偏向锁,那么JVM会找到线程1,查看线程1是否还存活着。

  2. 如果线程1已经执行完毕,即线程1已经不存在了(线程1自己不会主动去释放偏向锁),那么先将偏向锁置为0,对象头设置为无锁的状态,用CAS的方式尝试将线程2的ID放入到对象头中,不进行锁升级,还是偏向锁。

  3. 如果线程1还活着,就先暂停线程1,将锁标志位变成00(轻量级锁),将偏向锁变成了轻量级锁,然后继续执行线程1,此时线程2采用CAS的方式尝试获取锁。

轻量级锁 → 重量级锁:
​ 一旦竞争加剧(如自旋次数或自旋线程数超过阈值),轻量级锁就会膨胀为重量级锁,锁的状态变成10。此时,对象头中存储的就是指向重量级锁的栈帧的指针。而其他等待锁的线程要进入阻塞状态,等重量级锁被释放后,再被唤醒,然后去竞争锁。

​ 重型锁可以认为是,直接对应操作系统底层中的互斥量,通过使用互斥量来进行锁的竞争。由于直接使用底层操作系统的调度,会消耗大量性能,所以称之为重型锁

优点缺点使用场景
偏向锁加锁和解锁不需要CAS操作;没有额外的性能消耗;和执行非同步方法相比,仅存在纳秒级的差距。如果线程间存在锁竞争,会带来额外的锁撤销的消耗。适用于基本没有线程竞争锁的同步场景。
轻量级锁竞争的线程不会阻塞,使用自旋,提高了程序的响应速度。如果线程一直不能获取锁,长时间的自旋,会造成CPU的消耗。适用于少量线程竞争锁对象,且线程持有锁的时间不长,同步块执行速度较快,追求响应速度的场景。
重量级锁线程竞争不使用CPU自旋,不会因为CPU空转而消耗CPU资源。线程阻塞,响应时间较长,在多线程下,频繁的获取锁、释放锁,会带来巨大的性能消耗。很多线程竞争锁,且锁持有的时间较长,追求吞吐量的场景。
  • 7
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值