首页 > 其他分享 >JUC3-共享模型之管程

JUC3-共享模型之管程

时间:2024-08-17 18:51:40浏览次数:13  
标签:synchronized Thread 管程 JUC3 线程 debug new 共享 public

目录

共享带来的问题

分析

临界区Critical Section

竞态条件Race Condition

synchronized解决方案

语法1

语法2

线程八锁

变量的线程安全

成员变量和静态变量是否线程安全?

局部变量是否线程安全?

常用线程安全类

Monitor

Java对象头

Monitor(监视器/管程)

原理:synchronized

轻量级锁

锁膨胀

自旋优化

偏向锁

偏向状态

撤销-调用对象hashCode 

撤销-其他线程使用对象

撤销-调用wait/notify

批量重偏向

批量撤销

锁消除

锁粗化

wait、notify

原理

API介绍

同步模式之保护性暂停

异步模式之生产者/消费者

Park & Unpark

基本使用

特点

原理

线程状态转换

多把锁

活跃性

死锁

定位死锁

哲学家就餐问题

活锁

饥饿

ReentrantLock(可重入锁)

特点

基本语法

可重入

可打断

锁超时

公平锁

条件变量

同步模式之顺序控制

固定运行顺序

wait、notify版

park、unpark版

交替输出

wait、notify版

await、signal版

park、unpark版


共享带来的问题

分析

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

分析:

以上的结果可能是正数、负数、零

因为Java中对静态变量的自增、自减并不是原子操作,需要从字节码进行分析

例如对于i++而言(i为静态变量),会产生如下的JVM字节码指令:

i--:

而Java的内存模型如下,完成静态变量的自增、自减需要在主存和工作内存中进行数据交换:

如果是单线程,以上8行代码是顺序执行(不会交错),没有问题

但多线程下这8行代码可能交错运行

出现负数的情况:

出现正数的情况:

临界区Critical Section

一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源

  • 多个线程读共享资源没有问题
  • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

例如:

static int counter = 0;

static void increment()
//临界区
{
    counter++;
}

static void decrement()
//临界区
{
    counter--;
}

竞态条件Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

为了避免临界区的竞态条件发生,有多种解决方案:

  • 阻塞式:synchronized,Lock
  • 非阻塞式:原子变量

synchronized解决方案

synchronized,俗称对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个对象锁时就会阻塞住,这样就能保证拥有锁的线程可以安全地执行临界区内的代码,不用担心线程上下文切换

注意:

虽然Java中互斥和同步都可以采用synchronized关键字来完成,但存在区别:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后顺序不同,需要一个线程等待其他线程运行到某个点

语法1

synchronized(对象) // 线程1, 线程2(blocked)
{
   临界区
}

解决:

理解:

  • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
  • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
  • 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
  • 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去), 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
  • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给它,t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码

图示:

 

思考:

synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断

  • 如果把synchronized(obj)放在for循环的外面,如何理解?——原子性
  • 如果t1 synchronized(obj1),而t2 synchronized(obj2)会怎么运作?——不同的锁对象
  • 如果t1 synchronized(obj),而t2没有加会怎么样?——相当于都没有加锁

面向对象改进:

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test17")
public class Test17 {
    public static void main(String[] args) throws InterruptedException {
        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();
        t1.join();
        t2.join();
        log.debug("{}", room.getCounter());
    }
}

class Room {
    private int counter = 0;

    public void increment() {
        synchronized (this){
            counter++;
        }
    }

    public void decrement() {
        synchronized (this){
            counter--;
        }
    }

    public int getCounter() {
        synchronized (this){
            return counter;
        }
    }
}

语法2

成员方法:锁的是this对象

静态方法:锁的是类对象

线程八锁

考察synchronized锁住的是哪个对象

情况1:

12或21

情况2:

1s后122 1s后1

情况3:c方法没有synchronized,c和ab并行执行

3 1s后1 23 2 1s后1 或 2 3 1s后1

情况4: 两个对象n1和n2,不互斥

2 1s后1

情况5:a方法为静态方法,锁住的是类对象,与b方法锁住的对象不同,不互斥

