前言
多线程一直是头痛的问题。面对面试官的连环炮,不得不掏出我的笔记跟他好好battle一下。
以下笔记整理自Java Guide面经
&&Java核心卷
什么是多线程,为什么要有多线程
首先,我们明白一个知识点都是从,是什么,为什么这样去理解的。
在计算机中,我们是不是经常感觉到我们的软件都是一起运行的?实则不然,如果有小伙伴看过现代操作系统
这本书,那么会知道一个名词叫时间片轮转调度
。
时间片轮转调度:通俗的讲就是,你以为的一起运行,实际上就是cpu在每个软件上都运行了一会儿然后跳到了另一个软件,这就是我们所看到的一个假象。
多线程,意为着多操作。就比如说售票员卖票,你总不能一个人卖对吧,那排队得拍到什么时候。再者说,举个例子:你有一天给你的女神发了一句消息,女神回复你了。此时,你的女神就是资源,你就是线程1,当然,肯定还会有线程2,线程3。懂了吧。
那为什么要有多线程?简而言之,现在的cpu太牛逼,如果是单核,那么到无所谓,但是多核的话,总能一核有难,九核围观。所以,最终的目的还是为了合理的分配cpu资源。
并发与并行
经常听大佬们说高并发高并发,所以我就向大佬们请教一下到底什么是并发。来张图。
灵魂画手,请勿介意。
并发:在同一时间段,多个任务都在执行
再来看看并行
并行:在单位时间内,多个任务同时执行
这两张图应该比较好理解了。
线程的生命周期
Java线程的生命周期在运行时刻只会存在下面着六种状态中的一种。
/**
* 线程的生命周期状态
*
* @author: Zxy
* @Date: 2021-05-26
**/
public enum State {
/**
* 初始状态
*/
NEW,
/**
* 运行状态
*/
RUNNABLE,
/**
* 阻塞状态
*/
BLOCKED,
/**
* 等待状态
*/
WATTING,
/**
* 超时等待
*/
TIME_WAITING,
/**
* 终止状态
*/
TERMMINATED
}
Java线程的状态并不是一成不变的,而是随着我们的代码在改变。话不多说,上图:
创建线程的四种方式
1、Thread类创建
public class ThreadDemo01 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("ThreadDemo01线程执行第"+i+1+"次,当前时间为:"+ DateUtil.now());
}
}
}
2、实现Runnable接口
public class ThreadDemo02 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("ThreadDemo02线程执行第"+i+1+"次,当前时间为:"+ DateUtil.now());
}
}
}
3、实现Callable接口
public class ThreadDemo03 implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println("ThreadDemo03线程执行第" + i + 1 + "次,当前时间为:" + DateUtil.now());
}
return "Callable线程执行完成";
}
}
4、使用线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new ThreadDemo02());
for (int i = 0; i < 10; i++) {
System.out.println("ThreadDemoTest主线程执行第" + i + 1 + "次,当前时间为:" + DateUtil.now());
}
System.out.println("main主线程执行结束");
这里说一下Callable接口和Runnable接口的区别:
其实也看出来了,Callable接口会有返回值,Runnable没有。Callable接口需要用一个FutureTask类来接收值:
FutureTask<String> task = new FutureTask<>(new ThreadDemo03());
new Thread(task).start();
new Thread(new ThreadDemo02(),"createthread.ThreadDemo02").start();
for (int i = 0; i < 10; i++) {
System.out.println("ThreadDemoTest主线程执行第" + i + 1 + "次,当前时间为:" + DateUtil.now());
}
System.out.println(task.get());
线程的调度
一共两种:
- 时间片调度(上面我们已经说过了)
- 抢占式策略:线程的优先级决定谁先执行
线程优先级
一共分为三个等级:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5
我们可以使用线程实例的thread.getPriority();
方法拿到线程等级;也可以通过thread.setPriority(int i);
来设置线程的执行等级;
切记:优先级仅仅是在概率上来说,但是不一定是百分百能有优先的顺序。并不意味说只有高优先级执行完了之后低优先级才会执行
线程分类
这一部分还是看的狂神说的juc视频知道的。在Java中默认是有两个线程的,一个叫做守护线程
,一个main线程
守护线程其实就是我们的GC线程。当我们的代码执行完成之后,会有一个GC线程来帮我们处理垃圾。
线程安全问题
线程安全:就是多个线程对同一资源进行操作,会出现问题
举个有味道的例子:你在上厕所,此时你是线程1,厕所是资源。但是又来了个线程2,他不知道你在上厕所,那么也会进来上厕所,接下来的画面太美,不敢看。
这样的例子就会出现线程安全问题。那么怎么去解决呢?就类似于我们的门锁。
- 方法1:使用同步代码块
我们在代码中通过synchronized
关键字来修饰需要被锁住的方法。具体用法:
synchronized(监视器){
// 方法块
}
如果是在Thread类中,那么这个监视器就是对象实例或者当前类。换句话说,我们需要把他理解为资源。 - 方法2:使用同步方法
这个理解起来比较简单,我们把执行的方法抽取出来,用锁把这个方法锁住,那么在运行的时候,只有拿到了这个方法的钥匙才能执行这个方法块
synchronized void test(){
// 执行逻辑
} - 方法3:在jdk1.5新增了一个重入锁。默认是非公平机制,也就是说默认是不公平的,可能线程1用了下一个又是线程1继续使用,如果要改成公平的,那么就在构造方法中改为true
具体用法:
在try-catch-finally代码块中,使用lock.lock()方法来对方法块加锁,但是切记,最后使用完了一定要在finally中释放掉这个锁资源。否则的话会一直占有;
总结:
相同点:二者都可以解决线程安全问题
区别:synchronized会自动的释放监视器,ReentrantLock不会自动释放。但是ReentrantLock保证了线程的可见性,也就是说线程1干了啥,其他线程能看见。
建议在工作中使用顺序:lock–>同步代码块–>同步方法
单例模式下的线程安全问题
单例模式,意味着不管调用多少次,得到的实例永远只会有一个。这也符合Spring默认的设计模式。
其中单例模式又分为饿汉式单例和懒汉式单例;
两者区别:懒汉式单例好处就是在类加载的时候不会创建实例在jvm中,只有调用getInstance()方法才会创建。而饿汉式单例则会创建。
又因为饿汉式单例是在加载时期就创建,所以线程是安全的。来张图解释解释:
这里我们重点分析一下懒汉式单例。
首先我们不加锁,看看创建出来的实例是个什么鬼。
好家伙。创建出来这么多。
主要的意思就是把运行出来的实例放在set中,利用长度来检查是否有其他的实例。其实理解了多线程,这一点很简单。来个图。
那么怎么去解决这个问题呢?只需要加锁。让创建实例的代码块锁住,只能有一个线程去访问。这样就不会出现问题。
这样看似是没什么问题。但是你发现了没,我把睡眠去掉了,当我们用睡眠试试的话,又会出现新问题。这次又是什么呢?其实在jvm底层,当出现多线程问题的时候,会有一个指令重排的问题。意思就是多条汇编指令执行时, 考虑性能因素, 会导致执行乱序,
因为不是原子性操作,所以,原本的顺序
1.分配内存
2.查看是不是null
3.新建实例
但是在多线程情况下,这三者顺序会被打乱。
那么怎么去规避这个问题?可以使用volatile关键字,来保证线程之间的可见性
注意:volatile关键字并不能保证原子性,只是保证可见性。和syncchronized是互补的
所以,最终的代码是这样:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() throws InterruptedException {
if (instance == null) { // 第一层检查,判断当前的实例是不是为null,开始为内存分配空间
synchronized (Singleton.class) { // 第一层锁:保证只有一个线程拥有资源
// 第二层检查:因为在第一层,所有线程都可能会来到第一层检查,只有越过了synchronized之后,来到这儿的线程才只有一个
// 当线程拿到锁之后来到这儿,则会指向已有对象,如果没有,则分配内存给这个实例对象
// 如果没有第二层检查,则当第一个线程释放完锁之后,后面进来的线程也会创建对象
if (instance == null) {
// 因为加了volatile关键字,能防止指令重排,能严格的按照jvm运行的方式去执行
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
HashSet<Singleton> set = new HashSet<>();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
try {
set.add(Singleton.getInstance());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
System.out.println(set.size());
}
}
懒汉式的缺点:
这儿也是看了一篇大佬的笔记,觉得有意思,就记录下来了
其实上面这样已经能隔绝大部分问题了。但是我们忽略了一个重要的问题,那就是反射。接下来看个有意思的东西,我们尝试用反射去破解单例模式;
当然,工作中不要这样玩,否则你的老大要很友好的请你去扫厕所。
线程死锁
死锁,我们还是来一个比较有味道的例子。加入张三和李四都肚子痛,张三先去厕所,但是发现没带纸,李四手里有纸但是来到厕所发现张三居然把资源占了,谁也不让谁。这样就出现了一个循环死锁问题。
看一份代码。
public class DeadLockRunnable implements Runnable {
private int flag; // 决定线程走向的标记
// 锁对象1
private static Object o1 = new Object();
// 锁对象2
private static Object o2 = new Object();
public DeadLockRunnable(int flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag == 1) {
// 线程1代码
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "已获取到资源o1,正在请求o2");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "已获取到o1和o2");
}
}
} else {
// 线程2代码
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "已获取到资源o2,正在请求o1");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "已获取到o1和o2");
}
}
}
}
}
然后我们写个启动类来模拟两个线程操作。
解决方案
- 预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个条件,来防止死锁的发生
- 避免死锁:在资源的动态分配中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生
- 检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构即使检索死锁的发生
线程通信
为什么要线程通信?因为在对资源的操作中,一个线程结束之后需要另一个线程来对这个资源进行操作,我总得告诉他这个线程结束了。
线程之间的通信共有四种方式,其中我们最常用的是等待唤醒机制中的wait()和notify()和notifyAll();
方法 | 描述 |
---|---|
wait() | 调用此方法,线程进入阻塞状态,释放同步监视器 |
notify() | 唤醒一个线程,如果有多个线程,则优先唤醒优先级高的线程 |
notifyAll() | 唤醒全部线程 |
注意:使用此方式必须在同步代码块或者同步方法中
等待唤醒机制
- Object实现等待唤醒
此方法是Object类中的
Object obj = new Object();
obj.wait(); // 等待,这时会释放当前对资源的锁
obj.notify();// 唤醒一个线程
obj.notifyall();// 唤醒其他全部线程
- Condition等待唤醒
看看源码中的介绍:
Condition将Object监控器方法( wait , notify和notifyAll )分解为不同的对象,通过将它们与任意Lock实现结合使用,可以使每个对象具有多个等待集。 当“ Lock替换了synchronized方法和语句的使用时,“ Condition替换了“对象”监视器方法的使用
其中也提供了两种方式来唤醒和等待;
Condition con = new Condition();
con.await(); // 进入阻塞状态
con.signal();// 唤醒等待线程
con.signalAll();// 唤醒所有等待的线程
CountDownLatch方式
会初始化线程的数量,在每一个线程调用完成之后,countDownLatch计数就减掉1,说明线程执行完成;
import java.util.concurrent.CountDownLatch;
/**
* 使用运动员与教练的案例模拟CountDownLatch
*
* @author: Zxy
* @Date: 2021-05-08
**/
public class CoachReacerDemo {
private CountDownLatch countDownLatch = new CountDownLatch(3); // 设置要等待的运动员数量(也就是线程数量)
public void racer() {
// 1、获取运动员名称
String name = Thread.currentThread().getName();
// 2、运动员开始准备:打印准备信息
System.out.println(name + "正在准备...");
// 3、让线程睡眠,表示运动员在准备
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 4、运动员准备完毕,打印信息
System.out.println(name + "准备完毕...");
// 让线程数计数-1
countDownLatch.countDown();
}
public void coach() {
// 1、获取教练线程的名称
String name = Thread.currentThread().getName();
// 2、教练等待所有运动员准备完毕:打印等待信息
System.out.println(name + "等待运动员准备...");
// 3、调用countDownLatch.await()等待其他线程执行完毕
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 4、所有的运动员已经就绪,开始训练
System.out.println("所有运动员已经就绪," + name + "开始训练");
}
public static void main(String[] args) {
// 1、创建CoachReacerDemo实例
final CoachReacerDemo demo = new CoachReacerDemo();
// 2、创建三个运动员线程对象,调用racer方法
new Thread(new Runnable() {
@Override
public void run() {
demo.racer();
}
},"运动员1").start();
new Thread(new Runnable() {
@Override
public void run() {
demo.racer();
}
},"运动员2").start();
new Thread(new Runnable() {
@Override
public void run() {
demo.racer();
}
},"运动员3").start();
// 3、创建一个教练线程对象,调用coach方法
new Thread(new Runnable() {
@Override
public void run() {
demo.coach();
}
},"教练").start();
}
}
CyclicBarrier方式
这个类会将当前的所有线程全部等待完成后才会去同时启动这些线程。比如有a、b、c三个线程,那么这个类在a线程完成后会阻塞一段时间等待b和c的启动完成,然后同时执行这三个;
import cn.hutool.core.date.DateUtil;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* 演示CycliBarrier使用方法
*
* @author: Zxy
* @Date: 2021-05-08
**/
public class ThreeThreadStartDemo {
// 参数:所有参与启动的线程数量
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
public void startThread() {
// 1、打印线程准备启动
String name = Thread.currentThread().getName();
System.out.println(name + "准备启动...");
// 2、调用CycliBarrier的await方法等待线程全部准备完成
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
// 3、打印线程启动完毕信息
System.out.println(name + "线程启动完毕,启动时间:"+ DateUtil.now());
}
public static void main(String[] args) {
ThreeThreadStartDemo demo = new ThreeThreadStartDemo();
new Thread(new Runnable() {
@Override
public void run() {
demo.startThread();
}
},"线程1").start();
new Thread(new Runnable() {
@Override
public void run() {
demo.startThread();
}
},"线程2").start();
new Thread(new Runnable() {
@Override
public void run() {
demo.startThread();
}
},"线程3").start();
}
}
Semaphore
这个类的主要作用就是,会限定资源数量,但是,操作该资源数量的线程又有多个。保证资源不共享,当每一个线程使用完资源后释放,其他线程继续使用;
import java.util.concurrent.Semaphore;
/**
* Semaphore演示使用
* 8个工人,只有三台机器,这个资源不是共享的
*
* @author: Zxy
* @Date: 2021-05-08
**/
public class WorkerMachineDemo {
static class Work implements Runnable {
private int workerNum; // 工人数量
private Semaphore semaphore; // 机器数量
public Work(int workerNum, Semaphore semaphore) {
this.workerNum = workerNum;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
// 1、工人要去获取机器
semaphore.acquire();
// 2、打印工人获取到机器开始工作
String name = Thread.currentThread().getName();
System.out.println(name + "获取到机器,开始工作");
// 3、线程睡眠一秒模拟工人使用机器工作
Thread.sleep(1000);
// 4、使用完毕,释放机器,打印工人使用完毕
semaphore.release();
System.out.println(name + "使用完毕,释放机器");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
int workers = 8;
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < workers; i++) {
new Thread(new Work(workers, semaphore), "工人" + i + ": ").start();
}
}
}
sleep和wait的区别
这是面试的高频问法。我们最主要的记住一点;
sleep不会释放锁,wait会释放锁
当然还有其他的一些要点:
两者都可以暂停线程的运行
wait()通常用于线程交互,而sleep()通常用于暂停
wait()方法不会自动苏醒,需要别的线程调用同一个对象上的唤醒方法,而sleep()可以自动苏醒
多线程的三个特性
原子性:要么都成功,要么都失败
有序性:按照我们的代码顺序去执行
可见性:线程a修改了变量值,那么线程b是看得见的
多线程对三个特性的控制类
既然我们的多线程代码要保证这三个特性,那么必然就要有一些代码来实现控制;
ThreadLocal
假如我们去银行取钱,银行中的存款是100万,张三来取了30万,但是这时李四在另一台机器上取走了50万,要想让这两个线程之间是不干扰的,那我们就需要用ThreadLocal来保证他们俩的线程是自己的线程,而不是另一个线程。
如果我们创建了一个ThreadLocal变量,那么操作这个变量的线程都是私有的。这一点,如果有学过jdbc的人应该很清楚。事务之间的隔离性,必须要保持线程之间是不受影响的。
其中的get()
&&set()
方法就是往local里面存放各自的资源。我们还是来段代码演示一下。
public class ThreadLocalDemo {
// 1.创建银行对象:钱,取款。存款
static class Bank {
// 初始化钱的容量
private final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
// 取款
public Integer get() {
return threadLocal.get();
}
// 存款
public void set(Integer money) {
threadLocal.set(threadLocal.get() + money);
}
}
// 2.创建转账对象:从银银行中取钱,转账,保存到用户
static class Transfer implements Runnable{
private Bank bank;
public Transfer(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
bank.set(10);
System.out.println(Thread.currentThread().getName()+"账户余额:"+bank.get());
}
}
}
public static void main(String[] args) {
Bank bank = new Bank();
Transfer transfer = new Transfer(bank);
Thread t1 = new Thread(transfer, "客户1");
Thread t2 = new Thread(transfer, "客户2");
t1.start();
t2.start();
}
}
这两个客户线程之间是互不干扰的。
其实ThreadLocal底层默认就是一个Map结构,但是其中的key属于弱引用类型,但是value是强引用。也就是说在方法调用完成之后,key会被回收,而value不会,这样就会造成一个内存泄漏的问题。但是源码中已经给我们手动的处理好了这个问题。不过鉴于良好习惯,我们使用完成后还是调用一下remove()方法。
原子类
多线程为了保证它的原子性,特定提供了一个原子类供我们操作。当我们要对资源进行原子类型的操作时,应当首先考虑。
Java的java.util.concurrent.atomic包里面提供了很多可以进行原子操作的类,分为以下四类:
原子更新基本类型:AtomicInteger、AtomicBoolean、AtomicLong
原子更新数组:AtomicIntegerArray、AtomicLongArray
原子更新引用:AtomicReference、AtomicStampedReference等
原子更新属性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater
提供这些原子类的目的就是为了解决基本类型操作的非原子性导致在多线程并发情况下引发的问题。
来看看一个非原子类的操作,我们使用两个线程同时去对i++进行操作;
public class AtomicClass {
static int n = 0;
public static void main(String[] args) throws InterruptedException {
int j = 0;
while(j<100){
n = 0;
Thread t1 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n++;
}
}
};
Thread t2 = new Thread(){
public void run(){
for(int i=0; i<1000; i++){
n++;
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("n的最终值是:"+n);
j++;
}
}
}
最终得到的值很大几率不是2000。原因就是因为两个线程在同时操作一个资源。我们将它修改一下。
最终得到的值就只会是2000,其中的AtomicInteger类则会保证资源的原子性。
Lock锁
其实这一部分我们在上面就已经解释过了。不过我们还得继续介绍另外两种。那就是读写锁。顾名思义,读写锁,在读的时候不能写,写的时候不能读,两者之间不能影响。
我们可以看到,重入锁是Lock的子类,同级的还有ReadLock和WriteLock。
Lock和ReadWriteLock是两大锁的根接口
Lock 接口支持重入、公平等的锁规则:实现类 ReentrantLock、ReadLock和WriteLock。
ReadWriteLock 接口定义读取者共享而写入者独占的锁,实现类:ReentrantReadWriteLock。
重入锁我就不过多的去解释了,我们来看看读写锁;
package com.multithread.thread;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 读写操作类
*/
public class ReadWriteLockDemo {
private Map<String, Object> map = new HashMap<String, Object>();
//创建一个读写锁实例
private ReadWriteLock rw = new ReentrantReadWriteLock();
//创建一个读锁
private Lock r = rw.readLock();
//创建一个写锁
private Lock w = rw.writeLock();
/**
* 读操作
*
* @param key
* @return
*/
public Object get(String key) {
r.lock();
System.out.println(Thread.currentThread().getName() + "读操作开始执行......");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
return map.get(key);
} finally {
r.unlock();
System.out.println(Thread.currentThread().getName() + "读操作执行完成......");
}
}
/**
* 写操作
*
* @param key
* @param value
*/
public void put(String key, Object value) {
try {
w.lock();
System.out.println(Thread.currentThread().getName() + "写操作开始执行......");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
} finally {
w.unlock();
System.out.println(Thread.currentThread().getName() + "写操作执行完成......");
}
}
public static void main(String[] args) {
final ReadWriteLockDemo d = new ReadWriteLockDemo();
d.put("key1", "value1");
new Thread(new Runnable() {
public void run() {
d.get("key1");
}
}).start();
new Thread(new Runnable() {
public void run() {
d.get("key1");
}
}).start();
new Thread(new Runnable() {
public void run() {
d.get("key1");
}
}).start();
}
}
volatile
在上面的单例模式中,我们使用了volatile来规避了指令重排的现象。
一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(注意:不保证原子性)
禁止进行指令重排序。(保证变量所在行的有序性)
当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
线程池
为什么要使用线程池?
原因很简单。因为资源的时连时断会造成大量的浪费。当我们需要新的线程的时候,不需要去新建,而是直接就在池子中去拿,用完了之后归还,好方便下一个人使用。举个很简单的例子:我们在食堂吃饭的时候,用的碗你肯定是去碗柜拿的,而不是你去新建的,用完了之后也肯定是归还回去的。
多线程的缺点:
处理任务的线程创建和销毁都非常耗时并消耗资源。
多线程之间的切换也会非常耗时并消耗资源。
解决方法:采用线程池
使用时线程已存在,消除了线程创建的时耗
通过设置线程数目,防止资源不足
在Java中,有一个接口Executors用来创建,但是根据阿里巴巴的Java开发手册规定,我们不能这样去干,需要使用ThreadLocalPoolExecutor来创建线程池。
具体的解释参考一下Guide大佬的面经:
但是,用这个类的构造函数又很麻烦,所以,我们可以使用Executor框架的Executors类去创建线程池,其中一共有四种,我们分别看一下:
1、newCachedThreadPool
该方法创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
此类型线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE)
空闲的工作线程会自动销毁,有新任务会重新创建
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
2、newFixedThreadPool
该方法创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
优点:具有线程池提高程序效率和节省创建线程时所耗的开销。
缺点:在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
3、newSingleThreadExecutor
该方法创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO,优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。
单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
===我总觉得这个线程池和单线程没区别=
4、newScheduleThreadPool
该方法创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行
其实这四类线程池内部调用的还是ThreadLocalExecutorPool的构造方法;
总结
笔记整理不易,如果对你有帮助,请无情转发点赞和收藏,转发记得带上我的地址哦。马上秋招,祝各位小伙伴都找到自己心仪的工作,当然啦,也包括我