Lock接口通过底层框架的形式为设计更面向对象、可更加细粒度控制线程代码、更灵活控制线程通信提供了基础。实现Lock接口且使用得比较多的是可重入锁ReentrantLock以及读写锁ReentrantReadWriteLock(成员内部类:WriteLock、ReadLock)。
1. ReentrantLock
在里面,已经总结过通过使用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使用 ** 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 Mapmap = new HashMap ();//缓存器 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是个不错的方案。
参考资料