6.JUC之 Lock锁
- 6.1锁的可重入性
- 6.2 ReentrantLock 可重入锁
- 6.3 lockInterruptibly()方法
- 6.4 tryLock
- 6.5 newCondition()方法
- 6.6 公平锁与非公平锁
- 6.7 ReentrantLock中API
- 6.8 ReentrantReadWriteLock 读写锁
- 6.8.1 读读共享
- 6.8.2 写写互斥
- 6.8.3 读写互斥
- 6.9 Lock与Synchronized区别
JUC代表java并发包的简写
在 jdk1.5 之后,并发包中新增了 Lock 接口(以及相关实现类)用来实现锁功能,Lock 接口提供了与 synchronized 关键字类似的同步功能,但需要在使用时手动获取锁和释放锁。
6.1锁的可重入性
锁的可重入是指,当一个线程获得一个对象锁后,再次请求该对象锁时仍然可以获得该对象的锁。
public class LockTest01 {
private synchronized void print1() {
System.out.println("同步方法1...");
print2();
}
private synchronized void print2() {
System.out.println("同步方法2...");
print3();
}
private synchronized void print3() {
System.out.println("同步方法3...");
}
public static void main(String[] args) {
LockTest01 lockTest01 = new LockTest01();
new Thread(new Runnable() {
@Override
public void run() {
lockTest01.print1();
}
}).start();
}
}
// 线程执行 print1()方法,默认 this 作为锁对象,在 print1()方法中调用了 print2()方法,注意当前线程还是持有 this 锁对象的
// print2()同步方法默认的锁对象也是 this 对象, 要执行 print2()必须先获得 this 锁对象,当前 this 对象被当前线程持有,可以 再次获得 this 锁对象, 这就是锁的可重入性. 假设锁不可重入的话,可能会造成死锁
6.2 ReentrantLock 可重入锁
调用 lock()方法获得锁,调用 unlock()释放锁,同样可以重入锁。
Lock lock = new ReentrantLock();
lock.lock();
try{
// 可能会出现线程安全的操作
}finally{
// 一定在finally中释放锁
// 也不能把获取锁在try中进行,因为有可能在获取锁的时候抛出异常
lock.unlock();
}
public class C1Lock {
// 总票数
static int ticket = 10;
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Runnable runnable = () -> {
// 循环买票
while (true) {
// 获得锁
lock.lock();
try {
Thread.sleep(100);
if (ticket > 0) {
ticket--;
System.out.println(Thread.currentThread().getName()
+ "卖了一张票,剩余:" + ticket);
} else {
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
}
};
// 创建3个线程
Thread t1 = new Thread(runnable, "窗口1");
Thread t2 = new Thread(runnable, "窗口2");
Thread t3 = new Thread(runnable, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
6.3 lockInterruptibly()方法
lockInterruptibly() 方法的作用:如果当前线程未被中断则获得锁,如果当前线程被中断则抛出异常。
对于 synchronized 内部锁来说,如果一个线程在等待锁,只有两个结果:要么该线程获得锁继续执行;要么就保持等待。
对于 ReentrantLock 可重入锁来说,提供另外一种可能,在等待锁的过程中,程序可以根据需要取消对锁的请求。
/**
* 通过 ReentrantLock 锁的 lockInterruptibly()方法避免死锁的产生
*/
public class Test06 {
static class IntLock implements Runnable {
//创建两个 ReentrantLock 锁对象
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lockNum; //定义整数变量,决定使用哪个锁
public IntLock(int lockNum) {
this.lockNum = lockNum;
}
@Override
public void run() {
try {
if (lockNum % 2 == 1) {
//奇数,先锁 1,再锁 2
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "获得锁 1,还需要获得锁 2");
Thread.sleep(new Random().nextInt(500));
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "同时获得了锁 1 与锁 2....");
} else {
//偶数,先锁 2,再锁 1
lock2.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "获得锁 2,还需要获得锁 1");
Thread.sleep(new Random().nextInt(500));
lock1.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + "同时获得了锁1 与锁 2....");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) { //判断当前线程是否持有该锁
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
System.out.println(Thread.currentThread().getName() + "线程退出");
}
}
}
public static void main(String[] args) throws InterruptedException {
IntLock intLock1 = new IntLock(11);
IntLock intLock2 = new IntLock(22);
Thread t1 = new Thread(intLock1);
Thread t2 = new Thread(intLock2);
t1.start();
t2.start();
//在 main 线程,等待 3000 秒,如果还有线程没有结束就中断该线程
Thread.sleep(3000);
//可以中断任何一个线程来解决死锁, t2 线程会放弃对锁 1 的申请,同时释放锁 2, t1 线程会完成它的任务
if (t2.isAlive()) {
t2.interrupt();
}
}
}
6.4 tryLock
tryLock public boolean tryLock()
仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
1)如果该锁没有被另一个线程保持,并且立即返回 true 值,则将锁的保持计数设置为 1。
即使已将此锁设置为使用公平排序策略,但是调用 tryLock() 仍将 立即获取锁(如果有可用的),而不管其他线程当前是否正在等待该锁。
在某些情况下,此“闯入”行为可能很有用,即使它会打破公平性也如此。如果希望遵守此锁的公平设置,则使用 tryLock(0, TimeUnit.SECONDS) ,它几乎是等效的(也检测中断)。
2)如果当前线程已经保持此锁,则将保持计数加 1,该方法将返回 true。
3)如果锁被另一个线程保持,则此方法将立即返回 false 值。
指定者:
接口 Lock 中的 tryLock
返回:
如果锁是自由的并且被当前线程获取,或者当前线程已经保持该锁,则返回 true ;否则返回 false
作者:wuxinliulei
链接:https://www.zhihu.com/question/36771163/answer/68974735
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
// 仅在调用时锁是空闲的情况下才获取锁
boolean tryLock()
// 如果锁在给定的等待时间内是空闲的,并且当前线程没有被中断,则获取锁
boolean tryLock(long time, TimeUnit unit)
public class C2TryLock {
private ArrayList arrayList = new ArrayList<>();
//注意这个地方
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
final C2TryLock test = new C2TryLock();
Runnable run1 = new Runnable() {
@Override
public void run() {
test.insert(Thread.currentThread());
}
};
Runnable run2 = new Runnable() {
@Override
public void run() {
try {
test.insert2(Thread.currentThread());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable run3 = new Runnable() {
@Override
public void run() {
test.threadSleep();
}
};
// 情况1:开启线程3
new Thread(run3, "线程3").start();
// 情况2:不开启线程3,线程1执行中睡眠3秒
// new Thread(run3, "线程3").start();
// 情况3:线程1执行中睡眠2秒
new Thread(run1, "线程1").start();
new Thread(run2, "线程2").start();
}
public void insert(Thread thread) {
// 如果锁被占用直接返回false
if (lock.tryLock()) {
try {
System.out.println(thread.getName() + "得到了锁");
Thread.sleep(2000);
for (int i = 0; i < 5; i++) {
arrayList.add(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(thread.getName() + "释放了锁");
lock.unlock();
}
} else {
System.out.println(thread.getName() + "获取锁失败");
}
}
public void insert2(Thread thread) throws InterruptedException {
// 如果锁被占用,在等待3秒内获得锁返回true,超出等待时间后返回false
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
System.out.println(thread.getName() + "得到了锁");
for (int i = 0; i < 5; i++) {
arrayList.add(i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(thread.getName() + "释放了锁");
lock.unlock();
}
} else {
System.out.println(thread.getName() + "获取锁失败");
}
}
public void threadSleep() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"睡眠中...");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
6.5 newCondition()方法
// 返回一个绑定到这个Lock实例的新的Condition实例
Condition newCondition()
??:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重新获得锁并继续执行。
注意:
在调用 Condition 的 await()/signal()方法前,也需要线程持有相关的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前 Condition 对象的等待队列中唤醒 一个线程,唤醒的线程尝试获得锁,一旦获得锁成功就继续执行。若调用await()/signal()方法前没有持有相关Lock锁,会抛出java.lang.IllegalMonitorStateException
异常。
Interface Condition
Modifier and Type | Method | Description |
---|---|---|
void | await() | 使当前线程等待,直到它被通知或中断。 |
boolean | await(long time, TimeUnit unit) | 使当前线程等待,直到它被通知或中断,或指定的等待时间过去。 |
long | awaitNanos(long nanosTimeout) | 使当前线程等待,直到它被通知或中断,或指定的等待时间(纳秒)过去。 |
void | awaitUninterruptibly() | 使当前线程等待,直到它得到信号 |
boolean | awaitUntil(Date deadline) | 使当前线程等待,直到它被通知或被中断,或超过指定的截止日期。 |
void | signal() | 唤醒一个等待的线程。 |
void | signalAll() | 唤醒所有等待的线程。 |
举个??:
使用Condition实现两个线程交替打印字母和数字
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 使用Condition实现两个线程交替打印字母和数字
*
*/
public class AlternatePrint {
static Lock lock = new ReentrantLock();
static Condition figureCondition = lock.newCondition();
static Condition letterCondition = lock.newCondition();
private int num = 0;
public static void main(String[] args) {
AlternatePrint alternatePrint = new AlternatePrint();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 26; i++) {
alternatePrint.printLetter();
}
System.out.println("thread-0结束");
/*
* 需要释放两个Condition等待的线程,
*
* 问题:Thread-0线程打印完'Z'后,唤醒了Thread-1线程自身进入等待并释放锁
* Thread-1线程打印完26后唤醒Thread-0,自身进入等待并释放锁,自此Thread-1一致处于等待及获取锁的状态TIMED_WAITING
*
* 解决方式:
* Thread-0线程结束后唤醒另一个Condition的线程等待状态,使程序正常结束
* */
alternatePrint.releaseWail();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 26; i++) {
alternatePrint.printFigure();
}
System.out.println("thread-1结束");
alternatePrint.releaseWail();
}
}).start();
}
/**
* 打印字母
*/
private void printLetter() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"--->" + (char)(num + 65));
// 这里要先把对方释放,自身再等待,否则当前持有锁的线程会一直等待,其他线程也不能获取锁
figureCondition.signal();
letterCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 打印数字
*/
private void printFigure() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"--->" + (++num));
// 这里要先把对方释放,自身再等待,否则当前持有锁的线程会一直等待,其他线程也不能获取锁
letterCondition.signal();
figureCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private void releaseWail() {
lock.lock();
try {
letterCondition.signalAll();
figureCondition.signalAll();
} finally {
lock.unlock();
}
}
}
运行结果:程序没有完全结束,其中一个线程处于等待状态
修改后:
6.6 公平锁与非公平锁
大多数情况下,锁的申请都是非公平的,如果线程 1与线程 2都在请求锁 A,当锁 A 可用时,系统只是会从阻塞队列中随机的选择一个线程,不能保证其公平性。
公平的锁会按照时间先后顺序,保证先到先得,公平锁的这一特点不会出现线程饥饿现象。
synchronized 内部锁就是非公平的,ReentrantLock 重入锁提供了一个构造方法:ReentrantLock(boolean fair) ,当在创建锁对象时实参传递 true 可以把该锁设置为公平锁。
// 创建一个ReentrantLock实例,非公平锁
ReentrantLock()
// 使用给定的公平策略创建一个ReentrantLock实例。
ReentrantLock(boolean fair)
公平锁看起来很公平,但是要实现公平锁必须要求系统维护一个有序队列,公平锁的实现成本较高,性能也低。因此默认情况下锁是非公平的,不是特别的需求,一般不使用公平锁。
/**
* 公平锁与非公平锁
*/
public class Test01 {
// static ReentrantLock lock = new ReentrantLock(); //默认是非公平锁
static ReentrantLock lock = new ReentrantLock(true); //定义公平锁
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获得了锁对象");
} finally {
lock.unlock();
}
}
}
};
for (int i = 0; i < 5; i++) {
new Thread(runnable).start();
}
/*
运行程序
1)如果是非公平锁, 系统倾向于让一个线程再次获得已经持有的锁, 这种分配策略是高效的,非公平的
2)如果是公平锁, 多个线程不会发生同一个线程连续多次获得锁的可能,保证了公平性
*/
}
}
6.7 ReentrantLock中API
// 返回当前线程调用 lock()方法的次数
int getHoldCount()
// 返回正等待获得锁的线程预估数
int getQueueLength()
// 返回与 Condition 条件相关的等待的线程预估数
int getWaitQueueLength(Condition condition)
// 查询参数指定的线程是否在等待获得锁
boolean hasQueuedThread(Thread thread)
//查询是否还有线程在等待获得该锁
boolean hasQueuedThreads()
// 查询是否有线程正在等待指定的 Condition 条件
boolean hasWaiters(Condition condition)
// 判断是否为公平锁
boolean isFair()
// 判断当前线程是否持有该锁
boolean isHeldByCurrentThread()
// 查询当前锁是否被线程持有
boolean isLocked()
6.8 ReentrantReadWriteLock 读写锁
synchronized 内部锁与 ReentrantLock 锁都是独占锁(排它锁),同一时间只允许一个线程执行同步代码块,可以保证线程的安全性,但是执行效率低。
ReentrantReadWriteLock 读写锁是一种改进的排他锁,也可以称作共享/排他锁。允许多个线程同时读取共享数据,但是一次只允许一个线程对共享数据进行更新。
读写锁通过读锁与写锁来完成读写操作,线程在读取共享数据前必须先持有读锁,该读锁可以同时被多个线程持有,即它是共享的,线程在修改共享数据前必须先持有写锁,写锁是排他的。一个线程持有写锁时其他线程无法获得相应的锁(读/写锁)。
读锁只是在读线程之间共享,任何一个线程持有读锁时,其他线程都无法获得写锁,保证线程在读取数据期间没有其他线程对数据进行更新,使得读线程能够读到数据的最新值,保证在读数据期间共享变量不被修改。
获得条件 | 排他性 | 作用 | |
---|---|---|---|
读锁 | 写锁未被任意线程持有 | 对读线程是共享的, 对写线程是排他的 | 允许多个读线程可以同时读取共享数据,保证在读共享数据时,没有其他线程对共享数据进行修改 |
写锁 | 该写锁未被其他线程持有,并且相应的读锁也未被其他线程持有 | 对读线程或者写线程都是排他的 | 保证写线程以独占的方式修改共享数据 |
读写锁允许读读共享,读写互斥,写写互斥。
在 java.util.concurrent.locks包中定义了 ReadWriteLock接口,该接口中定义了 readLock()返回读锁,定义 writeLock()方法返回写锁,该接口的实现类是 ReentrantReadWriteLock。
Interface ReadWriteLock
// 返回用于读取的锁。
Lock readLock()
// 返回用于写入的锁。
Lock writeLock()
Class ReentrantReadWriteLock Implemented ReadWriteLock
// 返回用于读取的锁。
ReentrantReadWriteLock.ReadLock readLock()
// 返回用于写入的锁。
ReentrantReadWriteLock.WriteLock writeLock()
注意:
readLock()与 writeLock()方法返回的锁对象是同一个锁的两个不同的角色,不是分别获得两个不同的锁。ReadWriteLock 接口实例可以充当两个角色,读写锁的其他使用方法
// 定义读写锁
ReadWriteLock rwLock = new ReentrantReadWriteLock();
// 获得读锁
Lock readLock = rwLock.readLock();
// 获得写锁
Lock writeLock = rwLock.writeLock();
/*
读数据
*/
readLock.lock(); //申请读锁
try{
// 读取共享数据
} finally {
// 总是在 finally 子句中释放锁
readLock.unlock();
}
/*
写数据
*/
writeLock.lock(); //申请写锁
try{
// 更新修改共享数据
} finally {
// 总是在 finally 子句中释放锁
writeLock.unlock();
}
6.8.1 读读共享
ReadWriteLock 读写锁可以实现多个线程同时读取共享数据,即读读共享,可以提高程序的读取数据的效率。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* ReadWriteLock 读写锁可以实现读读共享,允许多个线程同时获得读锁
*/
public class Test01 {
static class Service {
// 定义读写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 定义方法读取数据
public void read() {
// 获得读锁
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "获得读锁,开始读取数据的时间--" + System.currentTimeMillis());
// 模拟读取数据用时3s
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放读锁
readWriteLock.readLock().unlock();
}
}
}
public static void main(String[] args) {
Service service = new Service();
// 创建 5 个线程,调用 read()方法
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// 在线程中调用 read()读取数据
service.read();
}
}).start();
}
// 运行程序后,多个线程几乎可以同时获得锁读,执行 lock()后面的代码
}
}
6.8.2 写写互斥
通过 ReadWriteLock 读写锁中的写锁,只允许有一个线程执行 lock()后面的代码。即获取写锁得线程间使互斥得。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 演示 ReadWriteLock 的 writeLock()写锁是互斥的,只允许有一个线程持有
*/
public class Test02 {
static class Service {
// 先定义读写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 定义方法修改数据
public void write() {
try {
// 申请获得写锁
readWriteLock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + "获得写锁,开始修改数据的时间--"
+ DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss").format(LocalDateTime.now()));
// 模拟修改数据的用时3s
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "修改数据完毕时的时间=="
+ DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss").format(LocalDateTime.now()));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放写锁
readWriteLock.writeLock().unlock();
}
}
}
public static void main(String[] args) {
Service service = new Service();
// 创建 5 个线程修改数据
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// 调用修改数据的方法
service.write();
}
}).start();
}
}
}
6.8.3 读写互斥
写锁是独占锁、是排他锁,读线程与写线程也是互斥的。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 演示 ReadWriteLock 的读写互斥
*
* 一个线程获得读锁时,写线程等待; 一个线程获得写锁时,其他线程等待
*/
public class Test03 {
static class Service {
//先定义读写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 获得读锁
Lock readLock = readWriteLock.readLock();
// 获得写锁
Lock writeLock = readWriteLock.writeLock();
/**
* 读取共享数据
*/
public void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获得读锁,开始读取数据的时间--"
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(LocalDateTime.now()));
// 模拟读取数据的操作用时
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "读取数据完毕时的时间=="
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(LocalDateTime.now()));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
} //定义方法修改数据
/**
* 更新共享数据
*/
public void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获得写锁,开始修改数据的时间--"
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
// 模拟修改数据的用时
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "修改数据完毕时的时间=="
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
}
public static void main(String[] args) {
Service service = new Service();
// 定义一个线程读数据
new Thread(new Runnable() {
@Override
public void run() {
service.read();
}
}).start();
// 定义一个线程写数据
new Thread(new Runnable() {
@Override
public void run() {
service.write();
}
}).start();
}
}
6.9 Lock与Synchronized区别
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
synchronized关键字可以直接修饰方法,也可以修饰代码块,而lock只能修饰代码块
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。(提供tryLock)
5)Lock可以提高多个线程进行读操作的效率。(提供读写锁)
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。