关键字synchronized的功能扩展:重入锁ReentrantLock
- ReentrantLock是一个 可重入 的互斥锁,又被称为“ 独占锁 ”。
- 互斥锁: 在同一个时间点只能被一个线程持有;
- 可重入: 单个线程能多次获得相同的锁;
- ReentrantLock分为“公平锁”和“非公平锁”。
- 区别:体现在获取锁的机制上是否公平。
- ReentraantLock是通过一个 FIFO的等待队列 来管理获取该锁所有线程的。
- 在“公平锁”的机制下,线程依次排队获取锁;
- “非公平锁”在锁是可获取状态时,按抢占机制,都可去请求锁。
- 在JDK5.0的早期版本中,重入锁的性能远远优于关键字synchronized,但从JDK6.0开始,JDK在关键字synchronized上做了大量的优化,使得两者的性能差距并不大;
重入锁ReentrantLock的功能
- ① 基本的加锁功能 、可以反复加锁、以及 加锁是对什么加锁?
package com.wwj.lockdemos;
import javax.sound.sampled.FloatControl;
import java.util.concurrent.locks.ReentrantLock;
public class ReenTrantLockDemo implements Runnable {
ReentrantLock lock = new ReentrantLock();
public static int count = 0 ;
@Override
public void run() {
for(int i=0 ; i<1000 ; i++){
try{
lock.lock();
lock.lock();
System.out.print(lock.getHoldCount()+" ");
count++;
}finally {
lock.unlock();
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
// Thread t1 = new Thread(new ReenTrantLockDemo()); 结果是1994,所以说明lock也是对象锁,和synchronized一样,但是底层原理不同
// Thread t2 = new Thread(new ReenTrantLockDemo());
ReenTrantLockDemo td = new ReenTrantLockDemo();
Thread t1 = new Thread(td);
Thread t2 = new Thread(td);
t1.start();
t2.start();
t1.join(); t2.join();
System.out.println(count);
}
}
//结果1:19861 (for循环为10000,因为1000太小了)
//结果2:2 2 2 2 2 2 2... 2000
- 分析:
- 通过结果2000可以得到,ReentrantLock实现了基本的加锁功能;
- 通过2可以得到,ReentrantLock实现了反复加锁(重加锁);
- 通过19861可以得到,实现的是对象锁;
- 重点:对于19861,这个程序代码,如果把
ReentrantLock lock = new ReentrantLock();
前面加上static
,则结果就是20000,因为这就相当于synchronized修饰类方法一样
- 注意:
- ReentrantLock虽然持有对象监视器,但是和synchronized持有的对象监视器不同。
- 手动释放锁unlock()👏👏👏👏👏👏👏👏👏
- ② 可中断功能(防止死锁)(避免死锁的第一种方法):
package com.wwj;
import java.util.concurrent.locks.ReentrantLock;
public class LockInterruptibilyDemo implements Runnable{
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
/*
@lock 控制加锁顺序,方便构造死锁
*/
public LockInterruptibilyDemo(int lock){
this.lock = lock;
}
@Override
public void run() {
try{
if(lock == 1){
lock1.lockInterruptibly();
try{
Thread.sleep(500);
}catch (InterruptedException e){ }
lock2.lockInterruptibly();
}else{
lock2.lockInterruptibly();
try {
Thread.sleep(500);
}catch (InterruptedException e){ }
lock1.lockInterruptibly();
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
if(lock1.isHeldByCurrentThread())
lock1.unlock();
if(lock2.isHeldByCurrentThread())
lock2.unlock();
System.out.println(Thread.currentThread().getId() + ":线程退出");
}
}
public static void main(String[] args) throws InterruptedException{
LockInterruptibilyDemo l1 = new LockInterruptibilyDemo(1);
LockInterruptibilyDemo l2 = new LockInterruptibilyDemo(2);
Thread t1 = new Thread(l1);
Thread t2 = new Thread(l2);
t1.start();t2.start();
Thread.sleep(1000);
t2.interrupt();
}
}
结果:
java.lang.InterruptedException
at java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:958)
at java.base/java.util.concurrent.locks.ReentrantLock$Sync.lockInterruptibly(ReentrantLock.java:161)
at java.base/java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:372)
at com.wwj.LockInterruptibilyDemo.run(LockInterruptibilyDemo.java:32)
at java.base/java.lang.Thread.run(Thread.java:832)
16:线程退出
15:线程退出
- 程序讲解:
- 线程 t1和t2启动后,t1先占用lock1,再占用lock2;t2先占用lock2,再占用lock1,这样很容易形成t1和t2之间的相互等待。
- 程序中的
Thread.sleep(1000);
说明主线程main处于休息状态,此时这两个线程处于死锁的状态。 t2.interrupt();
此时让t2先释放锁,这样就不满足死锁条件了,然后让t1获得锁执行完操作再退出。
- ③ 锁申请等待限时,防止一直等待锁(避免死锁的第二种方法)
package com.wwj.lockdemos;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TimeLockDemo implements Runnable{
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
if(lock.tryLock(5, TimeUnit.SECONDS)){
Thread.sleep(6000);
}else{
System.out.println("没有得到锁");
};
}catch (InterruptedException e){
e.printStackTrace();
} finally{
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
public static void main(String[] args) {
TimeLockDemo td = new TimeLockDemo();
Thread t1 = new Thread(td);
Thread t2 = new Thread(td);
t1.start();t2.start();
}
}
- 程序讲解:
- 这个程序表明如果超过5秒还没有得到锁,就会返回flase,如果成功得到锁,则返回true。
- 因此让占用锁的线程持有锁长达6秒,故另一个线程无法在5秒内获得锁,因此获得锁失败
- 再看一个程序:
package com.wwj;
import java.util.concurrent.locks.ReentrantLock;
public class LockInterruptibilyDemo implements Runnable{
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
/*
@lock 控制加锁顺序,方便构造死锁
*/
public LockInterruptibilyDemo(int lock){
this.lock = lock;
}
@Override
public void run() {
if (lock == 1) { //是为了让他们死锁用,保证先后调用得顺序
while (true) {
if (lock1.tryLock()) {
try {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
if (lock2.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + "我的工作完成");
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
}
} else {
while (true) {
if (lock2.tryLock()) {
try {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
if (lock1.tryLock()) {
try {
System.out.println(Thread.currentThread().getId() + "我的工作完成");
return;
} finally {
lock1.unlock();
}
}
} finally {
lock2.unlock();
}
}
}
}
}
public static void main(String[] args) throws InterruptedException{
LockInterruptibilyDemo l1 = new LockInterruptibilyDemo(1);
LockInterruptibilyDemo l2 = new LockInterruptibilyDemo(2);
Thread t1 = new Thread(l1);
Thread t2 = new Thread(l2);
t1.start();t2.start();
Thread.sleep(1000);
t2.interrupt();
}
}
结果: 16我的工作完成
15我的工作完成
-
程序讲解:上述代码非常容易造成死锁,但是通过tryLock()方法后,这种情况大大改善了。由于线程不能傻傻得等,而是不停地尝试。
-
④ 公平锁和非公平锁的实现
- ReentrantLock的一个很好的功能就是 指定锁是否是公平的 。其实 ReentrantLock内部都维持着一个同步队列 ,公平锁表示线程获取锁的顺序是按照线程排队的顺序来唤醒的,而非公平锁是随机唤醒的,是一个抢占机制。
- synchronized就是一种非公平锁
- 非公平锁可能造成线程的”饥饿“问题,涉及到了线程的优先级问题。后续会分析源码,现在只展示应用。
package com.wwj.lockdemos;
import java.util.concurrent.locks.ReentrantLock;
public class IsFairSycDemo {
public static void main(String[] args) {
Thread_0 do1 = new Thread_0();
Thread[] threads = new Thread[5];
for(int i=0 ; i<5 ; i++){
threads[i] = new Thread(do1);
}
for(int i=0 ; i<5 ; i++){
threads[i].start();
}
}
}
class Thread_0 implements Runnable{
DoSomeThing td = new DoSomeThing();
@Override
public void run() {
System.out.println(Thread.currentThread().getId()+"----->运行了");
td.doSomeThing();
}
}
class DoSomeThing {
ReentrantLock lock = new ReentrantLock();
public void doSomeThing(){
try{
lock.lock();
System.out.println(Thread.currentThread().getId()+"获得锁");
}finally {
lock.unlock();
}
}
}
结果:
17----->运行了
16----->运行了
19----->运行了
18----->运行了
15----->运行了
17获得锁
18获得锁
15获得锁
16获得锁
19获得锁
- 结果分析:
- 由线程的运行和获得锁的顺序可以得知,这并不是公平锁。
- 将ReentrantLock的构造器来改成公平锁:
ReentrantLock lock = new ReentrantLock(true);
19----->运行了
17----->运行了
18----->运行了
15----->运行了
16----->运行了
19获得锁
17获得锁
15获得锁
16获得锁
18获得锁
- 由线程的运行和获得锁的顺序可以得知,这是公平锁。
- 别的方法就不一一介绍了。
Condition
- synchronized与wait()和nitofy()/notifyAll()方法相结合可以实现等待/通知模型,那么ReentrantLock可以嘛?
- ReentrantLock同样可以,但是需要借助“Condition",且Condition有更好的灵活性,具体体现在:
- 一个Lock里面 可以创建多个Condition实例,实现多路通知(典型的就是生产消费者模式);
- notify()方法进行通知时,被通知的线程是JVM随机选择的,但是ReentrantLock结合Condition可以实现有选择性地通知;
- Condition的作用:
- 对锁进行更精确的控制。
- Condition中await()、signal()、singnalAll()和同步锁中的部分方法的对比:
- Condition中的await()方法相当于Object的wait()方法;Condition中的signal()方法相当于Object的notify()方法;Condition中的signalAll()相当于Object的notifyAll()方法。
- 不同的是,Object中的wait(),notify(),notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的;而Condition是需要与 “互斥锁”/“共享锁” 捆绑使用的。
- 程序1:Condition的基本使用(通过synchronized也可以做到):
package com.wwj.lockdemos;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockConditionDemo implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
@Override
public void run() {
try{
lock.lock();
System.out.println("持有锁,并等待");
condition.await();
System.out.println("唤醒锁");
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new ReentrantLockConditionDemo());
t1.start();
Thread.sleep(2000);
lock.lock();
condition.signal();
lock.unlock();
}
}
//结果:
//持有锁,并等待
//唤醒锁
- 程序分析:
condition.await();
要求线程在Condition对象上进行等待main()中的lock.lock();------ lock.unlock();
:指的是由main发出通知,告知等待在Condition上的线程可以继续执行了。- 与Object.wait()方法和notify()一样,当线程使用Condition.await()方法时,要求线程持有相关的重入锁,在Condition.await()调用后,这个线程会释放这把锁。
- 同理, 当线程执行condition.signal()时,要求线程持有相关的重入锁,执行signal()后,系统会从当前Condition对象的等待队列中唤醒一个线程。一旦线程被唤醒,它会重新尝试获得与 之绑定的重入锁。
- 因此,在signal()方法调用后,一般要释放相关的锁,让给被唤醒的线程,也就是t1;可以这样想,signal()、await()必须在lock()后执行,然后执行完还要有相应的unlock(),就如Object下的wait()和notify()执行必须在sychronized下执行一个道理。
- 程序2:Condition的精妙使用、精确控制:
package com.wwj.lockdemos;
import java.io.InputStream;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ProConBuffer{
final ReentrantLock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] buffers = new Object[5];
int putIdx , takeIdx , count; //putIdx , takeIdx 是为了防止超出边界
//放入方法
public void put(Object x) throws InterruptedException{
try{
lock.lock();
//如果缓冲满了则等待
if(count == buffers.length)
notFull.await();
//不满则放入
buffers[putIdx++] = x;
if(putIdx == buffers.length)
putIdx = 0;
count++;
notEmpty.signal();
System.out.println(Thread.currentThread().getName()+" put :" + (Integer)x);
}finally {
lock.unlock();// 释放锁
}
}
//输出方法
public Object take() throws InterruptedException{
try{
lock.lock();
if(count == 0)
notEmpty.await();
//非空则可以取出
Object y = buffers[takeIdx++];
if(takeIdx == buffers.length)
takeIdx = 0;
// 将“缓冲”数量-1
count--;
// 唤醒put线程,因为put线程通过notFull.await()等待
notFull.signal();
// 打印取出的数据
System.out.println(Thread.currentThread().getName()+" take:" + (Integer)y);
return y;
}finally {
lock.unlock();
}
}
}
public class ConditionBufferDemo {
private static ProConBuffer proConBuffer = new ProConBuffer();
public static void main(String[] args) {
for(int i=0 ; i<10 ; i++){
new PutThread("p"+i , i).start();
new TakeThread("t"+i).start();
}
}
static class PutThread extends Thread{
private int num;
public PutThread(String name ,int num ){
super(name);
this.num = num;
}
@Override
public void run() {
try{
Thread.sleep(1);
proConBuffer.put(num);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
static class TakeThread extends Thread{
public TakeThread(String name){
super(name);
}
@Override
public void run() {
try{
Thread.sleep(10);
Integer num = (Integer) proConBuffer.take();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
结果:
p0 put :0
p4 put :4
p7 put :7
p5 put :5
p3 put :3
t5 take:0
p6 put :6
t1 take:4
t2 take:7
t3 take:5
t6 take:3
t4 take:6
p9 put :9
p2 put :2
p1 put :1
p8 put :8
t7 take:9
t9 take:2
t8 take:1
t0 take:8
- 结果分析:
- Buffers 是容量为5的缓冲,缓冲中存储的是Object对象,支持多线程的读/写缓冲。多个线程操作“一个Buffers对象”时,它们通过互斥锁lock对缓冲区items进行互斥访问;而且同一个Buffers对象下的全部线程共用“notFull”和“notEmpty”这两个Condition。
- notFull用于控制写缓冲;
- notEmpty用于控制读缓冲;
- 当缓冲已满的时候,调用put的线程会执行notFull.await()进行等待;
- 当缓冲区不是满的状态时,就将对象添加到缓冲区并将缓冲区的容量count+1;
- 最后,调用notEmpty.signal()缓冲notEmpty上的等待线程(调用notEmpty.await的线程);
- 简言之,notFull控制“缓冲区的写入”,当往缓冲区写入数据之后会唤醒notEmpty上的等待线程。
- 同理,notEmpty控制“缓冲区的读取”,当读取了缓冲区数据之后会唤醒notFull上的等待线程。
- 在ConditionTest2的main函数中,启动10个“写线程”,向BoundedBuffer中不断的写数据(写入0-9);同时,也启动10个“读线程”,从BoundedBuffer中不断的读数据.