Java 并发学习笔记(二)

简介: 请参看前一篇文章:Java 并发学习笔记(一)——原子性、可见性、有序性问题

请参看前一篇文章:Java 并发学习笔记(一)——原子性、可见性、有序性问题


六、等待—通知机制

什么是等待通知—机制?当线程不满足某个条件,则进入等待状态;如果线程满足要求的某个条件后,则通知等待的线程重新执行。

等待通知机制的流程一般是这样的:线程首先获取互斥锁,当不满足某个条件的时候,释放互斥锁,并进入这个条件的等待队列;一直等到满足了这个条件之后,通知等待的线程,并且需要重新获取互斥锁。

1. 等待-通知机制的简单实现

等待-通知机制可以使用 Java 的 synchronized 关键字,配合 wait()、notify()、notifyAll() 这个三个方法来实现。

前面说到的解决死锁问题的那个例子,一次性申请所有的资源,使用的是循环等待,这在并发量很大的时候比较消耗 CPU 资源。

现在使用等待-通知机制进行优化:

final class Monitor {
    private List<Object> res = new ArrayList<>(2);
    /**
     * 一次性申请资源 
     */
    public synchronized void apply(Object resource1, Object resource2) {
        while (res.contains(resource1) || res.contains(resource2)){
            try {
                //条件不满足则进入等待队列
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        res.add(resource1);
        res.add(resource2);
    }
    /** 
     * 归还资源 
     */
    public synchronized void free(Object resource1, Object resource2){
        res.remove(resource1);
        res.remove(resource2);
        //释放资源之后,通知等待的线程开始执行
        this.notifyAll();
    }
}

2. 需要注意的地方

1) 每个互斥锁都有相应的等待队列,例如上面的例子,就存在两个等待队列,一是 synchronized 入口等待队列,二是 while 循环这个条件的等待队列。

2) 调用 wait() 方法,会使当前线程释放持有的锁,并进入这个条件的等待队列。满足条件之后,队列中的线程被唤醒,不是马上执行,而是需要重新获取互斥锁。例如上图中,if 条件的队列中的线程被唤醒后,需要重新进入 synchronized 处获取互斥锁。

3. wait 和 sleep 的区别

相同点:两个方法都会让渡 CPU 的使用权,等待再次被调度。

不同点:

  • wait 属于 Object 的方法,sleep 是 Thread 的方法
  • wait 只能在同步方法或同步块中调用,sleep 可以在任何地方调用
  • wait 会释放线程持有的锁,sleep 不会释放锁资源


七、管程理论

1. 什么是管程?

指的是对共享变量和对共享变量的操作的管理,使其支持并发,对应到 Java,指的是管理类的成员变量和方法,让这个类是线程安全的。

2. 管程模型

管程主要的模型有 Hasen、Hoare、MESA ,其中 MESA 最常用。管程的 MESA 模型主要解决的是线程的互斥和同步问题,和上面说到的等待-通知机制十分类似。示意图如下:

首先看看管程是如何实现互斥的?在管程的入口有一个等待队列,一次只允许一个线程进入管程。每个条件对应一个等待队列,当线程不满足条件的时候,进入对应的等待队列;当条件满足的时候,队列中的线程被唤醒,重新进入到入口处的等待队列获取互斥锁,这就实现了线程的同步问题。

3. 管程的最佳实践

接下来使用代码实现了一个简单的阻塞队列,这就是一个很典型的管程模型,解决了线程互斥和同步问题。

