目录
1.单例模式
单例模式是一个非常经典的设计模式,也是最经常考的设计模式
单例就是单个实例(对象)
有些场景中,我们希望有的类,只能有一个对象,不能有多个,在这样的场景下,就可以使用单例模式。
单例模式,需要两步走:
1.在类的内部,提供一个现成的实例
2.把构造方法设为private,避免其他代码能够创建出实例
通过上述方法,就强制了其他程序员在使用这个类的时候,就不会创建出多个对象了。
使用getInstance方法可以获得这个对象
//希望这个类能够有唯一实例
class Singleton {
private static Singleton instance = new Singleton();
//通过这个方法来获取刚才的实例
//后续如果相使用这个类,都通过getInstance方法获取
public static Singleton getInstance() {
return instance;
}
//把构造方法设置为私有,此时类外面的其他代码,就无法new出这个类的对象了
private Singleton() {
}
}
public class Demo21 {
public static void main(String[] args) {
//此处又有一个实例,这就不是单例了
// Singleton s1 = new Singleton();
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
//此时s1和s2是同一个对象
System.out.println(s1 == s2); //true
}
}
单例模式具体的实现方式有很多,最常见的就是“懒汉模式”和“饿汉模式”
1.1 饿汉模式
类加载的同时,创建实例.
1.2 懒汉模式
首次调用getInstancce的时候,才会创建出真正的实例。
“懒”其实是一个褒义词,非必要就不做,减少一些工作量,提高效率。
class SingletonLazy {
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance == null) { //同样的代码写两次,十分有意义
synchronized (SingletonLazy.class) { //一旦加锁,就可能产生阻塞,可能会阻塞很久很久,
// 进一步导致这个阻塞的过程中Instance就可能改变了(其他线程把Instance给new出来了)
if(instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
理解双重if判定/volatile:
加锁/解锁是⼀件开销⽐较⾼的事情.⽽懒汉模式的线程不安全只是发⽣在⾸次创建实例的时候.因此后续使⽤的时候,不必再进⾏加锁了。
外层的if就是判定下看当前是否已经把instance实例创建出来了。
同时为了避免"内存可⻅性"导致读取的instance出现偏差,于是补充上volatile。
当多线程⾸次调⽤getInstance,⼤家可能都发现instance为null,于是⼜继续往下执⾏来竞争锁,其中竞争成功的线程,再完成创建实例的操作。
当这个实例创建完了之后,其他竞争到锁的线程就被⾥层if挡住了.也就不会继续创建其他实例。
1.3 比较
饿汉模式,懒汉模式,两种写法,是否线程安全?(如果多个线程,同时调用getInstance是否会出现问题?)
如果多个线程,同时修改同一个变量,此时就可能出现线程安全问题
如果多个线程,同时读取同一个变量,这时候没事~不会出现线程安全问题
饿汉方式,getInstance,只是进行读取!!不是修改!!所以是安全的
而懒汉模式,既涉及到读取,也涉及到修改,就可能存在问题
那如何保证懒汉模式是安全的呢?
1.加锁
把if和new包裹在一起
这样写后,后续每次调用getInstance,都需要先加锁
但实际上,懒汉模式,线程安全问题,只是出现在最开始的时候(对象还没new的时候)
2.双重if
一旦对象new出来了,后续多线程调用getInstance,就只有读操作,就不会线程不安全了,此时就可以不加锁了
第一个if判定是否需要加锁
第二个if用来判定是否需要new对象
3.volatile - 指令重排序
编译器优化,编译器为了执行效率,可能会调整原有代码的执行顺序,调整的前提是保持逻辑不变
java当中的new操作可能触发指令重排序
1.申请内存空间 买房子
2.在内存空间上构造对象(构造方法) 装修
3.把内存地址,赋值给instance引用 交钥匙
可以1 2 3执行 精装房 也可以1 3 2执行 毛坯房
执行完13后instance就非空了 但是指向的是一个还没初始化的非法对象
此时2开始执行了,判定instance==null,条件不成立,于是t2直接return instance.
进一步t2的线程代码就可能访问instance里面的属性和方法了。
针对上述问题,解决办法,仍然是volatile
让volatile修饰Instance,此时就可以保证Instance在修改的过程中就不会出现指令重排序的现象了
这个代码中,涉及到三个要点
1.正确进行加锁
2.要进行两重if判断
3.用volatile修饰
2.阻塞队列
2.1 概念
多线程代码中常用到的一种数据结构。
阻塞队列是什么?
阻塞队列是⼀种特殊的队列.也遵守"先进先出"的原则.阻塞队列能是⼀种线程安全的数据结构,并且具有以下特性:
• 当队列满的时候,继续⼊队列就会阻塞,直到有其他线程从队列中取⾛元素。
• 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插⼊元素。
阻塞队列的⼀个典型应⽤场景就是"⽣产者消费者模型".这是⼀种⾮常典型的开发模型。
2.2 生产者-消费者模型
擀饺子皮 - 生产者
包饺子,消耗饺子皮 - 消费者
生产出来的饺子皮,设置一个专门的地方放置饺子皮 - 阻塞队列
2.2.1 生产者-消费者模型的好处
1.解耦合
两个模块联系越紧密,耦合越高,反之则越低
尤其对分布式系统来说,更加有意义
2.削峰填谷
峰:短时间内,请求量比较多
谷:短时间内,请求量比较少
A收到了较大的请求量,A会把对应的请求写入到队列中。 削峰
B仍然可以按照之前的节奏,来处理请求。
峰值情况,一般不会持续存在,只会短时间出现,过了A峰值后,A的请求量就恢复正常了,B就可以逐渐的把积压的数据给处理掉了。 填谷
有了这样的机制后,就可以保证在突发情况来临后,系统可以正常运行。
2.2.2 实现
在java标准库里,已经提供了现成的阻塞队列,供程序员使用
标准库里,针对BlockingQueue提供了两种最重要的实现方式:
1.基于数组
2.基于链表
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
public class Demo23 {
public static void main(String[] args) throws InterruptedException {
BlockingDeque<String> queue = new LinkedBlockingDeque<>(); //基于链表的BlockingDeque
queue.put("111");
queue.put("222");
queue.put("333");
queue.put("444");
String elem = queue.take();
System.out.println(elem); //111
elem = queue.take();
System.out.println(elem); //222
elem = queue.take();
System.out.println(elem); //333
elem = queue.take();
System.out.println(elem); //444
elem = queue.take();
System.out.println(elem); //没有任何打印
}
}
2.2.3 判断队列满
1.浪费一个格子,让tail指向head的前一个位置,就算满了
2.专门搞一个变量,size,来表示元素个数,size为0就是空,为数组最大值,就是满
class MyBlockingQueue {
// 此处这里的最大长度,也可以指定构造方法,由构造方法的参数来指定
private String[] data = new String[1000];
// 队列的起始位置
private volatile int head = 0;
// 队列的结束位置的下一个位置
private volatile int tail = 0;
// 队列中有效元素的个数
private volatile int size = 0;
// private final Object locker = new Object();
//提供核心方法入队列和出队列
public void put(String elem) throws InterruptedException {
synchronized (this) {
while (size == data.length) {
// 队列满了
// 如果是队列满,继续插入元素,就会阻塞
this.wait();
}
//队列没满,真正往里面添加元素
data[tail] = elem;
tail++;
//如果tail++后到达数组末尾,这个时候就需要让它回到开头
if(tail == data.length) {
tail = 0;
}
size++;
// 这个notify用来唤醒take中的wait
this.notify();
}
}
public String take() throws InterruptedException {
synchronized (this) {
while (size == 0) {
//队列空了
this.wait();
}
//队列不空,就可以把队首元素(head 位置的元素)删除掉,并进行打印
String ret = data[head];
head++;
if(head == data.length) {
head = 0;
}
size--;
this.notify(); //唤醒
return ret;
}
}
}
public class Demo24 {
public static void main(String[] args) {
//生产者,消费者,分别使用一个线程表示(也可以使用多个线程)
MyBlockingQueue queue = new MyBlockingQueue();
// 消费者
Thread t1 = new Thread(() -> {
while (true) {
try {
String result = queue.take();
System.out.println("消费元素: " + result);
//暂时先不sleep
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 生产者
Thread t2 = new Thread(() -> {
int number = 1;
while (true) {
try {
queue.put(number + "");
System.out.println("生产元素: " + number);
number++;
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
}
3.定时器
3.1 概念
日常开发常用组件
主线程执行schedule方法的时候,就是把这个任务给放到timer对象中
与此同时,timer里头也包含一个线程,这个线程就叫做“扫描线程“,一旦时间到,扫描线程就会执行刚才安排的任务了。
在Timer里,是可以安排多个任务的。
import java.util.Timer;
import java.util.TimerTask;
//定时器
public class Demo25 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("3000");
}
},3000);
timer.schedule(new TimerTask() { //使用匿名内部类的方法,继承了TimerTask,并且创造出一个实例
@Override
public void run() { //目的也是为了重写run 通过run描述任务的详细情况
System.out.println("2000");
}
},2000); //当前安排的任务,啥时候执行,此处填写的时间,就是从当前时刻为基准,往后推迟
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("1000");
}
},1000);
System.out.println("程序启动!");
}
}
3.2 实现
定时器的构成
• ⼀个带优先级队列(不要使⽤PriorityBlockingQueue,容易死锁!)
• 队列中的每个元素是⼀个Task对象.
• Task中带有⼀个时间属性,队⾸元素就是即将要执⾏的任务
• 同时有⼀个worker线程⼀直扫描队⾸元素,看队⾸元素是否需要执⾏
import java.util.PriorityQueue;
class MyTimerTask implements Comparable<MyTimerTask> {
//要有一个要执行的任务
private Runnable runnable;
//还要有一个执行任务的时间
private long time;
//此处的delay就是schedule方法传入的”相对时间“
public MyTimerTask(Runnable runnable,long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay; //这样就构造出了要执行任务的绝对时间戳
}
@Override
public int compareTo(MyTimerTask o) {
//这样的写法,就是让队首元素是最小时间的值
return (int) (this.time - o.time);
//这样的写法,就是让队首元素是最大时间的值
// return o.time - this.time;
}
public long getTime() {
return time;
}
public Runnable getRunnable() {
return runnable;
}
}
// 自己的定时器
class MyTimer {
// 使用一个数据结构,保存所有要安排的任务
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();
private Object locker = new Object();
public void schedule(Runnable runnable,long delay) {
synchronized (locker) {
queue.offer(new MyTimerTask(runnable,delay));
locker.notify();
}
}
//搞一个扫描线程
public MyTimer() {
// 创建一个扫描线程
Thread t = new Thread(() -> {
while (true) {
try {
synchronized (locker) {
// 不要使用if作为wait的判定条件
while (queue.isEmpty()) {
// 使用wait进行等待,但是要想使用,需要搭配synchronized,不能单独使用
// 需要由另外的线程唤醒
//添加了新的任务,就应该唤醒
locker.wait();
}
MyTimerTask task = queue.peek();
// 比较一下队首元素是否可以执行了
long curTime = System.currentTimeMillis();
if(curTime >= task.getTime()) {
// 当前时间已经达到了任务时间,就可以执行任务了
task.getRunnable().run();
queue.poll(); //任务执行完了,就可以从队列删除了
} else {
// 当前时间还没到任务时间,暂时不执行任务
// 暂时 啥也不管,等下一轮判定
locker.wait(task.getTime() - curTime); //当任务时间没到的时候,就wait阻塞
//线程不会在cpu上调度,也就把cpu资源让出来给别人了
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
public class Demo26 {
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("3000");
}
},3000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("2000");
}
},2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("1000");
}
},1000);
System.out.println("程序开始执行");
}
}
4.线程池
4.1 概念
优化频繁创建销毁线程的场景
线程诞生的意义,是因为进程的创建/销毁,太重量了(比较慢)
但是线程如果进一步提高创建销毁的频率,那么线程的开销也不能忽视了。
于是出现了线程池,线程池最⼤的好处就是减少每次启动、销毁线程的损耗。
其核心思想是:在使用第一个线程的时候,提前把2,3,4,5…线程创建好,后续如果需要使用,直接取就行,不需要消耗额外的资源。
4.2 创建
Executors创建线程池的⼏种⽅式
• newFixedThreadPool:创建固定线程数的线程池
• newCachedThreadPool:创建线程数⽬动态增⻓的线程池.
• newSingleThreadExecutor:创建只包含单个线程的线程池.
• newScheduledThreadPool:设定延迟时间后执⾏命令,或者定期执⾏命令.是进阶版的Timer.
Executors本质上是ThreadPoolExecutor类的封装.
public class Demo27 {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(4);
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
4.3 简单代码实现
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class MyThreadPool {
//任务队列
private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);
//通过这个方法,把任务加到队列中
public void submit(Runnable runnable) throws InterruptedException {
// 此处的拒绝策略,相当于是第五种策略,阻塞等待~~(下策)
queue.put(runnable);
}
public MyThreadPool(int n) {
//创建出n个线程,负责执行上述队列中的任务
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
//让这个线程,从队列中消费任务,并进行执行
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t.start();
}
}
}
public class Demo28 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool(4);
for (int i = 0; i < 1000; i++) {
int finalI = i;
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("执行任务:" + finalI);
}
});
}
}
}
5.总结
保证线程安全的思路:
1. 使⽤没有共享资源的模型
2. 适⽤共享资源只读,不写的模型
a. 不需要写共享资源的模型
b. 使⽤不可变对象
3. 直⾯线程安全(重点)
a. 保证原⼦性
b. 保证顺序性
c. 保证可⻅性