2 1s后1

情况6:a、b都为静态方法

1s后122 1s后1

情况7:锁对象不同,不互斥

2 1s后1

情况8:锁对象相同,互斥

1s后122 1s后1

变量的线程安全

成员变量和静态变量是否线程安全?

如果它们没有共享,则线程安全

如果它们被共享,根据它们的状态是否能够改变,分为两种情况:

  • 如果只有读操作,则线程安全
  • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

局部变量是线程安全的

但局部变量引用的对象则未必

  • 如果该对象没有逃离方法的作用范围,它是线程安全的
  • 如果该对象逃离方法的作用范围,需要考虑线程安全

每个线程调用test1()方法时,局部变量i会在每个线程的栈帧内存中被创建多份,因此不存在共享

局部变量的引用:

如果是成员变量

其中一种情况是,如果线程2还未add,线程1remove就会报错:

分析:

无论哪个线程的method2,引用的都是同一个对象中的list成员变量

将list修改为局部变量

分析:

list是局部变量,每个线程调用时会创建其不同实例,没有共享

如果把method2和method3的方法修改为public会不会有线程安全问题?

情况1:有其他线程调用method2和method3

  • 没有问题,不同线程创建不同的list对象

情况2:在情况1的基础上,为ThreadSafe类添加子类,子类覆盖method2或method3方法

  • 有问题,两个线程共享一个资源

常用线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent包下的类

这里的线程安全指的是多个线程调用它们同一个实例的某个方法时,是线程安全的

它们的每个方法是原子的,但它们方法的组合不是原子的

线程安全类方法的组合:

下面代码不是线程安全的

不可变类线程安全性

String、Integer等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的

Monitor

Java对象头

以32位虚拟机为例

普通对象

数组对象

其中Mark Word结构为

64位虚拟机Mark Word

Monitor(监视器/管程)

每个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,Thread-5 也来执行synchronized(obj),就会进入 EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程

注意:

  • synchronized必须是进入同一个对象的monitor才有上述的效果
  • 不加synchronized的对象不会关联监视器,不遵从以上规则

原理:synchronized

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化

轻量级锁对使用者是透明的,即语法仍然是synchronized

假设有两个方法同步块,利用同一个对象加锁:

  • 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

  • 让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录

  • 如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁

如果cas失败,有两种情况:

  • 如果是其他线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
  • 如果是自己执行了synchronized锁重入,那么再添加一条Lock Record作为重入的计数

  • 当退出synchronized代码块(解锁时),如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

当退出synchronized代码块(解锁时),锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头

  • 成功,则解锁成功
  • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

如果在尝试加轻量级锁的过程中,cas操作无法成功,这是一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,即为 Object 对象申请 Monitor 锁,让Object 指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED

  • 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒EntryList 中 BLOCKED 线程

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当时线程自旋成功(即这时持锁线程已经退出来同步块,释放了锁),这时当前线程就可以避免阻塞

自旋会占用cpu时间,单核cpu自旋就是浪费,多核cpu自旋才能发挥优势

在Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能

Java 7之后不能控制是否开启自旋功能

自旋重试成功的情况:

自旋重试失败的情况:

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行cas操作

Java 6引入了偏向锁来做进一步优化:只有第一次使用cas将线程ID设置到对象的Mark Word头,之后发现这个线程 ID是自己的就表示没有竞争,不用重新cas,以后只要不发生竞争,这个对象就归该线程所有

偏向状态

对象头格式:

一个对象创建时:

  • 如果开启了偏向锁(默认开启),那么对象创建后,markword值为0x05,即最后3位为101,这时它的thread、epoch、age都为0
  • 偏向锁默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数-XX:BiasedLockingStartupDelay=0来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword值为0x01,即最后3位为001,这时它的hashCode、age都为0,第一次用到hashCode时才会赋值

处于偏向锁的对象解锁后,线程id仍存储于对象头中

正常状态对象一开始是没有hashCode的,第一次调用才生成

撤销-调用对象hashCode 

