线程同步主要就是两种实现:
- ReentrantLock 显示锁
- Synchronized 隐式锁
一、对synchronized的一个整体认知
1、JDK1.6之前
整体了解那肯定就得从synchronized升级前后开始了解,正所谓士别三日当刮目相待,JDK1.6之前,synchronized确实很笨重,因为他是强依赖于操作系统的,不管你多少个线程,他都默认你竞争激烈,直接上重头戏,哈哈。
2、JDK1.6后,包含1.6
但是JDK1.6则对synchronized做了重大优化,引入了锁粗化,锁消除,偏向锁,轻量级锁,自旋锁等技术来减小锁操作的开销。
就比如当有一个线程时,就是偏向锁。
当两三个线程,但是由于执行速度快,所以竞争不激烈的情况,则是轻量级锁,就比如有线程A,B,C,A拿到线程,B,C两个线程并非严格等待阻塞着,而是循环请求,等待A释放,就是因为本身的竞争不激烈,这也就是所谓的自旋锁。
假如循环了100次还未拿到锁,这种就有可能升级成重量级锁去竞争。
二、锁的膨胀升级过程:
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。从JDK 1.6 中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。
1、偏向锁
偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段。
经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。
比如线程A有可能多次获得锁,当线程A获取了锁,那么引入偏向锁的概念之后,当再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能
默认开启偏向锁
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking
2、轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。
轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”。
适应场景:
轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
3、自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。
三、可修饰的对象
synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。
- 修饰一个代码块,
被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象 - 修饰一个方法
被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象 - 修改一个静态的方法
其作用的范围是整个静态方法,作用的对象是这个类的所有对象 - 修改一个类
其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象
其实理解也就两类,一种是this,取的锁是对象(代码块,非静态方法);一种是class,取的锁是类(静态方法,类)。
下篇则针对原理,实践理解。