JUC并发编程之:简单概述(二)
##内容重点
·分析多线程访问共享资源时,哪些代码片段属于临界区
·使用synchronized互斥解决临界区的线程安全问题
>synchronized锁对象语法
>synchronized加载成员方法和静态方法语法
>wait/notify同步方法
·使用ReentrantLock互斥解决临界区的线程安全问题
>lock使用细节:可打断、锁超时、公平锁、条件变量
·分析变量的线程安全性、常见线程安全类的使用
·线程活跃性:死锁、活锁、饥饿
##应用
·互斥:synchronized或ReentrantLock达到共享资源互斥效果
·同步:wait/notify或ReetrantLock的条件变量来达到线程间通信效果
##原理
·monitor、synchronized、wait/notify原理
·synchronized进阶原理
·park & unpark 原理
##模式
·同步模式-保护性暂停
·同步模式-顺序控制
·异步模式-生产者/消费者
一、共享带来的问题
@Slf4j
public class ShareData {
private static int counter = 0;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for(int i=0;i<5000;i++){
counter++;
}
},"t1");
Thread t2 = new Thread(()->{
for(int i=0;i<5000;i++){
counter--;
}
},"t2");
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("counter : {}",counter);
}
}
//上面结果不可预测
##临界区:Critical Section
·一个程序运行多个线程本身是没有问题的
·问题出在多个线程访问共享资源
>多个线程 读共享资源 其实也没有问题
>多个线程 对共享资源读写操作室发生指令交错 就会出现问题
·一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为【临界区】
##竟态条件:Race Condition
·多个线程再临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了
【竟态条件】
二、synchronized解决方案
2.1、初识synchronized
为了避免临界区的竟态条件发生,有多种手段可以达到目的:
·阻塞式的解决方案:synchronized lock
·非阻塞时的解决方案:原子变量
synchronized即对象锁,它采用互斥的方式让同一时刻之多只有一个线程能持有对象锁,其它
线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的
代码,不用担心线程上线文切换。
##语法:
synchronized(对象){
//临界区
}
·synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外不可分割,
不会被线程切换所打断。
·【sleep是不会让出锁的】
解决共享问题:
@Slf4j
public class ShareDataSynchronized {
private static int counter = 0;
private static Object obj = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
for(int i=0;i<5000;i++){
synchronized (obj){
counter++;
}
}
},"t1");
Thread t2 = new Thread(()->{
for(int i=0;i<5000;i++){
synchronized (obj){
counter--;
}
}
},"t2");
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("counter : {}",counter);
}
}
##思考:
·如果把synchronized(obj)放在for循环外面,如何理解?
·如果t1 synchronized(obj1)而t2 synchronized(obj2)会怎样运作?
·如果t1 synchronized(obj)而t2没有加会怎样?
锁对象面向对象改进:
/**
* 上面的代码时面向过程的,如果面向对象如何改进?
**/
@Slf4j
public class ShareDataObjSynchronized {
public static void main(String[] args) {
Room room = new Room();
Thread t1 = new Thread(()->{
for(int i=0;i<5000;i++){
room.increment();
}
},"t1");
Thread t2 = new Thread(()->{
for(int i=0;i<5000;i++){
room.decrement();
}
},"t2");
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("counter : {}",room.getCounter());
}
}
class Room{
private int counter = 0;
public void increment(){
synchronized (this){
counter++;
}
}
public void decrement(){
synchronized (this){
counter--;
}
}
public int getCounter(){
return counter;
}
}
2.2、方法上的synchronized
##语法:
//1、普通方法
public synchronized void test(){
}
//等价于
public void test(){
synchronized(this){
}
}
//静态方法:因为静态方法不需要靠对象获取,因此锁是类对象
public synchronized static void test(){
}
//等价于
public static void test(){
synchronized(当前类.class){
}
}
三、线程安全分析
3.1、变量的线程安全分析
##成员变量 和 静态变量 是否线程安全
·如果它们没有共享,则线程安全
·如果它们被共享了,根据他们的状态是否改变,又分两种情况:
>如果只有读操作,则线程安全
>如果有读写操作,则代码是临界区,需要考虑线程安全
##局部变量是否线程安全
·局部变量是线程安全的
·但局部变量引用的对象则未必
>如果该对象没有逃离方法的作用范围,它是线程安全的
>如果该对象逃离方法的作用范围,需要考虑线程安全
3.2、常见的线程安全类
·String (不可变类)
·Integer (不可变类)
·StringBuffer
·Random
·Vector
·Hashtable
·java.util.concurrent包下的类
四、Monitor
类型 | byte | bit | 取值范围 |
---|---|---|---|
byte | 1 | 8位 | -2^7 ~2^7 -1 |
short | 2 | 16位 | -2^15~2^15-1 |
int | 4 | 32位 | -2^31~2^31-1 |
long | 8 | 64位 | -2^63~2^63-1 |
float | 4 | 32位 | -2^31~2^31-1 |
double | 8 | 64位 | -2^63~2^63-1 |
char | 1 | 8位 | -2^7 ~2^7 -1 |
boolean | 1 | 8位 | -2^7 ~2^7 -1 |
Java对象头:
Minitor被翻译为监视器或者管程
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后
该对象的mark word中就被设置指向monitor对象的指针
##Monitor工作原理:
·刚开始Monitor中Owner为NULL
·当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,
Monitor中只能有一个Owner
·在Thread-2上锁的过程中,如果Thread-3和Thread-4也来执行synchronized(obj),
就会进入EntryList[Bolocked状态]
·Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争是
非公平的
·图中WaitSet中的Thread-0和Thread-1是之前获得过锁,但条件不满足进入waiting
状态的线程,之后在waiting-notify时分析
##注意:
·synchronized必须是进入同一个对象的monitor才有上诉的效果
·不加synchronized的对象不会关联监视器,不遵从以上规则
[每个Obj关联一个Monitor]
五、synchronized优化原理
5.1、轻量级锁
##轻量级锁使用场景:
如果一个对象虽然有很多线程访问,但多线程访问的时间是错开的(也就是没有竞争关系)
那么可以使用轻量级锁来优化
[如果轻量级锁失败会自动升级为重量级锁]
##轻量级锁对使用者是透明的,语法仍然是synchronized
/**
*例:假设有两个方法同步块,利用同一个对象锁
**/
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块A
method2();
}
}
public static method2(){
synchronized(obj){
//同步块B
}
}
分析一:创建锁记录对象(Lock Record)
每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
分析二:让锁记录中Object reference指向锁对象,
并尝试cas替换Object对象中的
Mark Word,将Mark Word的值存入锁记录
分析三:如果cas替换成功,对象头中存储了 锁记录地址和状态00,
表示由该线程给对象加锁
分析四:如果cas失败,有两种情况
>其他线程已经持有了该Object的轻量级锁,这是表明有竞争,进入 锁膨胀过程
>自己执行了 synchronized锁重入(如上method1加锁后 又调用method2加了同样的锁)
那么需要在栈帧中再添加一条Lock Record作为重入的计数
分析五:当退出synchronized代码块(解锁时)
>如果有取值为null的锁记录,表示有重入,这是重置锁记录,表示重入计数减一
>锁记录的值不为null,这是使用cas将 Mark Word的值恢复给对象头
·成功,则解锁成功
·失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
5.2、锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这是一种情况就是已经有其他线程为此
对象加上了轻量级锁(有竞争),这是需要进行 锁膨胀,将轻量级锁变为 【重量级锁】
(对应上面的 分析四)
/**
* 例:锁膨胀
**/
static object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块
}
}
##分析一:当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
##分析二:这时Thread-1加轻量级锁失败,进入锁膨胀流程
>即为Object对象申请Monitor锁,让Object指向重量级锁地址
>然后自己进入Monitor的EntryList BLOCKED
##分析三:当Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头失败
。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象设置Owner为NULL
,唤醒EntryList中BOLOCKED线程
5.3、自旋优化
·【重量级锁】竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候
持锁线程已经推出了同步块,释放了锁),这时当前线程就可以避免阻塞
{即:线程在进入阻塞状态之前会进行几次自旋获取Monitor的尝试,如果获取Monitor成
功,则表示获取锁成功,如果尝试几次后仍是失败则进入阻塞状态}
·JDK6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功
的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋
·自旋会占用CPU时间片,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势
·JDK7之后不能控制是否开启自旋功能
5.4、偏向锁
·轻量级锁在没有竞争时(就自己这个线程),每次锁重入仍然需要执行CAS操作。
·JDK6之后引入偏向锁来进一步优化:只有第一次使用CAS将线程ID设置到对象头的Mark
Word,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞
争,这个对象就归该线程所有
[轻量级锁是将Lock Record(锁记录地址)存入对象头的Mark Word]
[偏向锁是将线程ID存入对象头的Mark Word]
static final Object obj = new Object();
public static void m1(){
synchronized(obj){
//同步块A
m2();
}
}
public static m2(){
synchronized(obj){
//同步块B
m3();
}
}
public static m3(){
synchronized(obj){
//同步块C
}
}
偏向状态:
对象头格式
一、##一个对象创建时:
·如果开启了偏向锁(默认开启),那么对象创建后,MarkWord最后三位为101,这时它
的thread、epoch、age都为0
·偏向锁是默认延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数:
-XX:BiasedLockingStartupDelay=0来禁用延迟
·如果没有开启偏向锁,那么对象创建后,MarkWord后三位为001,这时它的hashcode,
age都为0,第一次用到还是从的时才会赋值
BiasedLocking (-禁用 +开启,默认开启)
二、##撤销偏向锁状态:
1、调用对象hashCode
调用了对象的hashCode,但偏向锁的对象MarkWord中存储的是Thread Id,如果调用
hashCode会导致偏向锁被撤销(成为Normal)
·轻量级所会在锁记录Lock Record中记录hashCode
·重量级锁会在Monitor中记录hashCode
在调用hashCode后使用偏向锁,记得去掉-XX:-UseBiasedLocking
2、其他线程使用该对象
当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
3、调用wait/notify
wait/notify只有重量级锁才有,即使用wait/notify时会自动升级为重量级锁
三、批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏
向T2,重偏向会重置对象的Thread Id
当撤销偏向锁阈值超过20次后,JVM会这样觉得:我是不是偏向错了,于是会在给这些
对象加锁时重新偏向至加锁线程
四、批量撤销
当撤销偏向锁阈值超过40次后,jvm就会觉得自己确实偏向错了,根本就不该偏向。
于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
六、wait和notify
##上图解析:
·WaitSet中的等待线程被notify唤醒后,重新进入EntryList队列等待
·线程在Owner状态调用wait方法进入WaitSet状态,WaitSet状态的线程调用
notify后进入EntryList状态
##一、原理之wait-notify:
·Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
·BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
·BLOCKED线程会在Owner线程释放锁时唤醒
·WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意
味着立刻获得锁,仍需要进入EntryList重新竞争
##二、API介绍:
·obj.wait()让进入object监视器的线程到waitset等待
·obj.wait(long timeout)让进入object监视器的线程到waitset等待,如果时
间结束没有被唤醒,则自动结束等待
·obj.notify()在object上正在waitset等待的线程中挑一个唤醒
·obj.notifyAll()让object上正在waitset等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于Object对象的方法,必须获得此对象的锁
才能调用这几个方法
@Slf4j
public class WaitNofifyThread {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (obj){
log.info("执行代码...");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("其他代码...");
}
},"t1").start();
new Thread(()->{
synchronized (obj){
log.info("执行代码...");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("其他代码...");
}
},"t2").start();
Thread.sleep(2000);
log.info("唤醒obj上其他线程");
synchronized (obj){
obj.notify();//唤醒waitset中的一个线程
// obj.notifyAll();//唤醒waitset中的所有线程
}
}
}
##三、wait(long n)和sleep(long n)的区别
·sleep是Thread静态方法,wait是Object方法
·sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用
·sleep在睡眠的同时,不会释放对象锁;但wait在等待的时候会释放对象锁
·相同点:它们的状态都是TIMED_WAITING
##四、wait和notify正确使用姿势
eg:六个人要同一间屋子一起干活(同一时间只能一个人),其中小南必须有烟才能干,
没烟他就罢工
/**
* 六个人要同一间屋子一起干活(同一时间只能一个人),其中小南必须有烟才能干,
* 没烟他就罢工
*/
@Slf4j
public class WaitNotifyUseThread1 {
static final Object room = new Object();//屋子
static boolean hasCigarette = false;//是否有烟
public static void main(String[] args) throws InterruptedException {
//小南
new Thread(()->{
synchronized (room){
log.info("有烟没:[{}]",hasCigarette);
if(!hasCigarette){
log.info("没烟,歇会儿!");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("现在有烟了没[{}]",hasCigarette);
if(hasCigarette){
log.info("可以开始干活了");
}
}
},"小南").start();
//其他五个人
for (int i=0;i<5;i++){
new Thread(()->{
synchronized (room){
log.info("可以开始干活了!");
}
},"其他人").start();
}
//主线程休息1秒
Thread.sleep(1000);
//送烟的
new Thread(()->{
synchronized (room){
hasCigarette = true;
log.info("烟到了");
}
},"送烟的").start();
}
}
##结果:
16:35:44.651 [小南] INFO xxxxxxxxx - 有烟没:[false]
16:35:44.666 [小南] INFO xxxxxxxxx - 没烟,歇会儿!
16:35:47.667 [小南] INFO xxxxxxxxx - 现在有烟了没:[false]
16:35:47.667 [送烟的] INFO xxxxxxxxx - 烟到了
16:35:47.667 [其他人] INFO xxxxxxxxx - 可以开始干活了!
16:35:47.668 [其他人] INFO xxxxxxxxx - 可以开始干活了!
16:35:47.668 [其他人] INFO xxxxxxxxx - 可以开始干活了!
16:35:47.669 [其他人] INFO xxxxxxxxx - 可以开始干活了!
16:35:47.669 [其他人] INFO xxxxxxxxx - 可以开始干活了!
##问题:
·小南使用sleep(),导致其他干活的线程一直阻塞,效率太低
·小南线程必须满足3s后才能醒来,就算烟提前送到了,也无法立刻醒来
·因为小南占用线程,送烟的线程也进不去
(sleep可以用interrupt方法叫醒)
@Slf4j
public class WaitNotifyUseThread2 {
static final Object room = new Object();//屋子
static boolean hasCigarette = false;//是否有烟
public static void main(String[] args) throws InterruptedException {
//小南
new Thread(()->{
synchronized (room){
log.info("有眼没:[{}]",hasCigarette);
if(!hasCigarette){
log.info("没烟,歇会儿!");
try {
room.wait(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("现在有烟了没[{}]",hasCigarette);
if(hasCigarette){
log.info("可以开始干活了");
}
}
},"小南").start();
//其他五个人
for (int i=0;i<5;i++){
new Thread(()->{
synchronized (room){
log.info("可以开始干活了!");
}
},"其他人").start();
}
//主线程休息1秒
Thread.sleep(1000);
//送烟的
new Thread(()->{
synchronized (room){
hasCigarette = true;
log.info("烟到了");
room.notify();
}
},"送烟的").start();
}
}
##结果:
16:45:43.598 [小南] INFO xxxxxxxxx - 有眼没:[false]
16:45:43.602 [小南] INFO xxxxxxxxx - 没烟,歇会儿!
16:45:43.602 [其他人] INFO xxxxxxxxx - 可以开始干活了!
16:45:43.602 [其他人] INFO xxxxxxxxx - 可以开始干活了!
16:45:43.602 [其他人] INFO xxxxxxxxx - 可以开始干活了!
16:45:43.602 [其他人] INFO xxxxxxxxx - 可以开始干活了!
16:45:43.602 [其他人] INFO xxxxxxxxx - 可以开始干活了!
16:45:44.598 [送烟的] INFO xxxxxxxxx - 烟到了
16:45:44.598 [小南] INFO xxxxxxxxx - 现在有烟了没[true]
16:45:44.598 [小南] INFO xxxxxxxxx - 可以开始干活了
##问题:
·room.notify叫醒了小南,但如果有其他线程也在等待条件呢?
/**
* 小南和小女在一间屋子干活,小南没烟罢工,小女没外卖罢工
*/
@Slf4j
public class WaitNotifyUseThread3 {
static final Object room = new Object();//屋子
static boolean hasCigarette = false;//是否有烟
static boolean hasTakeOut = false;//是否有外卖
public static void main(String[] args) {
//小南
new Thread(()->{
synchronized (room){
log.info("有烟没:[{}]",hasCigarette);
if(!hasCigarette){
log.info("没烟,歇会儿!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("现在有烟了没[{}]",hasCigarette);
if(hasCigarette){
log.info("可以开始干活了");
}else{
log.info("今天活是干不成喽");
}
}
},"小南").start();
//小女
new Thread(()->{
synchronized (room){
log.info("有外卖没:[{}]",hasTakeOut);
if(!hasTakeOut){
log.info("没外卖,歇会儿!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("现在有外卖了没[{}]",hasTakeOut);
if(hasTakeOut){
log.info("可以开始干活了");
}else{
log.info("今天活是干不成喽");
}
}
},"小女").start();
//主线程休息1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//送外卖的到了
new Thread(()->{
synchronized (room){
hasTakeOut = true;
log.info("外卖到了");
room.notify();
}
},"送外卖的").start();
//送烟的到了
new Thread(()->{
synchronized (room){
hasCigarette = true;
log.info("烟到了");
room.notify();
}
},"送烟的").start();
}
}
##结果:
17:12:04.726 [小南] INFO xxxxxxxxx - 有烟没:[false]
17:12:04.729 [小南] INFO xxxxxxxxx - 没烟,歇会儿!
17:12:04.729 [小女] INFO xxxxxxxxx - 有外卖没:[false]
17:12:04.729 [小女] INFO xxxxxxxxx - 没外卖,歇会儿!
17:12:05.725 [送外卖的] INFO xxxxxxxxx - 外卖到了
17:12:05.726 [小南] INFO xxxxxxxxx - 现在有烟了没[false]
17:12:05.726 [小南] INFO xxxxxxxxx - 今天活是干不成喽
17:12:05.726 [送烟的] INFO xxxxxxxxx - 烟到了
17:12:05.726 [小女] INFO xxxxxxxxx - 现在有外卖了没[true]
17:12:05.726 [小女] INFO xxxxxxxxx - 可以开始干活了
##问题
·room.notify是随机唤醒,本来应该唤醒小女的,但唤醒了小南
/**
* 小南和小女在一间屋子干活,小南没烟罢工,小女没外卖罢工
*/
@Slf4j
public class WaitNotifyUseThread4 {
static final Object room = new Object();//屋子
static boolean hasCigarette = false;//是否有烟
static boolean hasTakeOut = false;//是否有外卖
public static void main(String[] args) {
//小南
new Thread(()->{
synchronized (room){
log.info("有烟没:[{}]",hasCigarette);
while(true){
if(!hasCigarette){
log.info("没烟,歇会儿!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
log.info("可以开始干活了");
break;
}
}
}
},"小南").start();
//小女
new Thread(()->{
synchronized (room){
log.info("有外卖没:[{}]",hasTakeOut);
while(true){
if(!hasTakeOut){
log.info("没外卖,歇会儿!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
log.info("可以开始干活了");
break;
}
}
}
},"小女").start();
//主线程休息1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//送外卖的到了
new Thread(()->{
synchronized (room){
hasTakeOut = true;
log.info("外卖到了");
room.notifyAll();
}
},"送外卖的").start();
//送烟的到了
new Thread(()->{
synchronized (room){
hasCigarette = true;
log.info("烟到了");
room.notifyAll();
}
},"送烟的").start();
}
}
##结果:
17:21:00.640 [小南] INFO xxxxxxxxx - 有烟没:[false]
17:21:00.643 [小南] INFO xxxxxxxxx - 没烟,歇会儿!
17:21:00.643 [小女] INFO xxxxxxxxx - 有外卖没:[false]
17:21:00.643 [小女] INFO xxxxxxxxx - 没外卖,歇会儿!
17:21:01.638 [送外卖的] INFO xxxxxxxxx - 外卖到了
17:21:01.638 [小女] INFO xxxxxxxxx - 可以开始干活了
17:21:01.639 [小南] INFO xxxxxxxxx - 没烟,歇会儿!
17:21:01.639 [送烟的] INFO xxxxxxxxx - 烟到了
17:21:01.639 [小南] INFO xxxxxxxxx - 可以开始干活了
七、练习:保护性暂停
7.1、保护性暂停一
##一、定义:
·即Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点:
·有一个结果需要从一个线程传递到另一个线程,让他们关联同一个GuardedObject
·如果有结果源源不断不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
·JDK中,join的实现、Future的实现,采用的就是此模式
·因为要等待另一方的结果,因此归类到同步模式
/**
* 保护性暂停模式:
* 线程1等待线程2的结果
*/
@Slf4j
public class GuardedObjectThread1 {
public static void main(String[] args) {
GuardedObject guardedObject = new GuardedObject();
//等待结果线程
new Thread(()->{
log.info("等待结果...");
String resp = (String) guardedObject.get();
log.info("结果为:{}",resp);
},"t1").start();
//生产结果线程
new Thread(()->{
log.info("执行生产...");
guardedObject.produce("生产了一颗芒果");
try {
log.info("刚生忙了一会休息休息");
Thread.sleep(3000);
log.info("休息好了");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
}
}
class GuardedObject{
//结果
private Object response;
//获取结果
public Object get(){
synchronized (this){
while(response==null){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return response;
}
//生产结果
public void produce(Object response){
synchronized (this){
this.response = response;
this.notifyAll();
}
}
}
##结果:
10:47:03.020 [t1] INFO xxxxxxx - 等待结果...
10:47:03.020 [t2] INFO xxxxxxx - 执行生产...
10:47:03.023 [t2] INFO xxxxxxx - 刚生忙了一会休息休息
10:47:03.023 [t1] INFO xxxxxxx - 结果为:生产了一颗芒果
10:47:06.025 [t2] INFO xxxxxxx - 休息好了
##分析:
·join等待结果 需要t2线程全部运行结束才可,保护性暂停模式不需要,结果产生即可
·join等待结果的变量需要设计成全局的,保护性暂停模式可以设计成局部的
##扩展-增加等待超时
/**
* 保护性暂停模式:
* 线程1等待线程2的结果
* 扩展:增加等待超时
*/
@Slf4j
public class GuardedObjectThread2 {
public static void main(String[] args) {
GuardedObject2 guardedObject = new GuardedObject2();
//等待结果线程
new Thread(()->{
log.info("等待结果...");
String resp = (String) guardedObject.get(2000);
log.info("结果为:{}",resp);
},"t1").start();
//生产结果线程
new Thread(()->{
log.info("执行生产...");
try {
log.info("先休息一会再忙");
Thread.sleep(3000);
guardedObject.produce("生产了一颗芒果");
log.info("休息好了");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
}
}
class GuardedObject2{
//结果
private Object response;
//获取结果
public Object get(long timeout){
synchronized (this){
//开始时间
long beginTime = System.currentTimeMillis();
//等待的时间
long passedTime = 0;
while(response==null){
if(passedTime>=timeout){
break;
}
try {
this.wait(timeout-passedTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
passedTime = System.currentTimeMillis() - beginTime;
}
}
return response;
}
//生产结果
public void produce(Object response){
synchronized (this){
this.response = response;
this.notifyAll();
}
}
}
##结果:
10:14:36.587 [t1] INFO xxxxxx - 等待结果...
10:14:36.587 [t2] INFO xxxxxx - 执行生产...
10:14:36.590 [t2] INFO xxxxxx - 先休息一会再忙
10:14:38.590 [t1] INFO xxxxxx - 结果为:null
10:14:39.591 [t2] INFO xxxxxx - 休息好了
##分析:
t1不再一直再等待t2线程的结果了
7.2、join的原理
·保护性暂停模式:一个线程等待另一个线程的结果
·join:一个线程等待另一个线程结束后再继续运行
//join源码:底层即保护性暂停模式
public final synchronized void join(long millis){
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
7.3、保护性暂停二
·图中Futrues就好比邮箱,左侧T0 T2 T4好比收件人,右侧T1 T3 T5好比邮递员
·如果需要在多个类之间使用GuardedObject对象,作为参数传递不是很方便,因此设计一
各用来解耦的中间类,这样不仅能够解耦【生产者】和【消费者】,还能够同时支持多个任务
的管理
@Slf4j
public class GuardedObjectThread3 {
public static void main(String[] args) throws InterruptedException {
for (int i=0;i<3;i++){
new Consignee().start();
}
Thread.sleep(1000);
for (int id : MailBox.getIds()){
new Postman(id,"内容"+id).start();
}
}
}
//收件人
@Slf4j
class Consignee extends Thread{
@Override
public void run() {
//收信--我收信的信件ID是xxx
GurdedObject3 go = MailBox.createGurdedObject();
log.info("开始收信 id : {}",go.getId());
Object mail = go.get(5000);
log.info("收到信 id: {},内容:{}",go.getId(),mail);
}
}
//邮递员
@Slf4j
class Postman extends Thread{
private int id;
private String mail;
public Postman(int id,String mail){
this.id = id;
this.mail = mail;
}
@Override
public void run() {
//送信--我送信的信件ID是xxx
GurdedObject3 go = MailBox.getGurdedObject(id);
log.info("送信 id: {},内容:{}",go.getId(),mail);
go.produce(mail);
}
}
//邮箱
class MailBox{
//int为 gurdedObject id,hashtable是线程安全的
private static Map<Integer,GurdedObject3> boxes = new Hashtable<>();
//生产唯一ID
private static int id = 1;
private static synchronized int generatorId(){
return id++;
}
public static GurdedObject3 createGurdedObject(){
//创建信件
GurdedObject3 go = new GurdedObject3(generatorId());
//放入信箱
boxes.put(go.getId(),go);
return go;
}
public static GurdedObject3 getGurdedObject(int id){
//收到信件后移除从信箱中移除信件
return boxes.remove(id);
}
public static Set<Integer> getIds(){
return boxes.keySet();
}
}
//邮件
@Data
class GurdedObject3{
//唯一标识
private int id;
//结果
private Object response;
//获取结果
public Object get(long timeout){
synchronized (this){
//开始时间
long beginTime = System.currentTimeMillis();
//等待的时间
long passedTime = 0;
while(response==null){
if(passedTime>=timeout){
break;
}
try {
this.wait(timeout-passedTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
passedTime = System.currentTimeMillis() - beginTime;
}
}
return response;
}
//生产结果
public void produce(Object response){
synchronized (this){
this.response = response;
this.notifyAll();
}
}
//构造函数
public GurdedObject3(int id){
this.id = id;
}
}
11:30:54.052 [Thread-1] INFO xxxxxx - 开始收信 id : 1
11:30:54.052 [Thread-2] INFO xxxxxx - 开始收信 id : 3
11:30:54.052 [Thread-0] INFO xxxxxx - 开始收信 id : 2
11:30:55.051 [Thread-5] INFO xxxxxx - 送信 id: 1,内容:内容1
11:30:55.051 [Thread-3] INFO xxxxxx - 送信 id: 3,内容:内容3
11:30:55.051 [Thread-1] INFO xxxxxx - 收到信 id: 1,内容:内容1
11:30:55.051 [Thread-4] INFO xxxxxx - 送信 id: 2,内容:内容2
11:30:55.051 [Thread-2] INFO xxxxxx - 收到信 id: 3,内容:内容3
11:30:55.051 [Thread-0] INFO xxxxxx - 收到信 id: 2,内容:内容2
八、练习:生产者-消费者
·与之前的保护性暂停中GuardedObject不同,不需要生产结果和消费结果线程一一对应
·消费队列可以用来平和生产和消费的线程资源
·生产者仅负责生产结果数据,不关心数据该如何处理,而消费者专心处理结果数据
·消息队列是有容量限制的,满时不再加入数据,空时不再消费数据
【JDK中各种阻塞队列,采用的就是这种模式】
/**
* 生产者-消费者模式
*/
@Slf4j
public class ProducerAndConsumerThread {
public static void main(String[] args) {
MessageQueue mq = new MessageQueue(2);
//生产者
for (int i = 0; i <3 ; i++) {
int id = i;
new Thread(()->{
mq.put(new Message(id,"值"+id));
},"生产者"+i).start();
}
//消费者
new Thread(()->{
while(true){
try {
Thread.sleep(1000);
Message take = mq.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"消费者").start();
}
}
//消息队列,java线程间通信
@Slf4j
class MessageQueue{
//消息的队列集合
private LinkedList<Message> list = new LinkedList<>();
//队列容量
private int capcity;
public MessageQueue(int capcity) {
this.capcity = capcity;
}
//获取消息
public Message take(){
synchronized (list){
while(list.isEmpty()){
try {
log.info("队列为空,消费者等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//将消息从队头移除
Message message = list.removeFirst();
log.info("已消费消息 {}",message);
list.notifyAll();
return message;
}
}
//添加消息
public void put(Message message){
synchronized (list){
while(list.size()==capcity){
try {
log.info("队列已满,生产者等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//将消息添加到队尾
list.addLast(message);
log.info("已生产消息 {}",message);
list.notifyAll();
}
}
}
//消息
@Data
@ToString
@AllArgsConstructor
final class Message{
private int id;
private Object value;
}
##结果:
14:33:01.590 [生产者2] INFO xxx - 已生产消息 Message(id=2, value=值2)
14:33:01.594 [生产者0] INFO xxx - 已生产消息 Message(id=0, value=值0)
14:33:01.594 [生产者1] INFO xxx - 队列已满,生产者等待
14:33:02.589 [消费者] INFO xxx - 已消费消息 Message(id=2, value=值2)
14:33:02.589 [生产者1] INFO xxx - 已生产消息 Message(id=1, value=值1)
14:33:03.589 [消费者] INFO xxx - 已消费消息 Message(id=0, value=值0)
14:33:04.589 [消费者] INFO xxx - 已消费消息 Message(id=1, value=值1)
14:33:05.590 [消费者] INFO xxx - 队列为空,消费者等待
九、park & unpark
##一、基本使用
·它们是LockSupport类中的方法:
> LockSupport.park(); //暂停当前线程(状态为WAIT)
> LockSupport.unpark(暂停线程对象); //恢复某个线程的运行
##注意
·unpark既可以在park之后调用,也可以在park之前调用
·在park之前调用,将来可以恢复park的线程(提前预约恢复了)
·多次调用unpark,仅会解锁一次park线程
##特点
>与Object的wait和notify相比:
·wait notify和notifyAll必须配合Object monitor一起使用,而unpark不必
·park和unpark是以线程为单位来【阻塞】和【唤醒】线程,而notify只是随机唤醒
一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
·park和unpark可以先unpark,而wait和notify不能先notify
@Slf4j
public class ParkAndUnpark {
public static void main(String[] args){
Thread t1 = new Thread(()->{
log.info("start...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("park...");
LockSupport.park();
log.info("resume...");
},"t1");
t1.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("unpark");
LockSupport.unpark(t1);
}
}
##结果:
15:01:54.152 [t1] INFO xxx - start...
15:01:55.155 [t1] INFO xxx - park...
15:01:59.152 [main] INFO xxx - unpark
15:01:59.152 [t1] INFO xxx - resume...
@Slf4j
public class ParkAndUnpark {
public static void main(String[] args){
Thread t1 = new Thread(()->{
log.info("start...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("park...");
LockSupport.park();
log.info("resume...");
},"t1");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("unpark");
LockSupport.unpark(t1);
}
}
##结果:
15:03:09.448 [t1] INFO xxx - start...
15:03:10.447 [main] INFO xxx - unpark
15:03:12.451 [t1] INFO xxx - park...
15:03:12.451 [t1] INFO xxx - resume...
##结论:
unpark可以提前解锁park线程
##二、原理之 park & unpark
每个线程都有自己的一个Parker对象,由三部分组成_counter _cond和_mutex
·线程就像一个旅人,Parker就像它随身携带的背包,条件变量就好比背包中的帐篷.
_counter就好比背包中的备用干粮(0为耗尽1为充足)
·调用park就是要看需不需要停下来歇息
>如果备用干粮耗尽,那么钻进帐篷歇息
>如果备用干粮充足,那么不需要停留,继续前进
·调用unpark,就好比令干粮充足
>如果这时线程还在帐篷,就唤醒让他继续前进
>如果这时线程还在运行,那么下次它调用park时,不需要停留继续前进
>>因为背包空间优先,多次调用unpark金辉补充一份备用干粮
1> 当前线程调用unsafe.park()方法
2> 检查_counter,本情况为0,这是,获得_mutex互斥锁
3> 线程进入_cond条件变量阻塞
4> 设置_counter=0
1> 调用unsafe.unpark(Thread-0)方法,设置_counter为1
2> 唤醒_cond条件变量中的Thread-0
3> Thread_0恢复运行
4> 设置_counter为0
十、线程状态转换
##一、new - > runnable
·当调用t.start()方法时,由new->runnable
##二、runnable -> waiting -> runnable
·(1)、t线程调用synchronized(obj)获取了对象锁后
>调用obj.wait()方法时,t线程从runnable->waiting
>调用obj.notify()或notifyAll()或t.interrupt()时
>>竞争锁成功:t线程从waiting->runnable
>>竞争锁失败:t线程从waiting->blocked
·(2)、当前线程调用t.join()方法时,当前线程从runnable到waiting
>注意是当前线程在t线程对象的监视器上等待
>t线程运行结束,或调用了当前线程的interrupt()时,当前线程从waiting到runnable
·(3)、当前线程调用LockSupport.park()方法会让当前线程从runnable到waiting
>调用LockSupport.unpark()或调用了线程的interrupt(),会让目标线程从
waiting到runnable
##三、runnable -> timed_waiting -> runnable
·(1)、t线程用synchronized(obj)获取了对象锁后
>调用obj.wait(long n)方法时,t线程从runnable到timed_waiting
>t线程等待时间超过了n毫秒,或调用obj.notify()或notifyAll()或interrupt()时
>>竞争锁成功:t线程从timed_waiting->runnable
>>竞争锁失败:t线程从timed_waiting->blocked
·(2)、当前线程调用t.join(long n)方法时,当前线程从runnable到timed_waiting
>注意是当前线程在t线程对象的监视器上等待
>当前线程等待超过了n还秒,或t线程运行结束,或调用了当前线程的interrupt()时,当前
线程从timed_waiting到runnable
·(3)当前线程调用Thread.sleep(long n),当前线程从runnable到timed_waiting
>当前线程等待时间超过了n毫秒,当前线程从timed_waiting到runnable
·(4)当前线程调用LockSupport.parkNanos(long nanos)或LockSupport.parkUntil(long millis)时,
当前线程从runnable到timed_waiting
>调用LockSupport.unpark(目标线程)或调用了线程的interrupt()或是等待超时
会让目标线程从timed_waiting到runnable
##四、runnable -> blocked -> runnable
·t线程用synchronized(obj)获取了对象锁时如果竞争失败,从runnable到blocked
·持obj锁线程的同步代码块执行完毕,会唤醒该对象上所有blocked的线程重新竞争,如果
其中t线程竞争成功从blocked到runnable,其他失败的线程仍然blocked
##五、runnable -> terminated
当前线程所有代码运行完毕,进入terminated
十一、锁活跃性
11.1、多把不相干的锁
一间大屋子有两个功能:睡觉、学习,两者互不相干:
现小南学习,小女睡觉,但如果用一间屋子(一个对象锁)的话,那么并发度会很低
解决方法时准备多个房间(多个对象锁)
@Slf4j
public class BigRoomThread {
public static void main(String[] args) {
BigRoom br = new BigRoom();
new Thread(()->{
try {
br.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"小南").start();
new Thread(()->{
try {
br.study();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"小女").start();
}
}
@Slf4j
class BigRoom{
public void sleep() throws InterruptedException {
synchronized (this){
log.info("sleep 2个小时");
Thread.sleep(2000);
}
}
public void study() throws InterruptedException {
synchronized (this){
log.info("study 1个小时");
Thread.sleep(1000);
}
}
}
##结果:
16:25:41.620 [小南] INFO xxx - sleep 2个小时
16:25:43.623 [小女] INFO xxx - study 1个小时
study是在sleep执行后2秒才开始执行的,效率低下
##多把锁的情况:
@Slf4j
public class MultiRoomThread {
public static void main(String[] args) {
BigRoom2 br = new BigRoom2();
new Thread(()->{
try {
br.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"小南").start();
new Thread(()->{
try {
br.study();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"小女").start();
}
}
@Slf4j
class BigRoom2{
private final Object sleepRoom = new Object();
private final Object studyRoom = new Object();
public void sleep() throws InterruptedException {
synchronized (sleepRoom){
log.info("sleep 2个小时");
Thread.sleep(2000);
}
}
public void study() throws InterruptedException {
synchronized (studyRoom){
log.info("study 1个小时");
Thread.sleep(1000);
}
}
}
##结果:
16:30:18.981 [小女] INFO xxx - study 1个小时
16:30:18.981 [小南] INFO xxx - sleep 2个小时
sleep和study同时执行
##分析:
两个sleep和study业务毫无关联的情况下,将锁的粒度细分
·好处,是可以增强并发度
·坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
11.2、死锁
t1线程获得A对象锁,接下来想获取B对象的锁
t2线程获得B对象锁,接下来想获取A对象的锁
@Slf4j
public class DeadLockThread {
public static void main(String[] args) {
Object A = new Object();
Object B = new Object();
new Thread(()->{
synchronized (A){
log.info("lock A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B){
log.info("lock B");
log.info("操作.....");
}
}
},"A").start();
new Thread(()->{
synchronized (B){
log.info("lock B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A){
log.info("lock A");
log.info("操作.....");
}
}
},"B").start();
}
}
11.3、哲学家就餐问题
有五位哲学家,围坐在圆桌旁:
·他们只做两件事,思考和吃饭,思考一会吃一口,吃完饭后接着思考
·吃饭时要用两根筷子吃饭,桌上共有5根筷子,每位哲学家左右手边各有一根筷子
·如果筷子被身边的人拿着,自己就得等待
@Slf4j
public class PhilosopherEatThread {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底",c1,c2).start();
new Philosopher("柏拉图",c2,c3).start();
new Philosopher("亚里士多德",c3,c4).start();
new Philosopher("赫拉克利特",c4,c5).start();
new Philosopher("阿基米德",c5,c1).start();
//将阿基米德的C5C1换成C1C5也可解决死锁问题
//t1线程先获得A对象锁,接下来想获取B对象的锁
//t2线程先获得不了A对象锁,无法接下来再获取B对象的锁
//出现线程饥饿现象
//new Philosopher("阿基米德",c1,c5).start();
}
}
@Slf4j
class Philosopher extends Thread{
//左边的筷子
private Chopstick left;
//右边的筷子
private Chopstick right;
public Philosopher(String name,Chopstick left,Chopstick right){
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true){
//尝试获取左手边的筷子
synchronized (left){
//尝试获取右手边的筷子
synchronized (right){
//吃饭
eat();
}
}
}
}
public void eat(){
log.info("eat.....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Data
@AllArgsConstructor
class Chopstick{
private String name;
@Override
public String toString() {
return "筷子{ "+name+"}";
}
}
11.4、活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束
##解决方案:增加随机的睡眠时间
/**
* 活锁
*/
@Slf4j
public class LiveLockThread {
static volatile int count = 10;
public static void main(String[] args) {
new Thread(()->{
//期望count减到0停止
while(count>0){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
log.info("count : {}"+count);
}
},"t1").start();
new Thread(()->{
//期望count加到0停止
while(count<20){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
log.info("count : {}"+count);
}
},"t2").start();
}
}
11.5、饥饿
·一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束(读写锁是涉及界问题)
解决方案:大家按照相同的锁的顺序加锁
十二、ReentrantLock
12.1、基本语法
##相对于synchronized它具有的特点:
·可中断
·可设置超时时间
·可设置为公平锁(防止线程饥饿)
·支持多个条件变量
·与synchronized一样可重入
(相当于synchronized(obj)中的obj)
##基本语法
//获取锁
reentrantLock.lock();
try{
//临界区
}finally{
//释放锁
reentrantLock.unlock();
}
12.2、可重入
##可重入
·可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次
获取这把锁
·如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
@Slf4j
public class ReentrantLockTest {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try{
log.info("lock main");
m1();
}finally {
lock.unlock();
}
}
public static void m1(){
lock.lock();
try{
log.info("lock m1");
m2();
}finally {
lock.unlock();
}
}
public static void m2(){
lock.lock();
try{
log.info("lock m2");
}finally {
lock.unlock();
}
}
}
12.3、可打断
##可打断
·t线程在等待锁的过程中,其他线程可以使用interrupt()方法终止t线程的等待
(synchronized和lock方法是不可打断的)
·防止线程无限制的等待下去
##语法:
//如果没有竞争那么此方法就会获取lock对象锁
//如果有竞争就进入阻塞队列,但可以被其他线程使用interrupt()方法打断
reentrantLock.lockInterruptibly()
@Slf4j
public class ReentrantLockInterruptTest {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
log.info("t1尝试获取锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
log.info("t1获取锁失败");
e.printStackTrace();
return;
}
try {
log.info("t1获取锁成功");
}finally {
lock.unlock();
}
},"t1");
log.info("main线程获取锁");
lock.lock();
t1.start();
//main睡1s
Thread.sleep(1000);
//主线程打断t1
log.info("mian线程打断t1");
t1.interrupt();
}
}
12.4、锁超时
##锁超时
interrupt是被动的避免线程死等,锁超时是主动的避免死等
##语法
//尝试获取锁,获取不了之后直接失败
//返回的是true和false
reentrantLock.tryLock();
//尝试等待timeout时长,如果等待时长到了还获取不到所直接失败
//返回的是true和false
reentrantLock.tryLock(long timeout,TimeUnit u);
@Slf4j
public class ReentrantLockTryLockTest {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try{
log.info("t1尝试获取锁");
if(!lock.tryLock(2, TimeUnit.SECONDS)){
log.info("t1获取锁失败");
return;
}
} catch (InterruptedException e) {
log.info("t1获取锁失败");
e.printStackTrace();
return;
}
try{
log.info("t1成功获取锁");
}finally {
lock.unlock();
}
}, "t1");
log.info("main线程获取锁");
lock.lock();
t1.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("main线程释放锁");
lock.unlock();
}
}
12.5、哲学家就餐问题
·在11.3中哲学家就餐问题,因为使用synchronized哲学家会出现在持有左手筷子的情况下一
直等待右手筷子。我们可以使用reentrantLock的tryLock()方法防止锁无限制的等待下去,
如果拿不到右手筷子,我们主动释放左手筷子
/**
* reentrantLock解决哲学家就餐问题
*/
public class PhilosopherEatReentrantLock {
public static void main(String[] args) {
Chopstick2 c1 = new Chopstick2("1");
Chopstick2 c2 = new Chopstick2("2");
Chopstick2 c3 = new Chopstick2("3");
Chopstick2 c4 = new Chopstick2("4");
Chopstick2 c5 = new Chopstick2("5");
new Philosopher2("苏格拉底",c1,c2).start();
new Philosopher2("柏拉图",c2,c3).start();
new Philosopher2("亚里士多德",c3,c4).start();
new Philosopher2("赫拉克利特",c4,c5).start();
new Philosopher2("阿基米德",c5,c1).start();
}
}
@Slf4j
class Philosopher2 extends Thread{
//左边的筷子
private Chopstick2 left;
//右边的筷子
private Chopstick2 right;
public Philosopher2(String name,Chopstick2 left,Chopstick2 right){
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true){
//尝试获取左手边的筷子
if(left.tryLock()){
try{
//尝试获取右手边的筷子
if(right.tryLock()){
try {
//吃饭
eat();
}finally {
right.unlock();
}
}
}finally {
left.unlock();
}
}
}
}
public void eat(){
log.info("eat.....");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//筷子集成ReentrantLock
@Data
@AllArgsConstructor
class Chopstick2 extends ReentrantLock {
private String name;
@Override
public String toString() {
return "筷子{ "+name+"}";
}
}
12.6、公平锁
·synchronized的monitor锁是不公平的
(blocked状态下的锁在获取到竞争权时会一拥而上,谁抢到算谁的,毫无公平性可言)
·ReentrantLock锁默认是不公平的
·但我们可以通过ReentrantLock的构造方法设置其为公平锁
##语法:
//true为公平锁,false为不公平锁,默认是false
public ReentrantLock(boolean fair){
sync = fair ? new FairSync():new NonfairSync()
}
【公平锁主要是为了解决饥饿问题,一般没有必要使用,它会降低并发度】
12.7、条件变量
·synchronized中也有条件变量,就是waitSet休息室,当条件不满足时进入waitset等待
·ReentrantLock的条件变量比synchronized强大之处在于,它是支持多个条件变量的:
>synchronized是哪些不满足条件的线程都在一间休息室等待消息
>ReentrantLock支持多间休息室,唤醒时也是按休息室来唤醒
##使用流程:
·await前需要获得锁
·await执行后,会释放锁,进入conditionObject等待
·await的线程被唤醒(或打断、或超时)取重新竞争lock锁
·竞争lock锁成功后,从await后继续执行
##创建条件变量(休息室)
Condition c1 = rLock.newCondition();
##进入休息室等待
c1.await();//执行其前必须先获得锁:rLock.lock();
##从休息室中唤醒
c1.signal();//随机唤醒c1中的某一个等待线程
c1.signalAll();//唤醒c1中的全部等待线程
/**
* 小南和小女在一间屋子干活,小南没烟罢工,小女没外卖罢工
*/
@Slf4j
public class AWaitConditionReentrantLock {
static ReentrantLock ROOM = new ReentrantLock();//房间
static Condition cigaretteCondition = ROOM.newCondition();//等烟的休息室
static Condition takeOutCondition = ROOM.newCondition();//等外卖的休息室
static boolean hasCigarette = false;//是否有烟
static boolean hasTakeOut = false;//是否有外卖
public static void main(String[] args) {
//小南
new Thread(()->{
ROOM.lock();
try{
log.info("有烟没:[{}]",hasCigarette);
while(true){
if(!hasCigarette){
log.info("没烟,歇会儿!");
try {
cigaretteCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
log.info("可以开始干活了");
break;
}
}
}finally {
ROOM.unlock();
}
},"小南").start();
//小女
new Thread(()->{
ROOM.lock();
try{
log.info("有外卖没:[{}]",hasTakeOut);
while(true){
if(!hasTakeOut){
log.info("没外卖,歇会儿!");
try {
takeOutCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
log.info("可以开始干活了");
break;
}
}
}finally {
ROOM.unlock();
}
},"小女").start();
//主线程休息1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//送外卖的到了
new Thread(()->{
ROOM.lock();
try{
hasTakeOut = true;
log.info("外卖到了");
takeOutCondition.signal();
}finally {
ROOM.unlock();
}
},"送外卖的").start();
//送烟的到了
new Thread(()->{
ROOM.lock();
try {
hasCigarette = true;
log.info("烟到了");
cigaretteCondition.signal();
}finally {
ROOM.unlock();
}
},"送烟的").start();
}
}
##结果:
10:41:20.511 [小南] INFO xxx - 有烟没:[false]
10:41:20.514 [小南] INFO xxx - 没烟,歇会儿!
10:41:20.514 [小女] INFO xxx - 有外卖没:[false]
10:41:20.514 [小女] INFO xxx - 没外卖,歇会儿!
10:41:21.510 [送外卖的] INFO xxx - 外卖到了
10:41:21.510 [小女] INFO xxx - 可以开始干活了
10:41:21.510 [送烟的] INFO xxx - 烟到了
10:41:21.511 [小南] INFO xxx - 可以开始干活了
十三、练习:固定运行顺序
两个线程,一个打印1一个打印2,必须21
synchronized wait notify实现
@Slf4j
public class ThreadOrder_1 {
static final Object lock = new Object();//锁对象
static boolean if2runned = false;//线程2是否执行
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
while (!if2runned) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("1");
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lock) {
log.info("2");
if2runned = true;
lock.notify();
}
}, "t2");
t1.start();
t2.start();
}
}
synchronized park unpark实现
@Slf4j
public class ThreadOrder_2 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
LockSupport.park();//暂停t1线程,等待t2线程唤醒
log.info("1");
}, "t1");
t1.start();
new Thread(()->{
log.info("2");
LockSupport.unpark(t1);
},"t2").start();
}
}
十四、练习:交替输出
三个线程:t1输出a 5次 t2输出b 5次 t3输出c 5次
现要求输出abcabcabcabcabc
synchronized wait notify实现
@Slf4j
public class ThreadAlternately_1 {
public static void main(String[] args) {
WaitNotify waitNotify = new WaitNotify(1, 5);
new Thread(()->{
waitNotify.print("a",1,2);
},"t1").start();
new Thread(()->{
waitNotify.print("b",2,3);
},"t2").start();
new Thread(()->{
waitNotify.print("c",3,1);
},"t3").start();
}
}
/**
* 输出内容 等待标记flag 下一个标记flag
* a 1 2
* b 2 3
* c 3 1
*/
@Slf4j
@Data
@AllArgsConstructor
class WaitNotify{
//等待标记
private int flag;
//循环次数
private int loopNum;
//打印
public void print(String str,int waitFlag,int nextFlag){
for (int i = 0; i <5 ; i++) {
synchronized (this){
while(flag!=waitFlag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info(str);
flag = nextFlag;
this.notifyAll();
}
}
}
}
ReentrantLock await signal实现
@Slf4j
public class ThreadAlternately_2 {
public static void main(String[] args) {
AwaitNotify an = new AwaitNotify(5);
Condition aCond = an.newCondition();
Condition bCond = an.newCondition();
Condition cCond = an.newCondition();
new Thread(()->{
an.print("a",aCond,bCond);
},"t1").start();
new Thread(()->{
an.print("b",bCond,cCond);
},"t2").start();
new Thread(()->{
an.print("c",cCond,aCond);
},"t3").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//主线程唤醒t1
an.lock();
try {
aCond.signal();
}finally {
an.unlock();
}
}
}
@Slf4j
@Data
@AllArgsConstructor
class AwaitNotify extends ReentrantLock {
private int loopNum;//循环次数
//str打印内容 currentCond当前休息室 nextCond下一个休息室
public void print(String str, Condition currentCond,Condition nextCond){
for (int i = 0; i <loopNum ; i++) {
this.lock();
try{
currentCond.await();
//被唤醒后打印 内容
log.info(str);
//唤醒另一个线程
nextCond.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.unlock();
}
}
}
}
LockSupport park unpark
@Slf4j
public class ThreadAlternately_3 {
static Thread t1,t2,t3;
public static void main(String[] args) {
ParkUnpark pu = new ParkUnpark(5);
t1 = new Thread(()->{
pu.print("a",t2);
},"t1");
t2 = new Thread(()->{
pu.print("b",t3);
},"t2");
t3 = new Thread(()->{
pu.print("c",t1);
},"t3");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
}
}
@Slf4j
@Data
@AllArgsConstructor
class ParkUnpark{
private int loopNum;//循环次数
//str打印的内容 t被唤醒的线程
public void print(String str,Thread t){
for (int i = 0; i <loopNum ; i++) {
LockSupport.park();
log.info(str);
LockSupport.unpark(t);
}
}
}