调用了对象的hashCode,但偏向锁的对象markword中存储的是线程id,如果调用hashCode会导致偏向锁被撤销

  • 轻量级锁会在锁记录中记录hashCode
  • 重量级锁会在Minitor中记录hashCode

 在调用hashCode后使用偏向锁,记得去掉-XX:-UseBiasedLocking

撤销-其他线程使用对象

当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁

撤销-调用wait/notify

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程t1的对象仍有机会重新偏向t2,重偏向会重置对象的Thread ID

当撤销偏向锁阈值超过20次后,jvm会觉得是不是偏向错了,会在给这些对象加锁时重新偏向至加锁线程

批量撤销

当撤销偏向锁阈值超过40次后,jvm会觉得自己确实偏向错了,根本不应该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

参考资料: 

https://www.cnblogs.com/LemonFive/p/11246086.html

https://www.cnblogs.com/LemonFive/p/11248248.html

死磕Synchronized底层实现--概论 · Issue #12 · farmerjohngit/myblog · GitHub

锁消除

JIT编译器在编译的时候,进行逃逸分析,分析synchronized锁对象是不是只可能被一个线程加锁,不存在其他线程来竞争加锁的情况,这时就可以消除该锁,提升执行效率

锁粗化

对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,将前后的锁合并为一个锁,避免频繁加锁释放锁

wait、notify

原理

  • Owner线程发现条件不满足,调用wait方法,即可进入WaitSet,变为WAITING状态
  • BLOCKED和WAITING的线程都处于阻塞状态,不占用cpu时间片
  • BLOCKED线程会在Owner线程释放锁时唤醒
  • WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争

WAITING和BLOCKED区别:

  • WAITING:获得锁但需要等待某个条件发生来唤醒
  • BLOCKED:阻塞,没有获得锁

API介绍

  • obj.wait() 让进入 object 监视器的线程到 WaitSet 等待
  • obj.notify() 在 object 上正在 WaitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 WaitSet 等待的线程全部唤醒

它们都是线程之间进行协作的手段,都属于 Object 对象的方法,必须获得此对象的锁,才能调用这几个方法

import lombok.extern.slf4j.Slf4j;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    final static Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t2").start();

        // 主线程两秒后执行
        sleep(0.5);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
            obj.notify(); // 唤醒obj上一个线程
//            obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
}

notify:

notifyAll:

wait()方法会释放对象的锁,进入WaitSet等待区,从而让其他线程有机会获取对象的锁,无限制等待,直到notify为止

wait(long n):有时限的等待,到n毫秒后结束等待,或是被notify

sleep(long n)与wait(long n)的区别:

  • sleep是Thread方法,而wait是Object的方法
  • sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用
  • sleep在睡眠的同时不会释放对象锁,但wait在等待时会释放对象锁

它们的状态都是TIMED_WAITING

同步模式之保护性暂停

即 Guarded Suspension,用在一个线程等待另一个线程的执行结果

要点:

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程,那么可以使用消息队列(见生产者/消费者)
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式
  • 因为要等待另一方的结果,因此归类到同步模式

实现:

设置超时时间:

异步模式之生产者/消费者

要点:

  • 与前面的保护性暂停中的GuardObject不同,不需要产生结果和消费结果的线程一一对应
  • 消费队列可以用来平衡生产和消费的线程资源
  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据
  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
  • JDK中各种阻塞队列采用的就是这种模式

实现:

import lombok.extern.slf4j.Slf4j;

import java.util.LinkedList;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.Test21")
public class Test21 {

    public static void main(String[] args) {
        MessageQueue queue = new MessageQueue(2);

        for (int i = 0; i < 3; i++) {
            int id = i;
            new Thread(() -> {
                queue.put(new Message(id , "值"+id));
            }, "生产者" + i).start();
        }

        new Thread(() -> {
            while(true) {
                sleep(1);
                Message message = queue.take();
            }
        }, "消费者").start();
    }

}

