文章目录
下面介绍几个关于线程安全的实例吧
单例模式
啥是设计模式?
设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路. 按照套路来走局势就不会吃亏.
软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏.
饿汉模式
饿汉模式的代码:
饿汉模式中的
getInstance
方法仅仅是读取了变量的内容,如果多线程只是读同一个变量,不修改,此时是线程安全的
一个典型的案例:
notepad (记事本)这样的程序,再打开大文件的时候是很慢的(比如你要打开1G的文件,此时notepad就会尝试将这1G的所有内容读到内存中)[饿汉]
而一些其他的程序,再打开大文件的时候会有优化(比如打开一个大文件的时候,同样的有1G,但是只会先加载这一个屏幕中可以显示的部分内容)[懒汉]
懒汉模式
懒汉模式代码:
所以说懒汉模式不是线程安全的,我们要实现的是一个线程安全的单例模式
在多线程环境下,并发的调用getInstance
方法,可能会出现bug
如果真出现上图所示的现象,那么就会实例化两个对象,就不再是一个单例模式,如此可知,饿汉模式线程不安全,
懒汉模式(多线程版本)
针对饿汉模式的线程不安全,我们需要针对其进行加锁操作
加锁之后,线程安全问题得到了解决,但是又出现了新的问题
对于一开始的饿汉模式代码来说,线程不安全是发生在instance被初始化之前,未初始化时,多线程调用
getInstance
,可能同时涉及读和修改,但是一旦instance被初始化只有,getInstance
操作就剩下了两个读操作,线程就安全了按照上面的方式加锁,无论代码是初始化之后,还是初始化之前,每次调用
getInstance
都会进行加锁,也就意味着即使初始化之后仍然存在大量锁竞争,但是这样的竞争是没有必要的,同时让代码保证线程安全的同时,也付出了代价(程序的速度就慢了)
改进方案:让getInstance
初始化之前进行加锁,初始化之后就不用加锁,在加锁这里加一个条件判定条件即可
增加判定条件之后,发现又出现了一个新的问题,如果多个线程都去调用getInstance
就会造成大量的读instance内存的操作 可能会让编译器自动优化,出现内存可读性的问题.第一层判断就会读寄存里的instance值.所以我们需要给instance 加上volatile
至此,这就是最终的线程安全的懒汉模式的单例模式代.
阻塞式队列
什么是阻塞式队列
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
- 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
- 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.
生产者消费者模型
生产者消费者模型,是实际开发中非常有用的一种多线程开发手段,尤其是在服务器开发的场景中.
假设两个服务器,A和B,A作为服务器直接接受用户的网络请求,B作为应用服务器,来给A提供一些数据
如果使用生产者消费者模型,就可以降低这里的耦合
以上就体现了生产证消费者模型的第一个优点:能让多个服务器程序之间充分的解耦合
生产者消费者模型的第二个优点:能够对请求进行削峰填谷
“削峰”:这种峰很多时候不是持续的,就一阵,过去就恢复了
“填谷”:B依然可以按照原有的频率来处理之前积压的数据
在实际开发中,“阻塞队列” 并不是一个简答的数据结构,而是一个/一组专门的服务器程序,并且他提供的阻塞队列的功能,还会在一定的基础上人提供更富哦的功能(对于数据持久化存储,支持多个数据通道,支持多节点容灾冗余备份,支持管理面板…)
这样的队列又有一个新名字,叫做"消息队列"
标准库中的阻塞队列
标准库中阻塞队列的基本用法
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
BlockingQueue
是一个接口. 真正实现的类是LinkedBlockingQueue
.- put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
BlockingQueue
也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性
认识了标准库中的阻塞队列,自己实现一个基本的阻塞队列
阻塞队列的基本实现
-
实现一个普通的队列
class MyBlockingQueue{ private int[] data= new int[1000]; private int head = 0; private int tail = 0; private int size = 0; public void put(int value){ if(size == data.length){ return; } data[tail] = value; tail++; if(tail >= data.length){ tail = 0; } size++; } public Integer take(){ if(size == 0) { return null; } int ret = data[head]; head++; if(head >= data.length){ head = 0; } size--; return ret; } }
-
加上线程安全
通过看put和take两个方法,发现方法内的每一行代码都是在操作公共变量,既然如此,就将整个方法加锁即可.
class MyBlockingQueue{ private int[] data= new int[1000]; private int head = 0; private int tail = 0; private int size = 0; private Object locker = new Object(); public void put(int value){ synchronized (locker){ if(size == data.length){ return; } data[tail] = value; tail++; if(tail >= data.length){ tail = 0; } size++; } } public Integer take(){ synchronized (locker){ if(size == 0) { return null; } int ret = data[head]; head++; if(head >= data.length){ head = 0; } size--; return ret; } } }
-
加上阻塞
关键要点就是使用wait和notify机制
对于put来说,阻塞条件就是队列为满
对于take来说,阻塞条件就是队列为空
class MyBlockingQueue{
private int[] data= new int[1000];
private int head = 0;
private int tail = 0;
private int size = 0;
private Object locker = new Object();
public void put(int value) throws InterruptedException {
synchronized (locker){
if(size == data.length){
locker.wait();
//return;
}
data[tail] = value;
tail++;
if(tail >= data.length){
tail = 0;
}
size++;
locker.notify();
}
}
public Integer take() throws InterruptedException {
synchronized (locker){
if(size == 0) {
//return null;
locker.wait();
}
int ret = data[head];
head++;
if(head >= data.length){
head = 0;
}
size--;
locker.notify();
return ret;
}
}
}
定时器
就像一个闹钟,进行定时,在一定时间之后,被唤醒执行某个之前设定好的任务
标准库中的定时器
- 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
- schedule(安排) 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).
Timer里面有专门的线程,来执行注册的任务
实现定时器
Timer内部需要的东西:
- 描述任务:创建一个专门的类来表示一个定时器的中的任务(
TimerTask)
- 组织任务:使用一些数据结构把一些任务给放在一起
这里使用优先级队列PriorityQueue
进行组织
但是我们的队列可能要被多线程调用,考虑到线程安全问题,可能在多个线程里进行注册同时还有一个专门的线程来取任务,此处的队列就需要注意线程安全问题
PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
这是一个带有优先级,同时带有阻塞队列
此时,第二部分的工作也完成了
- 执行时间到了的任务
需要执行时间最靠前的任务,就需要一个线程,不停的检查当前优先级队列的队首元素,看看说当时最靠前的这个任务的时间是不是到了
我们通过在构造方法里创建一个线程,不断检查时间有没有到执行时间.
最后运行试一下:
错误原因:MyTask
不能够被转换为Comparable
刚写的MyTack
这个类里面的比较规则并不是默认就存在的,需要我们手动指定,按照时间大小来比较
标准库中的集合列类,很多都是有一定的约束限制的,不能随便拿个类都能往里放
此时的MyTack
就符合了比较规则了,通过时间戳进行比较
看似执行完成,其实仍然有缺陷,
使用wait而不是用sleep的原因:
sleep不能中途唤醒
wait可以中途唤醒
在等待过程中,可能要插入新的任务,新的任务可能出现在所有任务的最前面~~
所以我们还需要在schedule中加上notify
最后的完整代码为:
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()+after;
}
public void run(){
runnable.run();
}
public long getTime(){
return this.time;
}
@Override
public int compareTo(MyTask o) {
return (int)(this.time - o.time);
}
}
class MyTimer{
private Object locker = new Object();
PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
public void schedule(Runnable runnable,long delay){
MyTask myTask = new MyTask(runnable,delay);
queue.put(myTask);
//每次任务插入成功之后, 都唤醒一下扫描线程, 让线程重新检查一下队首的任务看是否时间到要执行~~
synchronized (locker){
locker.notify();
}
}
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();
}
}
线程池
进程比较重,频繁创建销毁进程,开销大,解决办法:进程池或线程
线程比较轻量,但是如果频繁创建销毁的频率进一步增加,仍然会发现开销还是有的,解决办法:线程池或协程
线程池的好处
我们将线程创建出来,放在池子里,后面需要线程,直接从池子里取,而不用再从系统这边申请了,线程用完了也不用还给系统,而是放回到池子里.
线程放在池子里,就比从系统那边申请释放来的更快的原因.
操作系统分为用户态和内核态
自己写的代码,就是在最上面的应用程序这一层来运行的这里的代码都称为"用户态"运行的代码
有些代码,需要调用操作系统的API,进一步的逻辑在内核中执行
例如:调用一个
System.out.println
本质是要经过write系统调用,进入内核中,内核会执行一堆逻辑,控制显示器会输出字符串,在内核中运行的代码,称为"内核态"运行的代码创建线程,本身就需要内核的支持(创建线程的本质是在内核中搞PCB,然后放在双向链表中),调用的
Thread.start
其实归根到底,也是要进入内核态来运行的.而放入到池子/从池子中取,这个过程不需要涉及到内核态,就是纯粹的用用户态代码来完成一般认为,纯用户态的操作,效率要比内核态处理的操作,效率要高很多
标准库中的线程池
ThreadPoolExecutor
它属于java.util.concurrent
Java中很多和多线程相关的组件都在这个包中
认识线程池的构造方法
int corePoolSize
核心线程数
int maximumPoolSize
最大线程数
为了更好的了解,我们将核心线程数比作是公司的正式员工的数量,最大线程数比作是正式员工和临时员工的数量 将一个线程池比作一个公司,正式员工允许摸鱼,临时工不允许摸鱼.
开始的时候,假设公司要完成的工作不多,正式工就可以搞定,就不需要临时工
如果公司的工作突然猛增,正式员工加班也搞不定,就需要雇佣一些临时工,过了一段时间,工作量又降低了,现在的活正式员工又可以搞定了,甚至还有富余(正式员工可以摸鱼了,临时工更可以摸鱼了) 于是就把临时工给辞退了
long keepAliveTime
允许临时工摸鱼时间
TimeUnit unit
时间的单位(s,ms,us)
BlockingQueue<Runnable> workQueue
任务队列,线程池会提供一个submit方法让程序员把任务注册到线程池中,加入到任务队列中
ThreadFactory threadFactory
线程工厂,线程是怎么被创建出来的
RejectExecutionHanler hanler
拒绝策略
虽然线程的参数这么多,但是使用的时候最终要的参数,还是第一组参数,线程池中线程的数量
有一个程序,这个程序要并发的/多线程来完成一些任务 如果使用现车池的话,这里的线程设置为多少合适?
要通过性能测试,找到合适的值
例如,写一个服务器程序,服务器通过线程池,多线程用户请求~
就可以对这个服务器进行性能测试,比如构造一些请求,发送给服务器.要测试性能,这里的请求就需要构造很多,比如每秒发送500/1000/2000…根据的业务场景, 构造一个合适的值~~根据这里不同的线程池的线程数来观察程序处理任务的速
程序持有的CPU占用率
当线程数多了,整体的速度会变快,但是CPU占用率也会高.
当线程数少了,整体的速度会变慢,但是CPU占用率也会下降.
需要找到一个让程序速度能接受,并且CPU占用也合理这样的平衡点
不同类型的程序,因为单个任务,里面 CPU 上计算的时间和阻塞的时间是分布是不相等的
因此光去拍脑门出来一个数字往往是不靠谱~~
标准库中还提供了一个简化版的线程池Executor ,本质是针对ThreadPoolExecutor
进行封装,提供了一些默认参数
Executors 创建线程池的几种方式
newFixedThreadPool
: 创建固定线程数的线程池newCachedThreadPool
: 创建线程数目动态增长的线程池.newSingleThreadExecutor
: 创建只包含单个线程的线程池.newScheduledThreadPool
: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
实现线程池
先能够描述任务 (使用Runnable)
需要组织任务 (使用BlockingQueue)
能够描述工作线程
需要组织这些线程
需要实现,往线程池里添加任务
class MyThreadPool{
//1. 描述一个任务,直接使用Runnable,不需要额外一个类
//2. 需要一个数据结构,来组织若干个任务
private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
//3. 描述一个线程,工作线程的功能就是从工作队列中取出线程并执行
static class Worker extends Thread{
//当前线程池中有若干Worker线程,这些线程内部 都持有上述的任务队列
private BlockingDeque<Runnable> queue = null;
public Worker(BlockingDeque<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();
}
}
}