目录
1. 单例模式(Singleton)
1.1 饿汉模式(比较急)
1.2 懒汉模式(不着急)
2. 阻塞式队列(BlockingQueue)
2.1 阻塞式队列与生产者消费者模型
2.2 标准库中的阻塞式队列
3.1 使用标准库中的定时器
4. 线程池(ExecutorService)
4.3 标准库中ThreadPoolExecuter构造方法(*)
4.4 线程池的执行流程和拒绝策略
4.5 线程池优点总结(*)
单例模式,是一种常见的设计模式,类似于“棋谱”(把在下棋过程中常见的情况,总结出来,可以让其他人看,棋谱就是一种比较“定式”的东西),也就是出现什么情况,我应该按照这个“棋谱”怎么去应对。
而在代码编程这里,也有总结出来的“棋谱”,也就是”设计模式”,这大大提高了程序员代码编程的下限。
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
类加载阶段创建实例,创建实例的时机是非常早的,非常迫切的
这种就叫“饿汉模式”
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
//把构造方法设置为private,此时在类外面,就无法继续new实例了
private Singleton() {
}
}
public class Demo01 {
public static void main(String[] args) {
//强制保证当前Singleton是“单例”了
Singleton instance = Singleton.getInstance();
}
}
类加载阶段创建实例,创建实例比“饿汉模式”更迟”,带来的效率更高
懒汉模式是线程不安全的
如果整个代码后续没有调用getInstance,这样就把构造实例的过程给节省下来了
效率也就提升了 ,或者即使代码后续调用getInstance,但是调用的时机比较晚,这个时候创建实例的时机也就迟了,就和其他耗时操作岔开了,效率也能提高(一般程序刚启动时,要初始化的东西很多,系统资源紧张)
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {
}
}
public class Demo02 {
SingletonLazy instance = SingletonLazy.getInstance();
}
下面对比一下,前面的懒汉和饿汉模式的线程安全问题
前面已经说过了,懒汉模式线程不安全,那么如何修改让线程安全?
要想线程安全就要“加锁”,但加锁不是随便加的,而且加锁开销代价也比较大,所以可以
在实例没有创建之前,因为线程是不安全的,需要加锁
在实例创建之后,线程是安全的,就不需要加
因此,在加锁的外面,再加上一层判定条件,来判断实例是否创建了
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (SingletonLazy.class) {
if(instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
public class Demo02 {
SingletonLazy instance = SingletonLazy.getInstance();
}
假设两个线程同时getInstance,第一个线程拿到锁了,进入到第二层if,开始new对象了
new操作的本质是三步走
(1)申请内存,得到内存首地址
(2)调用构造方法,来初始化实例
(3)把内存首地址赋值给 instance引用
这种情况,可能编译器会进行“指令重排序”的优化
在单线程角度下,(2)和(3)是可以调换顺序的,谁先执行,谁后执行效果一样
如果此时这里触发指令重排序,并且按照(1)(3)(2)的顺序执行
有可能在t1执行了(1)和(3)之后
(得到了不完全的对象,只是有内存,内存上的数据无效),在执行(2)之前
t2线程调用了getInstance,这个getInstance就会认为Instance非空,就直接返回了Instance,并且在后续可能就会针对Instance进行解引用操作(使用里面的属性/方法)
所以为 了解决“指令重排序”的问题,就要禁止指令重排序
可以使用volatile(既能保证内存可见性,也能禁止指令重排序)解决问题
//懒汉模式,实现单例模式
class SingletonLazy {
private volatile static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (SingletonLazy.class) {
if(instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
public class Demo02 {
SingletonLazy instance = SingletonLazy.getInstance();
}
1.阻塞式队列
阻塞式队列,是一个特殊队列,是“先进先出”的,它具有以下特性
(1)线程安全的
(2)带有阻塞功能
如果队列满,继续入队列,入队列操作就会阻塞,直到队列不满,入队列才能完成
如果队列空,继续出队列,出队列操作也会阻塞,直到队列不空,出队列才能完成
阻塞队列的应用场景:生产者消费者模型
2.生产者消费者模型
描述的是多线程协同工作的一种方式
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
(1)使用阻塞队列,有利于代码“解耦合”
(2)阻塞队列就相当于一个缓冲区,平衡生产者和消费者的处理能力.
比如在某些场景下,外面流量过来的压力特别强,直接压到服务器上,服务器可能撑不住,这时就可以把这些请求放到一个阻塞队列中,然后再由消费者线程和阻塞队列承受压力,这样就可以有效进行“削峰”,防止服务器请求因为压力过大而崩溃
(1)BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
(2)put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
(3)BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
通过 "循环队列" 的方式来实现.
使用 synchronized 进行加锁控制.
put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一 定队列就不满了, 因为同时可能是唤醒了多个线程).
take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
实现步骤:实现一个普通队列(基于数组的循环队列),加上线程安全(加锁),加上阻塞的实现
//实现一个阻塞队列
class MyBlockingQueue {
private int[] items = new int[1000];
private volatile int head = 0;
private volatile int tail = 0;
private volatile int size = 0;
//入队列
public void put(int elem) throws InterruptedException {
synchronized (this) {
//判断队列是否满了,满了就不能插入
while (size >= items.length) {
this.wait();
}
//进行插入操作,把elem 放到items里,放到tail指向的位置
items[tail] = elem;
tail++;
if(tail >= items.length) {
tail = 0;
}
size++;
this.notify();
}
}
//出队列,返回删除的元素内容
public Integer take() throws InterruptedException {
synchronized (this) {
//判断队列是否空,如果空,则不能出队列
while (size == 0) {
this.wait();
}
//进行取元素操作
int ret = items[head];
head++;
if(head >= items.length) {
head = 0;
}
size--;
this.notify();
return ret;
}
}
}
public class Demo02 {
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue();
Thread producer = new Thread(() -> {
int n = 1;
while (true) {
try {
queue.put(n);
System.out.println("生产元素 " + n);
n++;
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread customer = new Thread(() -> {
while (true) {
try {
int n = queue.take();
System.out.println("消费元素 " + n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
customer.start();
}
}
定时器就相当于“闹钟”,当到达某个时间后,就执行某个代码逻辑
比如
服务器开发中,客户端请求服务器
客户端发送请求之后,就需要等待服务器的响应
这就需要给客户端设置一个“超时时间”,这就可以使用定时器来实现了
import java.util.Timer;
import java.util.TimerTask;
public class Demo03 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
},3000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("world");
}
},6000);
System.out.println("开始计时");
}
}
a) schedule 第一个参数是一个任务,需要能够描述这个任务
任务包含两个方面的信息,一个要执行啥工作,一个是啥时候执行
b) 如果让MyTimer管理多个任务,一个timer是可以安排多个任务的
很多的任务中,设置的时间越短的任务,肯定要先执行
首先,想到的肯定是优先级队列PriorityQueue
但优先级队列不是线程安全的,而schedule是有可能在多线程中调用的
所以就可以使用阻塞队列BlockingQueue来实现
private BlockingQueuequeue = new PriorityBlockingQueue<>();
c)任务已经被安排到优先级阻塞队列中了
接下来就需要从队列中取元素了,创建一个单独扫描线程,让这个线程不停的来检查队首元素,时间是否到了,如果时间到了,则执行该任务
d)
设定一个任务,在系统时间到任务时间的这个时间段
程序是不停的检查时间的,完全干不了别的事
并且看时间对于整个任务的进程是没有啥影响的,所以这个循环是在“忙等”
CPU并没有被空闲出来,这里的等待也就没啥意义,
所以可以使用wait来解决这个问题
e) 但这里还有问题(可以详细看下面这幅分析图),take操作和后序的判断,put wait都不是原子的,所以还是需要加锁的
加锁,需要保证take到wait之间的逻辑是“原子的”,才能够避免出现这样的问题
下面上代码
import com.sun.xml.internal.ws.api.model.wsdl.WSDLOutput;
import java.util.PriorityQueue;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
//这个类表示一个任务
class MyTask implements Comparable{
//要执行的任务
private Runnable runnable;
//什么时间来执行任务 (是一个时间戳)
private long time;
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
this.time = System.currentTimeMillis() + delay;
}
public Runnable getRunnable() {
return runnable;
}
public long getTime() {
return time;
}
@Override
public int compareTo(MyTask o) {
return (int)(this.time -o.time);
}
}
class MyTimer {
private BlockingQueue queue = new PriorityBlockingQueue<>();
private Object locker = new Object();
public MyTimer() {
//创建一个扫描线程
Thread t = new Thread(() -> {
while (true) {
try {
synchronized (locker) {
//取出队首元素
MyTask task = queue.take();
//记录系统时间
long curTimer = System.currentTimeMillis();
//看一下系统时间 是否大于等于 任务时间
if (curTimer >= task.getTime()) {
//时间到了,该执行任务
task.getRunnable().run();
} else {
//时间还没到,就把task重新塞会队列中去,继续等待
queue.put(task);
locker.wait(task.getTime() - curTimer);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
public void schedule (Runnable runnable, long after) throws InterruptedException {
synchronized (locker) {
MyTask myTask = new MyTask(runnable,after);
queue.put(myTask);
locker.notify();
}
}
}
public class Demo04 {
public static void main(String[] args) throws InterruptedException {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("时间到1");
}
},3000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("时间到2");
}
},4000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("时间到3");
}
},5000);
System.out.println("开始计时");
}
}
String,字符常量池,MySQL JDBC,数据库连接池(DataSource)
而线程也有线程池
进程创建、销毁,比较低效(内存资源的申请和释放)
线程共享了内存资源,新的线程复用之前的资源,不用再重新申请了,效率提高了
如果线程创建的速率频繁,那么线程销毁的开销就不能忽略了
这就可以使用线程池进一步优化效率了
线程池最大的好处就是减少每次启动、销毁线程的损耗
当需要执行任务的时候,不需要创建线程了,而是直接从池里取一个现成的线程,直接使用,用完后也不释放线程,而是还回到线程池中
总结: 使用线程池是纯用户态操作,要比创建线程(经历内核态的操作)要快
从线程池里取,要比创建新线程要快,这是因为
创建线程,是要在操作系统内核中完成的,涉及到用户态-》内核态切换操作,存在一定开销
构造实例,最常用的就是使用构造方法new
new的过程中,需要调用构造方法,有时候希望类提供多种构造实例的方法
就需要重载构造方法,来实现不同的版本的对象创建,但重载要求参数个数/类型不同,就存在一定的限制,为了绕开这种局限性,就引入了 工厂模式
使用submit方法,把任务交给线程池就可以
线程池中,就会有一些线程来负责完成这些任务
public class Demo05 {
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("这是任务1");
}
});
}
}
写一个固定线程数目的简单线程池
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool {
private BlockingQueue queue = new LinkedBlockingQueue<>();
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
public MyThreadPool(int m) {
//在构造方法中,创建出M个线程,负责完成工作
for (int i = 0; i < m; i++) {
Thread t = new Thread(() -> {
while (true) {
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
}
public class Demo07 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool = new MyThreadPool(10);
for (int i = 0; i < 1000; i++) {
int taskId = i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("执行任务:" + taskId + "当前线程:" + Thread.currentThread().getName());
}
});
}
}
}
支持很多参数,支持很多选项,可以创建出不同风格的线程池
需要注意,线程池自定义线程数目, 不可能确定得出具体的个数的,因为
(1)主机的CPU的配置,不确定
(2)你的程序的执行特点,不确定
执行特点,指的是代码干了什么,
是CPU密集型的任务,(做了大量的算术运算和逻辑判断)
还是IO密集型的任务,(做了大量的读写网卡/读写硬盘)
还是代码既是CPU任务,也是IO任务,这就很难去判断两种任务的比例了
如果任务100%,是CPU密集型
那么线程数目最多也就是=N,此时CPU已经满了
但如果任务10%是CPU密集型,90%都是在操作IO(不使用CPU)
那么线程数目设置成10N也是可以的
(*)所以实际的处理方案就是,进行实践验证,针对当前程序进行性能测试,分别给线程设置成不同的数目,都试一试,分别记录每种情况情况下,程序的一些核心性能指标和系统负载的情况,最终选一个最合适的配置
线程池的执行流程:
(1)先判断当前线程数是否大于核心线程数,如果结果为true,则新建线程并执行任务
(2)如果结果为true,则判断任务队列是否已满,如果结果为fasle,就把任务添加到任务队列中等待线程执行
(3)如果结果为true,则判断当前线程数量是否超过最大线程数,如果结果为false,则新建线程执行次任务
(4)如果结果为true,就执行拒绝策略。
拒绝策略:
AbortPoliy:中止策略,线程池会抛出异常并中止执行此次任务
CallerRunPolicy:把任务交给添加此次任务main线程来执行
DiscardPolicy:忽略新任务,先执行旧任务
DiscardOldestPolicy:忽略最早的任务,执行新加入的任务
(1)降低资源消耗:通过重复利用已创建的线程来执行任务,降低线程创建和消耗造成的消耗
(2)提高响应速度:因为省去了创建线程这一步,所以当拿到任务时,可以立即开始执行
(3)提高线程的可管理性:我们可以自己加入新的功能,比如说定时、延时来执行某些线程,也可以监控线程,控制最大并发线程数等功能