JUC中的各种锁(ReentrantLock等)
ReentrantLock(可重入锁)
概念
ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:1. 重入性的实现原理;2. 公平锁和非公平锁
简介
ReentrantLock常常对比着synchronized来分析
ReentrantLock常常对比着synchronized来分析,我们先对比着来看然后再一点一点分析。
(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。(ReentrantLock等lock方法一定要在finally进行unlock)
(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。
(4 )ReentrantLock和synchronized最主要的就区别是ReentrantLock还可以实现公平锁机制。也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁(不公平锁是只要锁被释放,其他线程一起争抢锁)
公平锁和非公平锁
从ReentrantLock的构造中可以看到,ReentrantLock提供两种锁:公平锁和非公平锁,其内部实现了两种同步器NonfairSync、FairSync派生自AQS,主要才采用了模板方法模式,主要重写了AQS的tryAcquire、lock方法
ReentrantLock的实现原理
ReentrantLock在内部使用了内部类Sync来管理锁,所以真正的获取锁是由Sync的实现类控制的。Sync有两个实现,分别为NonfairSync(非公平锁)和FairSync(公平锁)。Sync通过继承AQS实现,在AQS中维护了一个private volatile int state来计数重入次数,避免了频繁的持有释放操作带来效率问题。所以说本质上常见的同步锁是使用CAS+volatile来实现的
代码
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T02_ReentrantLock2 {
//非公平锁
Lock lock = new ReentrantLock();
void m1() {
try {
lock.lock(); //synchronized(this)
for (int i = 0; i < 10; i++) {
TimeUnit.SECONDS.sleep(1);
System.out.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
void m2() {
try {
lock.lock();
System.out.println("m2 ...");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
T02_ReentrantLock2 rl = new T02_ReentrantLock2();
new Thread(rl::m1).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(rl::m2).start();
}
}
输出结果
==========================================
0
1
2
3
4
5
6
7
8
9
m2 ...
==========================================
使用场景
- 发现该操作已经在执行中则不再执行
- 如果该操作已经在执行,则等待一个个执行(同步执行,与synchronized类似)
- 如果该操作已经在执行,则尝试等待一段时间,超时则放弃执行
- 如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作
CountDownLatch
概念
- countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
- CountDownLatch是一个同步容器,但是有人叫它发令枪,也有人叫它门闩。初始化设定线程的个数,调用countDownLatch.await()阻塞所有线程,直到countDownLatch.countDown()为0,那么将继续执行剩余的操作。
主要方法
-
countDownLatch类中只提供了一个构造器:
public CountDownLatch(int count) {}
-
类中有三个方法是最重要的:
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行 public void await() throws InterruptedException { }; //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行 public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; //将count值减1 public void countDown() { };
代码示例
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchTest {
public static void main(String[] args) {
//new CountDownLatch(2);这里设置成2是因为下面只创建了两个新线程,如果大于2,则会到countDownLatch.countDown()方法中不肯能为0.导致主线程一直阻塞
final CountDownLatch countDownLatch = new CountDownLatch(2);
System.out.println("主线程开始执行");
//第一个子线程开始执行
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// countDownLatch.countDown(); 这一句话要写在finally中,不然的话出现异常会导致无法减一,然后出现死锁的
countDownLatch.countDown();
}
}
});
executorService.shutdown();
//第二个子线程开始执行
ExecutorService executorService2 = Executors.newSingleThreadExecutor();
executorService2.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
countDownLatch.countDown();
}
}
});
executorService2.shutdown();
System.out.println("等待两个线程执行完毕");
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
System.out.println("两个子线程执行完毕,继续执行主线程");
System.out.println("主线程执行完毕");
}
}
========================================
主线程开始执行
等待两个线程执行完毕
子线程pool-2-thread-1
子线程pool-1-thread-1
两个子线程执行完毕,继续执行主线程
主线程执行完毕
========================================
使用CountDownLatch使主线程阻塞,直到线程1,2执行完主线程才会继续执行
使用场景
- 游戏多线程加载图片,音乐等相关资源,需要全部加载完必须的资源才能正常的进入游戏
CyclicBarrier
概念
从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。它的作用就是会让所有线程都等待完成后才会继续下一步行动
例子:当你和朋友一起吃饭,有人先到,有人后到,正常情况下就会等到一起才会点菜吃饭
主要方法
- CyclicBarrier提供了一个构造器
//表示需要等到几个线程一起才能执行下一个线程
public CyclicBarrier(int parties)
//parties 是参与线程的个数, Runnable 参数,这个参数的意思是最后一个到达线程要做的任务
public CyclicBarrier(int parties, Runnable barrierAction这个线程做什么处理)
- 类中有两个重要方法
//线程调用 await() 表示自己已经到达栅栏
public int await() throws InterruptedException, BrokenBarrierException
//BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
代码示例
public class T07_TestCyclicBarrier {
public static void main(String[] args) {
// CyclicBarrier barrier = new CyclicBarrier(20);
CyclicBarrier barrier = new CyclicBarrier(20, () -> System.out.println("满20"));
for(int i=0; i<100; i++) {
new Thread(()->{
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
=====================================
满20
满20
满20
满20
满20
使用场景
- 可以用于多线程计算数据,最后合并计算结果的场景。
phaser
概念
phaser与CountDownLatch非常相似,允许我们协调线程的执行。与CountDownLatch相比,它具有一些额外的功能。Phaser是在线程动态数需要继续执行之前等待的屏障。在CountDownLatch中,该数字无法动态配置,需要在创建实例时提供
主要方法
- 构造方法
//无参构造默认阶段数为0,创建一个没有初始注册方,没有父级且初始阶段号为0的新相位器。使用此相位器的任何线程都需要首先为其注册
public Phaser() {
this(null, 0);
}
//创建指定数量为parties的相位器
public Phaser(int parties) {
this(null, parties);
}
- 核心方法
//使线程阻塞,等待指定线程数目之后再继续运行
public int arriveAndAwaitAdvance() {}
//注销到达的相关线程
public int arriveAndDeregister() {}
//注册一个新线程
public int register() {
return doRegister(1);
}
代码示例
import java.util.Random;
import java.util.concurrent.Phaser;
import java.util.concurrent.TimeUnit;
public class T09_TestPhaser2 {
static Random r = new Random();
static MarriagePhaser phaser = new MarriagePhaser();
public static void main(String[] args) {
phaser.bulkRegister(7);
for(int i=0; i<5; i++) {
new Thread(new Person("p" + i)).start();
}
new Thread(new Person("新郎")).start();
new Thread(new Person("新娘")).start();
}
static class MarriagePhaser extends Phaser {
@Override
//该方法会在phaser.arriveAndAwaitAdvance();进行相关判断,phase阶段一定是从0开始进行系列操作,只有当前Phaser中有指定数量的线程时才会进行运行
protected boolean onAdvance(int phase, int registeredParties) {
switch (phase) {
case 0:
System.out.println("所有人到齐了!" + registeredParties);
System.out.println();
return false;
case 1:
System.out.println("所有人吃完了!" + registeredParties);
System.out.println();
return false;
case 2:
System.out.println("所有人离开了!" + registeredParties);
System.out.println();
return false;
case 3:
System.out.println("婚礼结束!新郎新娘抱抱!" + registeredParties);
return true;
default:
return true;
}
}
}
static void milliSleep(int milli) {
try {
TimeUnit.MILLISECONDS.sleep(milli);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class Person implements Runnable {
String name;
public Person(String name) {
this.name = name;
}
public void arrive() {
milliSleep(r.nextInt(1000));
System.out.printf("%s 到达现场!\n", name);
phaser.arriveAndAwaitAdvance();
}
public void eat() {
milliSleep(r.nextInt(1000));
System.out.printf("%s 吃完!\n", name);
phaser.arriveAndAwaitAdvance();
}
public void leave() {
milliSleep(r.nextInt(1000));
System.out.printf("%s 离开!\n", name);
phaser.arriveAndAwaitAdvance();
}
private void hug() {
if(name.equals("新郎") || name.equals("新娘")) {
milliSleep(r.nextInt(1000));
System.out.printf("%s 洞房!\n", name);
phaser.arriveAndAwaitAdvance();
} else {
phaser.arriveAndDeregister();
/* phaser.register();
System.out.println(phaser.getPhase());*/
}
}
@Override
public void run() {
arrive();
eat();
leave();
hug();
}
}
}
======================================
p3 到达现场!
新郎 到达现场!
p2 到达现场!
p1 到达现场!
p0 到达现场!
p4 到达现场!
新娘 到达现场!
所有人到齐了!7
新娘 吃完!
p0 吃完!
p1 吃完!
p4 吃完!
新郎 吃完!
p2 吃完!
p3 吃完!
所有人吃完了!7
p0 离开!
p3 离开!
新郎 离开!
新娘 离开!
p2 离开!
p4 离开!
p1 离开!
所有人离开了!7
新娘 洞房!
新郎 洞房!
婚礼结束!新郎新娘抱抱!2
使用场景
- 在需要对数据进行拦截的时候
ReadWriteLock
概念
ReadWriteLock是JDK5中提供的读写分离锁。读写分离锁可以有效地帮助减少锁竞争,以提高系统性能
读写锁允许多个线程同时读,使得B1,B2,B3之间真正并行。但是,考虑都数据完整性,写写操作和读写操作间依然时需要相互等待和持有锁的
主要方法
//读锁
Lock readLock();
//写锁
Lock writeLock();
代码示例
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class T10_TestReadWriteLock {
static Lock lock = new ReentrantLock();
private static int value;
static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
static Lock readLock = readWriteLock.readLock();
static Lock writeLock = readWriteLock.writeLock();
public static void read(Lock lock) {
try {
lock.lock();
Thread.sleep(1000);
System.out.println("read over!");
//模拟读取操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void write(Lock lock, int v) {
try {
lock.lock();
Thread.sleep(1000);
value = v;
System.out.println("write over!");
//模拟写操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Runnable readR = ()-> read(readLock);
Runnable writeR = ()->write(writeLock, new Random().nextInt());
for(int i=0; i<5; i++) new Thread(readR).start();
for(int i=0; i<2; i++) new Thread(writeR).start();
}
}
==========
read over!
read over!
read over!
read over!
read over!
write over!
write over!
Semaphore
概念
semaphore 是一个计数信号量,必须由获取它的线程释放。
常用于限制可以访问某些资源的线程数量,例如通过 Semaphore 限流。
Semaphore 只有3个操作:
- 初始化
- 增加
- 减少
代码示例
import java.util.concurrent.Semaphore;
public class T11_TestSemaphore {
public static void main(String[] args) {
//Semaphore s = new Semaphore(2);
Semaphore s = new Semaphore(2, true);//设置是否是公平锁,默认是非公平锁
//允许一个线程同时执行
//Semaphore s = new Semaphore(1);
new Thread(()->{
try {
//进行请求
s.acquire();
System.out.println("T1 running...");
Thread.sleep(200);
System.out.println("T1 running...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
s.release();
}
}).start();
new Thread(()->{
try {
s.acquire();
System.out.println("T2 running...");
Thread.sleep(200);
System.out.println("T2 running...");
s.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
==========================
T1 running...
T2 running...
T1 running...
T2 running...
Exchanger
概念
一个用于两个工作线程之间交换数据的封装工具类,简单说就是一个线程在完成一定的事务后想与另一个线程交换数据,则第一个先拿出数据的线程会一直等待第二个线程,直到第二个线程拿着数据到来时才能彼此交换对应数据
主要方法
Exchanger():无参构造方法。
V exchange(V v):等待另一个线程到达此交换点(除非当前线程被中断),然后将给定的对象传送给该线程,并接收该线程的对象。
V exchange(V v, long timeout, TimeUnit unit):等待另一个线程到达此交换点(除非当前线程被中断或超出了指定的等待时间),然后将给定的对象传送给该线程,并接收该线程的对象。
代码示例
import java.util.concurrent.Exchanger;
public class T12_TestExchanger {
static Exchanger<String> exchanger = new Exchanger<>();
/*
* exchanger只能是两个线程之间,多个线程需要进行线程同步
* */
public static void main(String[] args) {
new Thread(()->{
String s = "T1";
try {
//在此会阻塞等待T2线程进行数据交换
s = exchanger.exchange(s);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + s);
}, "t1").start();
new Thread(()->{
String s = "T2";
try {
s = exchanger.exchange(s);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + s);
}, "t2").start();
}
}
=============================
t2 T1
t1 T2
LockSupport
概念
LockSupport是一个线程工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,也可以在任意位置唤醒。它的内部其实两类主要的方法:park(停车阻塞线程)和unpark(启动唤醒线程)。
主要方法
public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);
代码示例
public class T13_TestLockSupport {
public static void main(String[] args) {
Thread t = new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println(i);
if(i == 5) {
// 当前线程停止,进入到阻塞状态
LockSupport.park();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
//LockSupport.park()之后,可以使用unpark方法使进程继续运行
//不进行 LockSupport.unpark(t)则会导致线程一直都会阻塞
LockSupport.unpark(t);
}
}
park和unpark的优点
一个线程它有可能在别的线程unPark之前,或者之后,或者同时调用了park,那么因为park的特性,它可以不用担心自己的park的时序问题,否则,如果park必须要在unpark之前,那么给编程带来很大的麻烦!!
考虑一下,两个线程同步,要如何处理?
在Java5里是用wait/notify/notifyAll来同步的。wait/notify机制有个很蛋疼的地方是,比如线程B要用notify通知线程A,那么线程B要确保线程A已经在wait调用上等待了,否则线程A可能永远都在等待。另外,是调用notify,还是notifyAll?
notify只会唤醒一个线程,如果错误地有两个线程在同一个对象上wait等待,那么又悲剧了。为了安全起见,貌似只能调用notifyAll了。
park/unpark模型真正解耦了线程之间的同步,线程之间不再需要一个Object或者其它变量来存储状态,不再需要关心对方的状态。