线程的调度是无序的 , 随机的 . 但是 , 也有一定的需求场景 , 希望线程有序执行
join 是一种控制顺序的方式
wait 就是让某个线程先暂停下来等一等 , 发现条件不满足 , 就先阻塞等待
notify 就是把该线程唤醒 , 能够继续执行 , 其他线程构造了一个成熟的条件 , 就可以唤醒
wait 和 notify 都是 Object 的方法
wait 做的事情:
wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("wait 之前");
synchronized (object) {
object.wait();
}
System.out.println("wait 之后");
}
wait 结束等待的条件:
notify 也是要放到 synchronized 中使用的
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(() -> {
try {
System.out.println(" wait 开始");
synchronized (locker) {
locker.wait();
}
System.out.println("wait 结束");
}catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() -> {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
});
t2.start();
}
notify 也是要放到 synchronized 中使用的 .
必须要先执行 wait , 然后 notify , 此时才有效果 . 如果现在还没有 wait , 就 notify , 相当于 , 打空了 , 此时 wait 无法唤醒 , 代码不会出现其他异常
wait 和 sleep 的对比
单例模式 , 是一种经典的设计模式 (在校招阶段 , 主要考察两个模式 : 单例模式 , 工厂模式 )
单例 : 单个实例 , 一个程序中 , 某个类 , 只创建出一个实例 ( 一个对象 )
Java中实现单例模式有很多种写法 :这里只说两种
类加载的同时 , 创建实例
被 static 修饰 , 该属性是类的属性 , (类对象上 ) , JVM 中 , 每个类的类对象只有唯一一份 , 类对象里的这个成员自然也是唯一一份了
class Singleton {
// 唯一实例的本体
private static Singleton instance = new Singleton();
//获取到实例的方法
public static Singleton getInstance() {
return instance;
}
//禁止外部 new 实例
private Singleton() { }
}
此处 , 在类内部把 实例 创建好 , 同时禁止外部重新创建实例, 此时 , 就可以保证单例的特性了
类加载的时候不创建实例 , 第一次使用的时候才创建实例
//通过懒汉模式实现一个单例模式
class SingletonLazy {
//volatile 禁止指令重排序
volatile private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
// 这个条件 , 判定是否要加锁 , 如果对象已经有了 , 就不必加锁了 , 此时本身就是安全的
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() { }
}
理解双重 if 判定 / volatile:
小结: 单例模式 , 线程安全问题
阻塞队列 : 带有阻塞特性 也遵守 "先进先出"原则
public class ThreadDemo19 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue queue = new LinkedBlockingQueue<>();
// 阻塞队列核心方法, 主要有两个.
// 1. put 入队列
queue.put("hello1");
queue.put("hello2");
queue.put("hello3");
queue.put("hello4");
queue.put("hello5");
// 2. take 出队列
String result = null;
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
}
}
生产者消费者模式 就是通过 一个容器来解决生产者和消费者的强耦合问题
生产者和消费者彼此之间不直接通讯 , 而是通过阻塞队列来进行通讯 , 所以生产者生产完数据之后不用等待消费者处理 , 直接扔给阻塞队列 , 消费者不找生产者要数据 , 而是直接从阻塞队列里取
1.可以让上下游模块之间 , 进行更好的 “解耦合”
2.阻塞队列就相当于一个缓冲区 , 平衡了生产者和消费者的处理能力
标准库中的阻塞队列 :
BlockingQueue queue = new LinkedBlockingQueue<>();
//入队列
queue.put("abc");
//出队列 , 如果没有 put 直接 take , 就会阻塞
String elem = queue.take();
生产者消费者模型
public static void main(String[] args) {
BlockingDeque blockingDeque = new LinkedBlockingDeque<>();
Thread t1 = new Thread(() -> {
while (true) {
try {
int value = blockingDeque.take();
System.out.println("消费元素 :" + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
Thread t2 = new Thread(() -> {
int value = 0;
while (true) {
try {
System.out.println("生产元素:" + value);
blockingDeque.put(value);
value ++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
//上述代码中 , 让生产者 , 每隔 1s 生产一个元素
//让消费者则直接消费 , 不受限制
}
实现一个阻塞队列 , 需要三部:
class MyBlockingQueue {
private int[] items = new int[1000];
//约定 [head , tail ) 队列 的有效元素
volatile private int head = 0;
volatile private int tail = 0;
volatile private int size = 0;
//入队列
synchronized public void put(int elem) throws InterruptedException {
while (size == items.length) {
//队列满了 , 插入失败
//return;
this.wait();
}
//把新元素放到 tail 所在位置上
items[tail] = elem;
tail++;
//万一 tail 达到末尾 , 就需要让 tail 从头再来
if (tail == items.length) {
tail = 0;
}
size++;
this.notify();
}
//出队列
synchronized public Integer take() throws InterruptedException {
while(size == 0) {
//return null;
this.wait();
}
int value = items[head];
head++;
if (head == items.length) {
head = 0;
}
size--;
this.notify();
return value;
}
}
// 注意 上述两处代码的 wait 不可能同时阻塞 , 一个独立不可能即是空 又是满
public class ThreadDemo21 {
public static void main(String[] args) {
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue();
//消费者
Thread t1 = new Thread(() -> {
while (true) {
int value = 0;
try {
value = queue.take();
System.out.println("消费 :" + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//生产者
Thread t2 = new Thread(() -> {
int value = 0;
while (true) {
try {
System.out.println("生产 : " + value);
queue.put(value);
Thread.sleep(1000);
value++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
很有可能在别的代码里暗中 interrupt , 把 wait 给提前唤醒了 , 明明条件还没满足 (队列非空) ,但是 wait 唤醒之后就不继续往下走了 .
更稳妥的做法 , 是在 wait 唤醒之后 , 再判定一次条件
wait 之前 , 发现条件不满足 , 开始 wait ; 然后等到 wait 被唤醒了之后 , 在确认一下条件是不是满足 , 如果不满足 , 还可以继续 wait
解决: 将上述代码的 if 改成 while
设定一个时间 , 当时间到 , 就可以执行一个指定的代码
Timer timer = new Timer():
timer.schedule(new TimerTask () {
@Override
public void run() {
System.out.println("hello");
}
},3000);
定时器 , 内部管理的不仅仅是一个任务 , 可以管理很多个任务
虽然任务可能会有很多 , 他们触发的时间是不同的 , 只需要有一个/一组 工作线程, 每次都找到这个任务中 , 最先到达时间的任务 , 一个线程 , 先执行最早的任务 , 做完了之后再执行第二早的 ,时间到了就执行, 没到就等等
定时器的构成:
一个带优先级的阻塞队列
为什么要优先级
因为阻塞队列中的任务都有各自执行时刻(delay) . 最先执行的任务一定是delay 最小的 , 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来
队列中的每个元素是一个 Task 对象
Task 中带有一个时间属性 ,
同时又一个 woker 线程一直扫描队首元素 , 看队首元素是否需要执行
完整代码:
// 表示一个任务
class MyTask implements Comparable{
public Runnable runnable;
//为了方便后续判定 , 使用绝对的时间戳
public long time;
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
// 取当前时刻的时间戳 + delay ,作为该任务实际执行的时间戳
this.time = System.currentTimeMillis() + delay;
}
@Override
public int compareTo(MyTask o) {
//这样的写法意味着每次取出来的是时间最小的元素 ,
//
return (int)(this.time - o.time);
}
}
class MyTimer {
//创建一个锁对象
private Object locker = new Object();
//这个结构 , 带有优先级的阻塞队列 , 核心数据结构
private PriorityBlockingQueue queue = new PriorityBlockingQueue<>();
//此处的 delay 是一个形如 3000 这样子的数字 (多长时间之后 , 执行该任务)
public void schedule(Runnable runnable, long delay) {
// 根据参数 , 构造 MyTask , 插入队列即可
MyTask myTask = new MyTask(runnable, delay);
queue.put(myTask);
synchronized (locker) {
locker.notify();
}
}
//在这里构造线程, 负责执行具体任务
public MyTimer() {
Thread t = new Thread(() -> {
while (true) {
try {
//阻塞队列 , 只有阻塞的入队列和阻塞的出队列 , 没有阻塞的查看对首元素.
synchronized (locker) {
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if (myTask.time <= curTime) {
//时间到了 , 可以执行任务了
myTask.runnable.run();
}else {
//时间还没到
//把刚才取出来的任务 , 重新塞回队列中
queue.put(myTask);
locker.wait(myTask.time - curTime );
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
public class ThreadDemo23 {
public static void main(String[] args) {
// ms 级别的时间戳 , 当前时刻和基准时刻的 ms之差 , 基准时刻 : 1970 年 1 月 1 日 00:00:00
//System.out.println(System.currentTimeMillis());
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello2");
}
},2000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello1");
}
},1000);
System.out.println("hello0");
}
上述代码中 , 使用wait 等待而不是 sleep , wait方便随时提前唤醒 , wait 的参数是"超时时间" , 时间到达一定程度后 , 还没有 notify 就不等 , 如果时间还没到 , 就 notify 立即返回
线程池的最大好处就是 , 减少每次启动,销毁线程的损耗
从线程池取线程 , 是属于纯用户态操作 , 不涉及到和内核的交互
public static void main(String[] args) {
//创建一个10线程的线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("pool");
}
});
}
Executors 创建线程池的几种方式:
Executors 本质上是 ThreadPoolExecutor 类的封装
ThreadPoolExecutor 参数 :
corePoolSize : 核心线程数
maximumPoolSize : 最大线程数
如果当前任务比较多 , 线程池就会多创建一些"临时线程" , 如果当前任务较少 , 线程池就会把多出来的临时工程销毁掉
long keepAliveTime : “临时线程” 保持存活的时间
TimeUnit unit : 单位 s , 分钟 , ms
BlockingQueue workQueue : 线程池里要管理许多任务 , 这些任务也是通过阻塞队列来组织的 , submit 方法其实就是把任务放到该队列中(程序员可以手动指定给线程池一个队列 , 此时程序员就很方便地可以控制/获取队列中的信息了)
ThreadFactory threadFactory : 工厂模式 , 创建线程的辅助类
RejectedExecutionHandler handler : 线程池的拒绝策略 , 如果线程池满了 吗继续往里添加任务 , 如何进行拒绝
ThreadPoolExecutor.AbortPolicy | 如果满了,继续添加任务,添加操作直接抛出异常 |
---|---|
ThreadPoolExecutor.CallerRunsPolicy | 添加的线程自己负责执行这个任务 |
ThreadPoolExecutor.DiscardOldestPolicy | 丢弃最老的任务(指的是最先安排的任务) |
ThreadPoolExecutor.DiscardPolicy | 丢弃最新的任务 |
ThreadPoolExecutor 类的构造方法的参数 , 都要重点掌握 ,尤其是拒绝策略
class MyThreadPool {
//阻塞队列用来存放任务
private BlockingDeque queue = new LinkedBlockingDeque<>();
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
//此处实现一个固定线程数的线程池
public MyThreadPool(int n) {
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
try {
while (true) {
//此处需要让线程内部有个 while 循环 , 不停地取任务
Runnable runnable = null;
runnable = queue.take();
}
}catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
}
}
}
public class ThreadDemo25 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool = new MyThreadPool(10);
for (int i = 0; i < 1000; i++) {
//每次循环都是创建一个新的 number ,没有人修改 该number
int number = i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello" + number);
}
});
}
Thread.sleep(3000);
}
}
当前代码中 , 有十个线程的线程池 , 实际开发中 , 一个线程池的线程数量 ,设置成绩, 是比较合适的??
答 : 测试!!
不同的程序 , 线程的作用不一样 ;
1. cpu 密集型任务 , 主要做一些计算工作 , 要在cpu 上运行的
2. IO 密集型任务 , 主要是等待IO操作(等待读写硬盘 , 读写网卡)
线程的优点:
进程与线程的区别 :
证线程安全的思路
线程的优点:
进程与线程的区别 :