Java——多线程-程序员宅基地

技术标签: java  开发语言  

目录:

为什么要有多线程呢?
多线程的两个概念
多线程的实现方式
常见的成员方法
线程安全的问题
死锁
生产者和消费者

一、为什么要有多线程呢?

1.1 线程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位

1.2 进程

进程是程序的基本执行实体。

一个软件运行之后就是进程。

当互相独立的,能同事运行的功能比较多就是多线程。

例子:

从负责一个流水线到负责三个,工作效率提高了。

内存中创建变量也需要时间。在此期间,CPU只能等待。

单线程:从头往下依次执行。CPU不会切换到其他代码去运行,所以效率比较低。

多线程的特点:同时的做多件事情。CPU可以在多个程序之间切换,把等待的空闲时间充分利用。最大特点:提高程序的运行效率。

多线程的应用场景

方法多开与多个线程同时处理一个内存数据的故事。

总结

1.什么是多线程?
        有了多线程,我们就可以让程序同时做多件事情
2.多线程的作用?
        提高效率,利用程序运行当中的等待时间,让CPU在多个程序间进行切换。从而提高程序的运行效率。
3.多线程的应用场景?
        只要你想让多个事情同时运行就需要用到多线程
        比如:软件中的耗时操作、所有的聊天软件、所有的服务器

二、多线程的两个概念

并发:在同一时刻,有多个指令在单个CPU上交替执行。

并行:在同一时刻,有多个指令在多个CPU上同时执行。

右手拿鼠标、、可乐交替执行,就是并发。

电脑不就一个CPU吗?但是一个CPU有:

这里的4、8、16、32、64就表示电脑能够同时运行多少条线程。

以2核4线程为例:

当计算机中只有四个线程是不用切换的。

当线程越累越多,这4条线程就会在多个线程之间随机切换。

所以,在计算机当中,并行和并发是有可能同时发生的。

总结

1.并发:在同一时刻,有多个指令在单个CPU上交替执行
2.并行:在同一时刻,有多个指令在多个CPU上同时执行

三、多线程的实现方式

①继承Thread类的方式进行实现

Thread就表示Java中的一个线程,如果想拥有一个线程,就创建对象,并开启即可。

例子:

创建两个线程:t1、t2。如果不起名称,他俩执行时没有区别,无法显示,所以就setName,执行时getName()即可。

是两个线程交替执行的。


②实现Runnable接口的方式进行实现

这里不能直接使用getName方法进行获取线程名字,因为getName()是Thread类中的方法,MyThread直接调用父类的getName()方法。而现在的MyRun没有继承,所以没有getName()。

可以先获取到线程,再getName():

和刚刚的一样:

③利用Callable接口和Future接口方式实现

前两种的重写Run方法,没有返回值,无法知晓多线程运行的结果。

Future是一个接口不能直接创建对象,所以创建一个实现类FutureTask的对象。

这里的范型,表示返回结果的类型:

然后再重写抽象方法,call方法返回值和范型保持一致。

获取结果:FutureTask.get()即可,抛出异常。

总结

多线程三种实现方式对比

Java中是单继承。


四、常见的成员方法

线程优先级默认是5,最小是1,最大是10.

优先级越大,抢占的CPU的概率是越高的。

守护线程:备胎线程。

join不是静态方法。

细节

默认线程是否有名字:

源码:

线程细节:

构造方法不能继承,所以子类MyThread不能使用父类Thread的构造方法.

优先级?调度?

说到线程优先级,那就必须说到调度。

1.抢占式调度

多个线程抢夺CPU的执行权。CPU在什么时候执行那条线程是不确定的,执行时长也是不确定的。体现了——随机性。

2.非抢占式调度

所有的线程轮流执行,执行时间也是差不多的。你一次、我一次。

在Java中是采用抢占式调度的方式。

没有设置优先级,默认是5。

JVM中启动的main线程的优先级也是5.

 ctrl+f12:查看当前.java的所有方法。

优先级并不是绝对的,而是概率问题。

守护线程Daemon

守护线程应用场景?

当关闭聊天,传输文件也没有执行下去的必要了,设置为守护线程也会慢慢停掉。

