单例模式
这是一种常见的“设计模式”。
“设计模式”类似于“棋谱”。
场景:代码中的有些概念不应该存在多个实例,此时应该使用单例模式来解决
两种典型的方式实现单例模式:
1、饿汉模式:“饿”代表只要类被加载,就会立刻实例化 Singleton 实例,后续无论怎么操作,只要永远不使用 getlnstance,就不会出现其他的实例。
2、懒汉模式
类加载的时候,没有立刻实例化,第一次调用 getInstance 的时候才会真正实例化,如果要是代码一整场都没有调用getInstance 此时实例化的过程也就被省略了
那么单例模式和线程有什么关系呢?
刚才两种单例模式的实现方式中,饿汉是线程安全的,懒汉是线程不安全的。
原因:
首先回顾一下导致线程不安全的原因:1.线程的调度抢占式执行;2.修改操作不是原子的;3.多线程同时修改同一个变量;4.内存可见性;5.指令重排序。对于饿汉来说,多线程同时调用 getInstance,由于 getInstance 里只做了一件事:读取 instance 实例的地址,这就代表着多个线程在同时读取同一个变量,并不是修改,所以饿汉是线程安全的。
对于懒汉模式来说,多线程同时调用 getInstance ,getInstance中做了四件事:1.读取 instance 的内容;2.判断 instance 是否为 null;3.如果 instance 为 null,就 new 实例;4.返回实例的地址。在第二步操作中 new 实例会修改 instance 的值。所以是线程不安全的。
用一个时间轴来展示懒汉模式:
如何改进懒汉模式,让代码变成线程安全的?
第一种优化方式:加锁
下面展示一个错误的修改方式:
这样写,此时读取判断,操作和 new 修改操作让不是原子的,下面的操作为正确的解决办法
这两种写法都是正确的,认为上面的写法锁的粒度更小,下面的锁的粒度更大,(锁中包含的代码越多就认为“粒度”越大),一般代码的粒度越小越好。
另一种优化方式:在锁上方再加一个 if 后这样可以提高效率:
public static Singleton getInstance(){
if (instance == null) {
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
单例模式为了保证线程安全,涉及到三个要点:
- 加锁保证线程安全
- 双重 if 保证效率
- volatile 避免内存可见性引来的问题
阻塞队列
是并发编程中的一个重要基础组件,帮助我们实现“生产者-消费者模型”(这是一种典型的处理并发编程的模式)
“生产者-消费者模型”:
举一个生活中的例子,就是如果甲、乙、丙三个人需要一个桌子
他们需要的操作步骤就是:
1.用斧子砍树 2.把木头通过小车车运输 3.拼接成桌子
那么就会有两种情况:第一种情况是三个人每个人都操作一遍两个步骤;第二种情况就是甲砍树,乙运输,丙个人拼接桌子
第一种情况对于斧子(锁)的需求太高,而第二种情况就比较常见,其中第一个人就是生产者,另外两个人就是消费者
还有一个问题就是甲砍树太快,小车车放不下;或者乙运输的太快,丙不能很快拼接完成。
阻塞队列的特点也是如此,他是一个先进先出的队列:入队列的时候如果发现队列满了就会阻塞,直到有其他线程调用出队列操作让队列中有空位之后,才能继续入队列;如果出队列操作太快,队列空了额,继续出队列,也会阻塞,一直阻塞到有其他线程生产了元素,才能继续出队列
队列的基本操作:
- 入队列
- 出队列
- 取队首元素
阻塞队里只提供前两个操作,不支持取队首元素
//阻塞版本的入队列
public void put(int value) throws InterruptedException {
synchronized (this) {
if (size == array.length){
wait();
}
array[tail] = value;
tail++;
if (tail == array.length){
tail = 0;
}
size++;
notify();
}
}
//阻塞版本的出队列
public int take() throws InterruptedException {
int ret = -1;
synchronized (this){
if (size == 0){
wait();
}
ret = array[head];
head = 0;
if (head == array.length) {
head = 0;
}
size--;
notify();
}
return ret;
}
}
}
体会上面的两个wait 操作,一个在队列满的时候阻塞,一个在队列空的时候阻塞,两个操作永远不会冲突
假设两个线程入队列,一个线程入队列,一个线程出队列,此时如果队列已经满了,两个入队列线程就会线程就阻塞了,此时如果出队列操作
如果多个线程 wait notify 的时候唤醒哪个线程由操作系统调度器说了算(程序员的角度理解就是随机的)
如果没有 wait 执行了 notify 没有影响,有线程在 wait ,notify 就就唤醒一个线程,没有线程 wait 不会有任何负面影响
public static void main(String[] args) {
BlockingQueue blockingQueue = new BlockingQueue();
//第一次让消费者消费的快一些,生产者慢一些
//此时就会消费者等待
//第二次让消费者生产的快一些,消费者慢一些
//此时就会预期看到,生产者线程刚开始的时候会快速插入元素,直到队列满的时候就会阻塞
//此时就要消费了以后才能生产
Thread producer = new Thread(){
@Override
public void run(){
for (int i = 0; i < 10000; i++) {
try {
blockingQueue.put(i);
System.out.println("生产元素:" + i);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
producer.start();
Thread consumer = new Thread(){
@Override
public void run(){
while (true){
try {
int ret = blockingQueue.take();
System.out.println("消费元素:" + ret);
//
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
consumer.start();
}
}
运行效果:即使消费的比生产的快,但是还是要等生产完成后才能消费
public static void main(String[] args) {
BlockingQueue blockingQueue = new BlockingQueue();
//第一次让消费者消费的快一些,生产者慢一些
//此时就会消费者等待
//第二次让消费者生产的快一些,消费者慢一些
//此时就会预期看到,生产者线程刚开始的时候会快速插入元素,直到队列满的时候就会阻塞
//此时就要消费了以后才能生产
Thread producer = new Thread(){
@Override
public void run(){
for (int i = 0; i < 10000; i++) {
try {
blockingQueue.put(i);
System.out.println("生产元素:" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
producer.start();
Thread consumer = new Thread(){
@Override
public void run(){
while (true){
try {
int ret = blockingQueue.take();
System.out.println("消费元素:" + ret);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
consumer.start();
}
}
第二次运行效果:刚开始生产者比较快就快速生产,直到队列满了,就阻塞等待消费者消耗了以后才继续生产
当有两个消费者线程的时候
当两个消费者都触发 wait 操作后,接下来当我们调用 notifyAll 的时候,就把上面两个线程都唤醒了,于是两个线程就都去重新获取锁:
消费者1 ,先获取到锁,于是就执行下面出队列操作(执行完毕释放锁)
消费者2,后获取到锁,于是也会执行下面的出队列操作,但是注意:刚才生产者生产的一个元素,已经被消费者1 线程给取走了,当前实际是一个空队列,如果强行往下执行取队里取素操作,就会出现逻辑错误。
定时器
相当于一个闹钟,进行任务的管理。
定时器是多线程编程中的一个重要/常用组件,应用场景非常广泛,网络编程中特别常见。
定时器的构成:
- 使用一个类来描述“一段逻辑”(一个要执行的任务),同时也要记录这个任务在什么时间点执行
- 使用一个阻塞优先队列来组织若干个 Task。(使用优先队列是为了保证队首元素就是要被最早执行的任务)【阻塞队列既支持阻塞的特性,又支持优先级的“先进先出”,本质上是一个“堆”】
- 需要一个扫描线程,不停的扫描,判定队首是否时间到。(扫描线程要循环的检测,队首元素是否需要执行,如果需要执行的话,就执行这个任务。)
- 实现一个方法 schedule,给定时器内部安排一个任务。
- 为了避免忙等,还需要引入一个额外的对象,让扫描线程借助这个对象进行 wait 。(使用带超时时间版本的 wait)
随意一个对象都可以放入优先队列中么?
答:优先队里而需要知道对象之间的大小关系,才能把优先级排出来(才能保证队首元素是优先级最高的)
优先队列中的元素必须是可比较的
比较规则的指定主要是两种方式:1、让 Task 实现 Comparable 接口 2、让优先队列构造的时候,传入一个比较器对象(Comparator)
标准库中其实已经提供了阻塞队列,定时器等基本组件,实际工作中,可以直接运用,下面的代码是为了理解原理,也是为了加深对多线程的掌握。
import java.sql.Time;
import java.util.concurrent.PriorityBlockingQueue;
public class ThreadDemo1 {
//优先队列中的元素必须是可比较的
//比较规则的指定主要是两种方式
static class Task implements Comparable<Task> {
//Runnable 中有一个 run 方法,就可以借助这个 run 方法来描述要执行的具体任务是什么
private Runnable command;
//time 表示什么时候来执行 command,是一个绝对时间(ms级别的时间戳)
private long time;
//构造方法的 after 参数表示:after 秒后执行(是一个相对时间)
//这个相对时间的参数是为了而用起来方便
public Task(Runnable command, long after) {
this.command = command;
this.time = System.currentTimeMillis() + after;
}
//执行具体的逻辑
public void run(){
command.run();
}
@Override
public int compareTo(Task o) {
return (int) (this.time - o.time);
}
}
static class Worker extends Thread{
private PriorityBlockingQueue<Task> queue = null;
public Worker(PriorityBlockingQueue<Task> queue) {
this.queue = queue;
}
@Override
public void run() {
//实现具体的线程执行内容
while (true){
try {
//1.取出队首元素,检查时间是否到了
Task task = queue.take();
//2.检查当前任务时间是否到了
long curTime = System.currentTimeMillis();
if (task.time > curTime){
//时间还没到,就把任务再放回队列中
queue.put(task);
}else {
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
static class Timer{
//1.用一个 Task 类来描述任务
//2.用一个阻塞队队列来组织若昂的任务,队首元素就是时间最早的任务
private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
//3.用一个线程循环扫描挡墙的阻塞队列队首元素,如果时间到,就执行任务
public Timer(){
//创建线程
Worker worker = new Worker(queue);
worker.start();
}
//4.还需要提供一个方法,让调用者能把任务安排进来
public void schedule(Runnable command,long after){ //安排任务
Task task = new Task(command,after);
queue.put(task);
}
}
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hehe");
}
},5000);
}
}
我设置的任务为运行5s 后输出一个“hehe”
为了更直观的看到效果,可以把主函数的内容改成每隔 2s 执行一次输出
运行10s 后的结果
忙等
举个例子:我们定一个上课的闹钟时间为 9:00,小明现在看了一下时间为 8:00,又过了一会儿又看了一下时间8:01,又过了一会儿又看了一下时间8:02,剩下的时间还有将近一个小时,还可以做的事情有很多,但是小明一直在看时间,等待上课,这种频繁的盯着表的行为就叫作忙等。
我们的线程就可能会出现这种问题,扫描线程极快的运行 while 循环,有可能会大量的资源浪费 CPU 资源进行比较时间和入队列出队列操作。为了解决这个问题,我们就要借助 wait / notify 来解决。有下面几种情况
- wait() 死等,一直等到 notify 的通知过来
- wait(time),等待是有上限的,如果有 notify 就被提前唤醒,如果没有 notify,时间到了也一样可以被唤醒。
代码阻塞在 wait 处,避免了频繁占用 CPU
解决忙等问题部分的代码:
static class Worker extends Thread{
private PriorityBlockingQueue<Task> queue = null;
private Object mailBox = null;
public Worker(PriorityBlockingQueue<Task> queue,Object mailBox) {
this.queue = queue;
this.mailBox = mailBox;
}
@Override
public void run() {
//实现具体的线程执行内容
while (true){
try {
//1.取出队首元素,检查时间是否到了
Task task = queue.take();
//2.检查当前任务时间是否到了
long curTime = System.currentTimeMillis();
if (task.time > curTime){
//时间还没到,就把任务再放回队列中
queue.put(task);
synchronized (mailBox){
mailBox.wait(task.time - curTime);
}
}else {
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
static class Timer{
//为了避免忙等,需要使用 wait 方法
//使用一个单独的对象来辅助进行 wait
private Object mailBox = new Object();
//1.用一个 Task 类来描述任务
//2.用一个阻塞队队列来组织任务,队首元素就是时间最早的任务
private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
//3.用一个线程循环扫描挡墙的阻塞队列队首元素,如果时间到,就执行任务
public Timer(){
//创建线程
Worker worker = new Worker(queue,mailBox);
worker.start();
}
//4.还需要提供一个方法,让调用者能把任务安排进来
public void schedule(Runnable command,long after){ //安排任务
Task task = new Task(command,after);
queue.put(task);
synchronized (mailBox){
mailBox.notify();
}
}
}
在扫描线程内部加上 wait
在安排任务方法内部加上 notify
线程池