今天我们分析并发编程原子操作 AQS 思想之Lock 的标准使用
一、显式锁和
AQS:
1、显式锁
有了
synchronized
为什么还要
Lock
? Java 程序是靠
synchronized
关键字实现锁功能的,使用
synchronized
关键字;将会隐式地获取锁,但是它将锁的获取和释放固化了,也就是先获取再释放。
Lock
的标准用法:
代码示例:
public class LockCase {
private Lock lock = new ReentrantLock();
private int age = 100000;//初始100000
private static class TestThread extends Thread{
private LockCase lockCase;
public TestThread(LockCase lockCase, String name) {
super(name);
this.lockCase = lockCase;
}
@Override
public void run() {
for(int i=0;i<100000;i++) {//递增100000
lockCase.test();
}
System.out.println(Thread.currentThread().getName()
+" 递增后age = "+lockCase.getAge());
}
}
public void test() {
lock.lock();
try{
age++;
}finally {
lock.unlock();
}
}
public void test2() {
lock.lock();
try {
age--;
} finally {
lock.unlock();
}
}
public int getAge() {
return age;
}
public static void main(String[] args) throws InterruptedException {
LockCase lockCase = new LockCase();
Thread endThread = new TestThread(lockCase,"endThread");
endThread.start();
Thread.sleep(1000);
for(int i=0;i<100000;i++) {//递减100000
lockCase.test2();
}
System.out.println(Thread.currentThread().getName()
+" 递减后mainAge = "+lockCase.getAge());
}
}
执行结果:
在
finally
块中释放锁,目的是保证在获取到锁之后,最终能够被释放。 不要将获取锁的过程写在 try
块中,因为如果在获取锁(自定义锁的实现)。时发生了异常,异常抛出的同时,也会导致锁无故释放。
Lock
的常用
API:
代码展示:
2、ReentrantLock
锁的可重入
简单地讲
就是:“同一个线程对于已经获得到的锁,可以多次继续申请到该 锁的使用权”。而 synchronized
关键字隐式的支持重进入,比如一个
synchronized 修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得 该锁。ReentrantLock
在调用
lock()
方法时,已经获取到锁的线程,能够再次调用 lock()方法获取锁而不被阻塞。
公平和非公平锁
如果在时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的, 反之,是不公平的。公平的获取锁,也就是等待时间最长的线程最优先获取锁, 也可以说锁获取是顺序的。 ReentrantLock
提供了一个构造函数,能够控制锁是 否是公平的。事实上,公平的锁机制往往没有非公平的效率高。 在激烈竞争的情况下,
非公平锁的性能高于公平锁的性能的一个原因是
:
在恢 复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程 A 持有一个锁,
并且线程
B
请求这个锁。由于这个锁已被线程
A
持有
,
因此
B
将被挂 起。当 A
释放锁时
,B
将被唤醒
,
因此会再次尝试获取锁。与此同时
,
如果
C
也请求 这个锁,
那么
C
很可能会在
B
被完全唤醒之前获得、使用以及释放这个锁。这样 的情况是一种“双赢”的局面:B
获得锁的时刻并没有推迟
,C
更早地获得了锁
,
并 且吞吐量也获得了提高。代码示例如上:
3、读写锁
ReentrantReadWriteLock
之前提到锁(如
Mutex
和
ReentrantLock
)基本都是排他锁,这些锁在同一 时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问, 但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对 锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁 有了很大提升。 除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化 读写交互场景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它 大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写 操作完成之后的更新需要对后续的读服务可见。 在没有读写锁支持的(Java 5
之前)时候,如果需要完成上述工作就要使用 Java 的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入 等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写
操作之间依靠
synchronized
关键进行同步),这样做的目的是使读操作能读取到 正确的数据,不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取 读锁,操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程) 的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使 用等待通知机制的实现方式而言,变得简单明了。一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量
ReentrantReadWriteLock 其实实现的是
ReadWriteLock
接口
示例代码:
public class UseRwLock {
private final ReadWriteLock lock = new ReentrantReadWriteLock();//定义一个读写锁
private final Lock getLock = lock.readLock();//读锁
private final Lock setLock = lock.writeLock();//写锁
//读操作
public GoodsInfo getNum() {
getLock.lock();
try{
SleepTools.ms(5);
return null;
}finally {
getLock.unlock();
}
}
//写操作
public void setNum(int number) {
setLock.lock();
try{
SleepTools.ms(5);
//XXX
}finally {
setLock.unlock();
}
}
}
执行性能:
如果用内置锁:
public synchronized GoodsInfo getNum() {
//XXXX
}
public synchronized void setNum(int number) {
//XXXX
}
执行效率:
基本分析到此完成。