多线程

  • 一、线程通信
    • 1、等待集
    • 2、wait()方法
    • 3、notify() / notifyAll()方法
    • 4、等待队列/同步队列
    • 5、生产者消费者模型
  • 二、线程池
    • 1、jdk中的线程池
    • 2、自己实现线程池
  • 三、定时器
    • 1、jdk的定时器类
    • 2、自己实现定时器
      • 版本一:简易版
      • 版本二:优先级队列版
    • 补充:jdk的时间操作(Date的使用)

一、线程通信

1、等待集

对于java中的每一个对象:

  • 1)对象上关联着一个monitor lock(监视器锁)
  • 2)对象上还关联着一个数据结构,即:等待集(Wait Set)———保存的所有调用过这个对象.wait()方法的线程。

线程通信这引入了对象的等待集,是三个非常重要的方法:

  • wait():让当前线程进入等待状态
  • notify():唤醒当前对象上的等待的单个线程
  • notifyAll():唤醒当前对象上的等待的所有线程

这三个方法都是Object类的方法。另外,我们需要知道一个数据结构——等待集(Wait Set).

2、wait()方法

使用注意:

wait()方法必须用在synchronized代码段里。 如果调用wait()时,没有持有适当的锁,会抛出异常,如下图所示:

当一个对象(对象o)调用了wait()方法(即:o.wait() ),会产生什么结果呢?即:

wait()方法的作用:

  • 当前线程进入等待状态(阻塞等待,放弃抢占CPU的资格),线程状态由RUNNABLE ----> WAITING;会阻塞在此行代码。

  • 当前线程放弃所持有的对象锁如果没有对对象加锁,直接使用wait方法释放锁就会抛异常,如果加锁的对象找不到或者不存在也是会抛异常的.

  • 唤醒同步代码块所在的线程(即:synchronized加锁的线程,因为调用wait()方法会释放对象锁),但不能唤醒被wait()方法阻塞的线程

  • 如何被唤醒:只能通过别的线程调用notify()或notifyAll()来唤醒

3、notify() / notifyAll()方法

使用注意:

notify() / notifyAll()方法也是使用在synchronized代码块里

wait()方法的作用:

1> 唤醒被wait()方法阻塞的线程

  • notify() :是随机唤醒一个被wait()方法阻塞的线程;
  • notifyAll()是唤醒一个被wait()方法阻塞的全部线程

2> 唤醒时间:在当前线程的synchronied代码块执行完唤醒被wait()方法阻塞的线程。(一定要注意:不是一调用notify() / notifyAll()方法就可以l立即唤醒其他线程

4、等待队列/同步队列

让线程等待一共有三个状态:阻塞(BLOCKED)、等待(WAITING)、超时等待(TIME_WAITING)

阻塞态(BLOCKED):

1)产生阻塞态(BLOCKED)的条件: 同步代码块(synchronized)所在线程竞争锁失败
2)同步队列: 支持多线程操作,并且是线程安全的;

  • 作用:

    • synchronized导致线程竞争对象锁失败的线程(运行态-->阻塞态)全部会被JVM放到同步队列里;
    • 竞争锁成功的线程释放锁之后,JVM会把竞争同一对象锁失败的线程全部唤醒,让他们再次竞争,竞争失败的就又放回同步队列,依次下去。。。
  • 效率: 比较低,因为需要在阻塞态和唤醒态之间来回切换

等待(WAITING)、超时等待(TIME_WAITING):

1)产生这两种状态的条件:

  • 等待(WAITING):wait()、join()
  • 超时等待(TIME_WAITING):wait()、join()、sleep()