礼让线程(少)_yield()

能够让线程执行的时候尽可能的均匀点呢?

解释:当飞机线程执行完毕之后,就会出让CPU的执行权,那么下一次运行时,飞机和坦克就重新再抢夺CPU的执行权。有可能使执行结果尽可能的均匀。

插队/入线程_join()

main线程和t线程抢夺CPU执行权。

插入到当前线程之前,thread.join();

相当于把多线程又串在一起成了一个单线程。局部单线程可以看作是。

线程的生命周期

五、线程安全的问题

需求:某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票。

票不可能几秒钟之内卖光,所以需要模拟sleep一下。

但是现在的执行结果是有问题的。看起来窗口1、2、3是独立的票。现在相当于卖了300张票,但是现在只有100张票。

是因为数据不可见性,修改:

因为static修饰的基本数据类型和引用存在栈内存中,栈内存数据是共享的

现在仍有问题:1.超出范围的票         2.重复的票。

               

如何解决呢?

5.0买票引发的安全问题

线程在执行代码的时候,CPU的执行权随时有可能被其他线程抢走。

①相同的票出现了多次

当线程1抢到CPU执行权时,tocket就++变为了1,没来得及打印然后线程2抢到执行权,也对ticket做自增为2。如果现在又被线程3抢走了,ticket就变成了3。

这时候,当这三个线程在打印票号时,就都是3了。

根本原因:线程执行的随机性。


②出现了超出范围的票

当ticket到99了,此时3个线程抢夺CPU的执行权,当线程1抢到了CPU的执行权睡了10毫秒,睡的时候执行权被2抢到了,2也睡,2睡的时候被3抢了执行权,陆陆续续醒来继续执行下面的代码。

 1抢到执行权进行自增为100,没来得及打印,被2抢走自增为101,然后又被3抢走自增为102,最后陆陆续续打印。就都是102了。

修改

加锁:把操作共享数据的代码锁起来

因为只有操作共享库存时需要锁起来单个执行,其他的部分需要并行。

这里讲的是同步锁,更多的是保证安全的,效率肯定相对低,弹幕偏题了。

利用同步代码块,把操作共享数据的代码锁起来,让同步代码块中的代码是轮流执行的。

效果:

5.1同步代码块两个小细节

synchronized不能放到while的外面,当有线程1进来时,就会上锁,直到线程1把所有的代码(票卖完)执行完之后才会释放锁。

锁对象必须是唯一的。当是不同的锁的话,就没有意义了。1线程看a锁,2线程看b锁。

类锁只有一把。

我们可以将对象锁改为当前类的字节码文件:在同一个文件夹里面,字节码文件不能重复也就是只能有一个MyThread.class文件。

5.2 同步方法

就是把synchronized关键字加到方法上。

同步代码块,把一段操作共享数据的代码块锁起来,可以解决多线程操作共享数据时带来的数据安全问题。

当想要把一个方法里面的所有代码块都锁起来,就没必要使用同步代码块,而是直接使用同步方法

锁对象不能自己指定,是Java规定好的。

先写同步代码块,然后把同步代码块中的代码抽取成一个方法就ok了。

抽取方法:ctrl+alt+m

最终版:

非静态的锁是this,也就是mr,在这段代码中,mr是唯一的。

细节

当线程执行完毕后,锁是自动关闭的。无法监控锁的开关。

5.3 LOCK锁

手动加锁、释放锁。

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清请晰的表达如何加锁和释放锁JDK5以后提供了一个新的锁对象Lock

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
LOCK中提供了获得锁和释放锁的方法
void lock():获得锁
void unlock():释放锁

LOCK是接口不能直接实例化,这里采用它的实现类ReentrantLock:来实例化
ReentrantLock的构造方法
ReentrantLock():创建一个ReentrantLock的实例

直接使用空参构造即可。

在前面增加static,表示所有的对象共享同一把锁。

上面解决了重复问题,但是又有新的问题——程序没有停止:

会导致2、3线程一直停留在18行代码,那么程序就一直不会停止了。

ctrl+alt+t:采用try-catch-finally机制,无论如何都会执行lock.unlock();

六、死锁

