您的位置:首页 > 编程语言 > Java开发

读书笔记:Java并发实战 第13章 显式锁

2015-12-31 09:54 585 查看
1、一般用法

<pre name="code" class="java">	Lock lock = new ReentrantLock();

public void method(){
lock.lock();
try{
//...
}finally{
lock.unlock();
}
}




特点:

1、加锁和解锁的方法都是显式的

2、在调用lock()方法时,和进入同步代码块有相同的内存语义

3、在调用unlock()方法时,和退出同步代码块有相同的内存语义

4、独占和可重入

2、轮询锁

先看由于动态的参数顺序造成死锁的例子:

资源类:MyLock

public class MyLock {

public Lock lock = new ReentrantLock();

public String name;

public MyLock(String name){
this.name = name;
}

}


任务类:Executor

public class Executor {

public void work(MyLock a,MyLock b){
String threadName = Thread.currentThread().getName();
a.lock.lock();
System.out.println(threadName+"获得"+a.name+"的锁");
try{
TimeUnit.SECONDS.sleep(1);
System.out.println(threadName+"请求"+b.name+"的锁");
b.lock.lock();
}catch(Exception e){
//...
}finally{
b.lock.unlock();
a.lock.unlock();
}
}

}


测试类:

MyLock x = new MyLock("锁X");
MyLock y = new MyLock("锁y");
Executor exec = new Executor();
ThreadA a = new ThreadA("线程A",x,y,exec);
ThreadB b = new ThreadB("线程B",x,y,exec);
a.start();
b.start();


这段程序运行的结果就会造成死锁,下面,将Executor的work()方法改用轮询锁来解决死锁问题

轮询锁是通过tryLock()方法来实现的,这个方法尝试获取一个锁,如果获取到,返回true,否则返回false,这种立即返回的方式,避免了无效的等待。当嵌套调用锁时,如果里层的tryLock()方法返回false,就释放外层的锁,以便让其他线程获得所有的锁

使用轮询锁需要注意的一个重点是:各线程不要使用相同时间间隔来轮询锁,否则很容易发生活锁,如下面的例子

while(true){
String threadName = Thread.currentThread().getName();
if(a.lock.tryLock()){
System.out.println(threadName+"获得"+a.name+"的锁");
try{
TimeUnit.SECONDS.sleep(1);
System.out.println(threadName+"请求"+b.name+"的锁");
if(b.lock.tryLock()){
try{
System.out.println(threadName+"获得"+b.name+"的锁");
return;
}catch(Exception e){
//...
}finally{
b.lock.unlock();
}

}
}catch(Exception e){
//...
}finally{
a.lock.unlock();
}
}
}


发生活锁的原因是:

线程A得到锁X请求锁Y,线程B得到锁Y请求锁X,此时两个线程都不得到内层锁而放弃外层锁,然后又立即做相同的步骤,这样,活锁就发生了

我们只需要在每次释放外层锁之后,轮询之前等待一个随机时间,就能很大程度避免发生活锁

3、定时锁

在使用tryLock()方法时,可以加入参数,指定超时时间,在这个时间内如果等待不到锁,则会返回false

4、可中断锁

在等待获取内置锁的情况下是不能响应中断的,这让实现可取消的任务变得复杂。

Lock实现了可中断锁,能够响应中断:lock.lockInterruptibli();——获取一个可响应中断的锁

5、公平锁和非公平锁

ReentrantLock的构造函数中提供了公平锁(默认)和非公平锁的选择

公平锁:所有请求锁的线程在队列中等待锁,线程获得锁的顺序按先来先得的原则

非公平锁:当线程请求的锁不能得到时,就在队列中等待,如果线程请求锁时,该锁刚好被释放,那么该线程可以得到这个锁

一般情况下,非公平锁的性能要高于公平锁。因为公平性会造成较多的线程挂起和恢复操作,在恢复一个被挂起的线程与该线程真正运行之间存在严重的延迟,比如:在高并发的情况下,当线程A是否一个锁时,因等待锁而被挂起的线程B会被唤醒,在B被完全唤醒之前,其他线程可能已经完成了"获得-释放"该锁的动作,而并没有影响B获得锁的时刻,如果采用公平锁,其他线程无法请求这个锁,吞吐率就降低了

因此:如果线程持有锁的时间很短,应该尽量使用非公平锁

6、读写锁

互斥锁是最保守的加锁策略,在很多情况下并不需要如此严格的加锁方式,并发的读操作如果使用互斥锁是不必要的,而且会降低读的性能,我们只要保证不会读取到脏数据即可,即:读取数据时不允许写,但允许读。读写锁可以实现这个功能,看下面的例子:

private ReadWriteLock lock = new ReentrantReadWriteLock();
private Lock readLock = lock.readLock();
private Lock writeLock = lock.writeLock();

public void read(){

readLock.lock();

try {
//...
} catch (Exception e) {

}finally{
readLock.unlock();
}
}

public void write(){

writeLock.lock();

try {
//...
} catch (Exception e) {

}finally{
writeLock.unlock();
}
}


线程A持有读锁,线程B请求读锁,线程B可以立即获得读锁

线程A持有读锁,线程B请求写锁:线程B需要在线程A释放读锁后才能获得写锁

线程A持有写锁,线程B请求读锁:线程B需要在线程A释放写锁后才能获得读锁

线程A持有写锁,线程B请求写锁:线程B需要在线程A释放写锁后才能获得写锁

读、写锁之间可以定义不同的交互方式,包括:

1、当释放写锁时,读线程和写线程哪个优先得到锁

2、当释放读锁时,并且队列中有写线程正在等待,此时是否允许读线程插队

3、读、写锁是否允许重入

4、锁降级:线程将持有的写锁在不释放锁的情况下转变为读锁

5、锁升级:线程将持有的读锁在不释放锁的情况下转变为写锁

ReentrantReadWriteLock的公平锁情况下:如果一个线程持有读锁,当等待线程中有请求写锁时,优先于等待的读锁;锁降级是允许的,但锁升级不允许,这样会导致死锁,因为写锁是互斥的,获得写锁要等待所有其他线程释放读写锁,如果两个线程同时升级锁,那么会同时在等待对方释放锁

读写锁的性能

在读操作较多的情况下,读写锁能提高性能,其他情况下要比独占锁的性能差,因为它的实现比较复杂

总结:

Lock提供一种灵活的加锁机制,在很多场合,是不适合使用同步代码块的,同步代码块的缺点和优点集于一身:锁的获取和释放是在同一个代码块中的,好处是我们不必考虑代码块中遇到异常退出时需要如何释放锁,缺点是无法实现连锁式加锁

连锁式加锁是指:在锁分段的应用场景下,比如ConcurrentHashMap,它把散列桶分成不同的段,每个段使用一个独立的锁,当我们需要遍历时,需要持有某个段的锁,直到获得下一个段的锁才释放原有的锁,这种情况下,内置锁是无法完成的

关于synchronized和ReentrantLock的选择

在Java 5.0时代,ReentrantLock的性能比内置锁有更好的竞争性能,而Java 6之后改进了内置锁,二者性能相差无几

由于ReentrantLock存在更高的活跃性危险,因此一般情况下优先使用synchronized,只有需要使用ReentrantLock的特殊功能是才考虑ReentrantLock

并且,在后面的Java版本中可能会提升synchronized而不是ReentrantLock的性能,因为synchronized是JVM的内置属性,可以执行一些优化
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: