三、共享内存、管程
3.1 共享变量带来的问题
临界区
- 一个线程在运行多个线程本身没有问题
- 问题出现在多个线程访问共享的变量
- 多个线程读共享资源实际上也没有问题
- 多个线程在写共享资源的时候很有可能会发生指令交错的问题,就会出现问题
- 一段代码块如果存在对共享资源进行多线程读写操作,称这段代码块为临界区
竞态条件
- 多个线程在临界条件区执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
public class test5 {
static int counter =0;
public static void main(String[] args) throws InterruptedException {
Thread.yield();
Thread t1 = new Thread(()->{
for(int i = 0; i<5000;i++){
counter--;
}
});
Thread t2 = new Thread(()->{
for(int j = 0;j<5000;j++){
counter++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter);
}
}
- 两个线程同时对一个共享变量进行操作,虽然一个是对共享变量进行了+5000一个是-5000,由于竞态条件的影响导致最终的值出现了问题。
3.2 synchronized解决方案
- 为了解决多线程环境下,操作共享变量导致数据发生竞态条件,可以使用以下两种方式来避免
- 阻塞式解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
synchronized解决方式
- synchronized俗称对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想或得这个对象锁时就会阻塞住。这样就可以保证线程安全的执行临界区中的代码,不再担心线程上下文的切换。
- 对上面的程序进行synchronized进行优化
@Slf4j(topic = "c.test5")
public class test5 {
static int counter =0;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread.yield();
Thread t1 = new Thread(()->{
for(int i = 0; i<5000;i++){
synchronized (lock){
counter--;
}
}
});
Thread t2 = new Thread(()->{
for(int j = 0;j<5000;j++){
synchronized (lock){
counter++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
}
synchronized理解
-
线程2持有锁
-
线程1持有锁
思考
-
synchronized实际上使用了对象锁保证了临界区域内代码的原子性,使临界区域代码对外是不可以分割的,不会被线程的切换所打断。
-
为了加深理解以下问题
-
1、如果把synchronized(obj)放在循环的外面会怎么样->>>>>>这种情况是这样的,在for循环中看起来执行的是一条++操作或者是–操作,但是在底层实际上是有许多个字节码指令共同来完成这一句语句的。像++或者–就是4条指令,放在循环里面实际上每次保护这四条指令,放在循环外面就是一次保护5000*4条指令。
-
2、如果t1线程用synchronized锁的是obj1对象,t2线程锁的是obj2对象能起到保护作用吗?->>>>>>这样肯定是不能的,可以这样理解当发生线程切换的时候,t1锁的是obj1对象,t2去尝试获取锁的时候获取的是obj2对象锁,此时obj2没有被其他线程锁占用,所以t2线程会占用这个obj2对象锁而不是处在阻塞状态,那么它也能正常执行,就起不到保护的作用了。
-
3、如果只是t1线程加了synchronized进行占用对象锁,但是t2线程没有使用synchronized占用对象锁,可以起到保护作用吗?->>>>>>这样肯定也是不能的,因为当发生线程切换的时候,虽然t1占住了对象锁,但是t2线程没有加synchronized关键字,不会竞争锁就不存在阻塞状态,所以t2线程也能正常的执行,就起不到保护的作用了。
-
3.3 采用对象的思想用 synchronized 加锁保护
- 创建了一个room对象在room对象当中采用加锁的方式对counter进行操作,然后在main方法中调用room对象的方法即可
@Slf4j(topic = "c.test6")
public class test6 {
public static void main(String[] args) throws InterruptedException {
//通过调用room的方法即可完成原先的操作
Room room = new Room();
Thread t1 = new Thread(()->{
for(int i = 0; i<5000;i++){
room.add();
}
});
Thread t2 = new Thread(()->{
for(int j = 0;j<5000;j++){
room.sub();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",room.getCounter());
}
}
/**
* room对象用来封装两个被synchronized关键字修饰的方法
* 一个是对counter加5000次
* 一个是对counter减5000次
*/
class Room {
private int counter =0;
public void add(){
synchronized (this){
counter++;
}
}
public void sub(){
synchronized (this){
counter--;
}
}
public int getCounter(){
synchronized (this){
return this.counter;
}
}
}
-
synchronized除了加到对象上,锁住同一个对象实现线程安全的问题,其实synchronized还可以加在方法上
-
当synchronized加在成员方法上实际上是锁住了this对象
-
class Room { private int counter =0; //等价于锁住this对象 public synchronized void add(){ counter++; } public synchronized void sub(){ counter--; } public synchronized int getCounter(){ return this.counter; } }
-
-
当synchronized锁在静态方法上,实际上锁住的是静态方法所在的类。
3.4线程八锁
- 情况1:执行顺序1、2 或 2、1
@Slf4j(topic = "c.Number") class Number{
public synchronized void a() {
log.debug("1");
}
public synchronized void b() {
log.debug("2"); }
}
public static void main(String[] args) {
Number n1 = new Number(); new Thread(()->{
n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
- 情况2:1s 后1、2 或者2、1后1
@Slf4j(topic = "c.Number") class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2"); }
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
- 情况3:3、2 1s后1,3,1s后1、2 或2、3 1s后1
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
public void c() {
log.debug("3"); }
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
new Thread(()->{ n1.c(); }).start();
}
- 情况四 2, 1s后1
@Slf4j(topic = "c.Number") class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2"); }
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
- 情况5
@Slf4j(topic = "c.Number") class Number{
//这个synchronized修饰的是静态方法,所以他锁的是Number类
public static synchronized void a() {
sleep(1);
log.debug("1");
}
//这个synchronized修饰的是普通方法,所以他锁的是n1对象
public synchronized void b() {
log.debug("2");
}
}
//综上所述,这两个synchronized锁的不是同一个对象
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
- 情况6: 1s后1、2 或者 2 1s后1(两个都锁的是类对象)
@Slf4j(topic = "c.Number") class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2"); }
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
- 情况7: 2 1s后1(线程1锁的类对象,线程2锁的n2对象)
@Slf4j(topic = "c.Number") class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2"); }
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
- 情况8: 1s 后 1,2 或者 2 1s 后1(锁的都是Number类)
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2"); }
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
3.5 变量线程安全分析
成员变量和静态变量是否线程安全
- 如果他们没有共享,则线程安全
- 如果他们被共享了,根据他们的状态是否能够改变,分为两种情况
-
如果只有读操作,则线程安全
-
如果既有读操作又有写操作,则需要考虑线程安全的问题
-
局部变量是否线程安全
- 局部变量的是线程安全的
- 但是局部变量引用的对象则未必是线程安全的
- 如果该对象没有逃离方法的作用访问(局部变量的引用没有暴露给外部),它是线程安全的
- 如果该对象逃离方法的作用范围(局部变量的引用暴露给外部),需要考虑线程安全
3.6 常见线程安全的类
- String、Integer、StringBuffer、Random、Vector、HashTable、juc包下面的类
- 这里所说的线程安全是指,多个线程调用他们同一个实例的某个方法的时候,是线程安全的
Hashtable table = new Hashtable();
new Thread(()>{
table.put("key1","value1");
}).start();
new Thread(()>{
table.put("key2","value1");
}).start();
//看一下HashTable底层put方法的部分源码,可以明显的看到他是加了synchronized锁的,所以对于put这个方法来说他是线程安全的,所以多个线程在调用HashTable的put方法的时候是线程安全的
//其实实际上HashTable底层绝大多数方法都是加了synchronized锁的
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
.......
}
线程安全的方法的组合
- 分析下面的代码是否是线程安全的
Hashtable table = new Hashtable();
if(table.get("key")==null){
table.put("key",value);
}
-
这样就不是线程安全的了,因为单个方法是线程安全的,是一种原子性的操作,但是方法组合在一块就不是一个原子的操作了
-
如下图:当线程1判断了key为空时,发生了线程的上下文切换,线程二判断key是否为空,线程2判断为空之后直接调用了put方法,修改了key的值,然后线程切换到线程1,线程1在之前就判断过了,所以直接调用put方法覆盖了线程2put的值,所以说不是线程安全的。
-
-
总结:虽然方法的线程安全的,但是线程安全的方法组合在一起就不一定是线程安全的了。
不可变线程安全性
-
String、Integer等都是不可变的类,(String类的底层是一个final修饰的char数组,Integer底层的value是由final修饰的),正是因为他的内部状态是不可以变得,因此他们的方法都是线程安全的。
-
分析String的replace和substring方法,他们不是能修改字符串嘛?
-
substring底层源码
public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > value.length) { throw new StringIndexOutOfBoundsException(endIndex); } int subLen = endIndex - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } //实际上会发现在截取字符串的时候,substring方法会判断起始位置和终止位置是不是原始字符串的起始位置和终止位置,如果是的话直接返回的原先的字符串 //如果不是就有意思了,它实际上是先new了一个新的字符串然后将原先的字符串(字符数组拷贝了指定的起始位置和终止位置),然后将这个新的字符串放到了字符串常量池中,改变原先字符串引用指向新的字符串在字符串常量池中的位置即可 return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen); }
-
3.7 Monitor概念
Java对象头(以32位虚拟机为例)
- 普通对象
Mark Word(32bit) | Klass Word(32bit) |
---|
- 数组对象
Mark Word(32bit) | Klass Word(32 bit) | array length(32bit) |
---|
Monitor
- Monitor被翻译为监视器或者管程
- 每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Work中就被设置指向Monitor对象的指针。
- Monitor的结构如下
- 刚开始Monitor中的Owner为null
- 当Thread-2执行synchronized(obj)就会将Monitor的所有者Ower置为Thread-2,Monitor中只能有一个Ower
- 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也执行synchronized(obj),就会进入到EntryList 等待队列,也就是==BLOCKED(阻塞)==状态
- Thread-2执行完毕同步代码块中的内容之后,然后唤醒EntryList中等待的线程来竞争锁,竞争的时候是非公平的
- 注意
- synchronized必须是进入同一个对象的monitor才有上述的效果
- 不加synchronized的对象不会关联监视器,不遵从以上的规则
synchronized原理
- 下面这段代码
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args){
synchronized(lock){
counter++;
}
}
- 对应的字节码文件
- synchronized作用在代码块的时候,底层实现基于两条指令monitorenter、monitorexit两条指令
- monitorenter将锁对象的MorkWord置为指向Moniter的指针
- moniterexit将锁对象MarkWord重置,唤醒EntryList中被阻塞的线程,让他们去竞争锁
- 当发生异常的时候也能够释放锁,在底层字节码有一个异常表也是通过monitorexit指令来释放锁。
3.8 深入理解synchronized
轻量级锁
- 轻量级锁的使用场景:如果对一个对象虽然有多个线程访问,但是多线程之间的访问时间是错开的(也就是这些线程之间是不存在竞争的),这样的情况可以使用轻量级锁来进行优化
- 并且轻量级锁对使用者来说是透明的,不需要我们去显示的做什么,语法还是sunchronized,在他的底层会自动选择合适的方式处理线程同步的机制。
- 假设现在有两个同步方法块,利用同一个对象来加锁
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块A
method2();
}
}
public static void method2(){
synchronized(obj){
//同步块B
}
}
- 创建锁的记录(Look Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
- 让锁记录中的Object reference 指向Object锁对象,并尝试使用cas替换Object的Mark Word,将Mark Word的值存入到锁记录当中,交换成功就说明加锁成功
- 如果cas替换成功, 对象头中存储了锁记录地址和状态 00 ,表示该线程给对象加锁,这时的情况如下图所示
-
如果cas执行失败,有两种情况
-
如果其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
-
如果自己执行了synchronized锁重入,那么在加一条Lock Record作为重入的计数,就是一个栈帧中两个锁记录来竞争Object锁对象
-
-
锁重入虽然不会记录Object对象头中的Mark Word的相关信息,但是它可以做一个锁的记录,记录同一个线程进入这个Object对象锁多少次
- 当退出synchronized代码块的时候(解锁的时候),如果存在有取值为null的锁记录,表示有重入,这时重置锁记录将重入计数减1
-
- 当退出synchronized代码块的时候发现锁记录的值不为null,这时用cas将锁记录中的Mark Word的值还给Object对象头,他俩做个交换
- 若退出成功,则解锁成功
- 失败,说明轻量级锁进行了膨胀已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
-
如果在轻量级锁的过程当中,CAS操作无法成功,这时一种情况就是上面说的其他线程为此对象加上了轻量级锁(有竞争),这时就需要锁的膨胀,将轻量级锁升级为重量级锁
-
Thread1想要用cas操作来获取轻量级锁,但是发现Object对象的标志位是00,不是无锁状态换不了
-
-
加锁失败的Thread1就会进入锁膨胀的流程
- Object对象申请Monitor锁,让Object指向重量级锁地址
- 然后自己进入Monitor的EntyrList 的 BLOCKED(阻塞状态)
- 当Thread-0退出同步块解锁的时候,使用cas去交换锁记录对象和Object对象的MarkWord这时候就失败了,因为此时Object对像的MarkWord已经不再是原先锁记录中的内容了,已经指向了Monitor地址,也就是重量级锁,标志位是10,所以这个时候要进入到重量级锁的解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒等待队列中处于BOLOCKED阻塞状态的线程。
自旋优化
-
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时持有线程已经退出同步代码块,释放了锁),这是当前的线程就可以避免阻塞
-
自旋优化存在两种情况就是自旋成功、自选失败
-
自旋成功
-
当然不是每次就能自旋成功
- 当线程自选一定的次数后还没能竞争到锁就要进入到阻塞队列
-
-
在java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性就高,就多自旋几次;反之,就少自旋甚至不自旋,总之就是比较智能
-
自选会占用cpu时间,单核cpu自旋就是浪费,多核cpu才能体现出自旋带来的优势
-
java7之后不能控制是否开启自旋功能
偏向锁
- 对象头格式
- 一个对象在创建的时候,偏向锁默认是开启的
- 偏向锁也是默认有一点延迟的,不会再程序启动的时候立马生效,如果想要避免延迟可以通过控制VM参数来实现
偏向锁的撤销
- 第一种情况
- 调用偏向锁对象的hashcode方法会导致,对象的偏向锁撤销回到正常状态,且加锁从轻量级锁开始。
- 第二种情况
- 当有其他线程使用偏向锁的时候会将偏向锁升级为轻量级锁
批量重偏向
- 如果对象虽然是被多个线程访问,这时偏向了线程1的对象任然有机会重新偏向线程2,重偏向会重置对象的线程id
- 当偏向锁阈值超过了20次之后,jvm会觉得我是不是偏向错了呢,于是会在这些对象加锁时重新偏向至加锁的线程
批量撤销
- 当撤销偏向锁超过40次之后,jvm会觉得自己确实是偏向错了,根本不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也不可以偏向。
3.9 wait /notify
原理之 wait /notify
- 当Owner线程发现条件不满足的时候,调用wait方法,即进入WaitSet变为变为WAITTING状态。
- BOLCKED和WAITTING的线程都处于阻塞状态中但是阻塞的元原因不同,所以他们都不会占用cpu时间片,再cpu调度的时候不会考虑这俩。
- BLOCKED线程会在Owner线程释放锁之后被唤醒
- WAITTING线程会在Owner线程调用了notify或者notifyAll时唤醒,但唤醒后并不意味着立即或得锁,仍需进入EntryList重新竞争。
API介绍
- obj.wait();让进入object监视器的线程waitSet等待
- obj.wait(long n);可以指定等待的时间
- obj.notify();在object上正在waitSet等待的线程中挑一个唤醒
- obj.notifyAll();让object上正在waitSet等待的线程全部唤醒
- 他们都是线程之间进行协作的手段,都是与Object对象的方法。必须获得此对象的锁,才能调用这几个方法。也就是说必须是Ownner线程才能调用wait方法。
- 必须获得对象锁才能调用这几个方法的意思就是这几个方法必须在同步代码块中进行调用
sleep 和 wait方法的区别
- sleep是Thread的方法,wait是Object的方法
- sleep不需要强制和synchronized配合使用,wait强制和synchronized配合在一起使用
- sleep方法不会释放锁,wait方法会释放锁
避免虚假唤醒的所使用的wait notify方式
synchronized(lock){
while(条件不成立){
lock.wait();
}
//干活
}
synchronized(lock){
lock.notifyAll();
}
同步模式之保护性暂停
要点
- 有一个结果需要从一个线程传递到另外一个线程,让他们同时关联一个GiarderObject
- 如果有结果源源不断地从一个线程到另外一个线程那么可以使用消息队列
- JDK中的join地实现,Future就是基于这个实现地
生产者/消费者(异步模式)
要点
- 消费队列可以用来平衡生产和消费的线程资源
- 生产者仅生产资源,不关心数据该如何处理,而消费者只需要专心处理结果数据即可,不关心数据是如何产生地。
- 消息队列地容量也是有限制地,满的时候不会再加入数据,空的时候不会再消费数据
- JDK中的各种阻塞队列就是基于这种模式进行实现的
实现一个生产者消费者模式,并测试
package com.zb.juc.test;
import lombok.extern.slf4j.Slf4j;
import java.util.LinkedList;
/**
* @Description:
* @Author:啵啵啵啵啵啵唧~~~
* @Date:2022/4/8
*/
@Slf4j(topic = "c.test7")
public class test7 {
public static void main(String[] args) {
//初始化我们消息队列的最大容量是2
MessageQueue mq = new MessageQueue(2);
//生产者一共要生产3个消息
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 msg = mq.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"消费者").start();
}
}
/**
* 消息队列类,java线程之间通信
*/
@Slf4j(topic = "c.MessageQueue")
class MessageQueue{
/**
* 存放消息的队列
* */
private LinkedList<Message> mq = new LinkedList<>();
/**
* 消息队列的最大容量
*/
private int capacity;
public MessageQueue(int capacity) {
this.capacity = capacity;
}
/**
* 获取消息
* @return
*/
public Message take(){
//获取消息之前应该先判断消息队列当中的容量是否为空
//1、不为空从队头拿消息即可
//2、为空就调用wait方法等待消息存放
synchronized (mq){
while (mq.isEmpty()){
try {
log.debug("队列为空,消费者线程等待");
mq.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//走到这里说明mq中的元素不为空,可以获取mq中的队首元素
Message msg = mq.removeFirst();
log.debug("已消费信息{}",msg);
//这里消费一条消息之后,应该调用notifyAll方法,因为在下面的存放消息的方法中,
//有可能会因为消息队列满了陷入等待,所以这里消费一条消息之后,队列肯定会有空间来存放新的消息
mq.notifyAll();
return msg;
}
}
/**
* 存放消息
* @param msg
*/
public void put(Message msg){
//存放消息得是先看mq中的容量是否已经满了
//1、满了就进入等待,当有消息被获取之后再进行存放
//2、mq有空余位置,那么就将一条消息存放到mq的队尾
synchronized (mq){
while(mq.size()==capacity){
try {
log.debug("队列已满,生产者等待");
mq.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//走到这里就说明mq中有空余位置存放新的消息,现在来存放
mq.addLast(msg);
log.debug("已生产信息{}",msg);
//注意在存放消息完后要调用notifyAll方法,为什么呢?
//因为在消息获取的方法中,有可能之前消息队列为空,所以获取消息的方法一直在等待中,
//这里添加一条消息之后获取消息的方法就有东西获取了,所以调用notifyAll方法将其唤醒
mq.notifyAll();
}
}
}
/**
* 消息类
*/
class Message{
private int id;
private Object value;
public Message(int id, Object value) {
this.id = id;
this.value = value;
}
public int getId() {
return id;
}
public Object getValue() {
return value;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}
- 结果分析
12:48:16.740 c.MessageQueue [生产者0] - 已生产信息Message{id=0, value=值0}
12:48:16.745 c.MessageQueue [生产者1] - 已生产信息Message{id=1, value=值1}
//生产者最多生产3个消息,但是消息队列的最大容量是2,所以当生产两个消息的时候。队列已满等待消费者消费
12:48:16.745 c.MessageQueue [生产者2] - 队列已满,生产者等待
//消费者开始消费消息,消费了一条
12:48:17.740 c.MessageQueue [消费者] - 已消费信息Message{id=0, value=值0}
//消费者消费了一条之后,队列有空间存放生产者生产的信息了,所以生产者又开始生产信息,但是我们设定生产者最多生产两条消息,最开始已经生产过了两条消息,所以此时只能生产最后一条
12:48:17.740 c.MessageQueue [生产者2] - 已生产信息Message{id=2, value=值2}
//生产者不再生产消息之后,消费者开始消费消息
12:48:18.755 c.MessageQueue [消费者] - 已消费信息Message{id=1, value=值1}
12:48:19.770 c.MessageQueue [消费者] - 已消费信息Message{id=2, value=值2}
//当消息队列中两条消息已经消费完毕之后,消费者为空,生产者也不再生产消息了
12:48:20.776 c.MessageQueue [消费者] - 队列为空,消费者线程等待
3.10 park和unpark方法的使用
- 他们是lockSupport类的方法
//暂停当前线程
LockSupport.park();
//恢复某个线程的运行
LockSupport.unpark(暂停线程对象);
与Object的wait和notify相比较
- wait,notify,notifyAll必须配合Object Monitor一起使用而park,unpark不必
- park和unpark是以线程为单位来【阻塞】和【唤醒的线程】,而notify只能随机唤醒一个线程,notifyAll是唤醒所有的线程,不是那么的精确。
- park和unpark可以先unpark,但是wait和notify必须先wait才有notify
3.11线程状态转换
- 线程状态转换图
情况一: NEW----->RUNNABLE
- 当调用t.start()方法的时候,有NEW---->RUNNABLE
情况二:RUNNABLE <---->WAITTING(无时限等待)
在t线程用synchronized(obj)获取对象锁之后
- 调用obj.wait方法的时候,t线程会从RUNNABLE—>WAITTING
- 调用obj.notify(),obj.notifyAll(),t.interrupt()时
- 这个线程竞争锁成功那么就会由WATTING—>RUNNABLE
- 如果这个线程被唤醒之后竞争锁失败那么就会进入到BLOCKED状态
情况三:RUNNABLE <---->WAITTING
- 在当前线程中调用t.join()方法的时候,当前线程从RUNNABLE—>WATTING
- 注意当前线程是在t线程对象的监视器等待,也就是说当前线程只是在等待t线程的执行
- t线程运行结束,或调用当前线程的interrupt()时,当前线程从WATTING—>RUNNABLE
情况四:RUNNABLE <---->WAITTING
- 当线程调用LockSupport.park()方法会让当前线程从RUNNABLE —>WATTING
- 调用LockSupport.unpart(目标线程)或调用了线程的interrupt(),会让目标线程从WATTING---->RUNNABLE
情况五: RUNNABLE<---->TIMED_WATTING(有时限等待)
- 就是调用了一些有时限的等待方法如obj.wait(long n),Thread.sleep(long n),t.join(long n)
- 除了有时效和之前的情况无差别。
情况六:RUNNABLE <---->BLOCKED
- t线程用synchronized(obj)获取了对象锁时如果竞争失败,从RUNNABLE—>BLOCKED
- 持有obj锁线程的同步代码块执行完毕,会唤醒该对象上所有的BLOCKED的线程重新竞争,如果其中t线程竞争成功,从BLOCKED—>RUNNABLE,其他失败的线程仍然是BLOCKED!
3.12 多把锁
多把互不相干的锁
- 在一间大的房间有两个功能是睡觉和学习,两个是互不相干的
- 加入现在小南要学习,小女要睡觉,但是如果只用了一间屋子(一个对象锁的话),那么并发度就会很低,比如小南要学习了,他在进入房间后(拿到一个对象锁),就会将这个房间上锁,这时候小女要睡觉就不能再进入房间了,必须等待小南学习完毕释放锁之后才能拿到房间的锁进去睡觉,但是实际上我们说了,学习和睡觉之间实际上不相关的,所以这样就导致了效率的下降。
- 解决的方法是准备多个房间(多个对象锁),就是将房间分为书房和卧室,小南学习锁书房,小女睡觉锁卧室即可,这两个不会相互干扰。再提高线程安全的同时,提高了并发的效率。
- 这也是一种分段锁的思想吧,将锁进行细粒度划分!
- 好处就是增强并发,但是存在一些缺点就是容易发生死锁的情况。
3.13线程的活跃性
死锁
- 一个线程需要获取很多把锁的时候就容易出现死锁的情况
- t1线程获得A对象的锁,想要取拿B对象的锁
- t2线程获得B对象锁,接下来想获取B对象的锁
例:
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args){
test1();
}
private static void test1() {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(()->{
synchronized (A){
log.debug("lockA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//再去获得B锁的时候就陷入了阻塞状态
synchronized (B){
log.debug("lockB");
}
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (B){
log.debug("lockA");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//再去获得A锁的时候就陷入了阻塞状态
synchronized (A){
log.debug("lockB");
}
}
},"t2");
//A等B,B等A,这是多么凄美的爱情,相爱之人往往不能相拥
t1.start();
t2.start();
}
}
定位死锁的方式
- 检测死锁的方式使用jconsole工具,或者使用jps定位进程id,再用jstack定位死锁
经典死锁案例——哲学家就餐问题
活锁
- 和死锁的区别是,死锁是因为线程之间相互等待造成的,线程无法继续向下进行,此时线程都处在阻塞状态
- 两个线程没有没有阻塞,都在继续执行,继续消耗cpu资源,只不过就是线程之间影响了结束时机
- 可以通过增加随机睡眠时间来避免活锁的情况
饥饿状态
- 线程严格按照顺序加锁就有可能会出现饥饿状态
3.14ReentrantLock
相对于synchronized它具备的特点
- 可以中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
但是他和synchronized一样都是一种可重入的锁
基本语法
//先创建reentrantLock对象
ReentrantLock reentrantLock = new ReentrantLock();
//调用lock方法进行加锁
reentrantLock.lock();
//解锁必须放在try----finally语句块中
try{
//临界区
}finally{
//调用unlock方法释放锁
reentrantLock.unlock;
}
可重入
- 指的是同一个线程如果首次或得了这把锁,那么因为他是这把锁的拥有者,因此有权利再次获得这把锁
- 不可重入锁,那么第二次获得锁的时候,自己也会被锁挡住。
package com.zb.juc.test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Description:
* @Author:啵啵啵啵啵啵唧~~~
* @Date:2022/4/17
*/
@Slf4j(topic = "c.TestReentrantLock")
public class TestReentrantLock {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
//加锁
lock.lock();
try {
log.debug("进入主方法");
m1();
}finally {
lock.unlock();
}
}
//第二次加锁相当于是重入了,因为现在只有一个就是主线程
public static void m1(){
//加锁
lock.lock();
try {
log.debug("进入m1方法");
m2();
}finally {
lock.unlock();
}
}
//第三次加锁还是继续重入
public static void m2(){
//加锁
lock.lock();
try {
log.debug("进入m2方法");
}finally {
lock.unlock();
}
}
}
-
从结果可以看到线程能够成功执行,没发生被阻塞的情况
-
可打断
- 首先RenntrantLock的lock方法是不能被打断的
- RenntranLock的lockInterrputibly加锁方式才是可打断的一种加锁方式
- 当其他线程调用线程的interrput方法可以实现锁被打断
- 实际上加入可打断的机制也是防止某一个线程一直阻塞下去,造成死锁
例:
- 没有被打断的时候
@Slf4j(topic = "c.test8")
public class Test8 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(()->{
try {
//如果没有竞争,那么此方法就会获取lock对象锁
//如果有竞争就会进入阻塞队列
log.debug("尝试或得锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
//被打断没有获得锁,就不需要再往下走
log.debug("没有获得锁,返回");
return;
}
try{
log.debug("获取到锁");
}finally {
lock.unlock();
}
},"t1").start();
}
}
-
-
主线程先拿到锁,让t1线程进行等待,然后主线程调用t1线程的打断方法,将其打断
package com.zb.juc.test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
/**
* @Description:
* @Author:啵啵啵啵啵啵唧~~~
* @Date:2022/4/17
*/
@Slf4j(topic = "c.test8")
public class Test8 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
try {
//如果没有竞争,那么此方法就会获取lock对象锁
//如果有竞争就会进入阻塞队列
log.debug("尝试或得锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
//被打断没有获得锁,就不需要再往下走
log.debug("没有获得锁,返回");
return;
}
try{
log.debug("获取到锁");
}finally {
lock.unlock();
}
},"t1");
//主线程先加锁
lock.lock();
t1.start();
log.debug("主线程先加锁");
Thread.sleep(1000);
t1.interrupt();
}
}
锁超时
- 可打断机制为了避免死等,但是这种避免死等的方式是通过其他线程调用死等着的这个线程的interrupt方法,这种是一种被动的方式
- 现在有一种锁超时机制就是线程等待一定时间之后,锁还是没有释放就会自动的放弃,表示获取锁失败。
- 支持超时机制的锁是在加锁的时候调用tryLock接口
- 锁超时可以用来解决这个哲学家吃饭问题
公平锁
- 像这个synchronized锁就是一个非公平的锁,当一个新的线程在被创建的时候会无视阻塞队列当中的线程,先尝试获取一次锁,如果获取失败也会加到这个阻塞队列当中,处于阻塞队列当中线程还是会有序的进行锁的竞争
- 看一下ReentrantLock源码
通过ReentrantLock的这个构造方法来看,当我们在new一个ReentrantLock对象的时候若传递一个true为参数就能够创建一个FairSync()公平锁对象,这个构造方法默认是传递一个false也就是RenntrantLock默认创建的是一个非公平的锁。
3.15 控制线程的打印次序
固定打印顺序
- 例:每次让线程2先打印,采用wait和notify的方式
@Slf4j(topic = "c.Test9")
public class Test9 {
static final Object lock = new Object();
//表示t2是否运行过,打这个标记的作用就是必须t2运行过后才能让t1运行
static boolean t2Runned=false;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
//t1线程先执行,然后判断发现t2Runnable为false进入循环,调用对象的wait方法释放锁然后进入等待
synchronized (lock){
while (!t2Runned){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
log.debug("1");
},"t1");
Thread t2 = new Thread(()->{
//t2线程,在程序运行到这里的时候,因为t1线程的代码块中已经执行了wait方法将锁释放了,所以在这里t2线程直接拿到了锁,然后输出了2,将t2Runnable这个标记位打成了true,并且调用lock.notify()方法将线程1进行唤醒,然后此时线程1在执行的时候标记位就变成了false就不会进入等待就直接输出1
synchronized (lock){
log.debug("2");
t2Runned =true;
lock.notify();
}
},"t2");
t1.start();
t2.start();
}
}
- 例2,利用park和unpark来实现固定顺序输出
@Slf4j(topic = "c.Test10")
public class Test10 {
public static void main(String[] args) {
//使用LockSupport的park和unpark方法来实现
Thread t1 = new Thread(() -> {
//在没有执行unpark方法之前会陷入等待
LockSupport.park();
log.debug("1");
},"t1");
Thread t2 = new Thread(() -> {
log.debug("2");
//执行unpark方法后执行了park方法的线程就能及继续执行了
LockSupport.unpark(t1);
}, "t2");
t1.start();
t2.start();
}
}
交替输出
- 利用三个线程交替输出abc
package com.zb.juc.test;
import lombok.extern.slf4j.Slf4j;
/**
* @Description:利用三个线程交替输出abc
* @Author:啵啵啵啵啵啵唧~~~
* @Date:2022/4/17
*/
@Slf4j(topic = "c.Test11")
public class Test11 {
public static void main(String[] args) {
//创建一个WaitNotify对象
WaitNotify wn = new WaitNotify(1,5);
new Thread(()->{
wn.print("a",1,2);
}).start();
new Thread(()->{
wn.print("b",2,3);
}).start();
new Thread(()->{
wn.print("c",3,1);
}).start();
}
}
/*
输出的内容 等待的标记 下一个标记
a 1 2
b 2 3
c 3 4
*/
class WaitNotify{
/**
* 打印方法
* @param str 需要打印的内容
* @param waitFlag 等待标记
* @param nextFlag 接下来要打印的内容对应的标记
*/
public void print(String str,int waitFlag,int nextFlag){
for (int i=0;i<loopNumber;i++){
synchronized (this){
while(flag!=waitFlag){
//当前标记不等于等待标记,让当前线程进行睡眠
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//说明当前标记等于等待的标记可以打印
System.out.print(str);
//将当前标记修改为下一个等待的标记
flag=nextFlag;
//当标记修改之后说明本次打印已经完成,唤醒正在等待中的所有线程
this.notifyAll();
}
}
}
//等待标记
private int flag;
//循环打印的次数
private int loopNumber;
public WaitNotify(int flag, int loopNumber) {
this.flag = flag;
this.loopNumber = loopNumber;
}
}
- 采用park和unpark方法进行实现
package com.zb.juc.test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.LockSupport;
/**
* @Description: 利用三个线程交替输出abc
* @Author:啵啵啵啵啵啵唧~~~
* @Date:2022/4/17
*/
@Slf4j(topic = "c.Test12")
public class Test12 {
private static Thread t1;
private static Thread t2;
private static Thread t3;
public static void main(String[] args) {
ParkUnpark pu = new ParkUnpark(5);
t1 = new Thread(() -> {
pu.print("a",t2);
});
t2 = new Thread(() -> {
pu.print("b",t3);
});
t3 = new Thread(() -> {
pu.print("c",t1);
});
//分别启动三个线程,当三个线程在启动之后都会先被park住
t1.start();
t2.start();
t3.start();
//用主线程来启动t1线程
LockSupport.unpark(t1);
}
}
class ParkUnpark{
/**
*
* @param str 该线程要打印的内容
* @param next 下一个执行打印方法的线程
*/
public void print(String str,Thread next){
for (int i=0;i<loopNumber;i++){
LockSupport.park();
System.out.print(str);
//打印完毕唤醒下一个要执行打印的线程
LockSupport.unpark(next);
}
}
/**
* 循环打印的次数
*/
private int loopNumber;
public ParkUnpark(int loopNumber) {
this.loopNumber = loopNumber;
}
}
- 以上两种方式分别正确输出
如果有错误之处请帮忙指出哦