// 消息队列类 , java 线程之间通信
@Slf4j(topic = "c.MessageQueue")
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.debug("队列为空, 消费者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 从队列头部获取消息并返回
            Message message = list.removeFirst();
            log.debug("已消费消息 {}", message);
            list.notifyAll();
            return message;
        }
    }

    // 存入消息
    public void put(Message message) {
        synchronized (list) {
            // 检查对象是否已满
            while(list.size() == capcity) {
                try {
                    log.debug("队列已满, 生产者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 将消息加入队列尾部
            list.addLast(message);
            log.debug("已生产消息 {}", message);
            list.notifyAll();
        }
    }
}

final 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 +
                '}';
    }
}

Park & Unpark

基本使用

它们是LockSupport类中的方法

// 暂停当前线程
LockSupport.park(); 
 
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

先park再unpark:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.TestParkUnpark")
public class TestParkUnpark {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            sleep(1);
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        }, "t1");
        t1.start();

        sleep(2);
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
}

先unpark再park:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.TestParkUnpark")
public class TestParkUnpark {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("start...");
            sleep(2);
            log.debug("park...");
            LockSupport.park();
            log.debug("resume...");
        }, "t1");
        t1.start();

        sleep(1);
        log.debug("unpark...");
        LockSupport.unpark(t1);
    }
}

特点

与Object的wait & notify相比:

  • wait,notify和notifyAll必须配合Object Monitor一起使用,而park,unpark不必
  • park & unpark是以线程为单位来阻塞和唤醒线程,而notify只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么精确
  • park & unpark可以先unpark,而wait & notify不能先notify

原理

每个线程都有自己的一个 Parker 对象,由三部分组成 _counter , _cond 和 _mutex

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

1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1

2. 当前线程调用 Unsafe.park() 方法

3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行

4. 设置 _counter 为 0

线程状态转换

假设有线程Thread t

情况:1:NEW --> RUNNABLE

当调用t.start()方法时,由NEW --> RUNNABLE

情况2:RUNNABLE --> WAITING

t线程用synchronized(obj)获取了对象锁后:

调用obj.wait()方法时,t线程从RUNNABLE --> WAITING

调用obj.notify() , obj.notifyAll() , t.interrupt()时

  • 竞争锁成功,t线程从WAITING --> RUNNABLE
  • 竞争锁失败,t线程从WAITING --> BLOCKED
import lombok.extern.slf4j.Slf4j;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
    final static Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t1").start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        },"t2").start();

        // 主线程两秒后执行
        sleep(0.5);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
            obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }
}

 

情况3:RUNNABLE --> WAITING

当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING【注意是当前线程在t线程对象的监视器上等待】

t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

情况4:RUNNABLE --> WAITING

当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING

调用 LockSupport.unpark(目标线程) 或调用了线程的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE

情况5:RUNNABLE --> TIMED_WAITING

t 线程用 synchronized(obj) 获取了对象锁后

调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING

t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时

  • 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
  • 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

情况6:RUNNABLE --> TIMED_WAITING

当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING【注意是当前线程在t 线程对象的监视器上等待】

当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE

情况7:RUNNABLE --> TIMED_WAITING

当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING

当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

情况8:RUNNABLE --> TIMED_WAITING

当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING

调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE

情况9:RUNNABLE --> BLOCKED

t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED

持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

情况10:RUNNABLE --> TERMINATED

当前线程所有代码运行完毕,进入 TERMINATED

多把锁

一间大屋子有两个功能:睡觉、学习,互不相干, 现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,并发度很低

解决方法是准备多个房间(多个对象锁)

将锁的粒度细分:

  • 好处:可以增强并发度
  • 坏处:如果一个线程需要同时获得多把锁,就容易发生死锁

结果:

改进:

结果:

活跃性

死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

相互等待对方释放临界资源

t1 线程 获得 A对象锁,接下来想获取 B对象 的锁

t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁

import lombok.extern.slf4j.Slf4j;

import static cn.itcast.n2.util.Sleeper.sleep;