在程序当中出现了锁的嵌套,外面一个锁,里面一个锁。

死锁是一个错误。

A线程拿着A锁,B线程拿着B锁,他们都在等着对方释放锁。

七、生产者和消费者

也叫做等待唤醒机制。

生产者消费者模式是一个十分经典的多线程协作的模式。

线程的执行是有随机性的,有两条线程执行,结果可能是这样的:

等待唤醒机制就是要打破随机的规则,让两个线程轮流执行。

就是等待唤醒机制中多线程的执行结果。

7.0 生产者和消费者的理想情况

生产者强到CPU的执行权,生产数据,然后再让消费者执行进行消费数据。

但是由于线程的执行是随机性的。

只有两种情况。

7.1 生产者和消费者(消费者等待)

首先,消费者获得执行权,但是没有数据可以消费,所以就wait……

一旦wait,CPU的执行权一定会被生产者抢到,进行生产数据。此时,消费者还在wait,所以需要生产者唤醒(notify)。

消费者一旦被唤醒就开始消费数据。

7.2 生产者和消费者(生产者等待)

首先,生产者获取到CPU执行权,然后进行生产数据,并且唤醒等待中的消费者,此时没有待唤醒的线程也没有关系,代码不受影响。

下一次,仍然是生产者获取到CPU执行权,此时不能再生产数据了,因为桌子上已经有了。

所以生产者只能wait:

改进:

此时,一旦生产者wait,CPU的执行权就会被消费者获得进行消费。

7.3 完整逻辑

7.4 生产者和消费者(常见方法)

wait会释放资源。

等待唤醒机制(基本实现---第一种方式)

代码示例:

1.Foodie(消费者、吃货):
/**
 * 消费者、吃货
 */
public class Foodie extends Thread {
	@Override
	public void run() {
		/**
		 * 多线程的四部曲:
		 * 1.循环
		 * 2.同步代码块
		 * 3.判断共享数据是否到了末尾(到了末尾)
		 * 4.判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
		 */
		while (true) {
			synchronized (Desk.lock) {
				if (Desk.count == 0) {
//					System.out.println("没有菜了,吃货停止吃");
					break;
				} else {
					//先判断桌子上是否有面条
					if (Desk.foodFlag == 0) {
						//如果没有,就等待
						try {
							Desk.lock.wait(); //让当前线程跟锁进行绑定
						} catch (InterruptedException e) {
							throw new RuntimeException(e);
						}

					} else {
						//把吃的总数-1
						Desk.count--;
						//如果有,就开吃
						System.out.println("吃货还能吃" + Desk.count + "碗");
						//吃完之后,唤醒厨师继续做
						Desk.lock.notifyAll(); //唤醒跟这把锁绑定的所有线程

						//修改桌子的状态工,表示原本的一碗吃完了
						Desk.foodFlag = 0;

					}
				}
			}
		}
	}
}
2.Cook(生产者、厨师):
/**
 * 生产者
 */
public class Cook extends Thread {
	@Override
	public void run() {
		/**
		 * 多线程的四部曲:
		 * 1.循环
		 * 2.同步代码块
		 * 3.判断共享数据是否到了末尾(到了末尾)
		 * 4.判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
		 */
		while (true) {
			synchronized (Desk.lock) {
				if (Desk.count == 0) {
					//消费者吃不下了
					break;
				} else {
					//判断桌子上是否有食物
					if (Desk.foodFlag == 1) {
						//如果有,就等待
						try {
							Desk.lock.wait();
						} catch (InterruptedException e) {
							throw new RuntimeException(e);
						}
					} else {
						//如果没有就继续生产食物
						System.out.println("厨师做了一碗面条");
						//修改桌子上的食物状态
						Desk.foodFlag = 1;

						//唤醒等待的消费者开吃
						Desk.lock.notifyAll();
					}
				}
			}
		}
	}
}
3.ThreadDemo(主线程):
public class ThreadDemo {
	public static void main(String[] args) {
		/**
		 * 需求:完成生产者和消费者(等待唤醒机制)的代码
		 * 实现线程轮流交替执行的效果
		 */

		//创建线程对象
		Cook cook = new Cook();
		Foodie foodie = new Foodie();

		//给线程起名字
		cook.setName("厨师");
		foodie.setName("吃货");

		//开启线程
		cook.start();
		foodie.start();

	}
}

