1.Java中的锁
Java中的锁主要用于保障多并发线程情况下数据的一致性。在多线程编程中为了保障数据的一致性,我们通常需要在使用对象或者方法之前加锁,这时如果有其他线程也需要使用该对象或者该方法,则首先要获得锁,如果某个线程发现锁正在被其他线程使用,就会进入阻塞队列等待锁的释放,直到其他线程执行完成并释放锁,该线程才有机会再次获取锁进行操作。这样就保障了在同一时刻只有一个线程持有该对象的锁并修改对象,从而保障数据的安全。
锁从乐观和悲观的角度可分为乐观锁和悲观锁,从获取资源的公平性角度可分为公平锁和非公平锁,从是否共享资源的角度可分为共享锁和独占锁,从锁的状态的角度可分为偏向锁、轻量级锁和重量级锁。同时,在JVM中还巧妙设计了自旋锁以更快地使用CPU资源。下面将详细介绍这些锁。
2.乐观锁
乐观锁采用乐观的思想处理数据,在每次读取数据时都认为别人不会修改该数据,所以不会上锁,但在更新时会判断在此期间别人有没有更新该数据,通常采用在写时先读出当前版本号然后加锁的方法。具体过程为:比较当前版本号与上一次的版本号,如果版本号一致,则更新,如果版本号不一致,则重复进行读、比较、写操作。
Java中的乐观锁大部分是通过CAS(Compare And Swap,比较和交换)操作实现的,CAS是一种原子更新操作,在对数据操作之前首先会比较当前值跟传入的值是否一样,如果一样则更新,否则不执行更新操作,直接返回失败状态。
3.悲观锁
悲观锁采用悲观思想处理数据,在每次读取数据时都认为别人会修改数据,所以每次在读写数据时都会上锁,这样别人想读写这个数据时就会阻塞、等待直到拿到锁。
Java中的悲观锁大部分基于AQS(Abstract Queued Synchronized,抽象的队列同步器)架构实现。AQS定义了一套多线程访问共享资源的同步框架,许多同步类的实现都依赖于它,例如常用的Synchronized、ReentrantLock、Semaphore、CountDownLatch等。该框架下的锁会先尝试以CAS乐观锁去获取锁,如果获取不到,则会转为悲观锁(如RetreenLock)。
4.自旋锁
自旋锁认为:如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞、挂起状态,只需等一等(也叫作自旋),在等待持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程在内核状态的切换上导致的锁时间消耗。
线程在自旋时会占用CPU,在线程长时间自旋获取不到锁时,将会产CPU的浪费,甚至有时线程永远无法获取锁而导致CPU资源被永久占用,所以需要设定一个自旋等待的最大时间。在线程执行的时间超过自旋等待的最大时间后,线程会退出自旋模式并释放其持有的锁。
5.自旋锁的优缺点
- 优点:自旋锁可以减少CPU 上下文的切换,对于占用锁的时间非常短或锁竞争不激烈的代码块来说性能大幅度提升,因为自旋的CPU 耗时明显少于线程阻塞、挂起、再唤醒时两次CPU上下文切换所用的时间。
- 缺点:在持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,将引起CPU 的浪费。所以在系统中有复杂锁依赖的情况下不适合采用自旋锁。
6.自旋锁的时间阈值
自旋锁用于让当前线程占着CPU的资源不释放,等到下次自旋获取锁资源后立即执行相关操作。但是如何选择自旋的执行时间呢?如果自旋的执行时间太长,则会有大量的线程处于自旋状态且占用CPU资源,造成系统资源浪费。因此,对自旋的周期选择将直接影响到系统的性能!
JDK的不同版本所采用的自旋周期不同,JDK 1.5为固定DE时间,JDK1.6引入了适应性自旋锁。适应性自旋锁的自旋时间不再是固定值,而是由上一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的,可基本认为一个线程上下文切换的时间是就一个最佳时间。
7.synchronized
synchronized关键字用于为Java对象、方法、代码块提供线程安全的操作。synchronized属于独占式的悲观锁,同时属于可重入锁。在使用synchronized修饰对象时,同一时刻只能有一个线程对该对象进行访问;在synchronized修饰方法、代码块时,同一时刻只能有一个线程执行该方法体或代码块,其他线程只有等待当前线程执行完毕并释放锁资源后才能访问该对象或执行同步代码块。
Java中的每个对象都有个monitor对象,加锁就是在竞争monitor对象。对代码块加锁是通过在前后分别加上monitorenter和monitorexit指令实现的,对方法是否加锁是通过一个标记位来判断的。
synchronized的作用范围
- synchronized作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象。
- synchronized作用于静态方法时,锁住的是Class实例,因为静态方法属于Class而不属于对象。
- synchronized作用于一个代码块时,锁住的是所有代码块中配置的对象。
synchronized的用法简介
synchronized作用于成员变量和非静态方法时,锁住的是对象的实例。
public static void main(String[] args){
final SynchronizedDemo demo=new SynchronizedDemo();
new Thread(new Runnable(){
@Override
public void run(){
demo.generalMethod1();
}
}).start();
new Thread(new Runnable(){
@Override
public void run(){
demo.generalMethod2();
}
}).start();
}
//synchronized修饰普通的同步方法,锁住的是当前实例对象
public synchronized void generalMethod1(){
}
//synchronized修饰普通的同步方法,锁住的是当前实例对象
public synchronized void generalMethod2(){
}
上面的程序定义了两个使用synchronized修饰的普通方法,然后在main函数中定义对象的实例并发执行各个方法。我们看到,线程1会等待线程2执行完成才能执行,这是因为synchronized锁住了当前的对象实例synchronizedDemo导致的。具体的执行结果如下:
generalMethod1 execute 1
generalMethod1 execute 2
generalMethod2 execute 1
generalMethod2 execute 2
稍微把程序修改一下,定义两个实例分别调用两个方法,程序就能并发执行起来了:
final SynchronizedDemo demo1=new SynchronizedDemo();
final SynchronizedDemo demo2=new SynchronizedDemo();
new Thread(new Runnable(){
@Override
public void run(){
demo1.generalMethod1();
}
}).start();
new Thread(new Runnable(){
@Override
public void run(){
demo2.generalMethod2();
}
}).start();
具体的执行结果如下:
generalMethod1 execute 1
generalMethod2 execute 1
generalMethod1 execute 2
generalMethod2 execute 2
synchronized作用于静态同步方法,锁住的是当前类的Class对象,具体的使用代码如下,我们只需在以上方法上加上static关键字即可:
final SynchronizedDemo demo1=new SynchronizedDemo();
final SynchronizedDemo demo2=new SynchronizedDemo();
//synchronized修饰静态同步方法,锁住的是当前类的Class对象
public static synchronized void generalMethod1(){
}
//synchronized修饰静态同步方法,锁住的是当前类的Class对象
public static synchronized void generalMethod2(){
}
以上代码首先定义了两个static的synchronized方法,然后定义了两个实例分别执行这两个方法,具体的执行结果如下:
generalMethod1 execute 1
generalMethod1 execute 2
generalMethod2 execute 1
generalMethod2 execute 2
我们通过日志能清晰地看到,因为static方法是属于Class的,并且Class的相关数据在JVM中是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁住所有调用该方法的线程。
synchronized作用于一个代码块时,锁住的是在代码块中配置的对象。具体的实现代码如下:
String lockA="lockA";
final SynchronizedDemo demo=new SynchronizedDemo();
//synchronized用于方法块,锁住的是在括号里面配置的对象
public void generalMethod1(){
try{
synchronized(lockA){
}
}
}
//synchronized用于方法块,锁住的是在括号里面配置的对象
public void generalMethod2(){
try{
synchronized(lockA){
}
}
}
以上代码的执行结果很简单,由于两个方法都需要获取名为lockA的锁,所以线程 1会等待线程2执行完成后才能获取该锁并执行:
generalMethod1 execute 1
generalMethod1 execute 2
generalMethod2 execute 1
generalMethod2 execute 2
我们在写多线程程序时可能会出现A线程依赖B线程中的资源,而B线程又依赖于A线程中的资源的情况,这时就可能出现死锁。我们在开发时要杜绝资源相互调用的情况。
8.synchronized的实现原理
在synchronized内部包括ContentionList、EntryList、WaitSet、OnDeck、Owner、!Owner这6个区域,每个区域的数据都代表锁的不同状态。
- ContentionList:锁竞争队列,所有请求锁的线程都被放在竞争队列中。
- EntryList:竞争候选列表,在Contention List中有资格成为候选者来竞争锁资源的线程被移动到了Entry List中。
- WaitSet:等待集合,调用wait方法后被阻塞的线程将被放在WaitSet中。
- OnDeck:竞争候选者,在同一时刻最多只有一个线程在竞争锁资源,该线程的状态被称为OnDeck。
- Owner:竞争到锁资源的线程被称为Owner状态线程。
- !Owner:在Owner线程释放锁后,会从Owner的状态变成!Owner。
synchronized在收到新的锁请求时首先自旋,如果通过自旋也没有获取锁资源,则将被放入锁竞争队列ContentionList中。
为了防止锁竞争时ContentionList尾部的元素被大量的并发线程进行CAS访问而影响性能,Owner线程会在释放锁资源时将ContentionList中的部分线程移动到EntryList中,并指定EntryList中的某个线程(一般是最先进入的线程)为OnDeck线程。Owner线程并没有直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,让OnDeck线程重新竞争锁。在Java中把该行为称为“竞争切换”,该行为牺牲了公平性,但提高了性能。
获取到锁资源的OnDeck线程会变为Owner线程,而未获取到锁资源的线程仍然停留在EntryList中。
Owner线程在被wait方法阻塞后,会被转移到WaitSet队列中,直到某个时刻被notify方法或者notifyAll方法唤醒,会再次进入EntryList中。ContentionList、EntryList、WaitSet中的线程均为阻塞状态,该阻塞是由操作系统来完成的(在Linux内核下是采用pthread_mutex_lock内核函数实现的)。
Owner线程在执行完毕后会释放锁的资源并变为!Owner状态。
在synchronized中,在线程进入ContentionList之前,等待的线程会先尝试以自旋的方式获取锁,如果获取不到就进入ContentionList,该做法对于已经进入队列的线程是不公平的,因此synchronized是非公平锁。另外,自旋获取锁的线程也可以直接抢占OnDeck线程的锁资源。
synchronized是一个重量级操作,需要调用操作系统的相关接口,性能较低,给线程加锁的时间有可能超过获取锁后具体逻辑代码的操作时间。
JDK 1.6对synchronized做了很多优化,引入了适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等以提高锁的效率。锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫作锁膨胀。在JDK 1.6中默认开启了偏向锁和轻量级锁,可以通过-XX:UseBiasedLocking禁用偏向锁。
9.ReentrantLock
ReentrantLock继承了Lock接口并实现了在接口中定义的方法,是一个可重入的独占锁。ReentrantLock通过自定义队列同步器(AbstractQueued Sychronized,AQS)来实现锁的获取与释放。
独占锁指该锁在同一时刻只能被一个线程获取,而获取锁的其他线程只能在同步队列中等待;可重入锁指该锁能够支持一个线程对同一个资源执行多次加锁操作。
ReentrantLock支持公平锁和非公平锁的实现。公平指线程竞争锁的机制是公平的,而非公平指不同的线程获取锁的机制是不公平的。
ReentrantLock不但提供了synchronized对锁的操作功能,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
10.ReentrantLock的用法
ReentrantLock有显式的操作过程,何时加锁、何时释放锁都在程序员的控制之下。具体的使用流程是定义一个ReentrantLock,在需要加锁的地方通过lock方法加锁,等资源使用完成后再通过unlock方法释放锁。具体的实现代码如下:
public class ReentrantLock implements Runnable{
//定义一个ReentrantLock
public static ReentrantLock lock=new ReentrantLock();
public static int i=0;
public void run(){
for(int j=0;j<10;j++){
lock.lock();//加锁,可重入锁
}
try{
i++;
}finally{
lock.unlock();//释放锁
}
}
}
ReentrantLock之所以被称为可重入锁,是因为ReentrantLock锁可以反复进入。即允许连续两次获得同一把锁,两次释放同一把锁。将上述代码中的注释部分去掉后,程序仍然可以正常执行。注意,获取锁和释放锁的次数要相同,如果释放锁的次数多于获取锁的次数,Java就会抛出java.lang.IllegalMonitorStateException异常;如果释放锁的次数少于获取锁的次数,该线程就会一直持有该锁,其他线程将无法获取锁资源。
11.ReentrantLock如何避免死锁
响应中断
在synchronized中如果有一个线程尝试获取一把锁,则其结果是要么获取锁继续执行,要么保持等待。ReentrantLock还提供了可响应中断的可能,即在等待锁的过程中,线程可以根据需要取消对锁的请求。具体的实现代码如下:
//如果当前线程未被中断,则获取锁
lock.lockInterruptibly();
//检查当前线程是否持有该锁,如果持有释放该锁
if(lock.isHeldByCurrentThread){
lock.unlock();
}
Long time=System.currentTimeMillis();
InterruptiblyLock interruptiblyLock=new InterruptiblyLock();
Thread thread1=interruptiblyLock.lock1();
Thread thread2=interruptiblyLock.lock2();
//自旋一段时间,如果等待时间过长,则可能发生死锁等问题,主动中断并释放锁
while(true){
if(System.currentTimeMillis-time>3000){
thread2.interrupt();//中断线程1
}
}
可轮询锁
通过booleantryLock()获取锁。如果有可用锁,则获取该锁并返回true,如果无可用锁,则立即返回false。
定时锁
通过boolean tryLock(long time,TimeUnit unit)throws InterruptedException获取定时锁。如果在给定的时间内获取到了可用锁,且当前线程未被中断,则获取该锁并返回true。如果在给定的时间内获取不到可用锁,将禁用当前线程,并且在发生以下三种情况之前,该线程一直处于休眠状态。
- 当前线程获取到了可用锁并返回true。
- 当前线程在进入此方法时设置了该线程的中断状态,或者当前线程在获取锁时被中断,则将抛出InterruptedException,并清除当前线程的已中断状态。
- 当前线程获取锁的时间超过了指定的等待时间,则将返回false。如果设定的时间小于等于0,则该方法将完全不等待。
12.Lock接口的主要方法
Lock接口的主要方法如下。
- voidlock():给对象加锁,如果锁未被其他线程使用,则当前线程将获取该锁;如果锁正在被其他线程持有,则将禁用当前线程,直到当前线程获取锁。
- boolean tryLock():试图给对象加锁,如果锁未被其他线程使用,则将获取该锁并返回true,否则返回false。tryLock()和lock()的区别在于tryLock()只是“试图”获取锁,如果没有可用锁,就会立即返回。lock()在锁不可用时会一直等待,直到获取到可用锁。
- tryLock(long timeout TimeUnit unit):创建定时锁,如果在给定的等待时间内有可用锁,则获取该锁。
- void unlock():释放当前线程所持有的锁。锁只能由持有者释放,如果当前线程并不持有该锁却执行该方法,则抛出异常。
- Condition newCondition():创建条件对象,获取等待通知组件。该组件和当前锁绑定,当前线程只有获取了锁才能调用该组件的await(),在调用后当前线程将释放锁。
- getHoldCount():查询当前线程保持此锁的次数,也就是此线程执行lock方法的次数。
- getQueueLength():返回等待获取此锁的线程估计数,比如启动 5个线程,1 个线程获得锁,此时返回4。
- getWaitQueueLength(Condition condition):返回在Condition条件下等待该锁的线程数量。比如有5 个线程用同一个condition对象,并且这 5 个线程都执行了condition对象的await方法,那么执行此方法将返回5。
- hasWaiters(Condition condition):查询是否有线程正在等待与给定条件有关的锁,即对于指定的contidion对象,有多少线程执行了condition.await方法。
- hasQueuedThread(Thread thread):查询给定的线程是否等待获取该锁。
- hasQueuedThreads():查询是否有线程等待该锁。
- isFair():查询该锁是否为公平锁。
- isHeldByCurrentThread():查询当前线程是否持有该锁,线程执行lock方法的前后状态分别是false和true。
- isLock():判断此锁是否被线程占用。
- lockInterruptibly():如果当前线程未被中断,则获取该锁。
13.公平锁与非公平锁
ReentrantLock支持公平锁和非公平锁两种方式。公平锁指锁的分配和竞争机制是公平的,即遵循先到先得原则。非公平锁指JVM遵循随机、就近原则分配锁的机制。
ReentrantLock通过在构造函数ReentrantLock(boolean fair)中传递不同的参数来定义不同类型的锁,默认的实现是非公平锁。这是因为,非公平锁虽然放弃了锁的公平性,但是执行效率明显高于公平锁。如果系统没有特殊的要求,一般情况下建议使用非公平锁。
14.tryLock、lock和lockInterruptibly的区别
tryLock、lock和lockInterruptibly的区别如下。
- tryLock若有可用锁,则获取该锁并返回true,否则返回false,不会有延迟或等待;tryLock(long timeout,TimeUnit unit)可以增加时间限制,如果超过了指定的时间还没获得锁,则返回false。
- lock若有可用锁,则获取该锁并返回true,否则会一直等待直到获取可用锁。
- 在锁中断时lockInterruptibly会抛出异常,lock不会。
15.synchronized和ReentrantLock的比较
synchronized和ReentrantLock的共同点如下。
- 都用于控制多线程对共享对象的访问。
- 都是可重入锁。
- 都保证了可见性和互斥性。synchronized和ReentrantLock的不同点如下。
- ReentrantLock显式获取和释放锁;synchronized隐式获取和释放锁。为了避免程序出现异常而无法正常释放锁,在使用ReentrantLock时必须在finally控制块中进行解锁操作。
- ReentrantLock可响应中断、可轮回,为处理锁提供了更多的灵活性。
- ReentrantLock是API级别的,synchronized是JVM级别的。
- ReentrantLock可以定义公平锁。
- ReentrantLock通过Condition可以绑定多个条件。
- 二者的底层实现不一样:synchronized是同步阻塞,采用的是悲观并发策略;Lock是同步非阻塞,采用的是乐观并发策略。
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是由内置的语言实现的。
- 我们通过Lock可以知道有没有成功获取锁,通过synchronized却无法做到。
- Lock可以通过分别定义读写锁提高多个线程读操作的效率。
16.Semaphore
Semaphore是一种基于计数的信号量,在定义信号量对象时可以设定一个阈值,基于该阈值,多个线程竞争获取许可信号,线程竞争到许可信号后开始执行具体的业务逻辑,业务逻辑在执行完成后释放该许可信号。在许可信号的竞争队列超过阈值后,新加入的申请许可信号的线程将被阻塞,直到有其他许可信号被释放。
//创建一个计数阈值为5的信号量对象,即只能有5个线程同时访问
Semaphore semp=new Semaphore(5);
try{
//申请许可
semp.acquire();
try{
//执行业务逻辑
}catch(Exception e){
}finally{
//释放许可
semp.release();
}
}catch(InterruptedException e){
}
Semaphore对锁的申请和释放和ReentrantLock类似,通过acquire方法和release方法来获取和释放许可信号资源。Semaphone.acquire方法默认和ReentrantLock. lockInterruptibly方法的效果一样,为可响应中断锁,也就是说在等待许可信号资源的过程中可以被Thread.interrupt方法中断而取消对许可信号的申请。
此外,Semaphore也实现了可轮询的锁请求、定时锁的功能,以及公平锁与非公平锁的机制。对公平与非公平锁的定义在构造函数中设定。
Semaphore的锁释放操作也需要手动执行,因此,为了避免线程因执行异常而无法正常释放锁,释放锁的操作必须在finally代码块中完成。
Semaphore也可以用于实现一些对象池、资源池的构建,比如静态全局对象池、数据库连接池等。此外,我们也可以创建计数为1的Semaphore,将其作为一种互斥锁的机制(也叫二元信号量,表示两种互斥状态),同一时刻只能有一个线程获取该锁。
17.AtomicInteger
我们知道,在多线程程序中,诸如++i或i++等运算不具有原子性,因此不是安全的线程操作。我们可以通过synchronized或ReentrantLock将该操作变成一个原子操作,但是synchronized和ReentrantLock均属于重量级锁。因此JVM为此类原子操作提供了一些原子操作同步类,使得同步操作(线程安全操作)更加方便、高效,它便是AtomicInteger。
AtomicInteger为提供原子操作的Integer的类,常见的原子操作类还有AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference等,它们的实现原理相同,区别在于运算对象的类型不同。还可以通过AtomicReference将一个对象的所有操作都转化成原子操作。AtomicInteger的性能通常是synchronized和ReentrantLock的好几倍。具体用法如下:
class AtomicIntegerDemo implements Runnable{
//定义一个原子操作数
static AtomicInteger safeCounter=new AtomicInteger(0);
public void run(){
for(int m=0;m<100000;m++){
safeCounter.getAndIncrement();//对原子操作数执行自增操作
}
}
}
18.可重入锁
可重入锁也叫作递归锁,指在同一线程中,在外层函数获取到该锁之后,内层的递归函数仍然可以继续获取该锁。在Java环境下,ReentrantLock和synchronized都是可重入锁。
19.公平锁与非公平锁
- 公平锁(Fair Lock)指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程。
- 非公平锁(Nonfair Lock)指在分配锁时不考虑线程排队等待的情况,直接尝试获取锁,在获取不到锁时再排到队尾等待。
因为公平锁需要在多核的情况下维护一个锁线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多。Java中的synchronized是非公平锁,ReentrantLock默认的lock方法采用的是非公平锁。
20.读写锁:ReadWriteLock
在Java中通过Lock接口及对象可以方便地为对象加锁和释放锁,但是这种锁不区分读写,叫作普通锁。为了提高性能,Java提供了读写锁。读写锁分为读锁和写锁两种,多个读锁不互斥,读锁与写锁互斥。在读的地方使用读锁,在写的地方使用写锁,在没有写锁的情况下,读是无阻塞的。
如果系统要求共享数据可以同时支持很多线程并发读,但不能支持很多线程并发写,那么使用读锁能很大程度地提高效率;如果系统要求共享数据在同一时刻只能有一个线程在写,且在写的过程中不能读取该共享数据,则需要使用写锁。
一般做法是分别定义一个读锁和一个写锁,在读取共享数据时使用读锁,在使用完成后释放读锁,在写共享数据时使用写锁,在使用完成后释放写锁。在Java中,通过读写锁的接口java.util.concurrent.locks.ReadWriteLoc的实现类ReentrantReadWriteLock来完成对读写锁的定义和使用。具体用法如下:
public class SeafCache{
private final Map<String,Object> cache=new HashMap<String,Object>();
private final ReentrantReadWriteLock rwLock=new ReentrantReadWriteLock();
private final Lock readLock=rwLock.readLock();//定义读锁
private final Lock writeLock=rwLock.writeLock();//定义读锁
//在读数据时加读锁
public Object get(String key){
readLock.lock();
try{
return cache.get(key);
}finally{
readLock.unlock();
}
}
//在写数据时加写锁
public Object put(String key,Object value){
writeLock.lock();
try{
return cache.put(key,value);
}finally{
writeLock.unlock();
}
}
}
21.共享锁和独占锁
Java并发包提供的加锁模式分为独占锁和共享锁。
- 独占锁:也叫互斥锁,每次只允许一个线程持有该锁,ReentrantLock为独占锁的实现。
- 共享锁:允许多个线程同时获取该锁,并发访问共享资源。ReentrantReadWriteLock中的读锁为共享锁的实现。
ReentrantReadWriteLock的加锁和解锁操作最终都调用内部类Sync提供的方法。Sync对象通过继承AQS(Abstract Queued Synchronizer)进行实现。AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,分别标识AQS队列中等待线程的锁获取模式。
独占锁是一种悲观的加锁策略,同一时刻只允许一个读线程读取锁资源,限制了读操作的并发性;因为并发读线程并不会影响数据的一致性,因此共享锁采用了乐观的加锁策略,允许多个执行读操作的线程同时访问共享资源。
22.重量级锁和轻量级锁
重量级锁是基于操作系统的互斥量(Mutex Lock)而实现的锁,会导致进程在用户态与内核态之间切换,相对开销较大。
synchronized在内部基于监视器锁(Monitor)实现,监视器锁基于底层的操作系统的Mutex Lock实现,因此synchronized属于重量级锁。重量级锁需要在用户态和核心态之间做转换,所以synchronized的运行效率不高。
JDK在 1.6版本以后,为了减少获取锁和释放锁所带来的性能消耗及提高性能,引入了轻量级锁和偏向锁。
轻量级锁是相对于重量级锁而言的。轻量级锁的核心设计是在没有多线程竞争的前提下,减少重量级锁的使用以提高系统性能。轻量级锁适用于线程交替执行同步代码块的情况(即互斥操作),如果同一时刻有多个线程访问同一个锁,则将会导致轻量级锁膨胀为重量级锁。
23.偏向锁
除了在多线程之间存在竞争获取锁的情况,还会经常出现同一个锁被同一个线程多次获取的情况。偏向锁用于在某个线程获取某个锁之后,消除这个线程锁重入的开销,看起来似乎是这个线程得到了该锁的偏向(偏袒)。
偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,因为轻量级锁的获取及释放需要多次CAS(Compareand Swap)原子操作,而偏向锁只需要在切换ThreadID时执行一次CAS原子操作,因此可以提高锁的运行效率。
在出现多线程竞争锁的情况时,JVM会自动撤销偏向锁,因此偏向锁的撤销操作的耗时必须少于节省下来的CAS原子操作的耗时。
综上所述,轻量级锁用于提高线程交替执行同步块时的性能,偏向锁则在某个线程交替执行同步块时进一步提高性能。
锁的状态总共有4种:无锁、偏向锁、轻量级锁和重量级锁。随着锁竞争越来越激烈,锁可能从偏向锁升级到轻量级锁,再升级到重量级锁,但在Java中锁只单向升级,不会降级。
24.分段锁
分段锁并非一种实际的锁,而是一种思想,用于将数据分段并在每个分段上都单独加锁,把锁进一步细粒度化,以提高并发效率。ConcurrentHashMap在内部就是使用分段锁实现的。
25.同步锁与死锁
在有多个线程同时被阻塞时,它们之间若相互等待对方释放锁资源,就会出现死锁。为了避免出现死锁,可以为锁操作添加超时时间,在线程持有锁超时后自动释放该锁。
26.如何进行锁优化
减少锁持有的时间
减少锁持有的时间指只在有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间。
减小锁粒度
减小锁粒度指将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来增加锁的并行度,减少同一个锁上的竞争。在减少锁的竞争后,偏向锁、轻量级锁的使用率才会提高。减小锁粒度最典型的案例就是ConcurrentHashMap中的分段锁。
锁分离
锁分离指根据不同的应用场景将锁的功能进行分离,以应对不同的变化,最常见的锁分离思想就是读写锁(ReadWriteLock),它根据锁的功能将锁分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,既保证了线程的安全性,又提高了性能。
操作分离思想可以进一步延伸为只要操作互不影响,就可以进一步拆分,比如LinkedBlockingQueue从头部取出数据,并从尾部加入数据。
锁粗化
锁粗化指为了保障性能,会要求尽可能将锁的操作细化以减少线程持有锁的时间,但是如果锁分得太细,将会导致系统频繁获取锁和释放锁,反而影响性能的提升。在这种情况下,建议将关联性强的锁操作集中起来处理,以提高系统整体的效率。
锁消除
在开发中经常会出现在不需要使用锁的情况下误用了锁操作而引起性能下降,这多数是因为程序编码不规范引起的。这时,我们需要检查并消除这些不必要的锁来提高系统的性能。