@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("lock A");
                sleep(1);
                synchronized (B) {
                    log.debug("lock B");
                    log.debug("操作...");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                log.debug("lock B");
                sleep(0.5);
                synchronized (A) {
                    log.debug("lock A");
                    log.debug("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

 

定位死锁

检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:

避免死锁要注意加锁顺序

如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id来定位是哪个线程,最后再用 jstack 排查

哲学家就餐问题

有五位哲学家,围坐在圆桌旁

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子
  • 如果筷子被身边的人拿着,自己就得等待

import cn.itcast.n2.util.Sleeper;
import lombok.extern.slf4j.Slf4j;

import java.util.Random;

public class TestDeadLock {
    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();
    }
}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
    Chopstick left;
    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();
                }
            }
        }
    }

    Random random = new Random();

    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

class Chopstick {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

 

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束

import lombok.extern.slf4j.Slf4j;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            // 期望减到 0 退出循环
            while (count > 0) {
                sleep(0.2);
                count--;
                log.debug("count: {}", count);
            }
        }, "t1").start();
        
        new Thread(() -> {
            // 期望超过 20 退出循环
            while (count < 20) {
                sleep(0.2);
                count++;
                log.debug("count: {}", count);
            }
        }, "t2").start();
    }
}

饥饿

饥饿:一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束

顺序加锁解决:

ReentrantLock(可重入锁)

特点

相对于synchronized,它具有如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

与synchronized一样,都支持可重入

基本语法

// 获取锁
reentrantLock.lock();
try {
 // 临界区
} finally {
 // 释放锁
 reentrantLock.unlock();
}

可重入

可重入是指同一个线程如果首次获得了这把锁,因为它是这把锁的拥有者,因此有权利再次获取这把锁

如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

可打断

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.Test22")
public class Test22_1 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                //如果没有竞争,那么此方法就会获取lock对象锁
                //如果有竞争,就会进入阻塞队列,可以被其他线程用interrupt方法打断
                log.debug("尝试获得锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("没有获得锁,返回");
                return;
            }
            try {
                log.debug("获取到锁");
            }finally {
                lock.unlock();
            }
        }, "t1");


        lock.lock();
        t1.start();

        sleep(1);
        log.debug("打断t1");
        t1.interrupt();
    }
}

锁超时

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.Test22")
public class Test22_2 {
    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("尝试获得锁");
            try {
                if(!lock.tryLock(2,TimeUnit.SECONDS)){
                    log.debug("获取不到锁");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("获取不到锁");
                return;
            }
            try {
                log.debug("获得到锁");
            }finally {
                lock.unlock();
            }
        },"t1");

        lock.lock();
        log.debug("获得到锁");
        t1.start();
        sleep(1);
        log.debug("释放了锁");
        lock.unlock();
    }

}

使用 tryLock 解决哲学家就餐问题:

import cn.itcast.n2.util.Sleeper;
import lombok.extern.slf4j.Slf4j;

import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j(topic = "c.Test23")
public class Test23 {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();
}
}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick 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(); // 右手筷子获取失败,释放自己手里的左手筷子
                }
            }
        }
    }

    Random random = new Random();

    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

class Chopstick extends ReentrantLock {
    String name;

    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

公平锁

ReentrantLock 默认是不公平的

强行插入,有机会在中间输出

改为公平锁后:

强行插入,总是在最后输出

公平锁一般没有必要,会降低并发度

条件变量

synchronized 中也有条件变量,就是waitSet休息室,当条件不满足时进入 waitSet 等待

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室,唤醒时也是按休息室来唤醒

使用流程:

  1. await前需要获得锁
  2. await执行后,会释放锁,进入conditionObject等待
  3. await的线程被唤醒(或打断、或超时),重新竞争lock锁
  4. 竞争lock锁成功后,从await后继续执行
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import static cn.itcast.n2.util.Sleeper.sleep;

@Slf4j(topic = "c.Test24")
public class Test24 {
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;
    static ReentrantLock ROOM = new ReentrantLock();
    // 等待烟的休息室
    static Condition waitCigaretteSet = ROOM.newCondition();
    // 等外卖的休息室
    static Condition waitTakeoutSet = ROOM.newCondition();

