Lock接口通过底层框架的形式为设计更面向对象、可更加细粒度控制线程代码、更灵活控制线程通信提供了基础。实现Lock接口且使用得比较多的是可重入锁ReentrantLock以及读写锁ReentrantReadWriteLock(成员内部类:WriteLock、ReadLock)。
1. ReentrantLock
在synchronized使用及实现原理里面,已经总结过通过使用Synchronized关键字实现线程内的方法锁定。但使用Synchronized关键字有一些局限性,上锁和释放锁是由JVM决定的,用户没法上锁和释放进行控制。那么问题就来了:假如有一个线程业务类管理某一全局变量的读和写。对于每条线程,在读的时候数据是共享的可以让多个线程同时去读。但有某个线程在对该全局变量进行写的时候,其他的线程都不能够对变量进行读或者写(对应数据库内的读共享写互斥)。
ReentrantLock提供了一个可中断、拥有并发竞争机制[指线程对锁的竞争方式:公平竞争或不公平竞争]的方式。
正如ReentrantLock跟Synchronized关键字所使用的功能基本一样,而且Synchronized还能自己释放锁,那什么时候使用ReentrantLock?
- 在中断线程的时候,可以使用ReentrantLock进行控制:如线程1有一个耗时很大的任务在执行,执行时线程2必须进行等待。当线程1执行的任务时间实在太长了,线程2放弃等待进行线程后续的操作。该情况下如果使用Synchronized,只能通过抛出异常的形式进行异常操作。
- 多条件变量通讯:如有3条线程,线程1完成任务后通知线程2执行,线程2执行完业务逻辑以后通知线程3执行,线程3执行完通知线程1继续执行。用Synchronized关键字很难处理这种问题。用Lock却可以很好的处理这些内容。当然,线程1 、2、3 同样地可以换由一个线程组去执行这些任务。
1.1 ReentrantLock对线程中断的控制
首先,单纯地使用synchronized关键字不能进行锁中断控制. 在synchronized关键字控制的代码块内,不会因为线程中断而做出相关处理。
先查看使用synchronized关键字在处理线程中断时的结果。
业务逻辑主要为:开辟两条线程,一条线程对文件进行读操作,另一条线程对文件进行写操作。写操作内容需要时间较长,且先执行。读操作后执行,若读线程等待超过4秒。让读线程中断,进行格式化文件。
使用接口,区分使用synchronized关键字及Lock方式控制线程中断的业务逻辑。
public interface IFileHandler {
boolean isGetReadLock = false;
void read();
void write();
void formatFile();
}
在synchronized关键字控制代码块的前提下,对线程进行中断的业务逻辑代码。synchronized关键字不会去响应线程中断。
public class SyncFileHandler implements IFileHandler {
private volatile boolean isGetReadLock = false;
public boolean isGetReadLock() {
return isGetReadLock;
}
public void read() {
synchronized (FileHandlerByThreads.class.getClass()) {
System.out.println(Thread.currentThread().getName() + " start");
// 能进来则设置变量标志位
isGetReadLock = true;
}
}
// 模拟运行时间比较久的写操作
public void write() {
try {
synchronized (FileHandlerByThreads.class.getClass()) {
System.out.println(Thread.currentThread().getName() + " start");
long startTime = System.currentTimeMillis();
// 模拟一个耗时较长的操作
for (; ; ) {
if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE) {
break;
}
}
}
System.out.println("Writer has writered down everything! bravo");
} catch (Exception e) {
e.printStackTrace();
}
}
public void formatFile() {
System.out.println("begin to format the file");
// format the file
}
}
客户端测试代码
public class TestLock {
public static void main(String[] args) throws Exception {
// 1. 根据lock控制中断
// FileHandlerByThreads fileControl = new FileHandlerByThreads();
// Thread readthr = new Thread(new ReadThread(fileControl), "reader");
// Thread writethr = new Thread(new WriteThread(fileControl), "writer");
// 2. 使用synchronized关键字控制中断线程
SyncFileHandler sync = new SyncFileHandler();
Thread readthr = new Thread(new ReadThread(sync), "reader");
Thread writethr = new Thread(new WriteThread(sync), "writer");
writethr.start();
readthr.start();
long startTime = System.currentTimeMillis();
// 循环判是否有线程获取到了读锁断
while (!sync.isGetReadLock()) {
long endTime = System.currentTimeMillis();
// 如果4秒后读线程仍然没有等到读锁,离开等待
if (endTime - startTime > 4000) {
readthr.interrupt();
System.out.println("4 seconds have passed,try to interrupt reader Thread");
break;
}
}
}
}
class ReadThread implements Runnable {
private IFileHandler fileControl;
public ReadThread(IFileHandler fileControl) {
this.fileControl = fileControl;
}
@Override
public void run() {
fileControl.read();
// 测试单纯使用synchronized关键字控制线程中断
System.out.println("reader thread end");
fileControl.formatFile();
}
}
class WriteThread implements Runnable {
private IFileHandler fileControl;
public WriteThread(IFileHandler fileControl) {
this.fileControl = fileControl;
}
@Override
public void run() {
fileControl.write();
}
}
代码运行结果:线程未中断:
下面使用ReentrantLock实现可中断线程控制
public class FileHandlerByThreads implements IFileHandler {
private volatile boolean isGetReadLock = false;
private ReentrantLock lock = new ReentrantLock();
public boolean isGetReadLock() {
return isGetReadLock;
}
public void read() {
try {
// 等待20毫秒再进行后续操作,防止主线程操作过快
Thread.sleep(50);
// 使用reentrantlock
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " start");
isGetReadLock = true;
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("reader Thread leave the file and going to format the file");
}
}
// 模拟运行时间比较久的写操作
public void write() {
try {
// 1.使用lock实现写锁定
// 等待20毫秒再进行后续操作,防止主线程操作过快
Thread.sleep(20);
lock.lock();
System.out.println(Thread.currentThread().getName() + " start");
long startTime = System.currentTimeMillis();
// 模拟一个耗时较长的操作
for (; ; ) {
if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE) {
break;
}
}
System.out.println("Writer has writered down everything! bravo");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void formatFile() {
System.out.println("begin to format the file");
// format the file
}
}
客户端测试代码
public class TestLock {
public static void main(String[] args) throws Exception {
// 1. 根据lock控制中断
FileHandlerByThreads fileControl = new FileHandlerByThreads();
Thread readthr = new Thread(new ReadThread(fileControl), "reader");
Thread writethr = new Thread(new WriteThread(fileControl), "writer");
// 2. 使用synchronized关键字控制中断线程
// SyncFileHandler sync = new SyncFileHandler();
//Thread readthr = new Thread(new ReadThread(sync), "reader");
//Thread writethr = new Thread(new WriteThread(sync), "writer");
writethr.start();
readthr.start();
long startTime = System.currentTimeMillis();
// 循环判是否有线程获取到了读锁断
while (!fileControl.isGetReadLock()) {
long endTime = System.currentTimeMillis();
// 如果4秒后读线程仍然没有等到读锁,离开等待
if (endTime - startTime > 4000) {
readthr.interrupt();
System.out.println("4 seconds have passed,try to interrupt reader Thread");
break;
}
}
}
}
class ReadThread implements Runnable {
private IFileHandler fileControl;
public ReadThread(IFileHandler fileControl) {
this.fileControl = fileControl;
}
@Override
public void run() {
fileControl.read();
// 测试单纯使用synchronized关键字控制线程中断
System.out.println("reader thread end");
fileControl.formatFile();
}
}
class WriteThread implements Runnable {
private IFileHandler fileControl;
public WriteThread(IFileHandler fileControl) {
this.fileControl = fileControl;
}
@Override
public void run() {
fileControl.write();
}
}
1.2 ReentrantLock实现条件变量的控制
package lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* ReentrantLock Condition使用
* <p>
* Created by Jiacheng on 2018/7/3.
*/
public class ConditionLock {
/**
* BoundedBuffer 是一个定长100的集合,当集合中没有元素时,take方法需要等待,直到有元素时才返回元素
* 当其中的元素数达到最大值时,要等待直到元素被take之后才执行put的操作
*/
static class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
System.out.println("put wait lock");
lock.lock();
System.out.println("put get lock");
try {
while (count == items.length) {
System.out.println("buffer full, please wait");
notFull.await();
}
items[putptr] = x;
if (++putptr == items.length)
putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
System.out.println("take wait lock");
lock.lock();
System.out.println("take get lock");
try {
while (count == 0) {
System.out.println("no elements, please wait");
notEmpty.await();
}
Object x = items[takeptr];
if (++takeptr == items.length)
takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
final BoundedBuffer boundedBuffer = new BoundedBuffer();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1 run");
for (int i = 0; i < 1000; i++) {
try {
System.out.println("putting..");
boundedBuffer.put(Integer.valueOf(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
try {
Object val = boundedBuffer.take();
System.out.println(val);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t1.start();
t2.start();
}
}
2. ReentrantReadWriteLock (读写锁)
ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁
线程进入读锁的前提条件:
- 没有其他线程的写锁
- 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
线程进入写锁的前提条件:
- 没有其他线程的读锁
- 没有其他线程的写锁
到ReentrantReadWriteLock,首先要做的是与ReentrantLock划清界限。它和后者都是单独的实现,彼此之间没有继承或实现的关系。然后就是总结这个锁机制的特性了:
- 重入方面其内部的WriteLock可以获取ReadLock,但是反过来ReadLock想要获得WriteLock则永远都不要想。
- WriteLock可以降级为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有。反过来ReadLock想要升级为WriteLock则不可能,为什么?参看(a),呵呵.
- ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。
- 不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致。
- WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。
package lock;
import java.util.Random;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 读写锁
*
* Created by Jiacheng on 2018/7/3.
*/
public class ReadWriteLockTest {
public static void main(String[] args) {
final Queue3 q3 = new Queue3();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
while (true) {
q3.get();
}
}).start();
}
for (int i = 0; i < 3; i++) {
new Thread(() -> {
while (true) {
q3.put(new Random().nextInt(10000));
}
}).start();
}
}
}
class Queue3 {
private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public void get() {
rwl.readLock().lock();//上读锁,其他线程只能读不能写
System.out.println(Thread.currentThread().getName() + " be ready to read data!");
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "have read data :" + data);
rwl.readLock().unlock(); //释放读锁,最好放在finnaly里面
}
public void put(Object data) {
rwl.writeLock().lock();//上写锁,不允许其他线程读也不允许写
System.out.println(Thread.currentThread().getName() + " be ready to write data!");
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
this.data = data;
System.out.println(Thread.currentThread().getName() + " have write data: " + data);
rwl.writeLock().unlock();//释放写锁
}
}
下面使用读写锁模拟一个缓存器:
package lock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 读写锁模拟的缓存器
*
* Created by Jiacheng on 2018/7/3.
*/
public class CacheByReadWriteLock {
private Map<String, Object> map = new HashMap<String, Object>();//缓存器
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
}
public Object get(String id) {
Object value = null;
rwl.readLock().lock();//首先开启读锁,从缓存中去取
try {
value = map.get(id);
if (value == null) { //如果缓存中没有释放读锁,上写锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
if (value == null) {
value = "aaa"; //此时可以去数据库中查找
}
} finally {
rwl.writeLock().unlock(); //释放写锁
}
rwl.readLock().lock(); //然后再上读锁
}
} finally {
rwl.readLock().unlock(); //最后释放读锁
}
return value;
}
}
3. synchronized与lock的区别
- (用法)synchronized(隐式锁):在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
- (用法)lock(显示锁):需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类作为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。 如果没有主动释放锁,就有可能导致死锁现象。
- (机制)synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁,等待的线程会一直等待下去,不能够响应中断。Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就 是CAS操作(Compare and Swap)。
- (性能)synchronized是托管给JVM执行的,而lock是java写的控制锁的代码。在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时ReentrantLock是个不错的方案。
参考资料
Lock接口,ReentranctLock,ReentrantReadWriteLock
java中ReentrantReadWriteLock读写锁的使用
java并发控制:ReentrantLock Condition使用详解