单例模式
基本概念
单例模式是设计模式中一种非常典型的模式,也是比较简单的模式
ps:设计模式属于软性要求,框架属于硬性要求
单例模式强制要求某个类在某个程序中,只有唯一一个实例(不允许创建多个实例),只有一个实例,这样的要求是开发中非常常见的场景,通过机器或者程序来实现强制要求的目的
在代码中,如果创建了多个实例,就会直接编译失败
一般来说,我们通过饿汉方式和懒汉方式来实现单例模式
饿汉方式和懒汉方式实现
饿汉方式
因为instance是静态成员,静态成员的初始化,是在类加载阶段触发的.类加载往往是在程序一启动就会触发
饿汉方式的精髓在于无法new一个Singleton对象 我们后续通过getInstance这个方法来获取实例
java
class Singleton{
private static Singleton instance=new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton(){
}
}
懒汉方式
java
class SingletonLazy{
private static SingletonLazy instance=null;
public static SingletonLazy getInstance(){
if(instance==null){
instance=new SingletonLazy();
}
return instance;
}
private SingletonLazy(){
}
}
饿和懒是相对的,饿是尽早创建实例(在类加载的时候就创建),而懒是尽可能晚的创建实例(甚至有可能不创建),延迟创建
懒在计算机中是褒义词,意味着高效率
懒汉模式下,创建实例的时机是第一次使用的时候
懒汉模式和饿汉模式都是通过私有化构造方法使外部无法通过new直接创建对象,只能通过提供的getInstance来获取实例instance(静态私有成员变量),但这样的方式存在缺陷,比如我们可以通过反射的使用来创建该类的实例(但反射本身就不是常规的编程手段,在日常开发中,我们并不推荐使用)
线程安全
单例模式是否线程安全?
饿汉模式获取实例只涉及读操作,是线程安全的,而懒汉模式获取实例的方法是线程不安全的(=操作是原子的,但这里不只有赋值,还有判断并且创建的操作,是线程不安全的)
如何解决线程安全问题
1.解决线程安全问题的常规思路是加锁(把条件和赋值打包成原子的)

2.但加锁之后又引入了新的问题.当我们把实例创建好之后,后续再调用getInstance此时都是直接执行return操作,如果只是判定if+return,不涉及线程安全问题,但是由于加锁就会相互阻塞,影响程序的执行效率
如何解决这一问题呢?
按需加锁
(实例还没创建,涉及线程安全,加锁,如果已经创建,就不需要加锁)