2)等待队列:

  • 作用: 出现等待(WAITING)、超时等待(TIME_WAITING)这两种状态的线程都会被JVM放到等待队列。 只有满足一定条件才能被唤醒,并向下执行代码。

    • join():等待调用join()方法的线程执行完 才能从等待队列中唤醒当前线程,并让当前线程向下执行。
    • wait():等待其他线程通过notify()/notifyAll()方法调用才能从等待队列中唤醒。
    • sleep():到达休眠的时间才能从等待队列中唤醒。
  • 效率: 比较高,因为只用一个状态的转变,即等待/超时等待状态被唤醒转变为就绪态,不需要在不同状态之间来回切换.

5、生产者消费者模型

wait()/notify()/notifyAll()这三个方法最常见的应用就是生产者消费者模型;
所谓的生产者消费者模型就是开启几个线程作为生产者生产面包,再开启几个线程作为消费者消费面包。我们这里规定启动五个生产者生产面包,且每个生产者一次只能生产三个面包,可以生产20次;启动5个消费者线程消费面包,且每个消费者一次只能消费一个面包;并且规定库存只能大于0小于等于100.
代码:

public class BreadOperator {//初始值0public static volatile int SUM;public static void main(String[] args) {//启动五个生产者生产面包for (int i = 0; i <5 ; i++) {new Thread(new Producer(),"面包师傅"+i).start();}//启动5个消费者线程,消费面包for (int i = 0; i <5 ; i++) {new Thread(new Consumer(),"消费者"+i).start();}}//默认生产者:一次产生3个面包,每个师傅生产20次//内部类private static class Producer implements Runnable{@Overridepublic void run() {try {for (int i = 0; i < 20; i++) {synchronized (BreadOperator.class){//使用while,如果用if会出现问题//需要判断生产完以后库存是否大于100,也就是库存在97以上就不能生产了while(SUM+3>100){//需要释放对象锁,让其他线程进入同步代码块(可能是消费者也可能是其他生产者)//当前线程需要进入阻塞状态BreadOperator.class.wait();//释放对象锁并进入阻塞状态}SUM+=3;Thread.sleep(1000);BreadOperator.class.notifyAll();System.out.println(Thread.currentThread().getName()+"生产了,库存为:"+SUM);}Thread.sleep(100);}} catch (InterruptedException e) {e.printStackTrace();}}}//默认消费之,一次消费一个,消费者一致消费//内部类private static class Consumer implements Runnable{@Overridepublic void run() {try {while (true) {synchronized (BreadOperator.class) {while (SUM == 0)//库存为0不能消费{System.out.println("当前阻塞的消费者为:"+Thread.currentThread().getName());//阻塞当前线程不能消费BreadOperator.class.wait();}SUM--;Thread.sleep(1000);//生产完/消费完之后通知其他线程继续执行//notify()和notifyAll()都是通知调用wait方法被阻塞的线程//notify():随机唤醒一个被wait阻塞的线程;notifyAll():唤醒全部被wait阻塞的线程//是在synchronized 代码块结束之后,也就是释放锁之后才会通知(唤醒其他线程竞争对象锁)//等于说,synchronized 结束之后,wait()和synchronized 阻塞的线程都会被唤醒,所以会出现负数BreadOperator.class.notify();System.out.println(Thread.currentThread().getName()+"消费了一个面包 ,剩余库存为:"+SUM);}Thread.sleep(100);}} catch (InterruptedException e) {e.printStackTrace();}}}
}

运行结果:

我们从运行结果来分析:

执行过程:

  • 首先0号面包师傅生产了三个面包,然后释放了对象锁。
  • 消费者4竞争到对象锁消费1个面包,然后释放对象锁;依次是消费者3、消费者2进行消费,在消费者2消费完之后库存为0;
  • 然后此时消费者1又来进行消费,通过while条件中判断SUM==0;通过while循环体里边的wait()方法释放当前的对象锁,并被JVM放到等待队列中,并阻塞等待到此行代码;同理下面的消费者0也是如此。
  • 接着是生产者4、3…4进行生产,此时库存为21;此时生产者4代码行里边的notifyAll()方法唤醒了wait()方法阻塞的线程(消费者0和消费者1),并且来竞争对象锁,此时消费者0竞争到了对象锁开始依次执行。

