提示:以下是本篇文章正文内容,下面案例可供参考
单例模式是一种设计模式,指的是写代码时有一些常见场景,设计模式就是针对这些常见场景给出的一些经典解决方案。
单例模式有两种典型实现:饿汉和懒汉
两者的区别在哪里呢?举个例子:
饿汉的单例模式,是比较着急去创建实例;
懒汉的单例模式,是不着急去创建实例,用的时候再创建。
//通过Singleton这个类来实现单例模式,保证Singleton这个类只有唯一实例
class Singleton{
//1.使用static创建一个实例,并且立即实例化
//这个instance对应的实例就是该类的唯一实例
private static Singleton instance=new Singleton();
//2.为了防止程序员在其他地方不小心new这个Singleton,就可以把构造方法设为private
private Singleton(){}
//3.提供一个方法,让外面能拿到唯一实例
public static Singleton getInstance() {
return instance;
}
//static修饰的成员-更准确说是“类成员”
//不加static修饰的成员-更准确的说是“实例成员”
//一个java程序中,一个类对象只存在一份(JVM保证)
//进一步也就保证了类static成员也只有一份
}
public class Demo19 {
public static void main(String[] args) {
Singleton instance=Singleton.getInstance();
}
}
//实现单例模式-懒汉模式
class Singleton2{
//1.不立即初始化实例
private static Singleton2 instance=null;
//2.把构造方法设为private
private Singleton2(){
}
//3.提供一个方法来获取到上述单例的实例
// 只有当真正需要这个实例时,才创建
public static Singleton2 getInstance(){
if(instance ==null){
instance=new Singleton2();
}
return instance;
}
}
public class Demo20 {
public static void main(String[] args) {
Singleton2 instance=Singleton2.getInstance();
}
}
上面的懒汉模式实现线程是不安全的
举例说明:
我们现在给一种可能的线程t1和t2的执行情况
第一步:t1进行load——我们把null加载到cpu(寄存器)上
第二步:t2进行load——我们把null也给加载到cpu上
第三步:t1进行cmp比较,也就是下图红色箭头所指,与null比较,发现相等
发现相等之后,执行save操作
t1执行完save之后,
到第四步——t2也进行cmp,也是与null比,发现相等,执行save操作
可以看到,当我们两个线程按上图所示顺序执行,我们的实例就不是我们想要的那样只创建一份了(图示创建了2次)。如果同时有n个线程执行的话,就可能创建n份了,显然是存在bug了。
那如何保证我们懒汉模式的线程安全呢?——加锁!
//实现单例模式-懒汉模式
class Singleton2{
//1.不立即初始化实例
private static Singleton2 instance=null;
//2.把构造方法设为private
private Singleton2(){
}
//3.提供一个方法来获取到上述单例的实例
// 只有当真正需要这个实例时,才创建
public static Singleton2 getInstance(){
synchronized (Singleton2.class){
//使用类对象作为锁对象——类对象在一个程序中只有一个
//这样就能保证调用getinstance的时候针对的是同一个对象加锁
//我们加锁是要把多个不是原子的操作变成一个原子的操作,
//所以我们synchronized放if外面
//如果你synchronized放if里面,那么if的读和里面的修改仍然是两个分割的个体
if(instance ==null){
instance=new Singleton2();
}
}
return instance;
}
}
当前,虽然加锁后线程安全得到解决,但又有了新问题:
对于最初懒汉模式的代码说,线程不安全是发生在instance被初始化之前。未初始化的时候,多线程调用getinstance,就可能同时涉及到读和修改,但一旦instance被初始化之后(一定是null,if条件一定不成立了),getinstance操作只剩下两个读操作了——只剩读操作也就是一定线程安全了。
而按照上述的加锁方式,无论代码是初始化之后,还是初始化之前,每次调用getinstance都会进行加锁,也就意味着即使初始化之后(已经线程安全了),但是仍然存在大量锁竞争
ps:加锁确实能让代码保证线程安全,但也是牺牲了速度为代价
那我们也有改进方案:让getinstance初始化之前,才进行加锁,初始化之后,就不再加锁了。在加锁这里再加上一层条件判断即可。
条件就是当前是否已经初始化完成 if(instance==null)
//实现单例模式-懒汉模式
class Singleton2{
//1.不立即初始化实例
private static Singleton2 instance=null;
//2.把构造方法设为private
private Singleton2(){
}
//3.提供一个方法来获取到上述单例的实例
// 只有当真正需要这个实例时,才创建
public static Singleton2 getInstance(){
if(instance==null){
synchronized (Singleton2.class){
//使用类对象作为锁对象——类对象在一个程序中只有一个
//这样就能保证调用getinstance的时候针对的是同一个对象加锁
//我们加锁是要把多个不是原子的操作变成一个原子的操作,
//所以我们synchronized放if外面
//如果你synchronized放if里面,那么if的读和里面的修改仍然是两个分割的个体
if(instance ==null){
instance=new Singleton2();
}
}
}
return instance;
}
}
ps1:这两个if虽然相邻,但实际上两个条件执行时机可能相差很大:加锁的时候可能出现代码阻塞,就会产生时间差。而在这个时间差中间,instance也是可能被其他线程给修改的。
ps2:这里还有一个问题——如果多个线程都去调用这个getinstance,就会造成大量的读instance操作,可能会让编译器把读内存操作优化成读寄存器操作。一旦触发了优化,后续如果一个线程已经完成了针对instance的修改,那么紧接着后面的线程都将感知不到这个修改,仍然把instance当成null(内存可见性,可见笔者上一篇多线程文章有详解)
内存可见性问题,可能会引起第一个if判断失效,但是对第二个if判断影响不大。(synchronize本身也能保证内存可见性),因此这样的内存可见性问题,只引起了第一层条件的误判,也就是导致不该加锁的加锁了,但是不会引起第二层if的误判(不至于创建多个实例)
而内存可见性问题解决办法也很简单,给instance加上volatile即可
注意!!!:饿汉模式和懒汉模式是非常经典的面试问题,面试一般就是现场写代码,大家一定要着重掌握
阻塞队列同样是一个符合先进先出规则的队列,相比普通队列,阻塞队列又有一些其他方面的功能:
1.线程安全
2.产生阻塞效果
(1)如果队列为空,尝试出队列,就会出现阻塞,阻塞到队列不为空为止
(2)如果队列已满,尝试入队列,也会出现阻塞,阻塞到队列不为满为止
基于上述特性,就可以产生“生产者消费者模型”
生产者消费模型是我们日常开发中,处理多线程问题的一个典型方式。
我们举个例子:
我们过年包饺子,这个过程往往是需要多人分工,假设A,B,C三人来擀饺子皮,包饺子。。。
那我们现在有如下几种协作方式:
1.A、B、C分别每个人都是擀一个皮,然后包一个饺子;然后擀一个皮,包一个饺子。。。
存在一个问题:擀面杖只有一个(锁冲突比较激烈)
2.A专门擀饺子皮,B和C负责包饺子(常见情况-效率更高)
2这种就构成了——生产者消费模型:
A是饺子皮的生产者,要不断生产饺子皮;
B和C是饺子皮的消费者,要不断消耗饺子皮;
对于包饺子来说,用来放饺子皮的那个“盖帘”就是交易场所
而我们的阻塞队列就可以作为生产者消费模型中的交易场所
生产者消费模型,是实际开发中非常有用的一种多线程开发手段,尤其是在服务器开发的场景中。
举例说明:
假设,有两个服务器,A、B,A作为入口服务器直接接收用户的网络请求,B作为应用服务器,来给A提供一些数据。
如果不使用生产者消费模型,此时A和B的耦合性是比较强的!
耦合性比较强,也就是A和B联系性比较强。但是联系性比较强往往是坏事。
比如在开发A代码时,就得充分了解B提供的一些接口,
而开发B代码的时候也得充分了解到A是怎么调用的——这样会增加开发成本,
并且一旦我们想要把B服务器换成其他服务器,这样A的代码又需要较大改动
另外,一旦B挂了,A也会被连累挂掉
使用生产者消费者模型,就可以降低这里的耦合。
举例说明:
我们仍然是两个服务器A和B
但我们这里不再A直接调用B,而是在AB之间搞一个阻塞队列:
A给B发请求——A把请求送到阻塞队列里,然后B到阻塞队列里面取数据
B如果想返回结果——B把结果写回到队列里,A再从队列里取数据
对于请求来说:A是生产者,B是消费者
对于响应来说:A是消费者,B是生产者
而阻塞队列始终是交易场所
A只需要关注如何和阻塞队列交互,不需要认识B
B也只需要关注如何和阻塞队列交互,不需要认识A
A/B中的任意一个挂了,对于另一个影响几乎没有
综上生产者消费模型有如下优点:
优点1:能够让多个服务器程序之间更充分的解耦合
优点2:能够对于请求进行“削峰填谷”
削峰举例说明:
(1)不适用阻塞队列:
(2)使用阻塞队列
填谷举例说明:请求暴涨后它会有一个时间请求的大幅回落,而这个时候就可以把之前积压的请求由阻塞队列发给B服务器
削峰填谷:这样相对没有阻塞队列,就不会让B服务器太闲,也不会让它太忙
ps:实际开发中使用到的“阻塞队列”,并不仅仅是一个简单的数据结构了,而是一个/一组专门的服务器程序,并且它提供的功能也不仅仅是阻塞队列的功能,还会在这基础上提供更多的功能(对于数据持久化存储,支持多个数据通道、支持多节点容灾冗余备份、支持管理面板、方便配置参数。。。)
package thread;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class Demo21 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue=new LinkedBlockingDeque<>();
//队列实现可以基于数组,也可以基于链表,
//这里除了LinkedBlockingDeque<>,也可以是ArrayBlockingQueue<>
//入队列
queue.put("hello");
//这里会有一个异常抛一下:put操作可能会触发阻塞,阻塞需要去唤醒,唤醒可能就需要打断
//出队列
String s=queue.take();
//阻塞队列也提供:
// 入队列offer()
// 出队列poll()
//获取队头元素peek()等方法,
// 但是常见还是用put和take,因为put和take是带阻塞的,上面三个不带阻塞
}
}
我们自己实现阻塞队列也很简单:先实现一个普通队列,再加上线程安全和阻塞即可
而队列可以基于数组实现,也可以基于链表实现:
我们这里基于数组实现阻塞队列更简单,所以下面就直接写数组版本了(是一个循环队列)。
如下图:我们定义一个head指向队头,tail指向队尾。
其中,[head,tail)这样的前闭后开区间表示数组有效元素
入队列,就把新元素放到tail位置,并且tail++
比如我们这里插入一个1,tail++
再插入一个2,tail++
再插入一个3,tail++
以此类推,这样就构成了入队列操作。
那么出队列怎么办呢?
出队列,就把head位置的元素返回出去,并且head++
这里一个问题就是,tail一直++,然后加到数组尾部怎么办? 如下图:
这个时候tail想继续+,就得从头开始(循环队列)
另外,如何区分是空队列还是满队列呢?
我们之前说过,[head,tail)这样的前闭后开区间表示数组有效元素,那么如果head和tail重合了并且head位置,就是空的,如下图:
但是问题来了,如果整个数组都满了,head和tail也会重合,并且现在这种情况是不为空的。
对于上述情况,我们又下面的解决办法:
法(1)浪费一个格子:
head==tail认为空
head==tail+1认为是满
法(2)额外创建一个变量size,记录元素个数:
size=0,就是空
size=arr.length,就是满
代码如下:
class MyBlockingQueue{
//保存数据的本体
private int[] data=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== data.length){
locker.wait();//队列满了
//哪个对象加锁,就针对哪个对象wait
}
//队列不满,就把新元素放到tail位置上
data[tail]=value;
tail++;
//处理tail到达数组末尾的情况
if(tail>=data.length){
tail=0;
}//这里的if也可以直接写tail=tail%data.length,效果一样
size++;
//如果入队列成功,则队列非空,就唤醒take中的阻塞等待
locker.notify();
}
}
//出队列
public Integer take() throws InterruptedException {
synchronized (locker){
if(size==0){//为空不能出
locker.wait();
}
//不为空,取出head位置元素
int ret=data[head];
head++;
if(head>=data.length){
head=0;
}
size--;
//出队列成功之后,就唤醒put中的等待(之前满的时候要入,在wait)
locker.notify();
return ret;
}
}
}
public class Demo22 {
public static void main(String[] args) throws InterruptedException {
//测试
MyBlockingQueue Queue=new MyBlockingQueue();
Queue.put(1);
Queue.put(2);
Queue.put(3);
Queue.put(4);
int ret= Queue.take();
System.out.println(ret);//打印1
ret= Queue.take();
System.out.println(ret);//2
ret= Queue.take();
System.out.println(ret);//3
ret= Queue.take();
System.out.println(ret);//4
ret= Queue.take();
System.out.println(ret);//一直等待,不结束进程
}
}
注:
put里面如果是满的,会进行wait阻塞,然后如果有take成功了,数组里面不满,就会被take唤醒,然后进行put
take里面如果是空的,会进行wait阻塞,然后如果put成功了,数组里面不空,就会被put唤醒,然后进行take
可能会有同学问:如果没有等待,notify也会唤醒吗?
回答是——没有等待,notify啥也不干,没有副作用
另外,notify只能随机唤醒一个等待的线程,不能做到精准。
如果想精准,就必须使用不同的锁对象。
想唤醒t1,就o1.notify,让t1进行o1.wait
想唤醒t2,就o2.notify,让t2进行o2.wait
…
现在我们继续基于上面的队列实现一个生产者消费者模型:
public class Demo22 {
private static MyBlockingQueue queue=new MyBlockingQueue();
public static void main(String[] args) throws InterruptedException {
//实现一个简单的生产者消费者模型
//生产者
Thread producer=new Thread(()->{
int num=0;
while(true) {
try {
System.out.println("生产了"+num);
queue.put(num);
num++;
Thread.sleep(1000);
//通过sleep让生产者生产的慢一些,
//这样消费者就可以跟着生产者的步伐走(基本上就是生产一个消费一个)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
//消费者
Thread customer=new Thread(()->{
while(true){
try {
int num= queue.take();
System.out.println("消费了"+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
}
}
ps:到这里就写完了最简单的生产者消费者模式,如果你还想多几个消费者,你就再写几个线程。
定时器像是一个闹钟,进行定时。在一定时间后,(定时器)被唤醒并执行某个之前设定好的任务
我们之前学过的join(指定超时时间)和sleep(指定休眠时间)都是基于系统内部定时器来实现的。
标准库中有一个我们非常熟悉的java.util,而里面的java.util.Timer就是标准库中的定时器
java.util.Timer的核心方法就一个:schedule(安排)
也就是给定时器安排一个或者多个任务,通过这个方法,就可以把任务注册到定时器里面,然后我们给每个任务指定具体时间。于是也很好理解,我们这个方法有两个参数:1.任务是啥,2.多长时间后执行
我们先来看一下定时器的基本使用
public class Demo23 {
public static void main(String[] args) {
Timer timer=new Timer();
timer.schedule(new TimerTask() {//TimerTask实现了Runnable接口
@Override
public void run() {
System.out.println("我是 timer");
}
},3000);//第一个参数任务:说白了就是一段代码
System.out.println("我是main");
}
}
运行结果如下:
可能会有同学发现,我们的进程并没有跑结束,上面的红色方块依然存在。实际上,代码运行到这里,我们的进程没有跑完,你手动点一下红方块才会结束
为什么没完呢?——Timer内部有专门的线程,来负责执行注册的任务。代码走到里面任务结束之后,我们内部线程还要等待其他任务加进来。所以这个线程一直存在,从而导致进制不结束。
要实现定时器,我们要知道一个Timer内部需要有啥东西
(1)描述任务——创建一个专门的类来表示一个定时器中的任务
//创建一个类,表示一个任务
class MyTask{
//任务要做什么
private Runnable runnable;
//任务什么时候干,保存任务要执行的毫秒级时间戳
private long time;
public MyTask(Runnable runnable,long after){
//after表示一个时间间隔,不是绝对的时间戳的值
this.runnable=runnable;
this.time=System.currentTimeMillis()+after;//从现在开始的往后after一段时间
}
public void run(){
runnable.run();
}
public long getTime(){
return time;
}
}
(2)组织任务——使用一定的数据结构把一些任务给放到一起,通过一定的数据结构来组织
比如现在有多个任务过来了:
我们的需求就是,能够快速的找到所有任务中,时间最小的任务——使用数据结构中的堆
ps:排序成本比较高,而且我们很可能一边执行,一边加新的任务。在这种场景中,我们最高效的数据结构就是堆
而在标准库中,有一个专门的数据结构:PriorityQueue(优先级队列,内部是个堆)
class MyTimer{
//定时器内部要能够存放多个任务
private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
//此处队列要考虑线程安全问题,
//可能在多个线程里进行注册任务,同时还有一个专门的线程来取任务执行
//所以这里就涉及线程安全了,不能单纯的用PriorityQueue,我们用PriorityBlockingQueue
public void schedule(Runnable runnable,long after){
MyTask task=new MyTask(runnable, after);
queue.put(task);
}
}
(3)执行时间到了的任务
先执行时间最靠前的任务,就需要一个线程,不停的去检查当前优先队列的队头元素,看看当前最靠前的这个任务是不是时间到了。
我们再在MyTimer里面加构造方法:
class MyTimer{
//定时器内部要能够存放多个任务
private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
//此处队列要考虑线程安全问题,
//可能在多个线程里进行注册任务,同时还有一个专门的线程来取任务执行
//所以这里就涉及线程安全了,不能单纯的用PriorityQueue,我们用PriorityBlockingQueue
public void schedule(Runnable runnable,long after){
MyTask task=new MyTask(runnable, after);
queue.put(task);
}
public MyTimer(){
Thread t=new Thread(()->{
while(true){
try {
//先取出队头元素
MyTask task= queue.take();
//再比较一下看看当前这个任务时间到了没
long curTime=System.currentTimeMillis();
if(curTime<task.getTime()){
//时间没到,把任务再放回队列中
queue.put(task);
}else{
//时间到了,或者时间已经过了(比如你要7点交作业,你7点没交,那你后面得赶紧补交)
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
但是如果仔细分析,我们上述代码中,有两个比较严重的问题,
我们先来测试看一下第一个问题 :MyTask没有指定比较规则
调用代码如下,和标准库的调用是没啥区别的:
public static void main(String[] args) {
MyTimer myTimer=new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("我是timer");
}
},3000);
System.out.println("我是main");
}
但是运行的话,就报了的异常:
我们来仔细阅读一下这个异常:
而我们现在的代码为什么会有这种情况呢?
我们是要把元素放到优先级队列里面,而优先级队列内部又是一个堆,堆又是需要调整的,调整你就需要知道元素和元素之间的大小关系。
而我们刚才实现的MyTask这个类的比较规则,并不是默认存在的,需要我们手动指定,按照时间大小来比较。(当我们不是手动指定的时候,编译器也不知道按什么方式比较),于是就抛了异常
注:标准库中的集合类,很多都是有一定的约束和限制的,不是随便拿个类都能放到这些集合类里面去。
那我们现在让task实现compareable接口,然后重写一下compareTo方法
改进完后完整代码:
class MyTask implements Comparable<MyTask>{
//任务要做什么
private Runnable runnable;
//任务什么时候干,保存任务要执行的毫秒级时间戳
private long time;
public MyTask(Runnable runnable,long after){
//after表示一个时间间隔,不是绝对的时间戳的值
this.runnable=runnable;
this.time=System.currentTimeMillis()+after;//从现在开始的往后after一段时间
}
public void run(){
runnable.run();
}
public long getTime(){
return time;
}
@Override
public int compareTo(MyTask o){
return (int)(this.time-o.time);//让时间小的在前,时间大的在后
//time是long类型,我们这里返回是int,所以强转一下
}
}
class MyTimer{
//定时器内部要能够存放多个任务
private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
//此处队列要考虑线程安全问题,
//可能在多个线程里进行注册任务,同时还有一个专门的线程来取任务执行
//所以这里就涉及线程安全了,不能单纯的用PriorityQueue,我们用PriorityBlockingQueue
public void schedule(Runnable runnable,long after){
MyTask task=new MyTask(runnable, after);
queue.put(task);
}
public MyTimer(){
Thread t=new Thread(()->{
while(true){
try {
//先取出队头元素
MyTask task= queue.take();
//再比较一下看看当前这个任务时间到了没
long curTime=System.currentTimeMillis();
if(curTime<task.getTime()){
//时间没到,把任务再放回队列中
queue.put(task);
}else{
//时间到了,或者时间已经过了(比如你要7点交作业,你7点没交,那你后面得赶紧补交)
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
public class Demo24 {
public static void main(String[] args) {
MyTimer myTimer=new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("我是timer");
}
},3000);
System.out.println("我是main");
}
}
运行结果如下:
第一个问题解决了,运行结果好像和标准库里的一样,但其实还隐藏着第二个问题:
也就是任务还没到指定的开始时间,这个线程一直在等,既没有实质效果,也没有休息好——造成资源浪费。
解决办法:可以基于wait这样的机制来实现,
wait有一个版本,指定等待时间(不需要notify,时间到了自然唤醒)
我们让它等待什么时间呢——可以计算出当前时间和任务目标时间差,就等这个时间差。
private Object locker=new Object();
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{
//时间到了,或者时间已经过了(比如你要7点交作业,你7点没交,那你后面得赶紧补交)
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
这里可能还有同学问,既然是指定一个等待时间,为啥不直接用sleep而是再用一下wait呢?
sleep是不能中途被唤醒的,wait能中途被唤醒。
比如你现在没有任务,你老师让你5点写作业,你定一个时间10分后写作业。但是没过一会,你老师又让你去拿个试卷,你应该立即去而不是等10分钟写作业。
在等待过程中,可能要插入新的任务,而且新的任务可能出现在之前所有任务最前面!在schedule操作中,每次新加一个任务,就需要加上一个notify操作
public void schedule(Runnable runnable,long after){
MyTask task=new MyTask(runnable, after);
queue.put(task);
//每次任务插入成功之后,都唤醒一下扫描线程,
//让线程重新检查一下队头任务,看新插入的任务是否要执行
synchronized (locker){
locker.notify();
}
}
说到线程池,大家一定不会陌生,我们之前也是有接触过池的概念:
进程,比较重,如果频繁创建销毁,开销就比较大。那解决方案就是进程池或者线程。
线程,比进程轻,但是如果创建销毁的频率进一步增加,仍然会发现开销也还是较大的。解决方案:线程池或协程
线程池:把线程提前创建好,放到池子里备着,如果后面需要线程,直接从池子里取,不必从系统这里申请。如果线程用完了,也不必还给系统,直接放回池子,以备下次再用,这样创建和销毁的过程速度就会更快。
那这里就会有同学问:“为什么线程放池子里,就比从系统申请释放来的快?”
操作系统中我们分两种状态:用户态、内核态
如下,是我们操作系统的软硬件结构图:
我们自己写的代码,有一部分就是在最上面的应用程序这一层来运行的,这里的代码称为“用户态”运行的代码。
有些代码,需要调用操作系统的API,进一步的逻辑就会在内核中执行
例如,调用一个System.out.println,本质上要经过write系统调用,进入到内核中,内核执行一堆逻辑,然后控制器输出字符串。。。
在内核中运行的代码,就称为“内核态”运行的代码
创建线程,本身就需要内核的支持(创建线程本质是在内核中搞个PCB,加到链表里),调用的Thread.start其实归根结底,也是需要进入内核态来运行。
而把创建好的线程放到“池子里”,由于池子就是用户态实现的,这个放到池子里/从池子里取,这个过程就不需要设计到内核,就是纯纯的用户态代码就能完成。
所以,一般认为,纯用户态操作,效率要比经过内核态处理的操作效率更高。
标准库的线程池叫做:ThreadPoolExecutor
它一共有4个构造方法,我们看其中参数最复杂的一个,学会这个,其他3个就迎刃而解了。
举例说明:
我们把线程池想象成一个公司,公司里有很多员工在干活,把员工分成两类:
1.正式员工——允许摸鱼
2.临时员工——不允许摸鱼
比如刚开始的时候,假设公司要完成的工作不多,正常员工完全能搞定,就不需要临时员工了。
如果公司的工作突然猛增,正式员工加班也搞不定,就需要雇佣一批临时工
过了一段时间,公司工作很少了,正式员工自己摸鱼也可以搞定所有工作。那临时工就没事做了,让临时工摸鱼还给他发工资,公司就会亏损了,所以就不需要这些临时员工了,要辞退。
构造方法参数:
int corePoolSize 核心线程参数(正式员工数量)
int maximumPoolSize 最大线程数(正式员工+临时员工)
long keepAliveTime 允许临时工摸鱼时间
TimeUnit unit 时间的单位(s,ms,us…)
BlockingQueue < Runnable> workQueue 任务队列
线程池会提供一个submit方法,让程序员把任务注册到线程池中,加到这个任务队列中
ThreadFactory threadFactory 线程工厂,线程是怎么创建出来的
RejectedExecutionHandle handler 拒绝策略
换句话说,当任务队列满了我们怎么做?
1.忽略最新的任务
2.阻塞等待
3.丢弃最老的任务
…
虽然线程池参数很多,但是使用的时候最重要的参数还是第一组参数——线程池中线程的个数。
有一个程序,这个程序要并发的/多线程的来完成一些任务,如果使用线程池的话,这里的线程数比较合适?
标准库中还提供了一个简化版本的线程池:Executors
本质是针对ThreadPoolExecutor进行了封装,提供了一些默认参数。我们来看看是怎么操作的:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo25 {
public static void main(String[] args) {
//创建一个固定线程数目的线程池,参数指定了线程个数(常用版本)
ExecutorService pool= Executors.newFixedThreadPool(10);
//创建一个自动扩容的线程池,会根据任务量自动进行扩容
//Executions.newCachedThreadPool();
//创建一个只有一个线程的线程池
//Executors.newSingleThreadExecutor();
//创建一个带有定时器功能的线程池,类似于Timer
//Executors.newScheduledThreadPool();
for(int i=0;i<100;i++){//100个任务,分给10个线程来完成
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello threadpool");
}
});
}
}
}
我们这里仿照Executors实现一个线程池
而要实现一个线程池,就得先知道线程池里面有啥
1.能够描述任务(可直接使用Runnable)
2.能够组织任务(可直接使用BlockingQueue)
3.能够描述工作线程
4.能够组织这些线程
5.需要实现,能往线程池里添加任务
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
class MyThreadPool{
//1.描述一个任务——直接使用Runnable
//2.使用一个数据结构来组织若干任务——使用阻塞队列
private BlockingQueue<Runnable> queue =new LinkedBlockingDeque<>();
//3.描述一个线程
static class worker extends Thread{
//当前线程池里有若干worker线程,这些线程内部都持有了上述的任务队列
private BlockingQueue<Runnable> queue=null;
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<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();
}
}
}
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");
}
});
}
}
}