JUC指java中并发编程常用的包,即java.util.concurrent
。比如常见的原子类,lock框架,线程池,阻塞队列等,都在这个包内。有关于JUC的其他内容我会在后续的文章中阐述,这篇文章的主题是其中的lock框架,也是我们经常用到的并发安全框架。(写的不对的地方请大家多多指教)
为什么出现Lock框架
已经有了synchronized为什么还要有Lock的出现?
1、首先性能方面早已不是问题,1.6之后对synchronized进行了大量优化,所以两者性能相差不多。开发者开始提倡使用synchronized,因为它更安全。
2、出现lock的原因是synchronized存在一些无法解决的问题。比如响应中断
3、lock框架在使用方面更加灵活,比如线程先获得锁A,再获得锁B,然后释放A,获取C,释放B获取D,像这样嵌套式的获取锁,synchronized是无法完成的。
4、Lock可以实现公平锁
Lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,也是可重入的。
Lock和synchronized的区别
1、synchronized是java的关键字,是jvm层面上的实现。Lock是一个接口,是基于JDK层面的实现,通过这个接口可以实现同步访问。
2、synchronized方式是自动释放锁的,而lock接口的实现必须手动释放锁
3、调试时,因为Lock的非块结构特性,synchronized获取锁的操作可以特定的栈帧关联起来,而Lock不行
Lock接口
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException; // 可以响应中断
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 可以响应中断
void unlock();
Condition newCondition();
}
逐个来说明一下这些方法的用法。
lock()方法
这种方式获取不到锁的话就会进入阻塞状态,和synchronized类似。
Lock lock = new ReentrantLock();
lock.lock();
try {
//处理事务
} catch (Exception e) {
//处理异常
} finally {
lock.unlock();//必须手动释放锁
}
lockInterruptibly()
这种方式获取不到锁仍然会被阻塞,但是它可以接收中断信号,退出阻塞。
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
注意:当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为interrupt()方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。因此,当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,那么只有进行等待的情况下,才可以响应中断的。与 synchronized 相比,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
tryLock() & tryLock(long time, TimeUnit unit)
tryLock()不管是否获取到锁都不会阻塞,而是立即返回,成功返回true,失败返回false。tryLock(long time,TimeUnit unit)获取不到锁时会进入阻塞状态,但是指定时间未获取锁,则返回false,否则返回true。
Lock lock = ...;
if (lock.tryLock()) {//lock.tryLock(10, TimeUnit.SECONDS)记得处理异常
try {
//do something
} catch (Exception e) {
//do something for exception
} finally {
lock.unlock();
}
}
newCondition()
创建一个condition对象,一个Condition和一个lock关联起来。类比于内置锁和内置条件队列。相比内置的条件队列,condition提供了更丰富的功能。有关于条件队列,后面会写一篇专门来讲。
常用的锁
ReentrantLock(独占锁)
ReentrantLock默认是非公平锁,当构造参数为true是公平锁。
在这里说一下公平锁和非公平锁的区别:
在公平锁上,线程将按照他们发出请求的顺序来获取锁,但在非公平的锁上,则允许“插队”:当一个线程请求非公平锁时,如果发出请求的同时锁状态变为可用,那么这个线程将跳过队列中所有等待的线程获取这个锁。
关于ReentrantLock,写一个响应中断的例子吧
如果存在问题,欢迎大家指正!!
public class DoTest2 {
public void out() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+" out something");
Thread.sleep(5000);
}
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
DoTest2 test = new DoTest2();
Thread sim = new Thread(() -> {
lock.lock();
try {
test.out();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
},"simple");
Thread sim2 = new Thread(() -> {
try {
lock.lockInterruptibly();
test.out();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
},"simple2");
sim.start();
Thread.sleep(1000);//为了让sim先获得锁
sim2.start();
sim2.interrupt();
System.out.println("main finish");//证明主线程是正常结束的
}
}
ReadWriteLock(共享锁)
ReadWriteLock是一个接口
public interface ReadWriteLock {
Lock readLock();//读锁
Lock writeLock();//写锁,共享
}
读写锁实现的加锁策略中,允许多个读操作同时进行,但每次至允许一个写操作。
ReentrantReadWriteLock
ReentrantReadWriteLock是读写锁的一个实现,在构造时可以选择构造公平或非公平锁。在公平的锁中,等待时间最长的锁将优先获得锁;如果这个锁由读线程占有,另一个线程的请求写入的话,其他读线程都不能获取读锁,直到写线程使用完并释放锁。在非公平锁中,线程获得访问许可的顺序是不确定的。写线程降级为读线程可以,但是读线程不可以升级为写线程(这样会导致死锁)。写入锁只能有唯一的所有者,所以只能由获取锁的线程释放。
举个栗子:
public class DoTest3 {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
//private Lock r = rwl.readLock();
//private Lock w = rwl.writeLock();
private int num = 10;
public void get() {
rwl.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " num = " + num);
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwl.readLock().unlock();
}
}
public void set(int i) {
rwl.writeLock().lock();
try{
num = i;
System.out.println(Thread.currentThread().getName() + " num= " + num);
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwl.writeLock().unlock();
}
}
public static void main(String[] args) {
DoTest3 test = new DoTest3();
for (int i = 1; i <= 10; i++) {
new Thread(()-> test.get(),"read"+i).start();//测试读的共享
}
/*for (int i = 1; i <= 10; i++) {
int finalI = i;
new Thread(()-> {
test.set(finalI); //测试读的互斥
},"read"+i).start();
}*/
}
}
补:为什么不希望所有的锁都是公平的?
从主观来看,公平是一种很好的行为,而不公平则是一种不好的行为,对吗??在执行加锁操作时,公平锁将由于在挂起线程和恢复线程上存在巨大的消耗而极大的降低性能。在实际情况中,统计上的公平性保证——即保证被阻塞的线程最终获得锁,通常已经足够了,实际开销也小的多。有些算法依赖公平的队列以确保它们的正确性,但这些算法并不多见。所以,大多数情况下,公平锁的性能要比非公平锁的性能要低。
在锁竞争激烈下,公平锁低的一个原因就是,在恢复一个被挂起的线程和与该线程真正开始运行之间存在严重的延迟。比如A持有一个锁,B请求这个锁,则B被挂起。当A释放锁,B被唤醒,尝试再次获取锁。而此时有一个C也请求这个锁,那么C可能在B被完全唤醒之前就可以获得、使用以及释放这个锁。这样才是一个双赢的局面,B获取锁的时间并没有延迟,而C更早的获得了锁,吞吐量获得了巨大的提升。
只有当锁的持有时间相对较长,平均获取锁的时间间隔较长,才考虑使用公平锁。