这段代码由于两个instance==null的判断而看起来别扭,可实际上这是因为我们之前写的都是单线程代码,而且仔细分析会发现这两个if的目的并不相同,第一个是判断是否要加锁,第二个是判断是否需要new对象
3.这里还可能涉及一些问题
当一个线程读取instance的时候,另一个线程可能正在修改,这里是否会存在"内存可见性问题"呢?
这是可能会存在的,编译器的优化是非常复杂的,我们无法预测,但为了保险起见,我们可以给instance加上一个volatile,这样就避免了内存可见性问题出现的可能
但这里更关键的问题是指令重排序问题,
我们分析getInstance方法,发现这里的指令可分为三步
1`申请内存空间
2`在空间上构造对象(初始化)
3`内存空间的首地址,赋值给引用变量
正常情况下,是按照1 2 3的顺序来执行的,但是在指令重排序的情况下,可能会变成1 3 2,如果是1 3 2,在多线程情况下可能会出现bug,很可能拿着一个未初始化的对象来进行操作(这与双重if和指令重排序有关)
这也表名了加volatile的必要性
volatile的功能有两方面:1.确保每次读取操作都是读内存 2.关于改变量的读取和修改操作,不会触发重排序
阻塞队列
基本概念
阻塞队列是一种更加复杂的队列
1.线程安全
2.阻塞特性
a)队列为空,尝试出队列,出队列操作就会阻塞,阻塞到其他线程添加元素为止
b)队列为满,尝试入队列,入队列操作就会阻塞,阻塞到其他线程取走元素为止
Java标准库中提供了现成的阻塞队列BlockingQueue
当我们想要new 一个blocking queue是会发现他的构造方法都是多线程的(多线程是并发的解决方案)

我们通常使用ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue来构造
当用这个队列进行入队列出队列操作时,只有put和take才带有阻塞功能
并且我们在new一个blockingqueue时最好设置最大容量,如果不设置的话,就会默认一个非常大的数值
阻塞队列,没有提供一个"阻塞队列获取队首元素"的方法
主要应用场景--"生产者消费者模型"
阻塞队列一个最常用的场景就是实现"生产者消费者模型"(这是多线程编程中一个典型的编码技巧)
生产者消费者模型的两个重要优势
1.解耦合(不一定是两个线程之间,也可以是两个服务器之间)
2.削峰填谷
(也正是因为这样的优势,生产者消费者模型在日常开发中经常看到,并且消息队列这个东西也很常见)

生产者消费者模型付出的代价
1.引入队列之后,整体的结构会更加复杂,此时就需要更多的机器进行部署,生产环境的结构会更加复杂,管理起来更加麻烦
2.效率会有影响
java
public static void main(String[] args) throws InterruptedException {
BlockingQueue queue=new LinkedBlockingQueue(100);
Thread producer=new Thread(()->{
int n=0;
while(true){
try {
System.out.println("生产元素: "+n);
queue.put(n);
n++;
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"producer");
Thread consumer=new Thread(()->{
while(true){
try{
Object n=queue.take();
System.out.println("消费元素 :"+n);
Thread.sleep(1000);
}catch(InterruptedException e){
throw new RuntimeException(e);
}
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}
模拟实现一个简单的阻塞队列
java
class MyBlockingQueue{
private String[] queue;
//队首
private int head;
//队尾
private int tail;
public int size=0;
//提供构造方法
public MyBlockingQueue(int x){
this.queue=new String[x];
}
public void put(String n) throws InterruptedException {
synchronized (this){
if(size>=queue.length){
this.wait();
}
queue[tail]=n;
tail++;
if(tail>=queue.length){
tail=0;
}
size++;
this.notify();
}
}
public String take() throws InterruptedException {
synchronized (this){
if(size==0){
this.wait();
}
String ret=queue[head];
if(head>=queue.length){
head=0;
}
size--;
this.notify();
return ret;
}
}
}
这里可以直接使用this来作为take和put的锁对象是因为put和take是在互相唤醒,不可能有一些线程阻塞在take,有一些阻塞在put(也就是说队列不可能既是满又是空)
最开始,我们的判断条件时if但深入了解发现wait建议我们使用while循环(这是因为wait不一定只是被notify唤醒,也有可能被interrupt这样的方法唤醒,if作为判断条件,此时wait就存在被提前唤醒的风险)

于是我们就把这里的判断条件用while来设置,这样循环的目的是为了"二次验证",判断这里的条件是否成立,wait先判定一次,wait唤醒再判定一次,(再确认一下,队伍是否不为空)
如果只是一个线程take,一个线程put,不会出现自己唤醒自己的情况,多个线程take,多个线程put,会有这种风险,但可以通过while循环判定条件,避免这样的唤醒给程序带来风险
线程池
基本概念
常量池:字符串常量,在JAVA程序最初构建的时候,就已经准备好了,等程序运行的时候,这样的常量也就加载到内存中,省下了创建/销毁的开销
线程池的概念与之类似,就是为了让我们高效的创建销毁线程的
解决线程频繁创建销毁的方案:1.线程池 2.协程(轻量级线程)
线程池就是把线程提前创建好,放在一个地方(类似于数组)需要的时候随时去取,用完了还到池子里
这里我们了解一下操作系统的用户态和内核态
一个操作系统=内核+配套的应用程序(内核:包含操作系统中的各种核心功能(1.管理硬件设备 2.给软件提供稳定的运行环境))
一个软件系统,内核就是一份,一份内核要给所有的应用程序提供服务和支持的
如果一段代码是应用程序自主完成的,整个执行过程是可控的,如果有一段代码需要进入到内核中那么他就是不可控的
从线程池中提取线程,纯应用程序就可以完成(可控的)
从操作系统创建线程,就需要内核配合(不可控的)
核心方法
JAV标准库中也提供了直接使用的线程池,ThreadPoolExecutor
核心方法是submit(Runnable) 通过Runnable描述一段要执行的任务,通过submit任务放到线程池中,此时线程池就会这样的任务
线程池中的参数是什么意思(经典面试题)

我们只需要看最后一个参数最多的构造方法即可
1.int corePoolSize 核心线程数 至少有多少个线程,线程池一创建,这些线程也随之创建,直到整个线程池销毁,这些线程才被销毁
2.int maximumPoolSize 最大线程数 核心线程数+非核心线程数(自适应)(不繁忙就销毁,繁忙时就创建)
3.long keepAliveTime 非核心线程允许空闲的最大时间
4.TimeUnit unit 时间单位 (timeUnit是枚举)
5.BlockingQueue<Runnable> workQueue 工作队列,可以选择使用数组/链表,可以指定capacity,可以指定是否要有带有优先级,也可以指定比较规则
线程池的本质是什么?
线程池本质上就是生产者消费者模型,调用submit就是在生产任务,线程池中的线程就是在消费任务
6.ThreadFactory threadFactory 工厂模式(统一构造并初始化线程) 工厂模式也是一种设计模式,和单例模式是并列的关系(用来弥补构造方法的缺陷)
工厂模式(与单例模式一样,也是一种设计模式)
工厂模式给线程提供的工厂类,线程中有一些属性可以设置,线程池是一组线程
构造方法的重载要求参数类型和个数是不同的(这是C++和JAVA一个共用的问题),构造方法的名字是固定的,要想提供不同的版本,就要通过重载,但有时候又不一定能构成重载,这时候就要通过工厂模式来解决这个问题
工厂方式的的核心就是通过静态方法,把构造对象的new的过程各种属性初始化的过程封装起来了,提供多组静态方法,实现不同情况的构造
提供工厂方法的类,就可以称之为工厂类
java
class Point{
}
class Factory{
public static Point makePointByXY(double x, double y){
Point p=new Point();
return p;
}
public static Point makePointByRA(double r,double a){
Point p=new Point();
return p;
}
}
public class Demo25 {
public static void main(String[] args) {
Point p=Factory.makePointByRA(10,20);
}
}
7.RejectedExecutionHandler handler 拒绝策略
这是整个线程池参数中最为重要,最复杂的一个参数
submit把任务添加到任务队列中,任务队列就是阻塞队列,队列满了,再添加就会阻塞,一般不希望程序阻塞太多,对于线程池来讲,发现入队列操作时队列满了,并不会真的阻塞,而是执行拒绝策略相关的代码
|---------------------|---------------------|
| 拒绝策略 | 具体描述 |
| AbortPolicy | 线程池直接抛出异常(可能无法继续工作) |
| CallerRunsPolicy | 让调用submit的线程自己执行任务 |
| DiscardOldestPolicy | 丢弃队列中最早的任务 |
| DiscardPolicy | 丢弃队列中最新的任务 |
线程池的参数很多,为了方便使用,JAVA标准库也提供了另一组类,针对ThreadPoolExcutor进行进一步的封装,简化线程池的使用(这个也是基于工厂设计模式)
Executors.newFixedThreadPool (nThread )核心线程数和最大线程数一样
Executors.newCashedThreadPool() 最大线程是一个很大的数字(可以无限增加)
虽然这样的使用更加方便了,但Executors线程数目/拒绝策略都是隐式的,不好控制,在日后的应用中,应该根据需求来选择
模拟实现线程池
java
class MyThreadPool{
private BlockingQueue<Runnable> queue=null;
public MyThreadPool(int n){
//初始化线程池,创建固定个数的线程
queue=new ArrayBlockingQueue<>(1000);
for(int i=0;i<n;i++){
Thread t=new Thread(()->{
while(true){
try {
Runnable task=queue.take();
task.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
public void submit(Runnable task) throws InterruptedException {
queue.put(task);
}
}
public class Demo26 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool=new MyThreadPool(10);
for (int i = 0; i < 100; i++) {
int id=i;
myThreadPool.submit(()->{
System.out.println(Thread.currentThread().getName()+" id "+ id+"running");
});
}
}
}
这里我们要注意线程池中的线程是前台线程,阻止线程的结束
shutdown能把线程池中的线程全部关闭,但无法确定线程池中的每个任务全部执行完毕
如果我们想要全部执行完毕,就需要awaitTermination方法
定时器
基本概念
定时器类似闹钟,时间到了,执行一定的逻辑
标准库中的定时器
标准库中提供了一个Timer类,Timer类的核心方法为schedule
schedule包含两个参数,第一个参数即要执行的任务代码,第二个参数指定多长时间之后执行(单位为毫秒)
正常描述任务是Runnable,timer又进行封装timerTask
但本质核心还是重写run方法
Timer中也包含前台线程,这也意味着他会阻止进程结束
模拟实现一个定时器
1.创建一个类,表示一个任务
2.定时器中,能够管理多个任务,必须使用一些集合类把多个任务管理起来 (优先级队列)
3.实现schedule方法,把任务添加到队列中
4.额外创建一个线程,负责队列中的任务(要看任务的时间是否到了)
java
class MyTimerTask implements Comparable<MyTimerTask>{
private Runnable task;
private long time;
public MyTimerTask(Runnable task, long time) {
this.task=task;
this.time=time;
}
@Override
public int compareTo(MyTimerTask o) {
return (int)(this.time-o.time);
}
public long getTime(){
return time;
}
public void run(){
task.run();
}
}
//自己实现一个计时器
class MyTimer{
private PriorityQueue<MyTimerTask>queue=new PriorityQueue<>();
//这里我们也可以使用this作为锁对象
private Object locker=new Object();
public void schedule(Runnable task,long delay){
synchronized (locker){
MyTimerTask timerTask=new MyTimerTask(task,System.currentTimeMillis()+delay);
queue.offer(timerTask);
locker.notify();
}
}
public MyTimer(){
Thread t=new Thread(()->{
try{
while(true){
synchronized(locker){
while(queue.isEmpty()){
locker.wait();
}
MyTimerTask task=queue.peek();
if(System.currentTimeMillis()< task.getTime()){
locker.wait(task.getTime()-System.currentTimeMillis());
}else{
task.run();
queue.poll();
}
}
}
}catch (InterruptedException e){
throw new RuntimeException(e);
}
});
t.start();
}
}
public class Demo27 {
public static void main(String[] args) {
MyTimer timer=new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 3000");
}
},3000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 2000");
}
},2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello 1000");
}
},1000);
Executors.newScheduledThreadPool(4);
}
}
一些注意的细节:
1.计算机中以时间戳表示时刻,以1970.1.1 0时0分0秒为基准,计算当前时刻与基础时刻的秒数之差
(由于是我们自定义的类必须自定义比较规则)
2.wait和notify的合理应用,如果不用的话由于不知道delay时间是多少,就有可能涉及到忙等,用sleep是不太合理的,因为sleep无法设置合理的时间,所以我们推荐使用wait和notify
定时器除了基于堆方式来实现的定时器之外,还有一种方案,基于"时间轮"
类似搞了一个循环队列(数组)每个元素是一个时间单位,每个元素是一个链表,每到一个时间单位,光标指向下一个元素,同时把这个元素对应的链表中的任务执行一遍
优势:性能更高 缺点:时间精度不如优先队列
应用
由于定时器是一个非常重要的组件,在分布式系统中,把定时器专门提取出来,封装成了一个单独的服务器(和消息队列很像)