假设我们把while条件都换成if,会不会造成问题呢?

先看一下运行结果:我们发现结果出现了大于100的,这是为啥呢?

主要是因为面包师傅1获取到锁之后首先通过 if(SUM+3>100)判断已经大于100了,此时被wait()方法阻塞;然后后面被唤醒的时候,库存已经是98,而面包师傅1因为是if()条件已经判断过了,所以此时不会判断直接生产面包,库存就成为了101.这就是问题的关键!!
如果是while线程被唤醒之后会再次判断,就不会出现这种问题!

二、线程池

什么是线程池呢?用一个最好的例子就是外卖员送外卖,比如美团外卖公司会招收很多外卖员,如果哪家餐厅接到一单外卖,美团就会派出一名外卖员去配送。
这里的美团外卖公司就相当于线程池,而每个外卖员就相当于线程池中的开辟好的每个线程,需要配送订单的时候直接拿出来使用,而外卖员配送订单就相当于线程池中的线程再执行自己线程的任务。
因此线程池的优点就是:减少频繁创建、销毁线程的过程。

1、jdk中的线程池

创建线程池:

ExecutorService pool=new ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler) {this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), handler);

具体参数:

  • corePoolSize:核心线程数(正式员工),即:创建好线程池,正式员工就开始送外卖

  • maximumPoolSize: 最大线程数(最多数量的员工:正式员工+临时工);如果是0,表示不忙就立刻解雇临时工;

    • maximumPoolSize-corePoolSize=临时线程数(临时工),正式员工忙不过来就会创建临时工。
  • keepAliveTime:时间数量(对临时工的时间限制)

  • TimeUnit unit:时间单位(时间数量+时间单位表示一定范围的时间)

  • workQueue: 阻塞队列:存放包裹的仓库(存放任务的数据结构)

  • handler: 拒绝策略 ,达到阻塞队列数量就执行拒绝策略。拒绝策略种类如下:

    • CallerRunsPolicy:谁(execute代码行所在的线程)让我(快递公司)送快递,不好意思,你自己去送
    • AbortPolicy:直接抛出异常RejectedExecutionException
    • DiscardPolicy:从阻塞队列丢弃最新的任务(队尾)
    • DiscardOldestPolicy:从阻塞队列丢弃最旧的任务(队首)

使用:

 pool.execute(new Runnable() {@Overridepublic void run() {System.out.println("送外卖到北京");}});

相当于从线程池中派出了一个线程让它执行run方法中的任务。
使用实例代码:

