Java多线程学习笔记(三) 甚欢篇

简介: Java多线程学习笔记(三) 甚欢篇

wait 和 notify、notifyAll

Java多线程学习笔记(一) 初遇篇,我们已经介绍了多线程常用的API(废弃的接口不做介绍),但是我们还有两个比较重要的没有介绍,即wait(等待)、notify(通知),这是现实世界中比较常见的动作,比如你的女朋友说周末想跟你去看电影,然后你兴冲冲的去你女朋友家等她,然后你女朋友说让你等下(wait),她见心上人需要画个妆,画完妆之后,跟你说我画好了(notify),我们出门吧。 像下面这样:

public class RomaticDateThreadDemo implements Runnable {
    // 画妆标志位
    private  boolean flag;
    private static final String THREAD_GIRLFRIEND = "女朋友";
    public RomaticDateThreadDemo(boolean flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        synchronized (this) {
            // RomaticDateThreadDemo 的构造函数给的是false
            if (!flag) {
                flag = true; 
                String currentThreadName = Thread.currentThread().getName();
                if (THREAD_GIRLFRIEND.equals(currentThreadName)) {
                    // 输出这句话说明女朋友线程先进来
                    System.out.println("你死定了,敢让你女朋友等");
                } else {
                    // 假设男孩子线程先进来
                    System.out.println("...........女朋友正在化妆中.................,请等一会儿");
                    try {
                        //等待女朋友唤醒,wait代表释放对象锁(释放许可证)
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } else {
                String currentThreadName = Thread.currentThread().getName();
                if (THREAD_GIRLFRIEND.equals(currentThreadName)) {
                    // 走到这里,说明男孩子线程率先被线程调度器选中
                    System.out.println("..........要画十秒的妆.............");
                    try {
                        TimeUnit.SECONDS.sleep(5);
                        // 唤醒
                        this.notify();
                        System.out.println(currentThreadName+"说:我们走吧");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    // 走到这里,说明女朋友线程率先被线程调度器选中
                    System.out.println(currentThreadName+"说:我们走吧");
                    this.notify();
                }
            }
        }
    }
}
复制代码
public class WaitDemo {
    public static void main(String[] args) {
        RomaticDateThreadDemo romaticDateThreadDemo = new RomaticDateThreadDemo(false);
        Thread you = new Thread(romaticDateThreadDemo);
        you.setName("你");
        Thread girlFriend = new Thread(romaticDateThreadDemo);
        girlFriend.setName("女朋友");
        you.start();
        girlFriend.start();
    }
}
复制代码

但是notify、wait、notifyAll(唤醒所有处于等待中的线程)没有位于Thread类下,所有的类都具有这三个方法,那么请问Java的设计者是怎么做到呢?让所有的类都具备了这三个方法呢?当然是在Object类中做了,而且做成public,我们知道所有的类都继承自Object。由于wait方法在调用的时候是释放了当前线程持有的锁,那么我们可以大致得出一个结论,wait、notify、notifyAll只能配合synchronized使用,显式锁是通过调用unlock方法来实现释放锁的,而我们在Object看到,wait、notify、notifyAll是一个native(java和其他语言通信的一种手段,由于操作系统大多都采用C、C++编写而成,而一些文件的操作只能通过调用操作系统提供的接口完成,所以Java调用C、C++就通过native这种机制来实现调用操作系统的接口)方法。 那么为什么呢? 为什么要讲这三个原属于线程的方法放在所有类中呢?image.png那这就跟锁的位置有关系了,我们知道锁是放在对象中的,JVM(一般不加说明说的虚拟机都是HotSpot,Oracle发行的虚拟机)会为每个对象维护一个入口集(Entry Set)用于存储申请该对象内部锁的线程。除此之外,JVM还会为每一个对象维护一个等待集(Wait Set)的队列,该队列用于存储该对象上的等待线程。image.png首先synchronized是一个对象锁,从面向对象的角度来讲,获取锁是一种行为,释放锁也是一种行为,那么wait、notify、notifyAll放在Obejct中就是合理的,一切对象都可以是锁,所以锁放在Object中,所有的类都间接继承Object,这个设计合理。

那么现在我们假设我们就将wait、notify、notifyAll放入Thread类中,这个时候线程调用wait方法释放对应锁的时候,就要拿到对应的对象,由于一切对象都可以当锁来用,那么这个方法可能还需要一个泛型参数接收存储锁的对象(一个线程可能持有多把锁,假设如果调用者不说明要释放哪吧锁,JVM无从得知当前线程希望释放哪吧锁),用来释放锁。除此之外,notify方法的作用是唤醒相应对象上的等待线程,上面我们也提到JVM会为每一个对象维护一个入口集(Entry Set)用于存储申请该对象内部锁的线程,JVM还会为每一个对象维护一个等待集的队列,该对象用于存储该对象上的等待线程。如果这三个方法都放在Thread类内,那么这两个队列是直接跟每一个线程挂钩吗?但是线程可以持有的锁可不止一种,那有同学可能这里会讲,还是跟对象挂钩比较合理,这三个方法都接收一个对象,用于维护这两个队列和做释放锁操作,那如果是这样的话,你为什么不直接放入Object中。

使用wait、notify、notifyAll来实现生产者消费者模式

生产者消费者模式是web中比较常见的一种模式,比如消息队列,一方负责生产信息,一方负责消费信息,这是我们实现解耦的一种方式。本次我们做的是一个卖包子的案例,生产者负责卖包子,消费者负责卖包子。

public class GoodsStock {
    private int goodsNum;
    public GoodsStock(int goodsNum) {
        this.goodsNum = goodsNum;
    }
    public void produceGoods() throws InterruptedException {
        synchronized (this) {
            if (goodsNum < 100) {
                goodsNum++;
                System.out.println("正在生产第" + goodsNum + "个包子");
                // 休眠,防止执行的太快,影响我们分析问题
                TimeUnit.MILLISECONDS.sleep(50);
                // 生产之后,马上唤醒全部的线程
                notifyAll();
            } else {
                System.out.println("生产达到峰值,打工人可以休息");
                wait();
            }
        }
    }
    public void consumerGoods() throws InterruptedException {
        synchronized (this) {
            if (goodsNum == 100) {
                goodsNum--;
                System.out.println("正在消费第" + goodsNum + "个包子");
                // 休眠,防止执行的太快,影响我们分析问题
                TimeUnit.MILLISECONDS.sleep(50);
                // 消费一个,马上唤醒全部的线程
                notifyAll();
            }
            if (goodsNum == 0){
                System.out.println("还未开始生产,或已将包子全部售出");
                wait();
            }
        }
    }
}
复制代码
public class ProduceGoods implements Runnable {
    private GoodsStock goodsStock;
    public ProduceGoods(GoodsStock goodsStock) {
        this.goodsStock = goodsStock;
    }
    @Override
    public void run() {
        while (true) {
            try {
                goodsStock.produceGoods();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
复制代码
public class ConsumerGoods implements Runnable {
    private GoodsStock goodsStock;
    public ConsumerGoods(GoodsStock goodsStock) {
        this.goodsStock = goodsStock;
    }
    @Override
    public void run() {
        while (true) {
            try {
                goodsStock.consumerGoods();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
复制代码
public static void main(String[] args) {
        GoodsStock goodsStock = new GoodsStock(0);
        ProduceGoods produceGoods = new ProduceGoods(goodsStock);
        ConsumerGoods consumerGoods = new ConsumerGoods(goodsStock);
        Thread p1 = new Thread(produceGoods);
        Thread p2 = new Thread(produceGoods);
        Thread p3 = new Thread(consumerGoods);
        Thread p4 = new Thread(consumerGoods);
        p1.start();
        p2.start();
        p3.start();
        p4.start();
    }
复制代码

结果: image.png

wait、notify、notifyAll存在的问题

上面模拟的生产者消费者模式,在刚开始生产者会率先开始生产,即使消费者先被CPU选中,也会陷入等待状态,在生产完成100个包子之后,消费者开始消费包子。这个时候就是一种动态平衡了,因为按照我们的代码,在包子的数量达到100的时候,生产者唤醒所有的线程,于是我们大概就能看到一个动态平衡了,包子的数量总是维持在一百个。但是如果说我们希望的场景是消费者线程卖完了仅仅通知生产者线程呢?生产者生产够100个包子后仅唤醒消费者线程呢? 那么notify和notifyAll就不满足我们的要求了,notify仅仅唤醒任意一个处于对象锁下的线程,notifyAll通知所有。除此之外notify和notifyAll只能配合synchronzed使用,我们希望显式锁也能有类似的操作,实现通知和唤醒,这也就是condition类出现的原因。

那么Condition是如何做到的呢?

我们再来大致的看一下Lock和Condition的源码:image.pngimage.png每个Condition实例内部都维护了一个用于存储等待线程的队列:image.png那么生产者线程和消费者线程就放在了两个队列中, 就能够避免误唤醒的现象了。因为signalAll只唤醒所属的Condition实例上的等待线程。 这样说的可能有点抽象,我们这里举一个例子吧: 假设我们有两个Condition变量,一个叫con1,一个叫con2,执行con1.await()方法的线程,其生命周期的状态就变为等待,并被存入con1对应的等待队列中,con1.signal()会随机唤醒处于cond1等待队列中的任意一个线程。

condition版本的生产者消费者模式

基于上面的论述,我们就可以实现,生产者生产100个包子休息,消费者把包子卖完再通知生产者了。

public class GoodsStock {
    private int goodsNum;
    private static final ReentrantLock lock = new ReentrantLock();
    private final Condition produceCondition = lock.newCondition();
    private final Condition consumerCondition = lock.newCondition();
    public GoodsStock(int goodsNum) {
        this.goodsNum = goodsNum;
    }
    public void produceGoods() throws InterruptedException {
        lock.lock();
        if (goodsNum < 100) {
            goodsNum++;
            System.out.println("正在生产第" + goodsNum + "个包子");
            // 休眠,防止执行的太快,影响我们分析问题
            TimeUnit.MILLISECONDS.sleep(50);
        }
        if (goodsNum == 100) {
            System.out.println("生产达到峰值,打工人可以休息");
            // 通知所有的消费者线程
            consumerCondition.signalAll();
            produceCondition.await();
        }
        lock.unlock();
    }
    public void consumerGoods() throws InterruptedException {
        lock.lock();
        if (goodsNum > 0) {
            System.out.println("正在消费第" + goodsNum-- + "个包子");
            // 休眠,防止执行的太快,影响我们分析问题
            TimeUnit.MILLISECONDS.sleep(50);
            // 唤醒所有的生产者
        }
        if (goodsNum == 0) {
            System.out.println("还未开始生产,或已将包子全部售出");
             // 获取该produceCondition变量对应的等待线程队列的长度
            int waitQueueLength = lock.getWaitQueueLength(produceCondition);
            if (waitQueueLength > 0){
                produceCondition.signalAll();
            }
            // 消费者线程先暂停,放入消费者对应的队列中
            consumerCondition.await();
        }
        lock.unlock();
    }
}
复制代码

CountDownLatch简介

Thread.join()实现的是一个线程等待另外一个线程结束,有的时候一个线程可能只需要等待其他线程执行特定的操作结束即可,而不必等待这些线程完全执行完毕。我们可以用条件变量来实现,也可以用更加直接的工具类—JUC下的CountDownLatch。 那么问题来了,那么该线程是怎么知道其他线程特定操作结束的呢?这就需要一个计数器,CountDownLatch内部维护了一个表示未完成操作的计数器,线程调用CountDownLatch.countDown(),计数器减一,代表该线程已经执行了特定操作。 CountDownLatch.await()相当于一个未保护的方法,当计数器为0时,也就是其他线程都执行了特定操作时,调用该方法的线程才会恢复执行。 一个典型的应用场景是: 我们需要做一个统计,由五段SQL构成,我们需要取出这五段SQL的数据,在代码中形成前端对应的数据结构返回给前端,那这五段SQL每个都执行一秒,那么这个接口响应的时间就需要五秒,一种优化的思路就是启动五个线程去取数据,取完数据放在一个集合中,五个线程都取完数据就通知主线程处理数据,这样接口的相应时间就取决于哪段SQL的最长执行时间。其实这个我们也可以通过Thread.join方法来做,但是如果这五个线程取完数据还有别的动作呢? 但是处理数据的线程只需要知道数据取好了。

CyclicBarrier 栅栏 简介

有的时候多个线程需要相互等待对方执行到代码中的某个地方(集合点),这时这些线程才能往下执行,类似于你和你女朋友去看电影,你女朋友让你九点在楼下等她,然后只有你们两个都到齐了才会去电影院。又像寻找龙珠,七队人都去寻找龙珠,龙珠到了,才能许愿。

public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7);
        for(int i = 1;i <= 7; i++){
            int finalI = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 收集到第"+ finalI +"颗龙珠");
                try {
                    // 用于等待
                    cyclicBarrier.await();
                    System.out.println("****召唤神龙");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
复制代码

CyclicBarrier内部也维护了一个计数器,用于标识多个线程是否已经执行到了某个地方,通知这些线程往下执行。与CountDownLatch不同的在于,栅栏可以被使用多次,所有参与方处于唤醒状态,任何一个参与方再度调用CyclicBarrier.await()又会被暂停,直到最后一个参与方执行了CyclicBarrier.await()。

Semaphore 信号量与限购简介

一般到了节假日,风景区就会很火爆,但是一个风景区能够容纳的人就那么些,资源是给定的,但是有些景区格外紧俏,为了防黄牛,我们发明了限购。java.util.concurrent.Semaphore可以用来做流量控制,线程在访问资源之前必须申请许可证,如果当前许可证被申请完了,那么申请的线程会被暂停,许可证恢复了,这些被暂停的线程就会被唤醒。

线程池

线程池简介

前面几篇我们讲示例,都是直接new Thread()来一个一个的创建线程,那么在实际中,一般我们不建议显式的创建线程。相对于普通的对象,一个Thread的实例还占用了额外的存储空间,大约是1M。除此之外线程的销毁也有其开销。一个系统能够创建的线程总是受限于该系统所拥有的处理器数目。无论是CPU密集型还是I/O密集型:

  • CPU密集型

CPU密集型任务执行过程中消耗的资源就是CPU,一个典型的CPU密集型任务就是加密和解密,CPU的使用率会比较高。

  • IO密集型任务

执行过程中消耗的资源就是(I/O资源),比如文件读写,CPU的使用率不高。 我们的愿望是不那么频繁的创建线程,就像数据库连接池一样,一开始连接池里存放了若干连接,我们需要和数据库交互的时候从连接池重获取连接,提交我们的SQL即可。image.png线程池和数据库连接池有些不同,数据库连接池算是对象池内部维护了一定数量的对象,客户端代码需要一个对象时就向对象池借用一个对象,用完之后再将该对象返回给对象池,于是数据库连接池就可以先后被多个客户端线程所使用。线程池也是一个对象,我们并不是从线程池像数据库连接池一样借用线程,而是将我们希望让线程执行的任务提交给线程池,线程池将这些任务放在工作队列中,由线程池的线程将这些任务取走执行。因此,线程池可以被看做是一种基于生产者-消费者模式的一种服务,该服务内部维护的工作者线程相当于消费者线程,向线程池提交任务的线程相当于生产者线程。

image.png

如何创建线程池?

通过java.util.concurrent.ThreadPoolExecutor来创建线程池,ThreadPoolExecutor包含参数最多的一个构造函数如下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
复制代码

ThreadPoolExecutor的线程数量参数有三个:

  • 当前线程池大小(表示线程池中实际工作者线程的数量)
  • 最大线程池大小(表示线程池最大能够拥有多少工作者线程,maximumPoolSize)
  • 核心线程池大小(corePoolSize)(小于最大线程池,一般情况下当前线程池大小超过核心线程池时,新来的人任务会被存放于工作队列(workQueue)) 。 在刚开始的时候,线程池并不会先创建线程,客户端没提交一个任务,线程池就会启用有一个线程执行任务,随着提交的任务越来越多,在超过线程池核心数时,就会被放入阻塞队列中,那么阻塞队列也放不下了怎么办? 这也就是handler参数的作用,拒绝策略。线程池是通过threadFactory.newThread()方法来创建线程的。如果我们在创建线程池的时候没有指定线程工厂,那么ThreadPoolExecutor就会使用Executors.defaultThreadFactory()所返回的线程工厂。 RejectedExecutionHandler是一个接口,JDK有默认的实现:image.png在当前线程池的大小超过核心线程池的大小且阻塞队列中没有任务时,线程中的线程的空闲时间达到keepAliveTime所制定的时间时,线程池就会销毁掉这类不干活的线程,以达到节省资源的目的。

线程池常用的方法

ThreadPoolExecutor的prestartCoreThread方法可以让线程池在没有接到任何任务的情况下创建所有的核心线程,这样可以减少任务被线程池处理时所需的等待时间。

通数据库连接池不一样的是,数据库连接池伴随着web程序一直开启,随着我们web的程序关闭而关闭,曾经我也是这么理解线程池的。但是线程池和数据库连接池并不是相同的思路,线程池需要关闭(这个属于认知偏差,如果线程池作为一个局部变量那么该线程池是需要关闭的,就比如在方法中,每次调用方法产生的都是不同的线程池,不关闭的话系统性能损失太大,但是作为类的全局变量的时候,我们就可以不关闭线程池,让线程池驻留在那里,因为类的全局变量只加载一次。每次都创建线程池的话,大量请求进来,大量的创建线程可能会影响系统的正常使用),假设客户端提交的任务都执行完毕的话。毕竟线程的开销还是比较大的。我们使用线程池的初衷也是复用线程,一个线程执行任务完毕之后,还能接着执行任务,这就达到了复用的目的。那么问题来了,为什么线程池不设计成类似于数据库连接池这样的呢? 我们可以这么想,基本上对于一个服务端的程序基本上里面都需要执行SQL,都需要获取连接,但是并不是都需要线程池。我们上面已经强调过,线程相对于普通的对象更消耗资源,基于这种思想线程池设计了关闭的方法。我们再次强调一下线程池的价值,复用线程,通过new Thread创建出来的线程,执行完毕就结束了,等待GC回收其占用的资源。但是通过线程池创建的线程在任务执行完毕之后还可以去执行任务。 ThreadPoolExecutor.shutdown()/shutdownNow()方法可用来关闭线程池。使用shutdown关闭线程池的时候,已提交的任务会被继续执行,而新提交的任务会像线程池饱和那样被拒绝掉。即使ThreadPoolExecutor.shutdown()执行,线程池也不会马上销毁,因为线程池可能还有线程在执行任务。可通过awaitTermination(long timeout, TimeUnit unit)方法来等待线程池关闭结束,线程池关闭结束,该方法返回true。 ThreadPoolExecutor.submit(Runnable task)和execute(Runnable command)被用于提交任务,不同的是submit()返回线程的执行结果,execute不返回。

相关文章
|
6天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
36 6
|
19天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
14天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
14天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
38 3
|
15天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
20天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
74 6
|
19天前
|
监控 Java 数据库连接
Java线程管理:守护线程与用户线程的区分与应用
在Java多线程编程中,线程可以分为守护线程(Daemon Thread)和用户线程(User Thread)。这两种线程在行为和用途上有着明显的区别,了解它们的差异对于编写高效、稳定的并发程序至关重要。
26 2
|
19天前
|
监控 Java 开发者
Java线程管理:守护线程与本地线程的深入剖析
在Java编程语言中,线程是程序执行的最小单元,它们可以并行执行以提高程序的效率和响应性。Java提供了两种特殊的线程类型:守护线程和本地线程。本文将深入探讨这两种线程的区别,并探讨它们在实际开发中的应用。
25 1
|
20天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
54 1
|
7月前
|
存储 安全 Java
深入理解Java并发编程:线程安全与锁机制
【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。
下一篇
DataWorks