欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

synchronized深入理解和探究

程序员文章站 2022-06-17 18:54:15
synchronized线程安全问题的主要原因是:➢ 存在共享数据(也称临界资源)➢ 存在多条线程共同操作这些共享数据解决问题的根本方法:同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作互斥锁的特性互斥性:即在同一时间只允许一个线程持有某 个对象锁,通过这种特性来实现多线程的协调机制,这样在同-时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的...

互斥锁的特性

线程安全问题的主要原因是:
➢ 存在共享数据(也称临界资源)
➢ 存在多条线程共同操作这些共享数据

解决问题的根本方法:
同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行操作


互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称为操作的原子性。

可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值) ,否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。

synchronized满足以上两种特性,还要明确synchronized锁的是对象,不是代码。合理的给对象进行上锁是解决线程安全的关键。

锁的分类

根据获取的锁的分类:获取对象锁和获取类锁

获取对象锁的两种用法

1. 同步代码块 ( synchronized (this) , synchronized (类实例对象)) ,锁是小括号() 中的实例对象。指定加锁对象,
   对给定对象加锁,进入同步代码块前要获得给定对象的锁。

2. 同步非静态方法 ( synchronized method )synchronized修饰的方法 , 锁是当前对象的实例对象。作用于当前对象实例加锁,
   进入同步代码前要获得当前对象实例的锁,synchronized(this)代码块和synchronized 实例方法一样,都是锁定当前对象。

获取类锁的两种用法

3. 同步代码块( synchronized (.class) ) , 锁是小括号()中的类对象(Class对象)4. 同步静态方法 ( synchronized static method ) , 锁是当前对象的类对象(Class对象)synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法是允许的,不会发生互斥现象因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能!

对象锁和类锁的总结

1.有线程访问对象的同步代码块时 ,另外的线程可以访问该对象的非同步代码块;

2.若锁住的是同一个对象, 一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;

3.若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞;

4.若锁住的是同一个对象, 一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然;

5.同一个类的不同对象的对象锁互不干扰;

6.类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于一个类只有一把对象锁 ,
  所以同一个类的不同对象使用类锁将会是同步的;
  
7.类锁和对象锁互不干扰。

底层原理

synchronized深入理解和探究

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。synchronized深入理解和探究

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

Synchronized 和 ReenTrantLock 的对比

① 两者都是可重入锁

两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

③ ReenTrantLock 比 synchronized 增加了一些高级功能

相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)**

  • ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能,也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

如果想使用上述功能,那么选择ReenTrantLock是一个不错的选择。

④ 性能已不是选择标准

在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作

JDK1.6 之后的底层优化

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁消除

synchronized深入理解和探究
JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁,可以减少毫无意义的请求锁时间。

锁粗化

通过扩大加锁的范围,避免反复加锁和解锁。例如:连续的append操作,jvm会检测到一连串操作都对同一对象进行加锁,就会对加锁的同步范围进行粗化到整个操作的外部,使整个一连串的append操作只加一次锁就能完成。
synchronized深入理解和探究

偏向锁

因为大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得。为了减少同一线程获取锁的代价,引入了偏向锁。

核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何**同步**操作,即获取锁的过程只需要检查Mark Word的锁标记位偏向锁以及当前线程Id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作

但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

轻量级锁

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进 入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。适应的场景是:线程交替执行同步块,即

存在同一时间多线程访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作

轻量级锁能够提升程序同步性能的依据的经验是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁。
synchronized深入理解和探究

自旋锁与自适应自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段

互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。

Hotspot团队注意到,许多情况下,共享数据的锁定状态持续时间较短,为了这点点时间去挂起和恢复阻塞线程,这样的线程切换其实并不值得。我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋。即通过让线程执行”忙循环“等待锁的释放,不让出CPU。

缺点:若锁被其他线程长时间占用,会带来许多性能上的开销

因此”自旋“的时间要有一定的限度,如果在一定限度内没有成功获取到锁,就应该采取传统的方式进行挂起,可以通过“PreBlockSpin”进行限制。由于每次线程等待的时间和等待次数是不固定的,PreBlockSpin想要设计的比较合理,就比较困难,因此就出现了自适应自旋锁。

java6中引入了自适应自旋锁,自旋次数(等待时间)不在固定。由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,如果在同一个锁对象上自旋等待上一次刚刚成功获取锁并且持有锁的线程正在运行中,jvm会认为该锁自旋获取到锁的可能性较大,会自动增加等待时间,相反对于某个锁自旋很少成功获取到锁,那在以后要获取这个锁时,将可能省略自旋过程,避免浪费处理器资源。有了自适应自旋,jvm对锁的状态预测越来越精准,jvm也越来越聪明。


本文是通过学习“慕课网”视频,总结的学习笔记,也仅作为学习笔记,如有侵权请谅解,请勿转载。

本文地址:https://blog.csdn.net/ym15229994318ym/article/details/107379824

相关标签: 面试 java