Lock接口
-
为什么有synchronized了还要用Lock?
- 效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
- 不够灵活(读写锁更灵活):加锁和释放的时机单一、每个锁仅有单一的条件(某个对象)
- 无法知道是否成功获取到锁
-
主要方法:
-
lock()
- lock()就是最普通的获取锁。如果锁被其他线程获取,则进行等待
- Lock不会像synchronized一样在异常时自动释放锁
- 最佳实践应该是在finally中释放锁,以保证发生异常时锁一定被释放
- lock()方法不能被中断,这会带来隐患:一旦陷入死锁,lock()就会陷入永久等待
-
tryLock()
- tryLock()用来尝试获取锁,如果当前锁没有被其他线程占有,则获取成功,返回true,否则返回false,代表获取锁失败
- 该方法会立即返回,拿不到锁就不会等待
- 所以可以用tryLock()来避免死锁
public class TryLockDeadlock implements Runnable { int flag = 1; static Lock lock1 = new ReentrantLock(); static Lock lock2 = new ReentrantLock(); public static void main(String[] args) { TryLockDeadlock r1 = new TryLockDeadlock(); TryLockDeadlock r2 = new TryLockDeadlock(); r1.flag = 1; r2.flag = 0; new Thread(r1).start(); new Thread(r2).start(); } @Override public void run() { for (int i = 0; i < 100; i++) { if (flag == 1){ try { if (lock1.tryLock(800, TimeUnit.SECONDS)){ try { System.out.println("线程1获取到了锁1"); Thread.sleep(new Random().nextInt(1000)); if (lock2.tryLock(800, TimeUnit.SECONDS)){ try{ System.out.println("线程1获取到了锁2"); System.out.println("线程1获取到了两把锁"); break; }finally { lock2.unlock(); } }else { System.out.println("线程1获取锁2失败,已重试"); } } finally { lock1.unlock(); Thread.sleep(new Random().nextInt(1000)); } }else { System.out.println("线程1获取锁1失败,已重试"); } } catch (InterruptedException e) { e.printStackTrace(); } } if (flag == 0) { try { if (lock2.tryLock(3000, 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(); } } else { System.out.println("线程2获取锁1失败,已重试"); } } finally { lock2.unlock(); Thread.sleep(new Random().nextInt(1000)); } } else { System.out.println("线程2获取锁2失败,已重试"); } } catch (InterruptedException e) { e.printStackTrace(); } } } } }
-
lockInterruptibly
- 相当于tryLock(long time, TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断
public class LockInterruptibly implements Runnable { private Lock lock = new ReentrantLock(); public static void main(String[] args) { LockInterruptibly lockInterruptibly = new LockInterruptibly(); Thread thread0 = new Thread(lockInterruptibly); Thread thread1 = new Thread(lockInterruptibly); thread0.start(); thread1.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } thread1.interrupt(); } @Override public void run() { System.out.println(Thread.currentThread().getName() + "尝试获取锁"); try { lock.lockInterruptibly(); try{ System.out.println(Thread.currentThread().getName() + "获取到了锁"); Thread.sleep(5000); }catch (InterruptedException e){ System.out.println(Thread.currentThread().getName() + "睡眠期间被中断了"); }finally{ lock.unlock(); System.out.println(Thread.currentThread().getName() + "释放了锁"); } } catch (InterruptedException e) { e.printStackTrace(); System.out.println(Thread.currentThread().getName() + "获得锁的期间被中断了"); } } } ---------------------- Thread-0尝试获取锁 Thread-1尝试获取锁 Thread-1获取到了锁 Thread-1睡眠期间被中断了 Thread-1释放了锁 Thread-0获取到了锁 Thread-0释放了锁
-
乐观锁、悲观锁
-
操作过程
乐观锁 悲观锁 a. 线程1和线程2直接获取资源并各自计算 a. 两个线程都抢锁 b. 线程1先计算完并判断资源是否被修改 b. 线程1抢到,线程2等待 c. 线程1发现没人在计算期间修改资源,于是把自己的计算结果写到资源里 c. 线程1释放锁,线程2拿到 d. 线程2计算完并判断资源是否已被修改 d. 都释放锁 e. 线程2发现在计算期间有人修改了资源,于是报错或者重试 -
互斥同步锁的劣势
- 阻塞和唤醒带来的劣势
- 永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那些线程将永远得不到执行
- 优先级反转:优先级低的线程持有了互斥同步锁,一直不释放,那么优先级高的线程将得不到执行
-
乐观锁即非互斥同步锁,悲观锁即互斥同步锁
乐观锁典型例子 悲观锁典型例子 原子类 synchronized 并发容器 Lock Git -
悲观锁使用场景:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
-
乐观锁使用场景:适合并发写入少,大部分是读取的场景,不加锁能使读取性能大幅提高
可重入锁、非可重入锁
- 可重入锁:支持重新进入的锁,它表示该锁能支持一个线程对资源的重复加锁
//递归处理,每次处理前都要加锁
public class RecursionDemo {
private static ReentrantLock lock = new ReentrantLock();
private static void accessResource() {
lock.lock();
try {
System.out.println("已经对资源进行了处理");
if (lock.getHoldCount() < 5){
System.out.println(lock.getHoldCount());
accessResource();
System.out.println(lock.getHoldCount());
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
accessResource();
}
}
-----------------------------------
已经对资源进行了处理
1
已经对资源进行了处理
2
已经对资源进行了处理
3
已经对资源进行了处理
4
已经对资源进行了处理
4
3
2
1
公平锁、非公平锁
-
公平指的是按照线程请求的顺序来分配锁;非公平是不完全按照请求的顺序,在一定的情况下,可以插队
-
下面例子是一个演示公平锁和非公平锁情况下打印的例子,10个线程完成打印任务,每个线程都需要打印两次,公平锁下10个线程依次开始到达等待队列。线程0打印完一次后,本应开始下一次打印,但是其他线程在等待,由公平锁的原则,线程0就会排到等待队列的队尾,等待下一轮。
public class FairLock { public static void main(String[] args) { PrintQueue printQueue = new PrintQueue(); Thread[] thread = new Thread[10]; for (int i = 0; i < 10; i++) { thread[i] = new Thread(new Job(printQueue)); } for (int i = 0; i < 10; i++) { thread[i].start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Job implements Runnable{ PrintQueue printQueue; public Job(PrintQueue printQueue) { this.printQueue = printQueue; } @Override public void run() { System.out.println(Thread.currentThread().getName() + "开始打印"); printQueue.printJob(new Object()); System.out.println(Thread.currentThread().getName() + "打印完毕"); } } class PrintQueue{ /** * true:公平 false:不公平 */ private Lock queueLock = new ReentrantLock(true); public void printJob(Object document) { queueLock.lock(); try { int duration = new Random().nextInt(10) + 1; System.out.println(Thread.currentThread().getName() + "正在打印"); Thread.sleep(duration * 1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { queueLock.unlock(); } queueLock.lock(); try { int duration = new Random().nextInt(10) + 1; System.out.println(Thread.currentThread().getName() + "正在打印"); Thread.sleep(duration * 1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { queueLock.unlock(); } } }
-
非公平锁的情况下,线程可以出现插队现象
共享锁、排它锁
- 排它锁:又称独占锁、独享锁
- 共享锁:又称读锁,获得共享锁之后,可以查看但无法修改和删除数据
- 共享锁和排它锁的典型是读写锁
ReentrantReadWriteLock
,其中读锁是共享锁,写锁是独占锁 - 多个读操作可以同时进行,并没有线程安全问题
- 读写锁的规则:
- 多个线程只申请读锁,都可以申请到
- 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
- 如果有一个线程已经占用了写锁,则此时其他线程如果要申请写锁或者读锁,则申请的线程会一直等待释放写锁
- 要么多读,要么一写
public class CinemaReadWrite {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放读锁");
readLock.unlock();
}
}
private static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放写锁");
writeLock.unlock();
}
}
public static void main(String[] args) {
new Thread(()->read(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->write(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
}
}
读写锁
-
读锁插队策略
- 公平锁:不允许插队
- 非公平锁:
- 写锁可以随时插队
- 读锁仅在等待队列头结点不是想获取写锁的线程的时候可以插队
-
特殊情况:读锁可以在等待队列头结点是读锁的情况下插队。原因是如果读锁正在运行,等待队列头结点也是读锁,那就多个读锁可以同时运行,一般情况下头结点都会是写锁,这时不允许插队。但是也有特殊情况,会出现头结点是读锁。
下面例子中5个线程写读读写读依次运行,但是1000个读锁子线程会一直尝试插队,当Thread1运行完write之后,线程2运行read,此时等待队列头结点是线程3,同样是读线程,但这时子线程依然能插队
public class NonfairBargeDemo { private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock( false); private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); private static void read() { System.out.println(Thread.currentThread().getName() + "开始尝试获取读锁"); readLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取"); try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } } finally { System.out.println(Thread.currentThread().getName() + "释放读锁"); readLock.unlock(); } } private static void write() { System.out.println(Thread.currentThread().getName() + "开始尝试获取写锁"); writeLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入"); try { Thread.sleep(40); } catch (InterruptedException e) { e.printStackTrace(); } } finally { System.out.println(Thread.currentThread().getName() + "释放写锁"); writeLock.unlock(); } } public static void main(String[] args) { new Thread(()->write(),"Thread1").start(); new Thread(()->read(),"Thread2").start(); new Thread(()->read(),"Thread3").start(); new Thread(()->write(),"Thread4").start(); new Thread(()->read(),"Thread5").start(); new Thread(new Runnable() { @Override public void run() { Thread thread[] = new Thread[1000]; for (int i = 0; i < 1000; i++) { thread[i] = new Thread(() -> read(), "子线程创建的Thread" + i); } for (int i = 0; i < 1000; i++) { thread[i].start(); } } }).start(); } }
-
锁降级:不释放当前线程拥有的写锁,直接获取到读锁,随后释放写锁的过程
private static void writeDowngrading() { writeLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入"); Thread.sleep(1000); readLock.lock(); System.out.println("在不释放写锁的情况下,直接获取读锁,成功降级"); } catch (InterruptedException e) { e.printStackTrace(); } finally { readLock.unlock(); System.out.println(Thread.currentThread().getName() + "释放写锁"); writeLock.unlock(); } }
自旋锁、阻塞锁
- 自旋锁:即请求锁的线程不放弃CPU的执行时间,进行自旋,如果自旋完成后锁定同步资源的线程已经释放了锁,那么当前线程可以不必阻塞而是直接获取同步资源,从而避免了线程切换的开销
- 阻塞锁:如果遇到没有拿到锁的情况,会直接把线程阻塞,直到被唤醒
//简单实现自旋锁
public class SpinLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null, current)) {
System.out.println("自旋获取失败,再次尝试");
}
}
public void unlock(){
Thread current = Thread.currentThread();
sign.compareAndSet(current, null);
}
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
spinLock.lock();
System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了自旋锁");
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}