Lock与synchronized
Lock和synchronized在功能上是一样的。不过Lock提供了一些其他功能,包括定时的锁等待、可中断的锁等待、公平性,以及实现非块结构的加锁。
从性能上Lock的实现类ReentrantLock在JDK5.0之前要好于synchronized,在JDK6.0之后,synchronized做了优化,所以两者的性能相差无几了。
那在使用上应该选择哪个呢?在《Java并发编程实战》中有一句话:"仅当内置锁不能满足需求时,才可以考虑使用ReentrantLock"。在一些内置锁无法
满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮询的与可
中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。
在JDK5.0中,内置锁与ReentrantLock相比还有另外一个优点:在线程转储中能给出在哪些调用帧中获得了哪些锁,并能够检测出发生死锁的线程。JVM
并不知道哪些线程持有ReentrantLock,因此在调试使用ReentrantLock的线程问题时,将起不到帮助作用。JDK6.0解决了这个问题,它提供了一个管理和调试
接口,锁可以通过该接口进行注册,从而与ReentrantLock相关的加锁信息就能出现在线程转储中,并通过其他的管理接口和调试接口来访问。
在内置锁中,死锁是一个很严重的问题,恢复程序唯一的方法是重新启动程序,而防止死锁的唯一方法就是在构造程序时避免出现不一致的锁顺序。
ReentrantLock有可定时与可轮询的功能,这就从另一个方面避免了死锁的发生(注意,使用ReentrantLock一定要在finally中释放锁)。
Lock的实现类ReentrantLock(重入锁)
重入锁ReentrantLock,顾名思义,就是支持重进入的锁(synchronized隐式的支持重进入),它表示该锁能够支持一个线程对资源的重复加锁。除此之外,
该锁还支持获取锁时的公平和非公平性选择。
1、ReentrantLock的基本使用。
package org.burning.sport.javase.thread.reentrantlock; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockTest implements Runnable{ private static Lock lock = new ReentrantLock(); private int i; @Override public void run() { while(true) { increment(); } } public void increment() { try { lock.lock(); i++; System.out.println(Thread.currentThread().getName() + ":" + i); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } finally { //一定要在finally这里解锁,否则就是定时炸弹 lock.unlock(); } } public static void main(String[] args) { ReentrantLockTest test = new ReentrantLockTest(); Thread t1 = new Thread(test); Thread t2 = new Thread(test); Thread t3 = new Thread(test); t1.setName("线程一:"); t2.setName("线程二:"); t3.setName("线程三:"); t1.start(); t2.start(); t3.start(); } }
2、锁的公平性
如果在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。
public ReentrantLock(boolean fair); 默认是非公平的。公平锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换影响性能,非公平的锁则会造成线程的 “饥饿”。
3、Lock的中断响应
线程的中断 中可以看到,Lock是可以响应线程触发的中断的。只要在获取锁的时候用 lock.lockInterruptibly();就可以了。
4、轮询锁与定时锁
lock.tryLock(); 和 lock.tryLock(5, TimeUtil.SECONDS); 在内置锁中,死锁是一个很严重的问题,恢复程序唯一的方法是重新启动程序,而防止死锁的唯一方法就是在
构造程序时避免出现不一致的锁顺序。ReentrantLock有可定时与可轮询的功能,这就从另一个方面避免了死锁的发生(注意,使用ReentrantLock一定要在finally中释放锁)。
public class TryLockTest implements Runnable { private Lock lock = new ReentrantLock(); @Override public void run() { try { if (lock.tryLock(5, TimeUnit.SECONDS)) { lock.lock(); //do something ..... } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }
5、ReentrantLock的好搭档Condition条件
Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的new Condition()方法)
创建出来的,换句话说,Condition是依赖Lock对象的。
示例代码:
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ConditionUseCase { Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); public void conditionWait() throws InterruptedException { lock.lock(); try { condition.await(); } finally { lock.unlock(); } } public void conditionSignal() throws InterruptedException { lock.lock(); try { condition.signal(); } finally { lock.unlock(); } } }
Condition的(部分)方法及描述:
void await() throws InterruptedException
当前线程进入等待状态直到被通知(signal)或中断,当前线程将进入运行状态且从await()方法返回的情况,包括:
其他线程调用该Condition的signal()或signalAll()方法,而当前线程被选中唤醒
□ 其他线程(调用interrupt()方法)中断当前线程;
□ 如果当前等待线程从await()方法返回,那么表明该线程已经获取了Condition对象所对应的锁;
void awaitUninterruptibly()
当前线程进入等待状态直到被通知,从方法名称上可以看出该方法对中断不敏感
long awaitNanos(long nanosTimeout) throws InterruptedException
当前线程进入等待状态直到被通知、中断或者超时。返回值表示剩余的时间,如果在nanosTimeout纳秒
之前被唤醒,那么返回值就是(nanosTimeout - 实际耗时) ,如果返回值是0或者负数,那么可以认定已经超时了
boolean awaitUntil(Date deadline) throws InterruptedException
当前线程进入等待状态直到被通知、中断或者到某个时间。如果没有到指定时间就被通知,方法返回true,否则,
表示到了指定时间,方法返回false
void signal() 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁
void signalAll() 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁。
示例代码:
线程阻塞工具类:LockSupport
LockSupport是一个非常方便实用的线程阻塞工具,它可以在线程内任意位置让线程阻塞。和Thread.suspend()相比,它弥补了由于resume()在前发生,
导致线程无法记录执行的情况。和Object.wait()相比,它不需要先获得某个对象的锁,也不会抛出InterruptedException异常。
LockSupport提供的阻塞和唤醒的方法
void park() 阻塞当前线程,如果调用 unpark(Thread thread) 方法或者当前线程被中断,才能从park方法返回
void parkNanos(long nanos) 阻塞当前线程,最长不超过nonos纳秒,返回条件在park()的基础上增加了超时返回
void parkUntil(long deadline) 阻塞当前线程,知道deadline时间(从1970年开始到deadline时间的毫秒数)
void unpark(Thead thread) 唤醒处于阻塞状态的线程Thread
LockSupport sample demo:
package org.burning.sport.javase.thread.reentrantlock.lock.support; import java.util.concurrent.locks.LockSupport; public class LockSupportTest { private static Object u = new Object(); static ChangeObjectThread thread1 = new ChangeObjectThread("t1"); static ChangeObjectThread thread2 = new ChangeObjectThread("t2"); public static class ChangeObjectThread extends Thread { public ChangeObjectThread(String threadName) { super.setName(threadName); } @Override public void run() { synchronized (u) { System.out.println("in " + getName()); LockSupport.park(); } } } public static void main(String[] args) throws InterruptedException { thread1.start(); Thread.sleep(100); thread2.start(); LockSupport.unpark(thread1); LockSupport.unpark(thread2); thread1.join(); thread2.join(); } }
读写锁 ReentrantReadWriteLock
1、使用示例:
package org.burning.sport.javase.thread.readwritelock; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class Cache { static Map<String, String> map = new HashMap<>(); static ReadWriteLock lock = new ReentrantReadWriteLock(); static Lock readLock = lock.readLock(); static Lock writeLock = lock.writeLock(); public static final Object get(String key) { readLock.lock(); try { return map.get(key); } finally { readLock.unlock(); } } public static final void put(String key, String value) { writeLock.lock(); try { map.put(key, value); } finally { writeLock.unlock(); } } public static void clear() { writeLock.lock(); try { map.clear(); } finally { writeLock.unlock(); } } }
2、读写锁的实现分析:
2.1、读写状态设计
读写锁同样依赖自定义同步器来实现同步功能。而读写状态就是其同步器的同步状态。同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在
同步状态(一个整形变量)上维护多个读线程和一个写线程的状态。如果在一个整形变量上维护多种状态,就一定需要 “按位切割使用” 这个变量,读写锁将变量切分为了两
个部分,高16位表示读,低16位表示写。
2.2、写锁的获取与释放
写锁是一个支持重入的排它锁。如果当前线程已经获取了写锁,则增加写状态。写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态
为0时表示写锁已被释放。
2.3、读锁的获取与释放
读锁是一个支持重入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功的获取,而所做的也只(线程安全的)是增加读
状态。读锁的每次释放均减少读状态,减少的值是(1<<16)。
2.4、锁降级
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有)写锁,
再获取到读锁,随后释放写锁的过程。(ReentrantReadWriteLock不支持锁升级)
参考:
【1】《Java并发编程的艺术》,方腾飞
【2】《Java并发编程实战》,童云兰
【3】《Java高并发程序设计》,葛一鸣
【4】《Think In Java》,第21章 并发