目录
4.2.2 Executors(ThreadPoolExecutor的封装)
4.2.3 面试题:如果按照实际场景显示的指定相关线程的参数,程序运行会更可控。那线程个数指定为几比较合适呢?
一、单例模式
单例模式就能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例。这一点,在很多场景都得到了应用,如JDBC中的DataSource实例就只需要一个。
1.1 饿汉模式
class SingletonDataSource1{
//此处的static类成员的初始化时机是在“类加载的时候”,即程序一启动用到这个类,就会立刻加载,实例创建的时机比较早(比较着急)
private static SingletonDataSource1 instance = new SingletonDataSource1();
// 构造方法也是private,不能在内外被获取
private SingletonDataSource1(){
}
public static SingletonDataSource1 getInstance(){
return instance;
}
}
public class try1 {
public static void main(String[] args) {
// 无论在代码中的哪个地方调用getInstance,得到的都是同一个实例
SingletonDataSource1 dataSource1 = SingletonDataSource1.getInstance();
}
}
1.2 懒汉模式
class SingletonDataSource3{
private static SingletonDataSource3 instance = null;
private SingletonDataSource3(){
}
public static SingletonDataSource3 getInstance(){
if(instance == null){
instance = new SingletonDataSource3();
}
return instance;
}
}
与饿汉模式相比,主要区别在于实例创建的时机不同了,不再是类加载的时候立即创建实例。而是在首次调用getInstance的时候,才会真正创建实例。
以上两个版本的代码,在多线程环境下是否会存在多线程安全问题呢?
对于饿汉模式来说,多线程调用getInstance,只是针对同一个变量来读。因此是线程安全的。
对于懒汉模式来说,多线程调用getInstance,大部分情况是读,但是也可能会修改。这个修改发生在未初始化之前,多个线程同时调用getInstance就可能导致多线程同时修改。因此,线程不安全。
1.3 多线程版懒汉模式
将读和写打包成一个原子的操作,即instance == null和instance = new SingletonDataSource3();打包成一个原子的操作。
class SingletonDataSource3{
private static SingletonDataSource3 instance = null;
private SingletonDataSource3(){ }
public static SingletonDataSource3 getInstance(){
synchronized (SingletonDataSource3.class){
if(instance == null){
instance = new SingletonDataSource3();
}
}
return instance;
}
}
对于懒汉模式来说,线程不安全只是出现在未初始化的时候,一旦初始化成功,后续调用getInstance就变成了两次读操作,线程安全问题也就没有了。
但如果按照上述代码这样写,每次调用getInstance,都会触发这里的锁竞争,就是是在线程安全的情况下,仍然会触发锁竞争。
现在要想办法让这个加锁操作不要执行的太过频繁:在实例化之前加个锁,实例化之后,不再加锁
class SingletonDataSource3{
private static SingletonDataSource3 instance = null;
private SingletonDataSource3(){ }
public static SingletonDataSource3 getInstance(){
if(instance == null){ //这里是为了判断是否加锁
synchronized (SingletonDataSource3.class){
if(instance == null){ // 这里是判断是否要创建实例
instance = new SingletonDataSource3();
}
}
}
return instance;
}
}
又有了一个新问题,如果当前有很多个线程,同时调用getInstance,就会涉及到很多个线程都去读instance的内存值,相当于CPU读取了内存很多次,就可能会触发编译器的优化。后续的读内存操作可能就不读了,而是直接读CPU寄存器了。因此,加上volatile,可以避免这种情况。
class SingletonDataSource3{
private static volatile SingletonDataSource3 instance = null;
private SingletonDataSource3(){ }
public static SingletonDataSource3 getInstance(){
if(instance == null){ //这里是为了判断是否加锁
synchronized (SingletonDataSource3.class){
if(instance == null){ // 这里是判断是否要创建实例
instance = new SingletonDataSource3();
}
}
}
return instance;
}
}
二、阻塞队列
是一种比较特殊的队列,遵守先进先出,也是线程安全的队列。
2.1 特点:
带有阻塞功能,如果队列为空,尝试出队列就会阻塞,一直阻塞到队列不空。
如果队列满了,尝试进入队列,就会阻塞,一直阻塞队列不满为止。
基于阻塞队列,就可以实现生产者消费者模型。
2.2 作用:
在开发中起到服务器之间解耦合的作用
在请求突然暴增的时候,起到削峰填谷的效果。
2.3 标准库中的阻塞队列
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.put("hello");
String s = queue.take();
}
只有put和take这两个方法带有阻塞作用
2.4 自己实现阻塞队列
①通过循环队列的方式来实现
②使用synchronized关键字进行加锁控制
③put时,如果队列满了,就需要阻塞等待。等take一个元素时,队列就不满了,这时就可以唤醒这个线程。
④take时,如果队列为空,就需要阻塞等待。等put一个元素时,队列不空,就可以唤醒该线程了。
class MyBlockingQueue1{
private int[] items = new int[1000]; //队列的初始大小
private int size = 0; //有效元素的个数
private int head = 0;
private int tail = 0;
private Object locker = new Object(); // 定义一个锁对象
// 入队列
public void put(int value) throws InterruptedException {
synchronized (locker){
if(size == items.length){
// 如果队列满了,就触发阻塞操作
locker.wait();
//当take后,队列不满,就会被唤醒
}
items[tail] = value;
tail++;
if(tail >=items.length){
tail = 0;
}
size++;
// 唤醒take的阻塞操作
locker.notify();
}
}
// 出队列
public Integer take() throws InterruptedException {
synchronized (locker){
if(size == 0){
// 阻塞等待,当队列不为空的时候就唤醒
locker.wait();
};
int ret = items[head];
head++;
if(head>=items.length){
head = 0;
}
size--;
// 唤醒put的阻塞操作
locker.notify();
return ret;
}
}
}
public class try1 {
// 使用该队列作为交易场所
private static MyBlockingQueue1 queue1= new MyBlockingQueue1();
public static void main(String[] args) {
// 两个线程,一个作为生产者,一个作为消费者(这两种搞多个也是可以的)
Thread producer = new Thread(() -> {
int a = 1;
while(true){
try {
System.out.println("生产者生产了:"+a);
queue1.put(a);
a++;
// 如果给生产者加个sleep,生产慢,消费快,队列在这种情况下大部分是空的,消费者很多时间都在阻塞等待
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
Thread customer = new Thread(() -> {
while (true){
try {
int a = queue1.take();
System.out.println("消费者消费了"+a);
// 消费者加个sleep,生产的快,消费慢,队列不部分情况是满的,没消费掉一个元素,才能生产一个元素
// Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
}
}
三、定时器
3.1 概念
定时器是软件开发中的一个重要组件。达到一个设定的时间之后,就执行某个指定好的代码。
3.2 标准库中的定时器
标准库中提供了一个Timer类,Timer类的核心方法是schedule。schedule包含两个参数。
第一个参数表示任务,任务就是TimerTask类,类似于Runnable接口,也是重写run方法
第二个参数指定多长时间之后执行(单位:毫秒).
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
// 在这描述具体的任务
System.out.println("hello");
}
},5000);
}
3.3 自己实现定时器
3.3.1 分析
定时器的schedule操作可以加入多个任务。因此,需要对多个任务进行描述和组织。
描述:需要说清楚这个任务是具体内容,以及这个任务多久后执行。可以自定义一个Task类来实现。
组织:不仅能新增新的任务,还要从所有任务中找出最快要到执行时间的任务。有的任务10s后执行,有的任务1min后执行。当然,得先执行10s后要执行得任务。可以使用优先级队列将其组织,采用小根堆。这样就能保证堆顶的元素是最小的。
单独的扫描线程:需要不停的扫描堆中的最小元素,判断其是否到执行时间了。如果时间到了,就执行这个任务的代码。
3.3.2 具体实现
class MyTimer2{
// 使用Task2这个内部类表示当前的一个任务
static class Task2 implements Comparable<Task2>{
// 要执行的任务
private Runnable runnable;
// 多久后执行
private long time;
// 构造方法
public Task2(Runnable runnable,long after){
this.runnable = runnable;
this.time = System.currentTimeMillis()+after;
}
public void run(){
runnable.run();
}
@Override
public int compareTo(Task2 o) {
return (int)(this.time - o.time);
}
}
// 准备一个堆,存放所有的任务
// PriorityQueue 这个队列本身是不安全的,需要自己手动加锁
// PriorityBlockingQueue带有优先级的阻塞队列,是线程安全的,put插入元素,take取元素
private PriorityBlockingQueue<Task2> tasks = new PriorityBlockingQueue<>();
// 通过这个方法,往定时器中添加任务
public void schedule(Runnable runnable,long time){
Task2 task = new Task2(runnable,time);
tasks.put(task);
}
private Object locker = new Object();
// 创建一个扫描线程,让该线程不断的取堆顶元素,判断该任务是否可以执行
// 在MyTimer2实例化的时候,就创建该线程
public MyTimer2(){
Thread t = new Thread(() -> {
while (true){
try {
// 取堆顶元素
Task2 task = tasks.take();
long curTime = System.currentTimeMillis();
// 判断执行时间是否到了
if(curTime < task.time){
// 没到,放回去
tasks.put(task);
// 此处的locker存在的目的是为了能够进行等待,而不是为了保证互斥
synchronized (locker){
locker.wait(task.time - curTime);
}
}else{
task.run();
// tasks.poll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
public class try5 {
public static void main(String[] args) {
MyTimer2 myTimer2 = new MyTimer2();
myTimer2.schedule(new Runnable() {
@Override
public void run() {
System.out.println("sleep"+System.currentTimeMillis());
}
},1000);
myTimer2.schedule(new Runnable() {
@Override
public void run() {
System.out.println("work"+System.currentTimeMillis());
}
},3000);
System.out.println("main"+System.currentTimeMillis());
}
}
四、线程池
4.1 内核态和用户态
线程池最大的好处:减少每次启动、销毁线程的损耗。
4.2 标准库中的线程池
4.2.1 ThreadPoolExecutor
此处这个ThreadPoolExecutor使用比较复杂,光构造方法就需要很多操作。标准库中也提供了封装版本,提供了更简单的接口,直接使用。
4.2.2 Executors(ThreadPoolExecutor的封装)
newFixedThreadPool: 创建固定线程数的线程池;
newCachedThreadPool: 创建线程数目动态增长的线程池.
newSingleThreadExecutor: 创建只包含单个线程的线程池.
newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
4.2.3 面试题:如果按照实际场景显示的指定相关线程的参数,程序运行会更可控。那线程个数指定为几比较合适呢?
网上的回答:假设机器CPU是10核心,线程池线程个数:1 * 核心数、1.2*核心数、1.5*核心数、2*核心数·······
只要回答中有具体的数字,答案都是错的。
假设CPU是10核心,
极端情况1:线程的任务100%的时间都在使用CPU,此时线程数量不应该超过10;
极端情况2:线程的任务1%的时间都在使用CPU,99%的时间都在阻塞,此时线程数量不应该超过1000;
但是,在每个任务的实际执行过程中,有多少时间在使用CPU,多少时间在阻塞,是不好量化的。
在实际中,是通过测试的方式,来找到一个更合适的线程个数来设定。在测试的过程中,观察不同线程数下的CPU的使用情况。要保证CPU既不会特别空闲,也不会特别紧张。根据性能测试,找到一个合适的数值,来保证效率和可靠性之间的平衡点。
为什么不能让CPU特别空闲?
线程数设置的少,CPU就很空闲,整个任务的执行时间就更长。
为什么不能让CPU特别紧张?
线程数设置的多,CPU就很繁忙,整个任务的执行时间就更快。当然也不能让CPU太忙,要考虑到冗余,要能够预防突发情况。
4.3 实现一个简单的线程池
相当于是一个newFixedThreadPool,即固定线程数的线程池。
package thread;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool2{
// 1.描述任务,直接使用Runnable
// 2.组织任务,使用一个阻塞队列存放若干个任务
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
// 3.还需要描述一个工作线程是那样的
// 在实际使用中,工作线程不止一个,这多个工作线程共享一个任务队列
// 先描述一个工作线程长啥样
static class Worker extends Thread{
private BlockingQueue<Runnable> queue = null;
// 通过这里的构造方法,把上面建好的任务队列给传到线程里,方便线程去取任务
public Worker(BlockingQueue<Runnable> queue){
this.queue = queue;
}
// 描述一个线程要做的工作,即反复的从队列中读取任务,然后执行任务
@Override
public void run() {
while (true){
try {
// 如果任务队列不为空,此时就能立即取出一个任务并执行
// 如果任务队列为空,就会产生阻塞,阻塞到直到有人加入新的任务进去
Runnable task = queue.take();
task.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 4.需要组织若干个任务线程
private List<Worker> workerList = new ArrayList<>();
// 5.搞一个构造方法,指定一下有多少个线程在线程池中
public MyThreadPool2(int n){
for(int i=0;i<n;i++){
Worker worker = new Worker(queue);
// 创建新的线程,先让他跑起来,再保存到数组中
worker.start();
workerList.add(worker);
}
}
// 6.实现一个submit来注册任务到线程池中
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
}
public class try6 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool2 myThreadPool2 = new MyThreadPool2(10);
for (int i = 0; i < 100; i++) {
myThreadPool2.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
}