本章内容涵盖Lock的使用讲解,可重入锁、读写锁。Lock和Synchronized的对比等。 多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!!
沉下去,再浮上来
,我想我们会变的不一样的。
总是很喜欢这样的天
JUC 系列
- JUC系列(一)什么是JUC?
- JUC系列(二)回顾Synchronized关键字
- JUC系列(三)Lock 锁机制详解 代码理论相结合 正在持续更新中…
一、什么是 Lock
Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。
二、锁类型
可重入锁:在执行对象中所有同步方法不用再次获得锁
可中断锁:在等待获取锁过程中可中断
公平锁: 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利
读写锁:对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写
三、Lock接口
代码语言:javascript复制public interface Lock {
void lock(); //获得锁。
/**
除非当前线程被中断,否则获取锁。
如果可用,则获取锁并立即返回。
如果锁不可用,则当前线程将出于线程调度目的而被禁用并处于休眠状态,直到发生以下两种情况之一:
锁被当前线程获取;
要么其他一些线程中断当前线程,支持中断获取锁。
如果当前线程:
在进入此方法时设置其中断状态;
要么获取锁时中断,支持中断获取锁,
*/
void lockInterruptibly() throws InterruptedException;
/**
仅在调用时空闲时才获取锁。
如果可用,则获取锁并立即返回值为true 。 如果锁不可用,则此方法将立即返回false值。
*/
boolean tryLock();
//比上面多一个等待时间
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 解锁
void unlock();
//返回绑定到此Lock实例的新Condition实例。
Condition newCondition(); 。
}
下面讲几个常用方法的使用。
3.1、lock()、unlock()
lock()是最常用的方法之一,作用就是获取锁,如果锁已经被其他线程获得,则当前线程将被禁用以进行线程调度,并处于休眠状态,等待,直到获取锁。
如果使用到了lock的话,那么必须去主动释放锁,就算发生了异常,也需要我们主动释放锁,因为lock并不会像synchronized一样被自动释放。所以使用lock的话,必须是在try{}catch(){}
中进行,并将释放锁的代码放在finally{}
中,以确保锁一定会被释放,以防止死锁现象的发生。
unlock()的作用就是主动释放锁。
lock接口的类型有好几个实现类,这里是随便找了个哈。
代码语言:javascript复制Lock lock = new ReentrantLock();
try {
lock.lock();
System.out.println("上锁了");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
System.out.println("解锁了");
}
3.2、newCondition
关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock 锁的 newContition()方法返回 Condition 对象,Condition 类 也可以实现等待/通知模式。 用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以 进行选择性通知, Condition 比较常用的两个方法:
- await():会使当前线程等待,同时会释放锁,当等到其他线程调用
signal()
方法时,此时这个沉睡线程会重新获得锁并继续执行代码(在哪里沉睡就在哪里唤醒)。 - signal():用于唤醒一个等待的线程。
注意
:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关 的 Lock 锁,调用 await()后线程会释放这个锁,在调用singal()方法后会从当前 Condition对象的等待队列中,唤醒一个线程,后被唤醒的线程开始尝试去获得锁, 一旦成功获得锁就继续往下执行。
在这个地方我们举个例子来用代码写一下哈:
这里就不举例synchronized 实现了,道理都差不多。
例子:我们有两个线程,实现对一个初始值是0的number变量,一个线程当number = =0时 对number值 1,另外一个线程当number = = 1时对number-1。
代码语言:javascript复制class Share {
private Integer number = 0;
private ReentrantLock lock = new ReentrantLock();
private Condition newCondition = lock.newCondition();
// 1 的方法
public void incr() {
try {
lock.lock(); // 加锁
while (number != 0) {
newCondition.await();//沉睡
}
number ;
System.out.println(Thread.currentThread().getName() "::" number);
newCondition.signal(); //唤醒另一个沉睡的线程
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// -1 的方法
public void decr() {
try {
lock.lock();
while (number != 1) {
newCondition.await();
}
number--;
System.out.println(Thread.currentThread().getName() "::" number);
newCondition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public class LockDemo2 {
public static void main(String[] args) {
Share share = new Share();
new Thread(()->{
for (int i=0;i<=10;i ){
share.incr();
}
},"AA").start();
new Thread(()->{
for (int i=0;i<=10;i ){
share.decr();
}
},"BB").start();
/**
* AA::1
* BB::0
* AA::1
* BB::0
* .....
*/
}
}
四、ReentrantLock (可重入锁)
ReentrantLock
,意思是“可重入锁”。ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更 多的方法。
可重入锁
:什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
package com.crush.juc02;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println("第1次获取锁,这个锁是:" lock);
for (int i = 2;i<=11;i ){
try {
lock.lock();
System.out.println("第" i "次获取锁,这个锁是:" lock);
try {
Thread.sleep(new Random().nextInt(200));
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();// 如果把这里注释掉的话,那么程序就会陷入死锁当中。
}
}
} finally {
lock.unlock();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println("这里是为了测试死锁而多写一个的线程");
} finally {
lock.unlock();
}
}
}).start();
}
}
/**
* 第1次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0]
* 第2次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0]
* 第3次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@6b5fde1f[Locked by thread Thread-0]
* ...
*/
死锁的话,程序就无法停止,直到资源耗尽或主动终止。
代码中也稍微提了一下死锁的概念,在使用Lock中必须手动解锁
,不然就会可能造成死锁的现象。
五、ReadWriteLock (读写锁)
ReadWriteLock 也是一个接口,在它里面只定义了两个方法:
代码语言:javascript复制public interface ReadWriteLock {
// 获取读锁
Lock readLock();
// 获取写锁
Lock writeLock();
}
分为一个读锁一个写锁,将读写进行了分离,使可以多个线程进行读操作,从而提高了效率。
ReentrantReadWriteLock 实现了 ReadWriteLock 接口。里面提供了更丰富的方法,当然最主要的还是获取写锁(writeLock)和读锁(readLock)。
5.1、案例
假如多个线程要进行读的操作,我们用Synchronized 来实现的话。
代码语言:javascript复制public class SynchronizedDemo2 {
public static void main(String[] args) {
final SynchronizedDemo2 test = new SynchronizedDemo2();
new Thread(()->{
test.get(Thread.currentThread());
}).start();
new Thread(()->{
test.get(Thread.currentThread());
}).start();
}
public synchronized void get(Thread thread) {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName() "正在进行读操作");
}
System.out.println(thread.getName() "读操作完毕");
}
}
/**
* 输出
* Thread-0正在进行读操作
* Thread-0读操作完毕
* Thread-1正在进行读操作
* Thread-1正在进行读操作
* Thread-1正在进行读操作
* ....
* Thread-1读操作完毕
*/
改成读写锁之后
代码语言:javascript复制public class SynchronizedDemo2 {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final SynchronizedDemo2 test = new SynchronizedDemo2();
new Thread(()->{
test.get2(Thread.currentThread());
}).start();
new Thread(()->{
test.get2(Thread.currentThread());
}).start();
}
public void get2(Thread thread) {
rwl.readLock().lock();
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName() "正在进行读操作");
}
System.out.println(thread.getName() "读操作完毕");
} finally {
rwl.readLock().unlock();
}
}
}
/**
* 输出
* Thread-0正在进行读操作
* Thread-0读操作完毕
* Thread-1正在进行读操作
* Thread-1读操作完毕
*/
结论:改用读写锁后 线程1和线程2 同时在读,可以感受到效率的明显提升。
注意
:
- 若此时已经有一个线程占用了读锁,此时其他线程申请读锁是可以的,但是若此时其他线程申请写锁,则只有等待读锁释放,才能成功获得。
- 若此时已经有一个线程占用了写锁,那么此时其他线程申请写锁或读锁,都只有持有写锁的线程释放写锁,才能成功获得。
六、Lock 与的 Synchronized 区别
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个接口 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 |
锁的释放 | 1、当 synchronized 方法或者 synchronized 代码块执行完之后, 系统会自动让线程释放对锁的占用 (不需要手动释放锁)2、若线程执行发生异常,jvm会让线程释放锁 | 在finally中必须释放锁,不然容易造成线程死锁现象 (需要手动释放锁) |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 锁类型 | 可重入 可判断 可公平(两者皆可) |
性能 | 前提:大量线程情况下 同步效率较低 | 前提:大量线程情况下 同步效率比synchronized高的多 |
Lock可以提高多个线程进行读操作的效率。
七、自言自语
最近又开始了JUC的学习,感觉Java内容真的很多,但是为了能够走的更远,还是觉得应该需要打牢一下基础。
正在持续更新中,如果你觉得对你有所帮助,也感兴趣的话,关注我吧,让我们一起学习,一起讨论吧。
你好,我是博主宁在春
,Java学习路上的一颗小小的种子,也希望有一天能扎根长成苍天大树。
希望与君共勉