在开发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的好几倍。

Java常用锁机制简介的更多相关文章

  1. Java的锁机制--synchronsized关键字

    引言 高并发环境下,多线程可能需要同时访问一个资源,并交替执行非原子性的操作,很容易出现最终结果与期望值相违背的情况,或者直接引发程序错误. 举个简单示例,存在一个初始静态变量count=0,两个线程 ...

  2. [java多线程] - 锁机制&同步代码块&信号量

    在美眉图片下载demo中,我们可以看到多个线程在公用一些变量,这个时候难免会发生冲突.冲突并不可怕,可怕的是当多线程的情况下,你没法控制冲突.按照我的理解在java中实现同步的方式分为三种,分别是:同 ...

  3. java的锁机制

    一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁): 如果这个时候同步对象的锁被其他线程拿走了,他(这个线 ...

  4. lesson3:java的锁机制原理和分析

    jdk1.5之前,我们对代码加锁(实际是对象加锁),都是采用Synchronized关键字来处理,jdk1.5及以后的版本中,并发编程大师Doug Lea在concurrrent包中提供了Lock机制 ...

  5. 深入浅出 Java Concurrency 锁机制 : AQS

    转载:http://www.blogjava.net/xylz/archive/2010/07/06/325390.html 在理解J.U.C原理以及锁机制之前,我们来介绍J.U.C框架最核心也是最复 ...

  6. java的锁机制——synchronized

    一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在java里边就是拿到某个同步对象的锁(一个对象只有一把锁): 如果这个时候同步对象的锁被其他线程拿走了,他(这个线 ...

  7. [置顶] 深入探析Java线程锁机制

    今天在iteye上提了一个关于++操作和线程安全的问题,一位朋友的回答一言点醒梦中人,至此我对Java线程锁有了更加深刻的认识.在这里也做个总结供大家参考. 先看几段代码吧! 代码一: public  ...

  8. Java 线程锁机制 -Synchronized Lock 互斥锁 读写锁

    (1)synchronized 是互斥锁: (2)ReentrantLock 顾名思义 :可重入锁 (3)ReadWriteLock :读写锁 读写锁特点: a)多个读者可以同时进行读b)写者必须互斥 ...

  9. JAVA 各种锁机制

    可重入锁 可重锁是指同一个线程,外层函数获取锁后,内层函数可以自动获取到锁. java中synchronized和ReentrantLock都是可重入锁. 对于synchronized,其实现机制有j ...

随机推荐

  1. GitHub 初探

    前言:有时候在公司上班时自己写了一些代码,打算下班回家后继续写,或者在家修改好的代码第二天要拷到公司继续完善,代码就经常要在这两者之间来回同步,通常情况下我用网盘或者U盘,但是实在是很麻烦,不断的备份 ...

  2. MySQL 使用explain查看执行计划

    使用explain查看执行计划, 下面是针对这两条语句进行分析,其查询结果是一样的. EXPLAIN select n.id,n.title from info n inner join info_t ...

  3. Ruby--正则

    1. 只取数字(用的是字符串替换) gsub(/[^0-9]/, “”)

  4. 浏览器指纹 - HTTP cookie

    http://www.iefans.net/ruhe-fangfan-xielu-shangwang-yinsi/ http://www.iefans.net/cookie-yinsi-guanxi/ ...

  5. php session 跨页失效问题

    原因是session.savepath 目录不存在或者没有读写权限

  6. js 键盘移动div、img对象

    js 键盘移动div.img对象 <html> <script type="text/javascript"> var EXtype="" ...

  7. 面向对象之abstract

    1.abstract class,抽象类不能被实例化,只能被继承:抽象类中可以包含非抽象方法 2.abstract method();抽象方法只能在抽象类中进行声明,并且没有方法体,非抽象继承子类中必 ...

  8. js 判断js函数、变量是否存在

    //是否存在指定函数 function isExitsFunction(funcName) { try { if (typeof(eval(funcName)) == "function&q ...

  9. Android源码剖析之Framwork层后记篇(硬件消息传递、apk管理、输入法框架、编译过程)

    本文来自http://blog.csdn.net/liuxian13183/ ,引用必须注明出处! 既然写到后记篇,就代表本系列到此为止,暂时告一段落:其他一些Manager随后有时间再补,就像源码的 ...

  10. 使用C++还是QML

    本质上,Qt 是一个C++类库.在引入 QML 以前,所有的开发都是基于 C++ 的,但到了 Qt 5,QML 和 Qt Quick 成为了 Qt 的核心之一,导致很多初学者在犹豫是否还需要学习 C+ ...