    public static void main(String[] args) {


        new Thread(() -> {
            ROOM.lock();
            try {
                log.debug("有烟没?[{}]", hasCigarette);
                while (!hasCigarette) {
                    log.debug("没烟,先歇会!");
                    try {
                        waitCigaretteSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            } finally {
                ROOM.unlock();
            }
        }, "小南").start();

        new Thread(() -> {
            ROOM.lock();
            try {
                log.debug("外卖送到没?[{}]", hasTakeout);
                while (!hasTakeout) {
                    log.debug("没外卖,先歇会!");
                    try {
                        waitTakeoutSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("可以开始干活了");
            } finally {
                ROOM.unlock();
            }
        }, "小女").start();

        sleep(1);
        new Thread(() -> {
            ROOM.lock();
            try {
                hasTakeout = true;
                waitTakeoutSet.signal();
            } finally {
                ROOM.unlock();
            }
        }, "送外卖的").start();

        sleep(1);

        new Thread(() -> {
            ROOM.lock();
            try {
                hasCigarette = true;
                waitCigaretteSet.signal();
            } finally {
                ROOM.unlock();
            }
        }, "送烟的").start();
    }

}

同步模式之顺序控制

固定运行顺序

必须先 2 后 1 打印

wait、notify版

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test25")
public class Test25 {
    static final Object lock = new Object();
    // 表示 t2 是否运行过
    static boolean t2runned = false;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                while (!t2runned) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("1");
            }
        }, "t1");


        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                log.debug("2");
                t2runned = true;
                lock.notify();
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}

park、unpark版

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

@Slf4j(topic = "c.Test26")
public class Test26 {
    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            LockSupport.park();
            log.debug("1");
        }, "t1");
        t1.start();

        new Thread(() -> {
            log.debug("2");
            LockSupport.unpark(t1);
        },"t2").start();
    }
}

交替输出

线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次,现在要求输出 abcabcabcabcabc 怎么实现

wait、notify版

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test27")
public class Test27 {
    public static void main(String[] args) {
        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             1
 */
class WaitNotify {
    // 等待标记 1:a执行  2:b执行  3:c执行
    private int flag;
    // 循环次数
    private int loopNumber;

    public WaitNotify(int flag, int loopNumber) {
        this.flag = flag;
        this.loopNumber = loopNumber;
    }

    // 打印                  a           1             2
    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();
            }
        }
    }

}

 

await、signal版

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Test30 {
    public static void main(String[] args) throws InterruptedException {
        AwaitSignal awaitSignal = new AwaitSignal(5);
        Condition a = awaitSignal.newCondition();
        Condition b = awaitSignal.newCondition();
        Condition c = awaitSignal.newCondition();

        new Thread(() -> {
            awaitSignal.print("a", a, b);
        }).start();
        new Thread(() -> {
            awaitSignal.print("b", b, c);
        }).start();
        new Thread(() -> {
            awaitSignal.print("c", c, a);
        }).start();

        Thread.sleep(1000);
        awaitSignal.lock();
        try {
            System.out.println("开始...");
            a.signal();
        } finally {
            awaitSignal.unlock();
        }

    }
}

class AwaitSignal extends ReentrantLock{
    private int loopNumber;

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }
    
    //参数1:打印内容,参数2:进入哪一间休息室,参数3:下一间休息室
    public void print(String str, Condition current, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            lock();
            try {
                current.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                unlock();
            }
        }
    }
}

 

注意:该实现没有考虑 a 、b、c 线程都就绪再开始 

park、unpark版

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

@Slf4j(topic = "c.Test31")
public class Test31 {

    static Thread t1;
    static Thread t2;
    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);
        });

        t1.start();
        t2.start();
        t3.start();

        LockSupport.unpark(t1);
    }
}

class ParkUnpark {
    private int loopNumber;

    public ParkUnpark(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void print(String str, Thread next) {
        for (int i = 0; i < loopNumber; i++) {
            LockSupport.park();
            System.out.print(str);
            LockSupport.unpark(next);
        }
    }
}

标签:synchronized,Thread,管程,JUC3,线程,debug,new,共享,public
From: https://blog.csdn.net/m0_73902080/article/details/141228284

相关文章

