Java多线程与并发-synchronized详解

Java多线程与并发-synchronized详解

理解synchronized原理之前,先看一个例子:
public class Application implements Runnable{

  static Application application=new Application();
  
  public static void main(String[] args) throws Exception{
  
          Thread thread1=new Thread(application);
		  Thread thread2=new Thread(application);
		  thread1.start();
		  thread2.start();
  }
  
  @Override
  public void run(){
  synchronizd(Application.class){
       System.out.println("线程"+Thread.currentThread.getName());
	   try{
	      Thread.sleep(4000);
	   }catch(){
	      e.printStackTrace(); 
	   }
         System.out.println(Thread.currentThread.getName()+"结束");
     }
  }

}

两个线程先后被执行,一个线程必须在另一个线程执行完后才能执行,因为两个线程使用同一个当前实例对象锁,只有一个线程释放该锁后,另一个线程才能拿到该锁。

Synchronized的原理

分析其原理其实就是分析加锁和释放锁,通过反编译得到两个最重要的指令Monitorenter和Monitorexit,每一个锁对象同一时间只与一个monitor相关联,每一个锁对象在同一个时间只能被一个线程获得。一个对象锁获得monitor所有权后,monitor中计数器会发生变化。
一个线程在获得对象锁之前,monitor计数器必须为0,否则需要等待其他线程释放锁。一旦线程获得锁后monitor计数器加一,其他线程处于等待状态。如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加。
一个线程释放锁就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。
下图展示了线程、对象锁、同步队列、加锁、释放锁之间流程关系:

image
可重入原理:一个线程执行两个静态方法,但是两个方法拥有同一个对像锁,当一个方法被执行完后不需要再次获得该锁,直接计数器加一继续执行,只有一条monitorexit指令,并没有monitorenter获取锁的指令。
synchronized的锁类型
JVM中锁的monitorenter和monitorexit依赖底层操作系统中Mutex Lock来实现,我们知道传统锁操作需要很大系统性能开销。在单线程无抢锁环境下将严重浪费不必要系统性能开销。基于此jvm中对锁进行了优化,出现了轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、自旋锁与自适应自旋锁等技术减少锁操作带来的性能开销。synchronized同步锁一共有四种状态按照锁竞争升级依次为无锁、偏向锁、轻量级所、重量级锁。根据情况不同,锁会出现处于不同状态,目的是为了获取锁和释放锁提高效率,节省系统性能开销。

重量级锁:
传统锁jvm中内置锁在有多个线程竞争情况下,一个线程抢到该锁,其他线程被挂起和阻塞直到锁被释放,监视器锁直接对应底层操作系统中的互斥量(mutex lock)。系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等,这种同步方式的成本非常高,这种锁为“重量级锁”。

自旋锁:
在传统没有锁优化加入进来情况下,多线程在竞争一个锁时,出现如下情况:
image
在很大一部分情况下,一个线程对一个锁拥有时间很短,其他线程为了这一段很小时间去调用操作系统内核态实现挂起和恢复操作,这对操作系统带来很大且不必要的性能压力。在此情况下出现自旋锁,让另一个没有获取到锁的线程在门外等待一会(自旋),等待持有锁的线程释放锁。
image
优点:拥有锁的线程占用锁时间非常短,自旋锁性能就会非常好。
缺点:线程在自旋会一直占据cpu资源,如果锁占用的时间太长,那么自旋的线程会白白消耗掉CPU资源。因此必须设定自旋次数。默认为10次,超过了限定的次数仍然没有成功获取到锁,就应该使用传统的方式去挂起线程了。但是有一个问题线程锁在线程自旋刚结束就释放掉了锁,那么是不是有点得不偿失。所以这时候我们需要更加聪明的锁来实现更加灵活的自旋。

自适应自旋锁:
意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋 时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么JVM会认为该锁自旋获取到锁的可能性很大,会自动增加等待时间。比如增加到100此循环。相反,如果对于某个锁,自旋很少成功获取锁。那再以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
image

锁粗化:
如果存在连串的一系列操作都对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要地性能操作。比如JVM会检测到这样一连串地操作都是对同一个对象加锁,那么JVM会将加锁同步地范围扩展(粗化)到整个一系列操作的外部,只需要加锁一次就可以了。

轻量级锁:
相对于重量级锁而言,减少重量级锁对线程的阻塞带来地线程开销,从而提高并发性能。在一些情况下,同步代码块不存在共享数据,也就不存在竞争关系,此时可以使用轻量级锁。
线程执行同步块之前:
image
JVM在执行当前线程时,如果当前对象没有被锁定,那么锁标志位位01状态。
1. jvm首先会在当前线程栈帧中创建锁记录Lock Record。
2. jvm使用CAS操作将标记字段Mark Word拷贝到锁记录中。
3. 将对像锁中的Mark Word更新为指向Lock Record的指针。
4.对象Mark Word的锁标志位更新为(Mark Word中最后的2bit)00。
如果成功,表示该线程就有用了该对象的锁,该对象锁为轻量级锁状态。如果失败,jvm会检查当前的Mark Word中是否存在指向当前线程的栈帧的指针,如果有,说明该锁已经被获取,可以直接调用。如果没有,则表示被别的锁占有,就存在竞争关系,升级为重量级锁。

总结:
多个线程存在竞争情况下,为了保证共享数据安全性,其必须要加锁保护,但是加锁、释放锁操作是不小的系统性能开销。传统情况下,加锁保证了共享数据的安全性,但是也增加系统性能开销。为了在保证共享数据安全性前提下,同时降低系统性能开销,JVM在内置锁上做了非常多的优化,膨胀式的锁分配策略就是其一,根据线程竞争激烈程度不同,出现自旋锁、自适应自旋锁、偏向锁、轻量级锁、重量级锁。

hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » Java多线程与并发-synchronized详解