运行结果:

厨师做了一碗面条
吃货还能吃9碗
厨师做了一碗面条
吃货还能吃8碗
厨师做了一碗面条
吃货还能吃7碗
厨师做了一碗面条
吃货还能吃6碗
厨师做了一碗面条
吃货还能吃5碗
厨师做了一碗面条
吃货还能吃4碗
厨师做了一碗面条
吃货还能吃3碗
厨师做了一碗面条
吃货还能吃2碗
厨师做了一碗面条
吃货还能吃1碗
厨师做了一碗面条
吃货还能吃0碗

7.5 等待唤醒机制(阻塞队列方式实现---第2种方式)

上面是等待唤醒机制的最基本写法,还有另一种实现方式是阻塞队列方式。

原理:

厨师做好面条后就可以放到管道中,而消费者就可以从管道当中获取面条去吃。

可以规定管道当中最多可以放多少碗面条。

如果最多只能放一碗,则和唤醒机制的最基本写法一致(也就是第一种方式)。

做一碗吃一碗,中间的就是阻塞队列。

队列:数据在管道中排队。先放进去的队列最先被消费者拿到。先进先出。

阻塞:当队列中放满了,厨师就会wait()。或者队列中没有,吃货也会wait(),什么也干不了。

阻塞队列的继承结构

阻塞队列的体系结构

阻塞队列一共实现了4个接口:

最顶层是Iterable,也就表示阻塞队列可以使用迭代器或者是增强for来进行遍历。

第二个Collection,也就是说则色队列也就是单列集合。

第三个Queue表示队列。

第四个BlockingQueue表示阻塞队列。

注意:上面是四个接口,并不能直接创建对象,需要对实现类进行创建对象即可。

有界:有长度限制,创建时必须指定长度。

无界:没有长度限制,创建时不需要指定。最大值是int的最大值。

不能在Cook和Foodie中同时创建阻塞队列:此时,厨师用的自己的阻塞队列,吃货也用的是自己的阻塞队列,两个不是同一个。

可以直接写在测试类中。

ArrayBlockingQueue的put方法的底层实现了锁,所以就不需要我们再写锁了。

ArrayBlockingQueue的take方法原理:

代码示例:

1.cook():

public class Cook extends Thread {

	//通过构造方法传入阻塞队列
	//成员变量
	ArrayBlockingQueue<Object> queue;

	public Cook(ArrayBlockingQueue<Object> queue) {
		this.queue = queue;
	}


	@Override
	public void run() {
		while (true) {
			//不断的把面条放到阻塞队列当中
			try {
				queue.put("面条");
				System.out.println("厨师放了一碗面条");
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}

		}


	}
}

2.foodie():

public class Foodie extends Thread {

	//通过构造方法传入阻塞队列
	//成员变量
	ArrayBlockingQueue<Object> queue;

	public Foodie(ArrayBlockingQueue<Object> queue) {
		this.queue = queue;
	}

	@Override
	public void run() {
		while (true) {
			//不断从阻塞队列中获取面条
			try {
				Object food = queue.take();
				System.out.println(food);
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}

		}


	}
}

3.ThreadDemo():

public class ThreadDemo {
	public static void main(String[] args) {
		/**
		 * 需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码
		 *
		 * 细节:
		 *      生产者和消费者必须使用同一个阻塞队列
		 */
		//1.创建阻塞队列的对象
		ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<>(1);
		//2.再通过创建对象的方式,把阻塞队列传递给生产者和消费者,就可以实现两种使用的是同一种阻塞队列
		Cook cook = new Cook(queue);
		Foodie foodie = new Foodie(queue);

		//3.开启
		cook.start();
		foodie.start();


	}
}

并且这段代码不会自动停掉,需要手动停掉。

运行时发现,有重复的:这是因为souv语句是定义在锁外的就会导致重复。

因为queue里面的方法是带锁的,若此线程还没被唤醒,那么就会反复执行wait直到被唤醒,此时打印语句无论是wait还是notify都会执行一次,而我们只需要notify的时候才需要打印。