  • 基于ssm角色访问控制的文件共享系统-附源码161821
    摘要近些年,一些较大的企业经常自行开发科技角色访问控制的文件共享系统,这样做有一定的好处,如成本低廉,上级单位一次开发,基层单位可以重复利用,而且一定程度上能满足个性化需求。但是实践证明,这种开放方式一般难以成功。一方面企业内部开发人员往往生而缺乏项目经验,导......
  • 静态库与共享库详解
    静态库与共享库详解在开发和使用C语言编写程序时,库文件(Library)是一个重要的组成部分。库文件是目标文件的集合,可以被其他代码调用。将代码封装编译成库文件有助于简化使用、便于管理,并提高安全性和保密性。本文将详细介绍静态库和共享库(动态库),并演示如何创建和使用它们。......
  • Spring Boot中的跨域资源共享(CORS)处理
    SpringBoot中的跨域资源共享(CORS)处理大家好,我是微赚淘客返利系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!在Web应用开发中,跨域资源共享(CORS)是一个常见的问题。当一个Web应用需要与另一个域下的Web服务进行交互时,浏览器出于安全考虑,会默认阻止这种跨域请求。SpringBoot......
  • 赋能基层,融合创新:EasyCVR视频汇聚平台构建平安城市视频共享与智能分析平台
    一、雪亮工程建设的意义雪亮工程的核心在于通过高清视频监控、环境监测和智能预警等先进技术手段,构建一个高效、智能、安全、便捷的社会安全防控体系。这一工程的建设不仅代表了现代化科技手段在城市治安管理中的应用,更是提升社会安全保障能力、推动社会和谐发展的重要举措。雪......
  • java语言,MySQL数据库;基于Web的高校知识共享系统设计与实现 32050(免费领源码)计算机毕业
    摘 要信息化社会内需要与之针对性的信息获取途径,但是途径的扩展基本上为人们所努力的方向,由于站在的角度存在偏差,人们经常能够获得不同类型信息,这也是技术最为难以攻克的课题。针对高校知识共享系统等问题,对高校知识共享系统进行研究分析,然后开发设计出高校知识共享系统以......
  • [开题报告]FLASK框架校师生闲置物品共享系统f53dy(源码+论文)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景在倡导绿色生活、循环经济的今天,校园内师生间的闲置物品处理成为了一个亟待解决的问题。大量尚具使用价值的书籍、学习资料、生活用品等因......
  • 基于flask+vue框架的某高校学生学习笔记共享平台的设计与实现[开题+论文+程序]-计算机
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容研究背景在信息化高速发展的今天,高等教育领域正经历着前所未有的变革。随着知识量的急剧增长和学习方式的多样化,学生们面临着如何高效管理和利用学......
  • c# 多线程环境下对共享资源访问的办法
    Monitor:定义:Monitor 是C#中最基本的同步机制,通过 Enter 和 Exit 方法来控制对共享资源的访问。它提供了排他锁的功能,确保在任何时刻只有一个线程可以访问共享资源。优点:简单易用,适合对临界区进行粗粒度的同步控制。缺点:只能实现排它锁,不能实现读写锁,性能相对较低。......
  • windows核心编程 第三章,跨越进程边界共享内核对象,对象句柄的继承性,改变句柄的标志,命名
    windows核心编程3.3跨越进程边界共享内核对象3.3.1对象句柄的继承性3.3.2改变句柄的标志3.3.3命名对象3.3.4终端服务器的名字空间3.3.5复制对象句柄文章目录windows核心编程3.3跨越进程边界共享内核对象3.3.1对象句柄的继承性3.3.2改变句柄的标志3.3.3命名......
  • VMware ubuntu虚拟机与主机共享文件夹
    1安装VMware-tools更新最新VMware-tools组件编辑->首选项->更新->立即下载所有组件2安装VMware-tools虚拟机->安装VMware-tools安装后ubuntu会加载一个虚拟光驱,解压文件,在解压的文件目录下执行以下命令进行安装sudovmware-install.pl安装过程中输入“yes”即可。在......