目录
一.单例模式
单例模式其实是一种设计模式,而设计模式简单来说其实就是针对一些常见场景给出的一些经典解决方案,让程序员可以更好的解决问题,我们到目前为止一般涉及到的设计模式有两种:单例模式和工厂模式。而关于单例模式在面试中经常涉及到的一个问题就是让你实现一个线程安全的单例模式,那么应该怎么实现呢?我们一起来探讨一下!
单例模式右两个模式:饿汉模式(比较着急的去创建实例)和懒汉模式(不太着急只有在使用的时候才去创建实例,更加高效)
饿汉模式:
//通过Singleton这个类来实现单例模式,保证Singleton这个类只有唯一实例
class Singleton{//饿汉模式
//使用static创建一个实例,并且立即进行实例化(比较着急)
private static Singleton instance = new Singleton();//类成员,一般类对象只存在一份(和类相关,和对象无关)
//为了防止程序员在其他地方不小心的new Singleton,我们把这个构造方法设置为private的不能new
private Singleton(){}
//提供方法是在类外能获得到这个实例
public static Singleton getInstance(){
return instance;
}
}
public class Demo20 {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();//获得到饿汉模式的唯一实例
//Singleton s1 = new Singleton();//这是不行
}
}
懒汉模式:
class Singleton1{//懒汉模式
//不是立即初始化实例
private static Singleton1 instance = null;
//设置构造方法
private Singleton1(){}
//得到实例的方法
public static Singleton1 getInstance(){
if(instance == null){
instance = new Singleton1();//只有用到的时候才去创建
}
return instance;
}
}
public class Demo20 {
public static void main(String[] args) {
Singleton1 s2 = Singleton1.getInstance();//获得懒汉模式的唯一实例
}
}
懒汉模式是我们经常使用到的,也是经常见到的,比如你打开一个文档,它并不会将所有部分都加载出来,它只会加载你所能看到的当前页,后面你看那一页再加载那一页,这就是懒汉模式,我们可以看到到目前为止,恶汉模式是线程安全的,它仅仅读取了变量的内容,而懒汉模式不仅读变量,而且进行了判断,也进行了修改,而且这不是原子的,很容易就会乱序,就存在线程安全问题了,那么要实现线程安全的懒汉模式怎么写呢?那当然就是加锁了!!!
public static Singleton1 getInstance(){
synchronized (Singleton1.class){//将整个过程进行加锁,不能加在if里面,放到里面就没有起到原子性了,使用类对象(只有一个)来进行加锁,
if(instance == null){
instance = new Singleton1();//只有用到的时候才去创建
}
return instance;
}
}
但是这样又引入了新的问题:我们线程不安全只是第一次的时候(需要读和改)是线程不安全的,后面初始化了之后就只剩下读操作了,就没有线程安全的问题了,而每次调用getInstance方法时都需要加锁,也就有可能产生锁的竞争,但是这样的竞争是没有必要的,因此我们换需要再加条件,让初始化后就不用加锁了
public static Singleton1 getInstance(){
if(instance == null){//第一次看是否初始化了,是否存在线程安全,是否需要加锁
synchronized (Singleton1.class){
if(instance == null){//而内层判断的是是否要创建实例
instance = new Singleton1();//只有用到的时候才去创建
}
}
}
return instance;
}
这样就解决问题了,我们发现外层的判断和里面的判断重复了,那是不是就可以去掉了呢?答案当然是否定的,如果去掉我们就又回到了初始的问题,没有原子性了,这其实是一个"美丽的误会",外层判断判断的是是否要加锁,而内层判断的是是否要创建实例,这是完全不一样的,而且这两个的执行时机也是差别很大的,里面的加锁是有可能导致线程阻塞的,在两层判断的中间时差,instance是很有可能会被其他改动的,因此这两个判断是缺一不可的
但是这里面还是存在一个重要的问题的:如果多个线程,都去调用这里的getIntance方法的话,就会造成大量的度instance内存的操作,可能会让编译器把这个读内存操作给优化成读寄存器,那么就有出现内存可见性问题了,就又有线程安全问题了,对第二个判定条件问题不大,但是会造成第一个条件的误判导致给不该加锁的加锁了,那么这个又该怎么解决呢?那当然就是使用我们上一篇说过的volatile关键字了
private volatile static Singleton1 instance = null;//volatile关键字一定要加保证第一次的判断不会出错,不会出现不该加锁的加锁的内存可见性问题
这样就完整的写出了一个线程安全的单例模型,其中主要还是三个点:正确的位置加锁,双重if判定,volatile关键字,这三点缺一不可!
二.阻塞式队列
阻塞队列是一个特殊的队列,但也遵守"先进先出"的原则,相比普通队列,阻塞队列又有一些其他方面的功能,阻塞队列可以保证线程安全,而且可以产生阻塞效果(如果队列为空,尝试出队列,就会出现阻塞,直到队列不为空为止,队列为满,尝试入队列,就会出现阻塞,直到队列不满为止),通过这个我们就可以实现"生产者消费者模型"(日常开发中,处理多线程的一个典型方式,尤其是在服务器开发的场景中)(简单举个栗子:假设有A,B,C三个人一起来擀饺子皮+包~有以下两种方法:1.A,B,C分别每个人都是先擀一个皮,然后包一个饺子(此时锁冲突就会比较激烈因为擀面杖只有一个)2.A专门负责擀饺子皮,B和C专门负责包饺子,此时A就是饺子皮的生产者,要不断的生成一些饺子皮~B,C是饺子皮的消费者,要不断的使用/消耗饺子皮~对于包饺子来说,用来放饺子皮的地方就是"交易场所"(而此处的阻塞队列就可以作为生产者消费者模型中的"交易场所"))
用服务器来说:有两个服务器AB,A作为入口服务器的网络需求,B作为应用服务器,来给A提供一些数据,比如下面的情况
如果不使用生产者消费者模型的话,A和B的耦合性是比较高的,也就是A和B之间的联系比较强,这样在开发A代码是就得充分了解B提供的一些接口,开发B代码时也得充分了解到A是怎样调用的,一旦想把B换成C的话,A的代码就需要大量的改动了,如果B挂了的话,很有可能造成A的损失,这是我们所不想看到的,我们希望服务器之间都是高内聚低耦合的,因此我们在这里使用生产者消费者模型,就可以降低耦合
这个场景来说:此时A只需要关注如何来和队列来进行交互,不需要认识B,B也是只需要关注如何和队列交互,也不需要认识A,而且队列也是不变的,如果B挂了,对于A是没用影响的,或者改变A对于B来说也是没有影响的,这也是阻塞队列的优点之一:能够让多个服务器程序之间更充分的解耦合,而另一个优点是:能够对于请求进行"削峰填谷",下面解释一下
再看一下使用生产者消费者模型的效果
另外我们在实际开发中使用到的"阻塞队列",并不是一个简单的数据结构,而是一个/一组专门的服务器程序~~并且它提供的功能不仅仅是阻塞队列的功能,还会在这基础上提供更多的功能(对于数据持久化存储,支持多个数据通道,支持多节点容灾冗余备份,支持管理面板,方便配置参数.....),这样的队列又起了个新的名字:"消息队列"(未来开发中广泛使用到的组件)
再了解一下java标准库中的阻塞队列:
public class Demo21 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.put("hello");//入队列 这里的入队列和出队列都是带阻塞的
String s = queue.take();//出队列
}
}
使用代码实现一个阻塞队列
class MyQueue{
//存放数据的数组
private int[] nums = new int[1000];
//数组内的有效个数
private int size = 0;
//队首下标
private int head = 0;
//队尾下标
private int tail = 0;
//我们发现整个代码都在操作公共变量那么就可以直接够整个方法加锁了
public void put(int val){
synchronized (this){
if(size == nums.length){
try {
this.wait();//直接让这个线程陷入阻塞,直至有效元素不再满为止
} catch (InterruptedException e) {
e.printStackTrace();
}
//return;//队列满
}
//把新的元素放到tail位置,表示入了一个新的元素
nums[tail] = val;
tail ++;
//处理tail到达末尾的情况
if(tail >= nums.length){
tail = 0;
}//也可以写为 tail = tail % nums.length;
size ++;//有效个数增加
this.notify();//put成功之后就可以唤醒take的wait操作
}
}
public int take(){
synchronized (this){
if(size == 0){
try {
this.wait();//直接让这个线程陷入阻塞,直至有效元素不再空为止
} catch (InterruptedException e) {
e.printStackTrace();
}
//return -1;
}
//取出队首的元素
int ret = nums[head];
head ++;
if(head >= nums.length){
head = 0;
}
size --;
this.notify();//take成功之后就可以唤醒put中的阻塞操作 彼此唤醒对方的锁
return ret;
}
}
}
这里的两个wait操作是不可能同时发生的,因为两个的条件是截然不同的,而唤醒操作是没有影响的,因此这样就可以产生一个阻塞队列了
基于内置的阻塞队列,简单实现一个生产者消费者模型
当生产者生产的慢了消费者就得跟着生产者的步伐走
public class Demo22 {
private static MyQueue queue = new MyQueue();
public static void main(String[] args) {
//简单实现一个生产者消费者模型
Thread producter = new Thread(()->{
int num = 0;
while(true){
try {
System.out.println("生产了" + num);
queue.put(num);
num ++;
Thread.sleep(500);
//当生产者生产的慢了消费者就得跟着生产者的步伐走
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producter.start();
Thread customer = new Thread(()->{
while(true){
try {
int num = queue.take();
System.out.println("消费了" + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
}
}
当消费者者慢了生产者一瞬间可以生产很多,但消费者还是慢慢的消费切不会崩溃,然后消费了之后生产者才能再次生产
public class Demo22 {
private static MyQueue queue = new MyQueue();
public static void main(String[] args) {
//简单实现一个生产者消费者模型
Thread producter = new Thread(()->{
int num = 0;
while(true){
try {
System.out.println("生产了" + num);
queue.put(num);
num ++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producter.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();
}
}
这就是生产者消费者模型的基本思路了!
三.定时器
就像是一个闹钟一样,进行定时,在一定的时间之后,被唤醒并执行某个之前设定好的任务,类似于join(指定超时时间)/sleep(指定休眠时间)这些都是基于系统内部的定时器来实现的,那么我们首先来看一下java标准库中的定时器是怎么使用的:
import java.util.Timer;
import java.util.TimerTask;
public class Demo23 {
public static void main(String[] args) {
Timer t = new Timer();
t.schedule(new TimerTask() {//timeTask其实就是一个升级版本的runnable
@Override
public void run() {
System.out.println("hello");
}
},1000);//前面是代码,后面是具体的时间(多长时间后执行)
System.out.println("main");
}
}
自己实现一个定时器,那么先来思考一下Timer内部都需要啥东西:1.管理任务(①描述任务专门创建一个类来表示一个定时器的任务②组织任务,使用一定的数据结构把一些任务给放到一起,通过数据结构来组织)2.执行时间到了的任务
import java.util.concurrent.PriorityBlockingQueue;
//创建一个类,表示一个任务
class MyTask implements Comparable<MyTask>{
//任务具体要干啥(标准库中是通过实现runnable接口来描述任务的)
private Runnable runnable;
//任务具体啥时候做,保存任务要执行的毫秒级时间戳
private long time;
//time 表示的是时间间隔,表示delay时候后再去执行任务
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;//这里加上原本的时间得到的就是开始执行的时间了
}
//执行任务
public void run(){
runnable.run();
}
public long getTime(){
return time;
}
//让myTask类实现一下比较器,来表示按时间先后顺序来排序
@Override
public int compareTo(MyTask o) {
return (int)(this.time - o.time);//比较规则,时间小的在前,时间大的在后
}
}
class MyTimer{
//定时器内部要能够存放多个任务
//使用普通的堆还不行,因为这里不一定是在单线程环境下了,有可能在多线程环境下,而且未来执行这些线程还需要队列,既要在某些线程里面入队列,又要在某些线程里面出队列,使用普通的堆的话,可能会引起线程安全问题,因此我们要使用的队列是PriorityBlockingQueue
//这个优先级是既带有阻塞,又带有优先级的优先级队列
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 {
//如果这里的任务时空的就还好,这个线程就阻塞了,但是队列不为空的话切执行时间还没到,就会一直查看,这样既没有实质性的工作产出,
// 又没有进行休息,这被称为"忙等",这是非常耗费资源,非常浪费CPU的,而且没有什么意义,那怎么解决这个问题呢,就可以基于wait这样的机制来实现
MyTask task = queue.take();
//再比较一下看这个任务的时间是否到了
long time = System.currentTimeMillis();//当前时间
if(time < task.getTime()){
//时间还没到,再将这个任务再塞回去
queue.put(task);
}else{
//时间到了
task.run();//执行任务
//此时指定一个等待时间
synchronized (locker){
locker.wait(task.getTime() - time);//当前需要等待的时间,这里为什么不用sleep
// 因为wait可以提前唤醒,而sleep不能被中途唤醒,
//因为在等待过程中,是有可能插入一个比当前任务还要紧急的任务,那么我们就需要提前唤醒等待了
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
public class Demo24 {
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
},3000);
System.out.println("main");
}
}
再总结一下创建定时器的流程:1.先描述一个任务(runnable,time),2.使用优先队列(PriorityBlockingQueue),来组织多个任务,3.实现schedule方法来注册任务到队列中,4.创建一个扫描线程,来不停的获取队首元素,并判断等待时间是否到达,另外这里面有两点需要注意:1.MyTask需要指定比较规则,2.防止线程"忙等"问题
四.线程池
由于进程比较重,频繁创建销毁,开销是比较大的,解决方案就是:进程池/线程,而线程虽然比进程轻了,但是如果创建销毁的频率进一步增加,仍然会发现开销还是有的,那么此时的解决方案就是:线程池/协程(暂且不讲),线程池,简单来说就是提前把线程创建号,放到池子里,后面需要用线程,直接从池子里取,就不必从系统里面再申请了,线程用完了,也不用还给系统,而是放回到池子里面,以备下次再用,这样创建销毁线程的过程,速度就会更快了那为啥把线程放到池子里面,就会比从系统中申请快呢?
线程池在java标准库中的使用:
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");
}
});
}
}
}
这里我们简单实现第一个固定数目的线程池;先来分析一下线程池: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{
//描述任务,使用runnable
//管理任务使用BlockingQueue
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
//描述一个线程,工作线程的功能就是从任务队列里面获得任务并执行
static class Worker extends Thread{
//当线程池有若干个Worker线程,这些线程内部都持有了上述的任务队列
private BlockingQueue<Runnable> queue = null;
//构造方法传入当前queue
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<Worker> workers = new ArrayList<>();
//传入这些worker
public MyThreadPool(int n){
Worker worker = new Worker(queue);
worker.start();//启动
workers.add(worker);//将这些任务放进数组
}
//5.创建submit方法允许程序员来放任务到线程池中
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");
}
});
}
}
}
以上就是四个简单的多线程实例了!