Java的Lock接口与读写锁详解
作者:GeorgiaStar
一、Lock接口与synchronized关键字
锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。
在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。
虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。当然,这种方式简化了同步的管理,可是扩展性没有显式的锁获取和释放来的好。
synchronized关键字在使用的过程中会有如下几个问题:
1. 不可控性,无法做到随心的加锁和释放锁;
2. 效率比较低下,比如我们现在并发的读两个文件,读与读之间是互不影响的,但如果给这个读的对象使用synchronized来实现同步的话,那么只要有一个线程进入了,那么其他的线程都要等待;
3. 无法知道线程是否获取到了锁;
Lock是一个上层的接口,其原型如下,总共提供了6个方法:
public interface Lock { // 用来获取锁,如果锁已经被其他线程获取,则一直等待,直到获取到锁 void lock(); // 该方法获取锁时,可以响应中断,比如现在有两个线程,一个已经获取到了锁,另一个线程调用这个方法正在等待锁 //但是此刻又不想让这个线程一直在这死等,可以通过调用线程的Thread.interrupted()方法,来中断线程的等待过程 void lockInterruptibly() throws InterruptedException; // tryLock方法会返回bool值,该方法会尝试着获取锁,如果获取到锁,就返回true,如果没有获取到锁,就返回false, //但是该方法会立刻返回,而不会一直等待 boolean tryLock(); // 这个方法和上面的tryLock差不多是一样的,只是会尝试指定的时间,如果在指定的时间内拿到了锁,则会返回true, //如果在指定的时间内没有拿到锁,则会返回false boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 释放锁 void unlock(); // 实现线程通信,相当于wait和notify,后面会单独讲解 Condition newCondition(); }
使用Lock是需要手动释放锁的,但是如果程序中抛出了异常,那么就无法做到释放锁,有可能引起死锁,所以我们在使用Lock的时候,有一种固定的格式,如下:
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally {// 必须使用try,最后在finally里面释放锁 l.unlock(); }
在finally块中释放锁,目的是保证在获取到锁之后,最终能够被释放。 不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。
Lock接口提供的synchronized关键字所不具备的主要特性有:
- 尝试非阻塞地获取锁,当前线程获取锁时,如果锁没有被其他线程获取到,则成功获取并持有锁;
- 被中断地获取锁,与syncronized不同,获取到锁的线程能响应中断,当获取到锁的线程被中断时,会抛出中断异常,并释放锁;
- 超时获取锁,在指定的截止时间前获取锁,如果时间到了仍未获取到锁,则返回;
以多线程读取文件来示例:
public class LockDemo { // new一个锁对象,注意此处必须声明成类对象,保持只有一把锁,ReentrantLock是Lock的唯一实现类 Lock lock = new ReentrantLock(); public void readFile(String fileMessage){ lock.lock();// 上锁 try { System.out.println(Thread.currentThread().getName()+"得到了锁,正在读取文件……"); for (int i = 0; i< fileMessage.length(); i++){ System.out.print(fileMessage.charAt(i)); } System.out.println(); System.out.println("文件读取完毕!"); } finally { System.out.println(Thread.currentThread().getName()+"释放了锁!"); lock.unlock(); } } public static void main(String[] args) { LockDemo demo = new LockDemo(); String fileName = "H:/Java_Workspace_Console/test.txt"; // 创建若干个线程 new Thread(new Runnable() { @Override public void run() { demo.readFile(fileName); } }).start(); new Thread(new Runnable() { @Override public void run() { demo.readFile(fileName); } }).start(); } }
如果先把锁的那两行代码注释掉,看下效果如何:
多个线程读取到的内容错乱。 然后我们把锁的代码加上,看下效果如何:
如果我们把上面的readFile方法前面加上synchronized关键字,然后把锁去掉,效果是一样的。 tryLock方法的使用和Lock方法的使用类似,不做过多的说明了,代码如下:
public class TryLockDemo { // new一个锁对象,注意此处必须声明成类对象,保持只有一把锁,ReentrantLock是Lock的唯一实现类 Lock lock = new ReentrantLock(); public void readFile(String fileMessage){ // 上锁 if (lock.tryLock()) { try { System.out.println(Thread.currentThread().getName()+"得到了锁,正在读取文件……"); for (int i = 0; i< fileMessage.length(); i++){ System.out.print(fileMessage.charAt(i)); } System.out.println(); System.out.println("文件读取完毕!"); } finally { System.out.println(Thread.currentThread().getName()+"释放了锁!"); lock.unlock(); } } } public static void main(String[] args) { TryLockDemo demo = new TryLockDemo(); String fileName = "H:/Java_Workspace_Console/test.txt"; // 创建若干个线程 new Thread(new Runnable() { @Override public void run() { demo.readFile(fileName); } }).start(); new Thread(new Runnable() { @Override public void run() { demo.readFile(fileName); } }).start(); } }
二、读写锁
ReentrantLock是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。
一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的接口是ReadWriteLock:
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
该接口也有一个实现类ReentrantReadWriteLock,下面我们先看一下,多线程同时读取文件时,用synchronized实现的效果,代码如下:
public class SyncReadDemo { public synchronized void get(Thread thread) { System.out.println("start time:"+System.currentTimeMillis()); for(int i=0; i<5; i++){ try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(thread.getName() + ":正在进行读操作……"); } System.out.println(thread.getName() + ":读操作完毕!"); System.out.println("end time:"+System.currentTimeMillis()); } public static void main(String[] args) { final SyncReadDemo lock = new SyncReadDemo(); new Thread(new Runnable() { @Override public void run() { lock.get(Thread.currentThread()); } }).start(); new Thread(new Runnable() { @Override public void run() { lock.get(Thread.currentThread()); } }).start(); } }
测试结果如下:
整个过程耗时200ms
在加了synchronized关键字之后,读与读之间,也是互斥的,也就是说,必须等待Thread-0读完之后,才会轮到Thread-1线程读,而无法做到同时读文件,这种情况在大量线程同时都需要读文件的时候,效率低下。 下面我们来测试ReadAndWriteLock的性能,代码如下:
public class ReadAndWriteLockDemo { ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public void get(Thread thread) { lock.readLock().lock(); try{ System.out.println("start time:"+System.currentTimeMillis()); for(int i=0; i<5; i++){ try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(thread.getName() + ":正在进行读操作……"); } System.out.println(thread.getName() + ":读操作完毕!"); System.out.println("end time:"+System.currentTimeMillis()); }finally{ lock.readLock().unlock(); } } public static void main(String[] args) { final ReadAndWriteLockDemo lock = new ReadAndWriteLockDemo(); new Thread(new Runnable() { @Override public void run() { lock.get(Thread.currentThread()); } }).start(); new Thread(new Runnable() { @Override public void run() { lock.get(Thread.currentThread()); } }).start(); } }
整个过程耗时:100ms Thread-0和Thread-1是在同时读取文件。不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。读锁和写锁是互斥的。
可重入锁
如果锁具备可重入性,则称作为可重入锁。像synchronized和 ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。
举个简单的例子,当一 个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法 method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
可中断锁
可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以在构造函数中指定其为公平锁。
公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。非公平性锁可能使线程“饥饿”,为什么它又被设定成默认的实现呢?
如果把每次不同线程获取到锁定义为1次切换,公平性锁为了保证锁的获取按照FIFO原则,代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但会有更少的线程切换,保证了其更大的吞吐量。
到此这篇关于Java的Lock接口与读写锁详解的文章就介绍到这了,更多相关Lock接口与读写锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!