1 概述
ReentrantLock类具有完全互斥排它的特点,同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务,这样做保证了同时写实例变量的线程安全性,但 效率是非常低下的。在JDK提供了一种读写锁ReentrantReadWriteLock类,可以在同时进行读操作时不需要同步执行,提升运行速度,加快运行效率。这两个类之间没有继承关系。
读写锁表示有两个锁,一个是读操作相关的锁,也叫共享锁。另一个是写操作相关的锁,也叫排它锁。读锁之间不互斥,读锁和写锁互斥,写锁之间也互斥,说明只要出现写锁,就会出现互斥同步的效果。读操作是指读取实例变量的值,写操作是指向实例变量写入值。
2 ReentrantLock的缺点
ReentrantLock类与ReentrantReadWriteLock类相比主要的缺点是使用ReentrantLock对象时,所有的操作都同步,哪怕只对实例变量进行读取操作也会同步处理,这样会耗费大量的时间,降低运行效率。
public class MyService {
private ReentrantLock lock = new ReentrantLock();
private String username = "abc";
public void testMethod1(){
try {
lock.lock();
System.out.println("开始执行 testMethod1 方法的线程名 = " + Thread.currentThread().getName() + ";时间是 = " + Utils.data(System.currentTimeMillis()));
System.out.println("打印 username = " + username);
Thread.sleep(4000);
System.out.println("执行完毕 testMethod1 方法的线程名 = " + Thread.currentThread().getName() + ";时间是 = " + Utils.data(System.currentTimeMillis()));
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class ThreadA extends Thread{
private MyService service;
public ThreadA(MyService service) {
this.service = service;
}
@Override
public void run(){
service.testMethod1();
}
}
public class Run1 {
public static void main(String[] args) {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadA b = new ThreadA(service);
b.start();
b.setName("B");
}
}
从运行时间来看,2个线程读取实例变量共耗时8秒,每个线程占用4秒,非常浪费CPU资源。而读取实例变量的操作可以是同步进行的,也就是读锁之间可以共享。
3 读锁与读锁之间共享
修改上面MyService.java代码,在运行Run1.java类。
public class MyService {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private String username = "abc";
public void testMethod1(){
try {
lock.readLock().lock();
System.out.println("开始执行 testMethod1 方法的线程名 = " + Thread.currentThread().getName() + ";时间是 = " + Utils.data(System.currentTimeMillis()));
System.out.println("打印 username = " + username);
Thread.sleep(4000);
System.out.println("执行完毕 testMethod1 方法的线程名 = " + Thread.currentThread().getName() + ";时间是 = " + Utils.data(System.currentTimeMillis()));
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.readLock().unlock();
}
}
}
从控制台打印的时间来看,两个线程机会同时进入lock方法后面的代码,共耗时4秒,说明使用lock.readLock()方法读锁可以提高程序运行效率,允许多个线程同时执行lock方法后面的代码。
这个实现中如果不使用锁也可以实现异步运行的效果,那么为什么要使用锁呢?这是因为有可能有第三个线程在执行写操作,这时写操作在执行时,这两个读操作就不能与写操作同时运行了,只能在写操作结束后,这两个读操作才能同时运行,避免了出现非线程安全,而且提高了运行效率。
4 写锁与写锁之间互斥
再次修改上面的MyService.java,然后运行Run1.java。
public class MyService {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void testMethod1(){
try {
lock.writeLock().lock();
System.out.println("获得写锁 方法的线程名 = " + Thread.currentThread().getName() + ";时间是 = " + Utils.data(System.currentTimeMillis()));
Thread.sleep(10000);
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.writeLock().unlock();
}
}
}
使用写锁lock.writeLock()方法的效果就是同一时间只允许一个线程执行lock方法后面的代码。
5 读写互斥
public class MyService {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void read(){
try {
lock.readLock().lock();
System.out.println("获得读锁 方法的线程名 = " + Thread.currentThread().getName() + ";时间是 = " + Utils.data(System.currentTimeMillis()));
Thread.sleep(10000);
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.readLock().unlock();
}
}
public void write(){
try {
lock.writeLock().lock();
System.out.println("获得写锁 方法的线程名 = " + Thread.currentThread().getName() + ";时间是 = " + Utils.data(System.currentTimeMillis()));
Thread.sleep(10000);
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.writeLock().unlock();
}
}
}
public class ThreadA extends Thread{
private MyService service;
public ThreadA(MyService service) {
this.service = service;
}
@Override
public void run(){
service.read();
}
}
public class ThreadB extends Thread{
private MyService service;
public ThreadB(MyService service) {
this.service = service;
}
@Override
public void run(){
service.write();
}
}
public class Run1 {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
Thread.sleep(1000);
ThreadB b = new ThreadB(service);
b.start();
b.setName("B");
}
}
这个实验说明读写操作时互斥的,只要出现写操作的过程,就是互斥的。
6 写锁与读锁互斥
修改上面的Run1.java类。
public class Run1 {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
Thread.sleep(1000);
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
}
}
从控制台打印的结果看,写锁与读锁也是互斥的。
【总结】读写、写读、写写之间的操作都是互斥的,只有读读时异步非互斥的。