遗留问题:如何正确打印?why?

线程的状态

JVM中是没有定义运行状态的,这里是为了方便记。

新建、就绪、阻塞、等待、计时等待、结束(死亡)状态。

当前线程抢到CPU执行权时,此时虚拟机就会把当前线程交给操作系统去管理,虚拟机就不管了。

所以就没有定义运行状态。

综合练习

前三题简单

多线程练习1(卖电影票)

一共有100张电影票,可以在两个窗口领取,假设每次领取的时间为300毫秒,
要求:请用多线程模拟卖票过程并打印剩余电影票的数量

多线程练习2(送礼品)

有100份礼品,两人同时发送,当剩下的礼品小于10份的时候则不再送出。
利用多线程模拟该过程并将线程的名字和礼物的剩余数量打印出来.

多线程练习3(打印奇数数字)

同时开启两个线程,共同获取1-100之间的所有数字。
要求:将输出所有的奇数。

有坑。

多线程练习4(抢红包)

抢红包也用到了多线程。
假设:100块,分成了3个包,现在有5个人去抢。
其中,红包是共享数据
5个人是5条线程
打印结果如下:
        XXX抢到了XXX元
        XXX抢到了XXX元
        XXX抢到了XXX元
        XXX没抢到
        XXX没抢到

多线程练习5(抽奖箱抽奖)

有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为
{10,5,20,50,100,200,500,800,2,80,300,700};
创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2
随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
                每次抽出一个奖项就打印一个(随机)
                抽奖箱1又产生了一个10元大奖
                抽奖箱1又产生了一个100元大奖
                抽奖箱1又产生了一个200元大奖
                抽奖箱1又产生了一个800元大奖
                抽奖箱2又产生了一个700元大奖

多线程练习6(多线程统计并求最大值)

在上一题基础上继续完成如下需求:
        每次抽的过程中,不打印,抽完时一次性打印(随机)
        在此次抽奖过程中,抽奖箱1总共产生了几个奖项。(这里奖项是随机的,例子是6个)
                分别为:10,20,100,500,2,300最高奖项为300元,总计额为932元
        在此次抽奖过程中,抽奖箱2总共产生了几个奖项。(这里奖项是随机的,例子是6个)
                分别为:5,50,200,800,80,700最高奖项为800元,总计额为1835元

多线程内存图:

在Java中,堆内存是唯一的,而栈内存不是唯一的。

栈内存和线程有关,每个线程都有自己的栈。所以也会成为线程栈。

程序的主入口main方法,就是运行在main线程的栈当中。

首先会创建线程:

对象在堆中。在对象中会有一些成员变量,从父类中继承下来的name.

也一定会有成员变量a,每个对象都有自己的a。

当对象创建完毕之后,t1、t2分别记录这两个对象的地址值。前两行代码才算执行完毕。

setName方法会进入到main线程的栈当中,修改对象中的名字。

执行start()完毕之后,表示线程已经开启了。

在内存中的表示是多了线程1的栈和线程2的栈:每个线程都有自己独立的栈空间。

无论什么线程都会执行run方法,所以此时run方法就会进栈。而且线程1、2的栈都要进去。

run方法有一个局部变量b,在线程1、2中都有。

而且这两个b是互相独立的。

同理改为集合:

多线程练习7(多线程之间的比较)

在上一题基础上继续完成如下需求:
在此次抽奖过程中,抽奖箱1总共产生了6个奖项,分别为:10,20,100,500,2,300
        最高奖项为300元,总计额为932元 (找到最大值)
在此次抽奖过程中,抽奖箱2总共产生了6个奖项,分别为:5,50,200,800,80,700
        最高奖项为800元,总计额为1835元(找到最大值)
在此次抽奖过程中,抽奖箱2中产生了最大奖项,该奖项金额为800元 (最难)
以上打印效果只是数据模拟,实际代码运行的效果会有差异

思路1:比较两个线程的大小时,需要两个线程运行完毕才能比较。不知道哪个线程先执行完,也不知道比较代码写在哪里。

多线程练习8(多线程阶段大作业)

在IO的基础上继续做。

