Java常用锁机制简介

时间:2022-07-26 08:06:19

  在开发Java多线程应用程序中,各个线程之间由于要共享资源,必须用到锁机制。Java提供了多种多线程锁机制的实现方式,常见的有synchronized、ReentrantLock、Semaphore、AtomicInteger等。每种机制都有优缺点与各自的适用场景,必须熟练掌握他们的特点才能在Java多线程应用开发时得心应手。

  更多Java锁机制的详细介绍参见文档《Java锁机制详解》。

一、synchronized

  几乎每一个Java开发人员都认识synchronized,使用它来实现多线程的同步操作是非常简单的,只要在需要同步的对方的方法、类或代码块中加入该关键字,它能够保证在同一个时刻最多只有一个线程执行同一个对象的同步代码,可保证修饰的代码在执行过程中不会被其他线程干扰。使用synchronized修饰的代码具有原子性和可见性,在需要进程同步的程序中使用的频率非常高,可以满足一般的进程同步要求(详见《Java多线程基础》)。

  synchronized实现的机理依赖于软件层面上的JVM,因此其性能会随着Java版本的不断升级而提高。事实上,在Java1.5中,synchronized是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。到了Java1.6,synchronized进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的Java1.7与1.8中,均对该关键字的实现机理做了优化。

  需要说明的是,当线程通过synchronized等待锁时是不能被Thread.interrupt()中断的,因此程序设计时必须检查确保合理,否则可能会造成线程死锁的尴尬境地。

  最后,尽管Java实现的锁机制有很多种,并且有些锁机制性能也比synchronized高,但还是强烈推荐在多线程应用程序中使用该关键字,因为实现方便,后续工作由JVM来完成,可靠性高。只有在确定锁机制是当前多线程程序的性能瓶颈时,才考虑使用其他机制,如ReentrantLock等。

二、ReentrantLock

  可重入锁,顾名思义,这个锁可以被线程多次重复进入进行获取操作。ReentantLock继承接口Lock并实现了接口中定义的方法,除了能完成synchronized所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

  Lock实现的机理依赖于特殊的CPU指定,可以认为不受JVM的约束,并可以通过其他语言平台来完成底层的实现。在并发量较小的多线程应用程序中,ReentrantLock与synchronized性能相差无几,但在高并发量的条件下,synchronized性能会迅速下降几十倍,而ReentrantLock的性能却能依然维持一个水准,因此我们建议在高并发量情况下使用ReentrantLock。

  ReentrantLock引入两个概念:公平锁与非公平锁。公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁。反之,JVM按随机、就近原则分配锁的机制则称为不公平锁。ReentrantLock在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。这是因为,非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。

  ReentrantLock通过方法lock()与unlock()来进行加锁与解锁操作,与synchronized会被JVM自动解锁机制不同,ReentrantLock加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用ReentrantLock必须在finally控制块中进行解锁操作。通常使用方式如下所示:

 Lock lock = new ReentrantLock();
try {
lock.lock();
//...进行任务操作
} finally {
lock.unlock();
}

  下面我们详细介绍有关ReentrantLock提供的可响应中断锁、可轮询锁请求、定时锁等机制与操作方式。

  1、线程在等待资源过程中需要中断

  ReentrantLock的在获取锁的过程中有2种锁机制,忽略中断锁和响应中断锁。当等待线程A或其他线程尝试中断线程A时,忽略中断锁机制则不会接收中断,而是继续处于等待状态;响应中断锁则会处理这个中断请求,并将线程A由阻塞状态唤醒为就绪状态,不再请求和等待资源。

  lock.lock()可设置锁机制为忽略中断锁,lock.lockInterruptibly()可设置锁机制为响应中断锁。下述例子描述了,一个写线程和一个读线程分别操作同一个同一个对象的写方法和读方法,写方法需要执行10秒时间,主线程中在启动写线程writer和读线程reader后,启动了第三个线程,这个线程判断当程序执行5秒后,如果读线程依然处于等待状态,就将他中断,不再继续等待资源。

 import java.util.concurrent.locks.ReentrantLock;

 public class ReentrantLockInterrupt {
public static void main(String[] args) {
MyBuffer buffer = new MyBuffer(); //开启写线程
final WriteThread write = new WriteThread(buffer);
write.start(); //开启读线程
final ReadThread read = new ReadThread(buffer);
read.start(); //开启第三个线程,用于监听并中断读线程
new Thread(new Runnable() {
@Override
public void run() {
long readThreadMaxWaitTime = 5000; //读线程最大等待时间,单位:毫秒
long startTime = System.currentTimeMillis();
while(System.currentTimeMillis()-startTime<readThreadMaxWaitTime){}
System.out.println("读线程等待时间已超过"+readThreadMaxWaitTime/1000+"秒,请求中断....");
read.interrupt();
}
}).start();
}
} class WriteThread extends Thread{
private MyBuffer buffer;
public WriteThread(MyBuffer buffer){
this.buffer = buffer;
}
@Override
public void run() {
buffer.write();
}
} class ReadThread extends Thread{
private MyBuffer buffer;
public ReadThread(MyBuffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
try {
buffer.read();
} catch (InterruptedException e) {
System.out.println("读线程已经被中断.....");
}
}
} class MyBuffer {
//使用ReentrantLock锁
private ReentrantLock lock = new ReentrantLock(); //写操作
public void write(){
//lock操作必须放在此处,放于try内就会报错,为什么???
lock.lock();
try {
long writeNeedTime = 10000; //写操作需要时间,单位:毫秒
long writeStartTime = System.currentTimeMillis();
System.out.println("写操作开始,预计执行时间:"+writeNeedTime/1000+"秒....");
while(System.currentTimeMillis()-writeStartTime<writeNeedTime){}
System.out.println("写操作完成....");
} finally {
lock.unlock();
}
} //读操作
public void read() throws InterruptedException {
//lock()方法设置锁机制为“忽略中断锁”,当调用此方法的线程自身或被其他线程请求中断(interrupt)时,操作线程不响应请求,继续处于等待状态
//lockInterruptibly()方法可设置锁机制为“相应式中断锁”,当调用此方法的线程自身或被其他线程请求中断(interrupt)时,线程会相应请求,并在调用当前方法的操作时中断线程,中断后不操作线程后续任务
//以上的响应指的是线程正在获取锁的过程中被请求中断,若线程在其他非阻塞与阻塞状态时被请求中断,lockInterruptibly()是无法响应中断的,
//非阻塞状态可根据中断标记位Thread.currentThread().isInterrupted(),阻塞状态可通过抛出异常InterruptedException来中断线程
//详细可以参考http://www.cnblogs.com/hanganglin/articles/3517178.html中的Thread.interrupt资料
lock.lockInterruptibly();
try {
System.out.println("读操作完成....");
} finally {
lock.unlock();
}
}
}

  由例子可知,ReentrantLock.lockInterruptibly()方法可设置线程在获取锁的时候响应其他线程对当前线程发出的中断请求。但必须注意,此处响应中断锁是指正在获取锁的过程中,如果线程此时并非处于获取锁的状态,通过此方法设置是无法中断线程的,非阻塞状态可根据中断标记位Thread.currentThread().isInterrupted()在程序中手动设置中断,阻塞状态可通过抛出异常InterruptedException来中断线程,详细可参考博文《Java多线程基础》。

  2、实现可轮询的锁请求

  在synchronized中,一旦发生死锁,唯一能够恢复的办法只能重新启动程序,唯一的预防方法是在设计程序时考虑完善不要出错。而有了Lock以后,死锁问题就有了新的预防办法,它提供了tryLock()轮询方法来获得锁,如果锁可用则获取锁,如果锁不可用,则此方法返回false,并不会为了等待锁而阻塞线程,这极大地降低了死锁情况的发生。典型使用语句如下:

Lock lock = ...;
if(lock.tryLock()){
//锁可用,则成功获取锁
try {
//获取锁后进行处理
} finally {
lock.unlock();
}
} else {
//锁不可用,其他处理方法
}

  3、定时锁请求

  在synchronized中,一旦发起锁请求,该请求就不能停止了,如果不能获得锁,则当前线程会阻塞并等待获得锁。在某些情况下,你可能需要让线程在一定时间内去获得锁,如果在指定时间内无法获取锁,则让线程放弃锁请求,转而执行其他的操作。Lock就提供了定时锁的机制,使用Lock.tryLock(long timeout, TimeUnit unit)来指定让线程在timeout单位时间内去争取锁资源,如果超过这个时间仍然不能获得锁,则放弃锁请求,定时锁可以避免线程陷入死锁的境地。

  在上面的实例一中,其他线程在5秒后向正在等候锁的读线程发起中断请求,读线程响应请求并成功中断。也可以在读线程中设置定时锁,设定在5秒内争夺锁,超时则放弃锁,并结束当前的读线程,使用定时锁实现读方法代码如下:

public void read() throws InterruptedException{
//使用定时锁,如果在5秒内仍然不能获得锁,则放弃锁请求
if(lock.tryLock(5,TimeUnit.SECONDS)){
try {
System.out.println("读操作顺利完成,释放锁....");
} finally {
lock.unlock();
}
} else {
System.out.println("读线程在5秒内无法获取锁,放弃请求,结束读线程工作....");
}
}
三、Semaphore

  上述两种锁机制类型都是“互斥锁”,学过操作系统的都知道,互斥是进程同步关系的一种特殊情况,相当于只存在一个临界资源,因此同时最多只能给一个线程提供服务。但是,在实际复杂的多线程应用程序中,可能存在多个临界资源,这时候我们可以借助Semaphore信号量来完成多个临界资源的访问。

  Semaphore基本能完成ReentrantLock的所有工作,使用方法也与之类似,通过acquire()与release()方法来获得和释放临界资源。经实测,Semaphone.acquire()方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt()方法中断。

  此外,Semaphore也实现了可轮询的锁请求与定时锁的功能,除了方法名tryAcquire与tryLock不同,其使用方法与ReentrantLock几乎一致。Semaphore也提供了公平与非公平锁的机制,也可在构造函数中进行设定。

  Semaphore的锁释放操作也由手动进行,因此与ReentrantLock一样,为避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在finally代码块中完成

  Semaphore支持多个临界资源,而ReentrantLock只支持一个临界资源,笔者认为ReentrantLock是Semaphore的一种特殊情况。Semaphore的使用方法与ReentrantLock实在太过相似,在此不再举例说明。

四、AtomicInteger

  首先说明,此处AtomicInteger是一系列相同类的代表之一,常见的还有AtomicLong、AtomicLong等,他们的实现原理相同,区别在与运算对象类型的不同。令人兴奋地,还可以通过AtomicReference<V>将一个对象的所有操作转化成原子操作。

  我们知道,在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。通常我们会使用synchronized将该操作变成一个原子操作,但JVM为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger的性能是ReentantLock的好几倍。