public class ThreadPoolExecutorTest {public static void main1(String[] args) {ExecutorService pool=new ThreadPoolExecutor(//线程池----外卖公司3,//核心线程数(正式员工):创建好线程池,正式员工就开始取快递5,//最大线程数(最大员工数”正式员工+临时员工)30,//时间数量,如果是0,表示不忙就立刻解雇临时工;30:表示临时工空闲时间等于30的话就解雇临时工TimeUnit.SECONDS,//时间单位(时间数量+时间单位表示一定范围的时间)//方式一:创建阻塞队列new ArrayBlockingQueue<>(100),//阻塞队列,存放包裹的仓库,也就是存放任务的数据结构//方式二:线程池创建工厂类new ThreadFactory() {//线程池创建工厂类,没有提供的话,就是用线程池内部默认的创建线程的方式@Overridepublic Thread newThread(Runnable r) {return null;}},//拒绝策略:达到阻塞队列数量就执行拒绝策略new ThreadPoolExecutor.DiscardOldestPolicy()//拒绝策略);//使用线程池pool.execute(new Runnable() {@Overridepublic void run() {System.out.println("送外卖到北京");}});}

外卖公司(线程池)可以接收很多的外卖任务(可以>4,Runnable任务类),如果员工(线程池中创建的线程)没有空闲(正在干
活、忙碌),订单就在商家哪里(线程池内部的一个属性,阻塞队列)。员工不停接订单,送外卖,如果没有订单,员工就等待,一直等到有订单再派送(执行Runnable对象的run方法)

jdk内部的几种线程池:

     //线程池的正式员工为1ExecutorService pool=Executors.newSingleThreadExecutor();//正式员工的数量为4,没有临时工ExecutorService pool=Executors.newFixedThreadPool(4);//定时线程池,4个正式员工ScheduledExecutorService pool2=Executors.newScheduledThreadPool(4);//正式员工为0.临时工不限ExecutorService pool3=Executors.newCachedThreadPool();//延迟一秒执行任务,只执行一次pool2.schedule(new Runnable() {@Overridepublic void run() {System.out.println("lalal");}},1,TimeUnit.SECONDS);//每隔一秒执行一次pool2.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {System.out.println("lalal");}},1,1,TimeUnit.SECONDS);

2、自己实现线程池

这里只将正式员工创建出来,不涉及临时工。主要实现的功能就是:创建正式员工,并可以获取创建好的线程去执行任务。
代码:

public class MyThreadPool {private MyBlockingQueue<Runnable> queque;public MyThreadPool(int size,int capicity){queque=new MyBlockingQueue<>(capicity);//创建正式员工for(int i=0;i<size;i++){new Thread(new Runnable() {@Overridepublic void run() {try {while (true)//正式员工一直执行{//从仓库中取包裹===》// 1.成功取出包裹,方法返回往下执行//2.仓库中取不出包裹  :1.其他员工在取,阻塞在 synchronized ()代码行 2.wait方法阻塞:仓库中没有包裹Runnable task=queque.take();//正式员工(当前线程也就是new Thread)来送快递==》当前线程通过实例方法调用来执行任务task.run();}} catch (InterruptedException e) {e.printStackTrace();}}}).start();}}//从线程池中取出一个线程让执行任务。public void execute(Runnable task){try {queque.put(task);} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) {MyThreadPool pool=new MyThreadPool(5,100);pool.execute(new Runnable() {@Overridepublic void run() {System.out.println("a");}});pool.execute(new Runnable() {@Overridepublic void run() {System.out.println("b");}});}
}

三、定时器

1、jdk的定时器类

定时器:就是在不影响当前线程的情况下,设置一个延迟时间,等到这个延迟时间一过就开始执行某个任务。

JDK中的Timer实现——基于PriorityBlockingQueue(优先级阻塞队列——堆)

原理: 通过调用timer.schedule(task,10s)方法将任务加入到优先级队列中,此时,工作线程被唤醒,检查时间是否到达,到了就执行;没到就等待任务的延迟时间;

优点:优先级队列可以保证工作线程取到的一定是最先应该执行的一个任务。
具体更新最先执行的任务过程见下图:

使用:

 TimerTask task=new TimerTask() {@Overridepublic void run() {System.out.println("起床");}};
new java.util.Timer().schedule(task,3000,1000);

2、自己实现定时器

版本一:简易版

原理: 接收到新任务时,就在主线程中创建一个子线程,在子线程中先等待一定的延迟时间,然后执行该任务。

缺点: 创建的子线程太多,效率比较低。

代码:

public class MyTimer2 {//task:需要执行的任务//delay:从当前时间延迟多少毫秒执行任务// period:间隔时间:<=0就忽略掉,>0需要每间隔给定时间,就执行任务public void schedule(Runnable task,long delay,long period){try {Thread.sleep(delay);new Thread(task).start();while (period>0){Thread.sleep(period);new Thread(task).start();}} catch (InterruptedException e) {e.printStackTrace();}}
}

版本二:优先级队列版

原理:模拟jdk原生的定时器。
代码:

class MyTimer {//优先级队列private BlockingQueue<MyTimerTask> queue = new PriorityBlockingQueue();//构造方法---》一次产生count个线程public MyTimer(int count){for(int i=0; i<count; i++) {//从阻塞队列取任务new Thread(new MyWorker(queue)).start();}}/*** 执行定时任务* @param task 需要执行的任务* @param delay 从当前时间延迟多少毫秒,执行任务* @param period 间隔时间:<=0就忽略掉,>0需要每间隔给定时间,就执行任务*/public void schedule(Runnable task, long delay, long period){try {queue.put(new MyTimerTask(task, System.currentTimeMillis()+delay, period));synchronized (queue){queue.notifyAll();}} catch (InterruptedException e) {e.printStackTrace();}}private static class MyWorker implements Runnable{private BlockingQueue<MyTimerTask> queue;public MyWorker(BlockingQueue<MyTimerTask> queue) {this.queue = queue;}@Overridepublic void run() {try {while (true) {//blockingQueue本身就是线程安全的,所以这里的方法调用不用放在同步代码块MyTimerTask task = queue.take();//对MyWorker类里边的优先级队列的对象进行加锁synchronized (queue) {long current = System.currentTimeMillis();//task任务下次执行的时间大于当前的时间,也就是还没到下次执行的时间//就等待,等待的时间就是两个时间差if (task.next > current) {//阻塞当前线程并释放当前对象的synchronized对象锁queue.wait(task.next-current);//对谁加锁,让谁等待 //并把该对象放回对象锁queue.put(task);} else {//满足条件让其直接运行task.task.run();//判断是否有间隔时间,如果间隔时间大于0,//将task任务的下次执行时间进行修改并放回队列里if (task.period > 0) {task.next = task.next + task.period;queue.put(task);}}}}} catch (InterruptedException e) {e.printStackTrace();}}}//存放的任务需要实现Comparable接口,才能进行比较private static class MyTimerTask implements Comparable<MyTimerTask>{//定时任务private Runnable task;//下次执行的时间private long next;private long period;public MyTimerTask(Runnable task, long next, long period){this.task = task;this.next = next;this.period = period;}//比较@Overridepublic int compareTo(MyTimerTask o) {//jdk long类型默认的比较方法,每次取的都是next最小的//也就是执行时间最小的return Long.compare(next, o.next);}}public static void main(String[] args) {new MyTimer(4).schedule(new Runnable() {@Overridepublic void run() {System.out.println("起床了");}}, 3000, 1000);}}

补充:jdk的时间操作(Date的使用)

无参构造方法:返回系统当前时间

Date date1=new Date();

有参构造方法:一格林尼治时间1970-1-1开始,经过的毫秒数所到达的时间

Date date2=new Date(999999999);

格式化日期

DateFormat df=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(df.format(date1));
System.out.println(df.format(date2));

System.currentTimeMillis():返回的是从1970-1-1开始到当前时间所经历的毫秒数

long current=System.currentTimeMillis();

多线程 4——线程通信、线程池、定时器相关推荐

  1. 线程通信机制之定时器队列

    线程通信机制之定时器队列 --每周杂谈 第008篇 作者:Tocy    时间:2012-06-01 关键词:定时器队列,ITC 定时器队列(Timer Queue)可以使用CreateTimerQu ...

  2. Python3进阶--Socket编程、多线程(创建方式、线程通信、线程锁、线程池)

    第一章 变量.常用循环体.代码结构.代码练习 第二章 列表.元组等数据结构.字符串驻留机制及字符串格式化操作 第三章 函数.面向对象.文件操作.深浅拷贝.模块.异常及捕获 第四章 项目打包.类和对象高 ...

  3. 【转】JAVA 并发性和多线程 -- 读感 (二 线程间通讯,共享内存的机制)

    原文地址:https://www.cnblogs.com/edenpans/p/6020113.html 参考文章:http://ifeve.com/java-concurrency-thread-d ...

  4. Python网络编程(线程通信、GIL、服务器模型)

    什么是进程.进程的概念? 进程的概念主要有两点: 第一,进程是一个实体.每一个进程都有它自己的地址空间, 一般情况下,包括文本区域(text region).数据区域(data region)和堆栈( ...

  5. PyQt之科学使用线程处理耗时任务以及线程通信方法

    目录 前言 PyQt线程科学用法 非科学用法样例 科学用法 线程类 线程通信 线程类在主界面实例化与使用 开启线程 补充(信号的方式实现线程双向通信): 线程类 线程实例化与开启线程挂在后台 发送信号 ...

  6. Linux下的C编程实战(开发平台搭建,文件系统编程,进程控制与进程通信编程,“线程”控制与“线程”通信编程,驱动程序设计,专家问答)

    Linux下的C编程实战(一) ――开发平台搭建 1.引言 Linux操作系统在服务器领域的应用和普及已经有较长的历史,这源于它的开源特点以及其超越Windows的安全性和稳定性.而近年来,Linux ...

  7. 递归锁、信号量、GIL锁、基于多线程的socket通信和进程池线程池

    递归锁.信号量.GIL锁.基于多线程的socket通信和进程池线程池 递归锁 死锁现象:是指两个或两个以上的进程和线程因抢夺计算机资源而产生的一种互相等待的现象 from threading impo ...

  8. Java:多线程(同步死锁、锁原子变量、线程通信、线程池)

    5,同步和死锁 5.1,线程同步 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象: 修饰一个方法,被修饰的方法称为同步方法,其作用 ...

  9. 多线程——线程实现、线程状态、线程同步、线程通信、线程池

    多线程 一.线程 1.普通方法调用和多线程 2.程序.进行.线程 二.线程创建 1.继承Thread类 2.实现Runable接口 3.实现Callable接口 4.静态代理模式 5.Lamda表达式 ...

最新文章

  1. iPhone浏览器性能测试
  2. 运行时间_一种简单、实用的测量程序运行时间的方法
  3. 搜索推荐炼丹笔记:单网络内部集成学习
  4. [转]在Winform(C#)中使用Flash控件
  5. python列表中随机两个_随机化两个列表并在python中维护顺序
  6. UIImagePickerController按钮的中文问题
  7. Linux shell 脚本中, $@ 和$# 分别是什么意思?
  8. 从《觉醒年代》看如何用Python来绘制可视化仪表盘
  9. 从Android转大前端半年,我的一些思考
  10. 【路径规划】基于matlab GUI改进的迪杰斯特拉算法路径规划【含Matlab源码 1031期】
  11. startActivity报错exposed beyond app through Intent.getData()
  12. winpe装双系统linux_自制WINPE+MAC安装U盘及双系统存储U盘(增加多系统安装)
  13. CVE-2020-7961 Liferay Portal 命令执行漏洞
  14. 联想IdeapadU410重装系统win10
  15. 基于jQuery的图片懒加载插件
  16. 二十、数据库的高可用是怎么实现的?
  17. 计算机CPU加,减,乘,除的原理
  18. html表头纵向,实现纵向表头的table
  19. 如何理解照片后期处理
  20. P2738 [USACO4.1]篱笆回路Fence Loops

热门文章

  1. Python实现-RRT-Rapidly-exploring Random Tree-快速搜索随机树
  2. python爬虫 爬取JD商城快消品的保质期
  3. 北美票房排行榜 实时_快手直播丨主播实时直播监测数据分享——思文22号美妆童装专场...
  4. 中美合资氟橡胶制造商晨光科慕生产线改造升级
  5. 安裝打印機或者掃描器驅動時,出現「unknown device」(不明裝置)的提示,怎麼辦?...
  6. 上海豪宅现排队买房 半夜12点还在签约
  7. FineBI 取日期的最大max、最小值min
  8. unity(VR方向)实习生面试
  9. 【转】Thunderbird on Ubuntu 12.04 – 调整邮件列表行间距
  10. 洒脱是人生的一种境界