文章目录
- 1. volatile 关键字
- 1.1volatile 能保证内存可见性
- 1.2volatile 不保证原子性
- 2 wait 和 notify
- 2.1wait()方法
- 2.2notify 方法
- 2.3 notifyAll()方法
- 2.4 wait 和 sleep 的对比(面试题)
- 3 单例模式(常考)
- 3.1饿汉模式
- 4 阻塞式队列
- 4.1阻塞队列是什么
- 4.2 生产者消费者模型
- 4.3 标准库中的阻塞队列
- 4.3.1阻塞队列实现
- 5 定时器
- 5.1定时器是什么
- 5.2标准库中的定时器
- 5.3实现定时器
- 6 线程池
- 6.1线程池是什么
- 6.2标准库中的线程池
- 6.3实现线程池
- 7 线程的优点
- 8 进程与线程的区别
1. volatile 关键字
1.1volatile 能保证内存可见性
volatile 修饰的变量, 能够保证 “内存可见性”.
代码在写入 volatile 修饰的变量的时候,
改变线程工作内存中volatile变量副本的值
将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候,
从主内存中读取volatile变量的最新值到线程的工作内存中
从工作内存中读取volatile变量的副本
代码示例:
在这个代码中
创建两个线程 t1 和 t2
t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
预期当用户输入非 0 的值的时候, t1 线程结束.
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)
t1 读的是自己工作内存中的内容.
当 t2 对 flag 变量进行修改, 此时 t1 感知不到 flag 的变化.
如果给 flag 加上 volatile
static class Counter {
public volatile int flag = 0;
}
// 执行效果
// 当用户输入非0值时, t1 线程循环能够立即结束.
1.2volatile 不保证原子性
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.
代码示例
这个是最初的演示线程安全的代码.
给 increase 方法去掉 synchronized
给 count 加上 volatile 关键字.
static class Counter {
volatile public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
给 t1 的循环内部加上 synchronized, 并借助 counter 对象加锁.
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (true) {
synchronized (counter) {
if (counter.flag != 0) {
break;
}
}
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
2 wait 和 notify
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.
完成这个协调工作, 主要涉及到三个方法
wait() / wait(long timeout): 让当前线程进入等待状态.
notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的方法.
2.1wait()方法
wait 做的事情:
使当前执行代码的线程进行等待. (把线程放到等待队列中)
释放当前的锁
满足一定条件时被唤醒, 重新尝试获取这个锁.
wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
wait 结束等待的条件:
其他线程调用该对象的 notify 方法.
wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedExceptio
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("wait 前");
object.wait();
System.out.println("wait 后");
}
}
}
//wait() / wait(long timeout): 让当前线程进入等待状态.
wait内部会做三件事:
1、先释放锁
2、等待其他线程的通知
3、收到通知之后,重新获取锁,并且继续往下执行,因此要想使用wait/notify,就得搭配synchronized
2.2notify 方法
是唤醒等待的线程.
方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的。
其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
代码示例: 使用notify()方法唤醒线程
public class Demo1 {
private static Object locker = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// 进行 wait
synchronized (locker) {
System.out.println("wait 之前");
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wai之后");
}
});
t1.start();
Thread.sleep(3000);
Thread t2 = new Thread(() -> {
// 进行 notify
synchronized (locker) {
System.out.println("notify 之前");
locker.notify();
System.out.println("notify 之后");
}
});
t2.start();
}
}
//notify() / notifyAll(): 唤醒在当前对象上等待的线程.
wait,notify都是针对同一个对象来操作的
假如:有十个线程,都调用了o.wait此时10个线程都会阻塞状态。如果调用了o.notify,就会把其中的一个唤醒,针对o.notifyAll就会的把所有的10个线程都唤醒。
2.3 notifyAll()方法
notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.
2.4 wait 和 sleep 的对比(面试题)
其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间.
当然为了面试的目的,我们还是总结下:
- wait 需要搭配 synchronized 使用. sleep 不需要.
- wait 是 Object 的方法 ,sleep 是 Thread 的静态方法.
3 单例模式(常考)
啥是设计模式?
设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路. 按照套路来走局势就不会吃亏.
软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏.
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个.
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.
3.1饿汉模式
举例:上顿吃饭吃饭用了5个碗,下一顿吃饭把所有碗都洗了,就是饿汉模式。
上顿吃饭用了5个碗,下一次吃饭需要3个碗,它就只洗了三个碗(懒汉模式)。
static修饰的变量更准确的说,应该叫做类成员=》类属性/类方法,不加static修饰的成员,叫做实例成员=》实例属性/实例方法。
一个Java程序中,一个类对象只存一份(JVM保证)进一步的也保证了类的static成员也是只有一份的。
类对象!=对象
类对象=类名.class=.class文件(包括:类名是啥,类的属性有哪些,每个属性是啥类型,是public、private…)
类:相当于实例的模板,基于模板可以创建出很多的对象
// 通过 Singleton 这个类来实现单例模式. 保证 Singleton 这个类只有唯一实例
// 饿汉模式
class Singleton {
// 1. 使用 static 创建一个实例, 并且立即进行实例化.
// 这个 instance 对应的实例, 就是该类的唯一实例.
private static Singleton instance = new Singleton();
// 2. 为了防止程序猿在其他地方不小心的 new 这个 Singleton, 就可以把构造方法设为 private
private Singleton() {}
// 3. 提供一个方法, 让外面能够拿到唯一实例.
public static Singleton getInstance() {
return instance;
}
}
public class Demo19 {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
// Singleton instance2 = new Singleton();
}
}
// 实现单例模式 - 懒汉模式
class Singleton2 {
// 1. 就不是立即就初始化实例.
private static Singleton2 instance = null;
// 2. 把构造方法设为 private
private Singleton2() {}
// 3. 提供一个方法来获取到上述单例的实例
// 只有当真正需要用到这个 实例 的时候, 才会真正去创建这个实例.
public static Singleton2 getInstance() {
// 如果这个条件成立, 说明当前的单例未初始化过的, 存在线程安全风险, 就需要加锁~~
if (instance == null) {
instance = new Singleton2();
}
return instance;
}
}
public class Demo20 {
public static void main(String[] args) {
Singleton2 instance = Singleton2.getInstance();
}
}
要实现一个线程安全的单例模式,线程安全不安全,具体指多线程环境下,并发的调用getInstance方法,是否存在bug.
饿汉模式中getInstance方法,仅仅是读取变量的内容,如果多个线程只是读取一个变量,不修改,此时线程是安全的。
懒汉模式既包含了读也包含了修改,这里的读和写还是分两步进行,存在线程安全问题。
t1执行load,把null给到1,t2执行load,把null给到2,t1比较相等,执行里面的代码,t2比较相等,执行面的代码,一个代码被执行了两次,导致多个实例被创建出来,存在bug.
如何让懒汉模式的线程安全(加锁)
// 实现单例模式 - 懒汉模式
class Singleton2 {
// 1. 就不是立即就初始化实例.
private static Singleton2 instance = null;
// 2. 把构造方法设为 private
private Singleton2() {}
// 3. 提供一个方法来获取到上述单例的实例
// 只有当真正需要用到这个 实例 的时候, 才会真正去创建这个实例.
public static Singleton2 getInstance() {
//使用类对象作为锁对象,因为只有一份
synchronized (Singleton2.class) {
if (instance == null) {
instance = new Singleton2();
}
}
return instance;
}
}
public class Demo20 {
public static void main(String[] args) {
Singleton2 instance = Singleton2.getInstance();
}
}
虽然加锁之后,线程安全了,但是又有新的问题。
对于刚才的这个懒汉模式代码来说,线程不安全,是发生在instance初始化之前的,为初始化的时候,多线程调用getInstance,可能就涉及到读和修改,但是一旦instance被初始化之后(一定不是null,if条件一定不成立)getInstance操作就只剩下两个读操作,也就线程安全。
按照上述加锁方式,无论代码初始化之前,还是之后,每次调用getInstance都会进行加锁,意味着即使初始化之后(已经线程安全了)是但任然存在大量的锁竞争。
改进方案:让getInstance初始化之前,进行加锁,初始化所之后,就不加锁了,在加锁这里加上一层条件判断即可。
条件就是当前是否已经初始化完成(instance==null)
// 实现单例模式 - 懒汉模式
class Singleton2 {
// 1. 就不是立即就初始化实例.
private static volatile Singleton2 instance = null;
// 2. 把构造方法设为 private
private Singleton2() {}
// 3. 提供一个方法来获取到上述单例的实例
// 只有当真正需要用到这个 实例 的时候, 才会真正去创建这个实例.
public static Singleton2 getInstance() {
// 如果这个条件成立, 说明当前的单例未初始化过的, 存在线程安全风险, 就需要加锁~~
if (instance == null) {
synchronized (Singleton2.class) {
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
}
public class Demo20 {
public static void main(String[] args) {
Singleton2 instance = Singleton2.getInstance();
}
}
如果多个线程,都去调用这里的getInstance,就会造成大量的读instance内存操作=》可能会让编译器把这个读内存操作优化成读寄存器,所以需要给instance加上volatile。
// 实现单例模式 - 懒汉模式
class Singleton2 {
// 1. 就不是立即就初始化实例.
private static volatile Singleton2 instance = null;
// 2. 把构造方法设为 private
private Singleton2() {}
// 3. 提供一个方法来获取到上述单例的实例
// 只有当真正需要用到这个 实例 的时候, 才会真正去创建这个实例.
public static Singleton2 getInstance() {
// 如果这个条件成立, 说明当前的单例未初始化过的, 存在线程安全风险, 就需要加锁~~
if (instance == null) {
synchronized (Singleton2.class) {
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
}
public class Demo20 {
public static void main(String[] args) {
Singleton2 instance = Singleton2.getInstance();
}
}
这个是安全线程的单例模式:
1、正确的位置加锁
2、双重if判定
3、volatile
理解双重 if 判定 / volatile:
加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候.
因此后续使用的时候, 不必再进行加锁了.
外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了.
同时为了避免 “内存可见性” 导致读取的 instance 出现偏差, 于是补充上 volatile .
当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁,
其中竞争成功的线程, 再完成创建实例的操作.
当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.
4 阻塞式队列
4.1阻塞队列是什么
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.
4.2 生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
1) 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.
比如在 “秒杀” 场景下, 服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求,
服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求.
这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮
2) 阻塞队列也能使生产者和消费者之间 解耦.
比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺子皮的人就是 “生产者”, 包饺子的人就是 “消费者”.
擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超市买的).
4.3 标准库中的阻塞队列
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
String elem = queue.take();
生产者消费者模型
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>();
Thread customer = new Thread(() -> {
while (true) {
try {
int value = blockingQueue.take();
System.out.println("消费元素: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者");
customer.start();
Thread producer = new Thread(() -> {
Random random = new Random();
while (true) {
try {
int num = random.nextInt(1000);
System.out.println("生产元素: " + num);
blockingQueue.put(num);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "生产者");
producer.start();
customer.join();
producer.join();
}
4.3.1阻塞队列实现
通过 “循环队列” 的方式来实现.
使用 synchronized 进行加锁控制.
put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一
定队列就不满了, 因为同时可能是唤醒了多个线程).
take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
队列可以基于数组实现,也可以基于链表实现。
实现一个阻塞队列
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Demo21 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.put("hello");//入队列,带阻塞的
String s = queue.take();//出队列,带阻塞的
//queue.offer();入队列
//queue.poll();出队列
//queue.peek();获取队首元素
}
}
循环队列
class MyBlockingQueue {
// 保存数据的本体
private int[] data = new int[1000];
// 有效元素个数
private int size = 0;
// 队首下标
private int head = 0;
// 队尾下标
private int tail = 0;
// 专门的锁对象
private Object locker = new Object();
// 入队列
public void put(int value) throws InterruptedException {
synchronized (locker) {
if (size == data.length) {
// 队列满了. 暂时先直接返回.
// return;
locker.wait();
}
// 把新的元素放到 tail 位置上.
data[tail] = value;
tail++;
// 处理 tail 到达数组末尾的情况
if (tail >= data.length) {
tail = 0;
}
// tail = tail % data.length;
size++; // 千万别忘了. 插入完成之后要修改元素个数
// 如果入队列成功, 则队列非空, 于是就唤醒 take 中的阻塞等待.
locker.notify();
}
}
// 出队列
public Integer take() throws InterruptedException {
synchronized (locker) {
if (size == 0) {
// 如果队列为空, 就返回一个非法值.
// return null;
locker.wait();
}
// 取出 head 位置的元素
int ret = data[head];
head++;
if (head >= data.length) {
head = 0;
}
size--;
// take 成功之后, 就唤醒 put 中的等待.
locker.notify();
return ret;
}
}
}
public class Demo22 {
private static MyBlockingQueue queue = new MyBlockingQueue();
public static void main(String[] args) {
// 实现一个简单的生产者消费者模型
Thread producer = new Thread(() -> {
int num = 0;
while (true) {
try {
System.out.println("生产了: " + num);
queue.put(num);
num++;
// 当生产者生产的慢一些的时候, 消费者就得跟着生产者的步伐走.
// Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
Thread customer = new Thread(() -> {
while (true) {
try {
int num = queue.take();
System.out.println("消费了: " + num);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
// 简单验证看这个队列是否能正确工作.
// MyBlockingQueue queue = new MyBlockingQueue();
// queue.put(1);
// queue.put(2);
// queue.put(3);
// queue.put(4);
// int ret = 0;
// ret = queue.take();
// System.out.println(ret);
// ret = queue.take();
// System.out.println(ret);
// ret = queue.take();
// System.out.println(ret);
// ret = queue.take();
// System.out.println(ret);
}
}
1、实现阻塞效果,关键要点,使用wait和notify机制。
对于put来说,阻塞条件,就是队列未满。
对于take来说,阻塞条件,就是队列为空。
2、针对那个对象加锁就使用那个对象,针对this加锁就是用this.wait。
3、put中wait要由take来唤醒
4、对于take中的等待,条件是队列为空,也就是put成功之后来唤醒
5、精准唤醒:想唤醒t1,就o1.notify,让t1进行o1.wait.想唤醒t2,就o2.notify,让t2进行o2.wait.
6、有人等待,notify能唤醒,没有人等待notify没有任何副作用
5 定时器
5.1定时器是什么
定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.
5.2标准库中的定时器
标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).
join(指定超时时间),sleep(休眠指定时间)两个都是基于系统内部的定时器来实现的。
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
}, 3000);
//Timer内部都有专门的线程来负责执行注册任务
5.3实现定时器
定时器的构成:
一个带优先级的阻塞队列
为啥要带优先级呢?
因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来.
队列中的每个元素是一个 Task 对象.
Task 中带有一个时间属性, 队首元素就是即将
同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行。
Timer内部都需要啥东西?
1、描述任务
创建一个专门的类来表示定时器的任务(TimerTask)
Timer 类提供的核心接口为 schedule, 用于注册一个任务, 并指定这个任务多长时间后执行.
import java.util.PriorityQueue;
import java.util.concurrent.PriorityBlockingQueue;
// 创建一个类, 表示一个任务.
class MyTask implements Comparable<MyTask> {
// 任务具体要干啥
private Runnable runnable;
// 任务具体啥时候干. 保存任务要执行的毫秒级时间戳
private long time;
// after 是一个时间间隔. 不是绝对的时间戳的值
public MyTask(Runnable runnable, long after) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}
public void run() {
runnable.run();
}
}
2、组织任务(使用一定的数据结构给放到一起。)是有优先队列,即带有优先级,又带有阻塞队列。
Task 类用于描述一个任务(作为 Timer 的内部类). 里面包含一个 Runnable 对象和一个 time(毫秒时间戳)
这个对象需要放到 优先队列 中. 因此需要实现 Comparable 接口.
class MyTimer {
// 定时器内部要能够存放多个任务
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable, long delay) {
MyTask task = new MyTask(runnable, delay);
queue.put(task);
// 每次任务插入成功之后, 都唤醒一下扫描线程, 让线程重新检查一下队首的任务看是否时间到要执行~~
synchronized (locker) {
locker.notify();
}
}
3、执行时间到了的任务
注意事项:
1、MyTask有没有指定比较规则
2、如果不加任何限制,循环就会执行的非常快,如果线程阻塞了,队列中的任务有不为空,任务时间有没有到,会出现“忙等”既没有实质性的输出,同时有没有进行休息,很浪费CPU。
**解决办法:**可以基于wait机制实现,可以指定等待时间(不需要notify,时间到了自然唤醒,计算出当前时间和任务的目标时间的时间差,就是等待时间)sleep不能被中途唤醒,wait能够被中途唤醒
完整代码
import java.util.PriorityQueue;
import java.util.concurrent.PriorityBlockingQueue;
// 创建一个类, 表示一个任务.
class MyTask implements Comparable<MyTask> {
// 任务具体要干啥
private Runnable runnable;
// 任务具体啥时候干. 保存任务要执行的毫秒级时间戳
private long time;
// after 是一个时间间隔. 不是绝对的时间戳的值
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}
public void run() {
runnable.run();
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {
// 到底是谁见谁, 才是一个时间小的在前? 需要咱们背下来.
return (int) (this.time - o.time);
}
}
class MyTimer {
// 定时器内部要能够存放多个任务
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable, long delay) {
MyTask task = new MyTask(runnable, delay);
queue.put(task);
// 每次任务插入成功之后, 都唤醒一下扫描线程, 让线程重新检查一下队首的任务看是否时间到要执行~~
synchronized (locker) {
locker.notify();
}
}
private Object locker = new Object();
public MyTimer() {
Thread t = new Thread(() -> {
while (true) {
try {
// 先取出队首元素
MyTask task = queue.take();
// 再比较一下看看当前这个任务时间到了没?
long curTime = System.currentTimeMillis();
if (curTime < task.getTime()) {
// 时间没到, 把任务再塞回到队列中.
queue.put(task);
// 指定一个等待时间
synchronized (locker) {
locker.wait(task.getTime() - curTime);
}
} else {
// 时间到了, 执行这个任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
public class Demo24 {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello timer!");
}
}, 3000);
System.out.println("main");
}
}
6 线程池
6.1线程池是什么
虽然创建线程 / 销毁线程 的开销大
想象这么一个场景:
在学校附近新开了一家快递店,老板很精明,想到一个与众不同的办法来经营。店里没有雇人,而是每次有业务来了,就现场找一名同学过来把快递送了,然后解雇同学。这个类比我们平时来一个任务,起一个线程进行处理的模式。
很快老板发现问题来了,每次招聘 + 解雇同学的成本还是非常高的。老板还是很善于变通的,知道了为什么大家都要雇人了,所以指定了一个指标,公司业务人员会扩张到 3 个人,但还是随着业务逐步雇人。于是再有业务来了,老板就看,如果现在公司还没 3 个人,就雇一个人去送快递,否则只是把业务放到一个本本上,等着 3 个快递人员空闲的时候去处理。这个就是我们要带出的线程池的模式。
线程池最大的好处就是减少每次启动、销毁线程的损耗。
举例:
6.2标准库中的线程池
使用java.util.concurrent:
使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
返回值类型为 ExecutorService通过 ExecutorService.submit 可以注册一个任务到线程池中.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
利用Executors实现一个线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo25 {
public static void main(String[] args) {
// 创建一个固定线程数目的线程池. 参数指定了线程个数
ExecutorService pool = Executors.newFixedThreadPool(10);
// 创建一个自动扩容的线程池. 会根据任务量来自动进行扩容
// Executors.newCachedThreadPool();
// 创建一个只有一个线程的线程池.
// Executors.newSingleThreadExecutor();
// 创建一个带有定时器功能的线程池. 类似于 Timer
// Executors.newScheduledThreadPool();
for (int i = 0; i < 100; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello threadpool");
}
});
}
}
}
Executors 创建线程池的几种方式
newFixedThreadPool: 创建固定线程数的线程池
newCachedThreadPool: 创建线程数目动态增长的线程池.
newSingleThreadExecutor: 创建只包含单个线程的线程池.
newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
Executors 本质上是 ThreadPoolExecutor 类的封装.
6.3实现线程池
线程池有啥?
1、先能够描述任务(直接使用Runnable)
2、需要组织代码(直接使用BlockingQueue)
3、能够描述工作线程
4、组织这些线程
5、实现,往线程里添加任务
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool {
// 1. 描述一个任务. 直接使用 Runnable, 不需要额外创建类了.
// 2. 使用一个数据结构来组织若干个任务.
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
// 3. 描述一个线程, 工作线程的功能就是从任务队列中取任务并执行.
static class Worker extends Thread {
// 当前线程池中有若干个 Worker 线程~~ 这些 线程内部 都持有了上述的任务队列.
private BlockingQueue<Runnable> queue = null;
public Worker(BlockingQueue<Runnable> queue) {
this.queue = queue;
}
@Override
public void run() {
// 就需要能够拿到上面的队列!!
while (true) {
try {
// 循环的去获取任务队列中的任务.
// 这里如果队列为空, 就直接阻塞. 如果队列非空, 就获取到里面的内容~~
Runnable runnable = queue.take();
// 获取到之后, 就执行任务.
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 4. 创建一个数据结构来组织若干个线程.
private List<Thread> workers = new ArrayList<>();
public MyThreadPool(int n) {
// 在构造方法中, 创建出若干个线程, 放到上述的数组中.
for (int i = 0; i < n; i++) {
Worker worker = new Worker(queue);
worker.start();
workers.add(worker);
}
}
// 5. 创建一个方法, 能够允许程序猿来放任务到线程池中.
public void submit(Runnable runnable) {
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Demo26 {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(10);
for (int i = 0; i < 100; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello threadpool");
}
});
}
}
}
7 线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多。
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
- 线程占用的资源要比进程少很多。
- 能充分利用多处理器的可并行数量。
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
8 进程与线程的区别
- 进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
- 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
- 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
- 线程的创建、切换及终止效率更高。