给每日一记增加导出和导入的功能
提示:
1,增加菜单,菜单中显示导出和导入功能
2,导出:将所有日记文件打包成压缩包放到桌面上压缩包名为data.zip
3,导入:默认将桌面上的data.zip压缩包解压,获取里面所有的数据展示出来

因为导出和导入是浪费时间的耗时操作,所以可以开启一个线程单独完成。

五、线程池

5.0 以前写多线程的弊端

之前是当需要线程的时候就去创建一个线程对象,用完,线程就会消失。

这样会浪费操作系统的资源,所以需要改进。

准备一个容器存放线程,这个容器就叫做线程池。

开始时,线程池是空的,没有线程,当给线程池提交一个任务时,线程池就会自动的创建一个线程,然后用这个线程去执行任务,执行完毕,再把线程还给线程池。

当再次提交任务时,就不需要创建线程了,而是拿着已经存在的线程去执行任务。

执行完毕,再还给线程池,这就是线程池的执行原理。

5.1 线程池(特殊情况)

java21的虚拟线程速度慢,吞吐量高了。

场景:在提交第二个任务的时候,线程还正在执行第一个任务,没有还回去,此时线程池就会创建一个新的线程。拿着新的线程去执行第二个任务。在这个过程中,又提交了很多其他的任务。

此时就会创建新的线程,执行新提交的任务。

执行完毕,会把线程还给线程池。

线程池上限

线程池上限可以自己设置。

此时设置最大线程数量为3,那么这三个线程就会执行前面的三个任务。

后面的两个任务只能先排队等着:

5.2 线程池主要核心原理

①创建一个池子,池子中是空的

②提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子。
下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可。

③如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待。

 线程池代码实现

1,创建线程池


2,提交任务

提交任务时,线程池的底层会创建线程或者是复用已经存在的线程。这些代码不需要自己写,是线程池的底层自动去实现。

我们要负责的就是给他提交任务就可以了。

3,所有的任务全部执行完毕,关闭线程池

一般实际开发过程中,线程池是不会关闭的。因为服务器是24小时运行的,服务器不关闭,也就是随时随地可能会有任务执行,那么线程池也就不会关闭。

Executors工具类

Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。

这里的newCachedThreadPool并不是真正意义上的没有上限,上限为int的最大值。

但是一般没有到最大值,电脑就崩溃了。

public static ExecutorService newCachedThreadPool()      //创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool(int nThreads)        //创建有上限的线程池

可以提供Runnable和Callable实现类。

JDK 17弃用警告: 自Java 9开始,JDK对Executors类的部分静态工厂方法(如newFixedThreadPool、newSingleThreadExecutor和newCachedThreadPool)发出弃用警告,因为它们没有提供对线程池大小。

 新建时是空的:

workQueue是当前正在排队的任务:

总结

①创建一个池子,池子中是空的
②提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子
下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
3 如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待

通过Java工具类去获取到线程池,方便但是不够灵活:

场景

当提交的任务比较多,任务就会排队等待,如果想修改队伍的长度,此时就修改不了。

那能够不用工具类Executors而自己创建线程池的对象呢?这样就可以对想要的参数进行设置。

newFixedThreadPool底层创建了个ThreadPoolExecutor对象:

7个参数

临时线程创建是阻塞队列满了才会创建。

5.3 自定义线程池

核心线程不会被销毁,除非销毁整个线程池。

而临时线程则在一定的空闲时间之后没有任务调用它,则会销毁。

线程池中的最大线程数量包括核心线程和临时线程。

5.3.1 场景:

当前线程池核心线程3个,临时线程3个。

当提交了三个任务,此时就会创建3个线程去处理这三个任务。

当提交了5个任务时,不是直接创建5个线程去执行任务:

因为线程池只有3个核心线程,所以创建3个线程去执行3个任务,其他2个任务在阻塞队列中排队等待,等有了空闲的线程时再去执行任务。

当提交了8个任务时,由于核心线程为3,所以新建3个线程去处理3个任务,其任务他排队:

当此时队列长度为3:

任务4、5、6会进入到队列中等待:

还剩下任务7、8,这时候线程池才会去创建临时线程去执行任务7、8.

什么时候创建临时线程?

