多线程
一、线程通信
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 {
//初始值0
public 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
{
@Override
public 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
{
@Override
public 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() {
@Override
public 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() {//线程池创建工厂类,没有提供的话,就是用线程池内部默认的创建线程的方式
@Override
public Thread newThread(Runnable r) {
return null;
}
},
//拒绝策略:达到阻塞队列数量就执行拒绝策略
new ThreadPoolExecutor.DiscardOldestPolicy()//拒绝策略
);
//使用线程池
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println("送外卖到北京");
}
});
}
外卖公司(线程池)可以接收很多的外卖任务(可以>4,Runnable任务类),如果员工(线程池中创建的线程)没有空闲(正在干
活、忙碌),订单就在商家哪里(线程池内部的一个属性,阻塞队列)。员工不停接订单,送外卖,如果没有订单,员工就等待,一直等到有订单再派送(执行Runnable对象的run方法)
jdk内部的几种线程池:
//线程池的正式员工为1
ExecutorService pool=Executors.newSingleThreadExecutor();
//正式员工的数量为4,没有临时工
ExecutorService pool=Executors.newFixedThreadPool(4);
//定时线程池,4个正式员工
ScheduledExecutorService pool2=Executors.newScheduledThreadPool(4);
//正式员工为0.临时工不限
ExecutorService pool3=Executors.newCachedThreadPool();
//延迟一秒执行任务,只执行一次
pool2.schedule(new Runnable() {
@Override
public void run() {
System.out.println("lalal");
}
},1,TimeUnit.SECONDS);
//每隔一秒执行一次
pool2.scheduleAtFixedRate(new Runnable() {
@Override
public 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() {
@Override
public 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() {
@Override
public void run() {
System.out.println("a");
}
});
pool.execute(new Runnable() {
@Override
public void run() {
System.out.println("b");
}
});
}
}
三、定时器
1、jdk的定时器类
定时器:就是在不影响当前线程的情况下,设置一个延迟时间,等到这个延迟时间一过就开始执行某个任务。
JDK中的Timer实现——基于PriorityBlockingQueue(优先级阻塞队列——堆)
原理: 通过调用timer.schedule(task,10s)方法将任务加入到优先级队列中,此时,工作线程被唤醒,检查时间是否到达,到了就执行;没到就等待任务的延迟时间;
优点:优先级队列可以保证工作线程取到的一定是最先应该执行的一个任务。
具体更新最先执行的任务过程见下图:
使用:
TimerTask task=new TimerTask() {
@Override
public 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;
}
@Override
public 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;
}
//比较
@Override
public 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() {
@Override
public 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();