public class BlockingQueue<T> {
    private int capacity;
    private int size;
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    /**
     * 入队列
     */
    public void enqueue(T data){
        lock.lock();
        try {
            //如果队列满了,需要等待,直到队列不满
            while (size >= capacity){
                notFull.await();
            }
            //入队代码,省略
            //入队之后,通知队列已经不为空了
            notEmpty.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    /**
     * 出队列
     */
    public T dequeue(){
        lock.lock();
        try {
            //如果队列为空,需要等待,直到队列不为空
            while (size <= 0){
                notEmpty.await();
            }
            //出队代码,省略
            //出队列之后,通知队列已经不满了
            notFull.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        //实际应该返回出队数据
        return null;
    }
}

八、Java 中的线程


1. 线程的生命周期

Java 中的线程共分为了 6 种状态,分别是:

  • NEW(初始化状态)
  • RUNNABLE(可运行/运行状态)
  • BLOCKED(阻塞状态)
  • WAITING(无限时等待)
  • TIMED_WAITING(限时等待)
  • TERMINATED(终止状态)

2. 线程状态转换

  • RUNNABLE 与 BLOCKED 状态的转换:在线程等待 synchronized 的锁时,会进入 BLOCKED 状态,当获取到锁之后,又转换到 RUNNABLE 状态。
  • RUNNABLE 与 WAITING 状态的转换:1) 线程获取到 synchronized 锁之后,并且调用了 wait() 方法。 2) 调用 Thread.join() 方法,例如线程 A 调用 join() 方法,线程 B 等待 A 执行完毕,等待期间 B 进入 WAITING 状态,线程 A 执行完后,线程 B 切换到 RUNNABLE 状态。3) 调用 LockSupport.park() 方法
  • RUNNABLE 与 TIMED_WAITING 状态的转换:以上三种情况,分别在方法中加上超时参数即可。另外还有两种情况:Thread.sleep(long millis) 方法,LockSupprt.parkNanos(Object blocker, long deadline)。
  • NEW 到 RUNNABLE 状态的转换:在 Java 中新创建的线程,会立即进入 NEW 状态,然后启动线程进入 RUNNABLE 状态。Java 中新建线程一般有三种方式:
  • 继承 Thread 类
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("I am roseduan");
    }
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
• 实现 Runnable 接口,并将其实现类传给 Thread 作为参数
public class MyThread {
    public static void main(String[] args) {
        Thread thread = new Thread(new Print());
        thread.start();
    }
}
class Print implements Runnable{
    @Override
    public void run() {
        System.out.println("I am roseduan");
    }
}
• 实现 Collable 接口,将其实现类传给线程池执行,并且可以获取返回结果
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        //线程池
        BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(5);
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 1,
                TimeUnit.HOURS, queue);
        //执行
        Future<?> submit = threadPool.submit(new Demo());
    }
}
class Demo implements Callable<String> {
    @Override
    public String call() {
        System.out.println("I am roseduan");
        return "I am roseduan";
    }
}
  • NEW 到 TERMINATED 状态的转换:线程执行完 run() 方法后,会自动切换到 TERMINATED 状态。如果手动中止线程,可以使用 interrupt() 方法。

3. 局部变量的线程安全性

局部变量存在于方法中,每个方法都有对应的调用栈帧,由于每个线程都有自己独立的方法调用栈,因此局部变量并没有被共享。所以即便多个线程同时调用同一个方法,方法内部的局部变量也是线程安全的,不需要单独加锁。

相关文章
|
3月前
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
76 2
|
3月前
|
存储 Java
Java学习笔记 List集合的定义、集合的遍历、迭代器的使用
Java学习笔记 List集合的定义、集合的遍历、迭代器的使用
|
11天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
16天前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
34 2
|
1月前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
26 1
|
2月前
|
存储 安全 Java
Java修仙之路,十万字吐血整理全网最完整Java学习笔记(基础篇)
从Java环境的搭建到实际代码的编写,从基本用法的讲解到底层原理的剖析,深度解析Java基础知识。本文是《Java学习路线》专栏的起始文章,旨在提供一套完整的Java学习路线,覆盖Java基础知识、数据库、SSM/SpringBoot等框架、Redis/MQ等中间件、设计模式、架构设计、性能调优、源码解读、核心面试题等全面的知识点,并在未来不断更新和完善,帮助Java从业者在更短的时间内成长为高级开发。
Java修仙之路,十万字吐血整理全网最完整Java学习笔记(基础篇)
|
2月前
|
存储 安全 Java
Java修仙之路,十万字吐血整理全网最完整Java学习笔记(进阶篇)
本文是Java基础的进阶篇,对异常、集合、泛型、Java8新特性、I/O流等知识进行深入浅出的介绍,并附有对应的代码示例,重要的地方带有对性能、底层原理、源码的剖析。适合Java初学者。
Java修仙之路,十万字吐血整理全网最完整Java学习笔记(进阶篇)
|
2月前
|
Java API 容器
JAVA并发编程系列(10)Condition条件队列-并发协作者
本文通过一线大厂面试真题,模拟消费者-生产者的场景,通过简洁的代码演示,帮助读者快速理解并复用。文章还详细解释了Condition与Object.wait()、notify()的区别,并探讨了Condition的核心原理及其实现机制。
|
1月前
|
Java 数据安全/隐私保护
java学习笔记(基础习题)
java学习笔记(基础习题)
33 0
|
1月前
|
Java 程序员 开发工具
java学习笔记
java学习笔记
34 0