Java并发编程:显式锁Lock

Lock显式锁是在JDK1.5引入的,在JDK1.5之前处理多线程并发使用的是synchronized和volatile关键字。在JDK1.5之后增加了一种新的机制Lock,虽然与synchronized类似都是提供加锁机制,但是Lock锁并不是提供了一种替代内置锁synchronized的方式,而是当内置锁机制不适用时,提供了一种可选择的更高级的功能。

synchronized的局限性

synchronized是Java关键字,是JVM的内置属性,当一个线程获取到内置synchronized锁后,是不可以手动释放锁的,其它线程只能一直等待占有锁的线程释放锁。如果使用内置锁synchronized添加锁,那么释放锁一般以如下三种情况之一:

占有锁的线程执行完了该代码块,然后释放对锁的占有; 占有锁线程执行发生异常,此时JVM会让线程自动释放锁; 占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。

原因之一,使用synchronized时无法中断一个正在获取锁的线程,因此在synchronized修饰的线程中如果产生死锁,我们无法使用interrupt()方法中断线程,示例代码文章后面会提供。但是如果使用显示锁Lock的lockInterruptibly()方法线程则会响应中断。

原因之二,在某些场景下效率会降低。由于互斥性是一种强硬的加锁策略,因此synchronized在加锁时,因此也就不必要的限制了并发性。互斥是一种保守的加锁策略,虽然可避免“写/写”冲突和“写/读”冲突,但是也同样不可避免的设置了“读/读”冲突,在某些开发场景下,如果“读/读”场景使用过多则会很大程度降低程序运行的效率。

原因之三,使用内置锁synchronized无法判断线程有没有成功获取到锁。

上面三种大致说明了一下synchronized锁的局限性,而且这三种局限性都可以使用显示锁Lock解决。这里也就验证了文章开始所说的当内置锁机制不适用时,显示锁Lock提供了一种可选择的更高级的功能,关于显示锁Lock相关的类位于java.util.concurrent.locks包下。

显示锁Lock相关方法及类介绍

Lock接口定义了一组抽象的加锁操作,Lock所有的加锁以及解锁都是显示的,Lock接口提供的方法如下:

方法描述
void lock() 获取锁。
void lockInterruptibly() 如果当前线程未被中断,则获取锁。
Condition newCondition() 返回绑定到此 Lock 实例的新 Condition 实例。
boolean tryLock() 仅在调用时锁为空闲状态才获取该锁。
boolean tryLock(long time, TimeUnit unit) 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。
void unlock() 释放锁。

lock方法

下面是使用Lock锁的标准形式,使用显示锁Lock比内置锁synchronized稍微复杂一些,必须在finally中释放锁。否则在被保护的代码块中发生异常时,将永远无法释放锁。

Lock lock = ...;
lock.lock();
try{
    //处理任务
	//捕获异常,并在必要时恢复不变形条件
}finally{
    lock.unlock();   //释放锁
}

在显示锁使用过程中一定不可以忘记释放锁,否则一旦发生异常将很难定位到发生异常的地方,因为没有记录应该释放锁的位置和时间。这也是显示锁Lock不能完全替代synchronized内置锁的原因,因为当程序执行完成离开代码块时不会自动释放锁,虽然在finally中释放锁很容易,可是也容易忘记。

tryLock方法

tryLock()方法是有返回值的,如果锁可用,则获取锁,并立即返回值true。如果锁不可用,则此方法将立即返回值false。

tryLock()还有一个带参数的方法tryLock(long time, TimeUnit unit)如果锁可用,则此方法将立即返回值true。如果锁不可用,出于线程调度目的,将禁用当前线程,并且在发生以下三种情况之一前,该线程将一直处于休眠状态:

锁由当前线程获得; 其他某个线程中断当前线程,并且支持对锁获取的中断; 已超过指定的等待时间。

如果超过了指定的等待时间,则将返回值 false。如果 time 小于等于 0,该方法将完全不等待。

Lock lock = ...;
if (lock.tryLock()) {
  try {
	  //处理任务
	  //捕获异常,并在必要时恢复不变形条件
  } finally {
	  lock.unlock();
  }
} else {
  // 如果不能获取锁,则直接做其他事情
}

lockInterruptibly方法

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。显示锁可以中断等待获取锁就是通过该方法,而内置锁synchronized没有办法中断一个正在获取锁的线程的。下面是一个简单的demo验证lockInterruptibly()方法与synchronized锁不同,让两个线程产生死锁,然后分别调用线程的interrupt()方法中断线程。

public class Thread01 extends Thread{

	private Object resource01;
	private Object resource02;
	
	public Thread01(Object resource01, Object resource02) {
		this.resource01 = resource01;
		this.resource02 = resource02;
	}

	@Override
	public void run() {
		synchronized(resource01){
			System.out.println("Thread01 locked resource01");
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (resource02) {
				System.out.println("Thread01 locked resource02");
			}
			
		}
	}
}
//Thread02代码直接将两个所交换一下即可
public class MainTest {
	
	public static void main(String[] args) {
		
		final Object resource01="resource01";
		final Object resource02="resource02";
		
		Thread01 thread01=new Thread01(resource01, resource02);
		Thread02 thread02=new Thread02(resource01, resource02);
		
		thread01.start();
		thread02.start();
		
		
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		thread01.interrupt();
		thread02.interrupt();
	}

}