(当核心线程都在工作,并且阻塞队列都排满了才会去创建临时线程。)

临时线程创建时机:当核心线程全都占用且等待队列中的数量已经超过设定的阈值的时候,才会触发临时线程的创建。

先提交的任务先执行?

任务执行时一定是按照任务提交的顺序来执行的吗?

不是。此时任务4、5还在队列中排队,而后提交的7、8已经在执行了:

当提交的 任务数量 > 核心线程数+临时线程数+队伍长度 时:

此时如果会有其他的任务就会出发拒绝策略:

5.3.2 自定义线程池(任务拒绝策略)

将等待最久的任务4丢弃,然后将任务10加入到队列中。

5.3.3 创建自己的线程池对象

观察底层线程工厂,其实也就是在底层new Thread()

策略是ThreadPoolExecutor中的静态内部类.

why把拒绝策略定义为内部类呢?

内部类是依赖外部类而存在的,单独出现没有任何的意义。而内部类的本身又是一个独立的个体。

总结:

自定义线程池的七个参数:

线程池的工作原理:

5.3.4 线程池多大合适呢?

线程池的大小不会随便写的,是有计算公式的。

先来搞明白几个概念。

最大并行数

与电脑的CPU型号有关。

4核8线程:电脑有4个大脑,能够同时并行的去做4件事情。

英特尔发明了超线程技术,可以把原本的4个大脑虚拟成8个——8线程。

针对4核8线程的电脑的最大并行数8.

查看最大并行数

win:1.打开我的电脑(win+e)——右键空白处、属性——(左上角)设备管理器

——点击处理器(可以看到有多少个并行处理器)

2.任务管理器——性能——CPU(可以看几核几线程)

特殊情况

极少部分的操作系统,不会把CPU的所有资源都交给一个软件用,所以从任务管理器看不是很稳妥。可以通过代码去获取。

	public static void main(String[] args) {
		//向Java虚拟机返回可用处理器的数目
		int i = Runtime.getRuntime().availableProcessors();
		System.out.println("i = " + i);
	}

返回的数量=本机电脑的最大并行数,就说明Java是可以使用操作系统中的所有资源的。

线程池多大合适呢?

根据项目是什么类型的,一般将项目分为两种类型:

1. CPU密集型运算

如果项目中计算比较多,读取本地文件或数据库比较少。

公式:最大并行数+1

这样可以实现最优的CPU调用。

why要+1呢?

为了保证当前项目由于夜缺失故障,或者其他原因导致线程暂停,当线程有问题了,额外的这个线程就可以顶上去,可以保障CPU的时钟周期不被浪费。

2. I/O密集型运算

如果项目中读取本地文件比较多,读取数据库也比较多,就是I/O密集型运算型项目。

在这个项目中,CPU并不总是处于繁忙状态的。比如当执行业务计算的时候,此时会使用CPU资源,但是当进行I/O操作的时候,或者远程调用RPC操作的时候/操作数据库时,这时CPU就闲下来了,就可以利用多线程技术把闲下来的时间利用起来,从而提高CPU的利用率。

4核8线程举例:

读取数据和硬盘有关系,和CPU没有关系,只有下面的相加和CPU有关系。假设都是耗时1秒钟,那么读取数据就相当于CPU闲置等待的时间。

这个公式也就是说,等待时间越长,可以越多的设置线程。

那么就可以规定此时的线程池大小就是16.

但是CPU计算时间和等待时间,我们并不知道,可以利用thread dump进行测试,然后就可以拿到结果套用公式了。

how use thread dump?

5.4 线程池的其他问题

如果是分布式微服务就非常有用了,平时很少用。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Chao_nengli/article/details/137459337

智能推荐

攻防世界_难度8_happy_puzzle_攻防世界困难模式攻略图文-程序员宅基地

文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文

达梦数据库的导出(备份)、导入_达梦数据库导入导出-程序员宅基地

文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作  导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释:   cwy_init/init_123..._达梦数据库导入导出

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

随便推点

Thinkpad X250 secure boot failed 启动失败问题解决_安装完系统提示secureboot failure-程序员宅基地

文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf

推荐文章

热门文章

相关标签