一.Lock接口
简介、地位、作用
1.锁是一种工具,用于控制对共享资源的访问。
2.Lock和synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同。
3.Lock并不是用来代替synchronized的,而是当使用synchronized不合适或不足以满足要求的时候,来提供高级功能的。
4.Lock接口最常见的实现类是ReentrantLock
5.通常情况下,Lock只允许一个线程来访问这个共享资源。不过有的时候,一些特殊的实现也可允许并发访问,比如ReadWriteLock里面的ReadLock。
为什么synchronized不够用?为什么需要Lock ?
1) 效率低∶锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程。
2) 不够灵活(读写锁更灵活)加锁和释放的时机单一,每个锁仅有单一的条件(某个对象)可能是不够的
3) 无法知道是否成功获取到锁
方法介绍
void lock()
public class MustUnlock {
private static Lock lock=new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
System.out.println("获取了锁!");
}finally {
lock.unlock();
System.out.println("释放锁对象!");
}
}
}
lock方法不能被中断,一旦陷入到死锁,lock()就会陷入到永久等待。所以一定要finally释放锁。
boolean tryLock();
tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,则返回true,否则返回false,代表获取锁失败。
boolean tryLock(long time, TimeUnit unit)
相当于在tryLock()方法的基础上加了等待时间,以下示例演示了如何避免死锁的问题。
public class TryLockDeadLock implements Runnable{
private static Lock lock1=new ReentrantLock();
private static Lock lock2=new ReentrantLock();
private int flag;
public static void main(String[] args) {
TryLockDeadLock r1 = new TryLockDeadLock();
TryLockDeadLock r2 = new TryLockDeadLock();
r1.flag = 0;
r2.flag = 1;
new Thread(r1).start();
new Thread(r2).start();
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (flag==0){
try {
if (lock1.tryLock(800,TimeUnit.MILLISECONDS)){
try {
System.out.println("线程1获取了锁1!");
Thread.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800,TimeUnit.MILLISECONDS)){
try {
System.out.println("线程1获取了锁2!");
System.out.println("线程1获取到了两把锁");
break;
}finally {
lock2.unlock();
System.out.println("线程1释放了锁2");
}
}else{
System.out.println("线程1获取锁2失败!重试。。");
}
}finally {
lock1.unlock();
System.out.println("线程1释放了锁1!");
}
}else{
System.out.println("线程1获取锁1失败!重试。。");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag==1){
try {
if (lock2.tryLock(800,TimeUnit.MILLISECONDS)){
try {
System.out.println("线程2获取了锁2");
Thread.sleep(new Random().nextInt(1000));
if (lock1.tryLock(800,TimeUnit.MILLISECONDS)){
try {
System.out.println("线程2获取了锁1!");
System.out.println("线程2获取了两把锁");
break;
}finally {
lock1.unlock();
System.out.println("线程2释放了锁1");
}
}else{
System.out.println("线程2获取锁1失败!重试。。");
}
}finally {
lock2.unlock();
System.out.println("线程2释放了锁2");
}
}else{
System.out.println("线程2获取锁2失败。重试。。");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
void lockInterruptibly()
相当于tryLock(long time, TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断。
以下代码演示了不同打断时机会造成不同的异常:
public class LockInterruptibly implements Runnable {
private static Lock lock=new ReentrantLock();
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
try {
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + "获取到了锁");
//执行业务逻辑
Thread.sleep(5000);
}catch (Exception e){
System.out.println(Thread.currentThread().getName()+"已经获取了锁,但是在执行业务逻辑时发生异常!");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName()+"线程释放!");
}
} catch (InterruptedException e) {
System.out.println("获取锁的时候就被打断,发生了异常。");
}
}
public static void main(String[] args) throws InterruptedException {
LockInterruptibly lockInterruptibly = new LockInterruptibly();
Thread thread0 = new Thread(lockInterruptibly);
Thread thread1 = new Thread(lockInterruptibly);
thread0.start();
thread1.start();
Thread.sleep(1000);
thread1.interrupt();
}
}
结果有如下两种:
结果1:
Thread-1尝试获取锁
Thread-0尝试获取锁
Thread-1获取到了锁
Thread-1已经获取了锁,但是在执行业务逻辑时发生异常!
Thread-1线程释放!
Thread-0获取到了锁
Thread-0线程释放!
-----------
结果2:
Thread-0尝试获取锁
Thread-1尝试获取锁
Thread-0获取到了锁
Thread-1获取锁的时候就被打断,发生了异常。
Thread-0线程释放!
实际业务场景之中,我们需要针对不同catch去做不同的处理。如果代码改成如下,会发生两种异常情况。
可以看到,两种情况分别如下:
1.当线程0获取了锁,执行业务逻辑时线程1发生了打断,这个时候线程0会正常执行,但是线程1此时还没有获取到锁对象,所以会报没有获取到锁对象的错误。
2.当线程1获取了锁对象,在执行业务逻辑时被打断,此时会报错已经获取了锁对象,但在业务处理中被打断了,此时线程0在线程1释放掉锁以后正常执行。
可见性保证
锁的分类
乐观锁和悲观锁
可重入锁和非可重入锁,已ReentrantLock为例(重点)
公平锁和非公平锁
共享锁和排它锁:以ReentrantReadWriteLock读写锁为例(重点)
自旋锁和阻塞锁
可中断锁∶顾名思义,就是可以响应中断的锁
锁优化