运行上面代码可以发现,使用synchronized内置锁产生死锁后再次调用interrupt()方法并没有中断对应的线程,下面看一下使用Lock的lockInterruptibly()方法。

public class Thread01 extends Thread{

	private Lock lock01;
	private Lock lock02;
	
	public Thread01(Lock lock01, Lock lock02) {
		this.lock01 = lock01;
		this.lock02 = lock02;
	}

	public void run() {
		
		try {
			lock02.lockInterruptibly();
			System.out.println("Thread01 locked02");
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			lock01.lockInterruptibly();
			System.out.println("Thread01 locked01");
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock01.unlock();
			lock02.unlock();
		}
	}
}
//Thread02代码直接将两个所交换一下即可
public class MainTest {
	
	public static void main(String[] args) {
		
		final Lock lock01=new ReentrantLock();
		final Lock lock02=new ReentrantLock();
		
		Thread01 thread01=new Thread01(lock01, lock02);
		Thread02 thread02=new Thread02(lock01, lock02);
		
		thread01.start();
		thread02.start();
		
		
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		thread01.interrupt();
		thread02.interrupt();
	}

}

这里运行之后发现死锁已经解除,因为对应的线程都响应了中断。

ReentrantLock

ReentrantLock类实现了Lock接口,该类可以说是显示锁最常使用的一个类。它一个可重入的互斥锁 Lock,它具有与使用 synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。该类的构造方法接受一个可选的boolean类型fair参数,默认值为false。当设置为 true时,在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程,也就是有序的按照FIFO。否则此锁将无法保证任何特定访问顺序。与采用默认设置(使用不公平锁)相比,使用公平锁的程序在许多线程访问时表现为很低的总体吞吐量(即速度很慢,常常极其慢),但是在获得锁和保证锁分配的均衡性时差异较小。不过要注意的是,公平锁不能保证线程调度的公平性。因此,使用公平锁的众多线程中的一员可能获得多倍的成功机会,这种情况发生在其他活动线程没有被处理并且目前并未持有锁时。还要注意的是,未定时的 tryLock方法并没有使用公平设置。因为即使其他线程正在等待,只要该锁是可用的,此方法就可以获得成功。

ReadWriteLock与ReentrantReadWriteLock

public interface ReadWriteLock {
    
    Lock readLock();
 
    Lock writeLock();
	
}

读写锁ReadWriteLock有两个锁,一个用于读操作一个用于写操作。ReadWriteLock读写锁的出现是为了解决文章开始所说的“读/读”操作synchronized效率低的问题,读写锁允许在多CPU中真正的并行操作,因此效率上比synchronized高一些。但是这种效率的高也不是一定的,由于ReadWriteLock也是使用的Lock实现的读写部分,因此发现使用读写锁没有提高性能,那么则可以替换为独占锁实现。

ReentrantReadWriteLock实现了ReadWriteLock接口,也为读和写提供了可重入的语义。ReentrantReadWriteLock类也提供了类似ReentrantLock构造方法,该类构造方法接受一个可选的boolean类型fair参数,默认值为false。读写锁在读的时候虽然允许有多个持有者,但是写入锁只能有一个持有者。

public class MainTest {
	private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

	private static long start;

	public static void main(String[] args) {
		final MainTest test = new MainTest();
		start = System.currentTimeMillis();
		new Thread() {
			public void run() {
				test.get(Thread.currentThread());
			};
		}.start();

		new Thread() {
			public void run() {
				test.get(Thread.currentThread());
			};
		}.start();

	}

	public void get(Thread thread) {
		rwl.readLock().lock();
		try {
			for (int i = 0; i < 1000; i++) {
				System.out.println(thread.getName() + "正在进行读操作");
			}
			System.out.println(thread.getName() + "读操作完毕 " + (System.currentTimeMillis() - start));
		} finally {
			rwl.readLock().unlock();
		}
	}
}

显示锁Lock与内置锁synchronized选择

显示锁Lock在加锁和内存上提供了与内置锁synchronized相同的语义。

显示锁Lock是一个接口,是JDK层面上提供了,而synchronized是Java关键字,是在JVM层面实现的。

显示锁Lock必须手动释放锁,无论执行过程中是否发生异常,否则就如同一个定时炸弹,容易发生死锁且不容易定位,而内置锁synchronized在使用后会自动释放锁,这一点上面不容易发生死锁。

显示锁Lock可以知道是否已经成功获取锁,并且可以响应中断,则是内置锁synchronized无法办到的,同样显示锁Lock还提供了读写锁ReentrantReadWriteLock,在多核CPU上面可以真正的实现并行执行。

内置锁synchronized简洁紧凑,为多数开发人员熟悉,且在开发中是被最常用的一种加锁方式,而且许多现有的程序都是内置锁实现,除非在开发过程中已经明确验证显示锁Lock确实可以提供代码执行效率,否则还是直接使用内置锁synchronized。

与内置锁synchronized相比,显示锁Lock提供了一些功能扩展,在处理锁上面具有更高的灵活性,但是显示Lock不能完全代替内置锁synchronized,只有在内置锁synchronized无法满足时才可以使用显示锁。

参考资料

Java并发编程:Lock

Java 并发开发:Lock 框架详解

Java并发编程】之二十:并发新特性—Lock锁和条件变量(含代码)

Java中的锁

Java中的读/写锁

深入java并发Lock一

评论

您确定要删除吗?删除之后不可恢复