【Java学习】多线程JUC万字超详解

2024-09-01 11:12

本文主要是介绍【Java学习】多线程JUC万字超详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

 所属专栏:Java学习      

在这里插入图片描述

1. 多线程的概念

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

下面这些每一个能够运行的软件就是一个进程

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

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

2. 多线程的实现方式

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

实现方式:

1. 定义一个类,继承Thread

2. 重写run方法

3. 创建子类对象,并启动线程

public class MyThread extends Thread{@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName() + "hello");}}
}
public class ThreadDemo1 {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();//设置线程对象名称t1.setName("线程一:");t2.setName("线程二:");//开启线程t1.start();t2.start();}
}
 

在开启线程之后,会交替执行线程一和线程二

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

实现方式:

  1. 自定义一个类,实现Runnable接口

  2. 重写里面的Run方法

  3. 创建自定义类的对象

  4. 创建一个Thread类的对象,并开启线程

public class MyRun implements Runnable{@Overridepublic void run() {for(int i = 0;i < 10;i++){//获取当前线程对象Thread t = Thread.currentThread();System.out.println(t.getName() + "hello");}}
}
public class ThreadDemo2 {public static void main(String[] args) {//创建MyRun对象,表示多线程要执行的任务MyRun myRun = new MyRun();//创建线程的对象Thread t1 = new Thread(myRun);Thread t2 = new Thread(myRun);//给线程设置名字t1.setName("线程一:");t2.setName("线程二:");//开启线程t1.start();t2.start();}
}

开启线程之后也是交替执行线程一和线程二

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

实现方式:

1. 创建一个类MyCallable实现Callable接口

2. 重写call方法(返回值代表多线程运行的结果)

3. 创建MyCallable对象(表示多线程要执行的任务)

4. 创建FutureTask对象(作用管理多线程运行的结果)

5. 创建Thread类的对象并启动(表示线程)

import java.util.concurrent.Callable;
​
public class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i < 100; i++) {sum += i;}return sum;}
}
public class ThreadDemo3 {public static void main(String[] args) throws ExecutionException, InterruptedException {//创建MyCallable对象(表示多线程要执行的任务)MyCallable myCallable = new MyCallable();//创建FutureTask对象(作用管理多线程运行的结果)FutureTask<Integer> ft = new FutureTask<>(myCallable);//创建Thread类的对象并启动(表示线程)Thread t1 = new Thread(ft);t1.start();Integer res = ft.get();System.out.println(res);}
}

2.4 三种实现方式对比

优点缺点
继承Thread类编程简单,可以直接使用Thread中的方法可拓展性差,不能再继承其他类
实现Runnable接口拓展性强,实现该接口同时还可以继承其他类编程相对复杂,不能直接使用Thread中的方法
实现Callable接口

3. 常见的成员方法

3.1 getName()和setName()

对于setName()来说,如果没有给线程设置名称,也是有默认的名字的,格式:Thread - X(x序号从0开始)

根据Thread类的空参构造可以看出,在创建对象时的默认名称格式

那怎么在自定义类中创建对象时就传入对象名称进行构造呢?

在多线程第一种实现方式中,自定义的类继承了Thread类,但是构造方法并没有继承,所以还需要在自定义类中手动的去实现构造方法

3.2 currentThread()和sleep()

currentThread可以获取当前线程的对象

当JVM虚拟机启动之后,会自动开启多条线程,其中一条线程就叫做main线程,作用就是调用main方法,并执行里面的代码

sleep()是让线程休眠指定的时间,单位是毫秒,哪条线程执行到这个方法,那么哪条线程就会休眠,时间到了之后会继续执行下面的操作

这里的异常可以直接抛出

 

再来看MyThread类,这里的异常处理不能使用Throw抛出了,因为父类Thread没有抛出,这里只能使用try-catch处理异常  

3.3 getPriority()和setPriority()

3.3.1 CUP的调度方式

CUP的调度方式是有两种的:分为抢占性调度和非抢占性调度

抢占式调度是一种允许高优先级线程中断低优先级线程的执行,从而立即获得CPU资源的调度方式。在这种模式下,操作系统会定期检查线程的优先级,并根据需要切换线程的执行, 对线程的访问是随机的

非抢占式调度是一种允许线程独占CPU直到其主动放弃或执行完毕的调度方式。在这种模式下,线程的执行时间由线程本身控制,调度器不会中断正在执行的线程,轮流执行线程

3.3.2 优先级

来看Thread类中优先级的设置,最小为1,最大为10,默认是5,优先级越高抢占到CUP的概率越高,只是说概率高,并不是优先级高的就肯定比优先级低的要先抢占到CUP

 

public class ThreadTest2 {public static void main(String[] args) {//创建线程要执行参数的对象MyRunable myRunable = new MyRunable();//创建线程对象Thread thread1 = new Thread(myRunable,"线程一");Thread thread2 = new Thread(myRunable,"线程二");//获取线程优先级System.out.println(thread1.getPriority());thread2.setPriority(10);System.out.println(thread2.getPriority());System.out.println(Thread.currentThread().getPriority());}
}

3.4 setDaemon()

设置为守护线程也类似于备胎线程,当其他线程执行完毕之后,守护线程会陆续结束(并不是立即结束)

public class ThreadTest3 {public static void main(String[] args) {MyThread1 thread1 = new MyThread1();MyThread2 thread2 = new MyThread2();thread1.setName("女神");thread2.setName("备胎");//把第二个线程设置为守护线程thread2.setDaemon(true);thread1.start();thread2.start();}
}

应用场景:例如在聊天软件中,当打开聊天窗口之后,开启聊天窗口的线程和发送文件的线程,这时就可以把发送文件设置为守护线程,当聊天窗口关闭之后,守护线程也就没有存在的必要了  

4. 多线程的生命周期

多线程的生命周期是指一个线程从创建到消亡的整个过程。在这个过程中,线程会经历不同的状态。一般来说,线程的生命周期可以归纳为以下几个主要阶段:

1. 新建(New)

  • 状态描述:线程被创建但尚未启动。使用new关键字和Thread类或其子类创建一个线程对象后,该线程就处于新建状态。此时,线程仅在内存中被分配了空间,但还没有开始执行。

  • 注意:在操作系统层面,真正的线程还没有被创建,只有调用了start()方法后,线程才会被操作系统创建并进入下一个状态。

2. 就绪(Runnable)

  • 状态描述:线程已经启动,但尚未获得CPU执行权,处于等待CPU分配资源的阶段。调用线程的start()方法后,线程会进入就绪状态。此时,线程已经具备了运行条件,但还需要等待系统为其分配CPU资源。

  • 注意:就绪状态并不是执行状态,线程需要等待CPU的调度才能进入运行状态。

3. 运行(Running)

  • 状态描述:线程获得CPU资源并执行其run()方法中的代码。当就绪状态的线程被系统选中并获得CPU执行权时,它会进入运行状态。

  • 状态转变:运行状态的线程可以转变为阻塞状态、就绪状态和死亡状态。如果线程失去了CPU资源,它会从运行状态转变为就绪状态;如果线程执行了某些导致阻塞的操作(如调用sleep()wait()方法或进行I/O操作),它会进入阻塞状态;如果线程的run()方法执行完毕或被强制终止,它会进入死亡状态。

4. 阻塞(Blocked)

  • 状态描述:线程因为某些原因(如等待I/O操作完成、等待获取同步锁等)而暂停执行。处于阻塞状态的线程不会占用CPU资源,直到阻塞的原因被消除后,线程才会重新进入就绪状态并等待CPU的调度。

  • 状态转变:阻塞状态的线程可以转变为就绪状态和死亡状态。当阻塞的原因被消除(如I/O操作完成、获取到同步锁等),线程会重新进入就绪状态;如果线程在等待过程中被强制终止(如调用stop()方法),它会进入死亡状态。

5. 死亡(Terminated/Dead)

  • 状态描述:线程的生命周期结束。当线程的run()方法执行完毕或被强制终止时,线程会进入死亡状态。此时,线程所占用的资源会被释放。

  • 注意:死亡的线程不能被再次启动。如果尝试在死亡的线程上调用start()方法,会抛出IllegalThreadStateException异常。

5. 线程安全问题

先来看一个现象: 

使用多线程实现三个窗口卖票的业务

这时就出现了一些小问题,售卖的票中有相同的票,也有超出范围的票,出现这个问题的原因就是线程执行时是有随机性的,当一个线程休眠时,其他的线程就可以抢到CPU了,休眠之后就又可以争夺CPU,此时如果一个线程刚好执行到target++,还没来得及打印,其他线程抢回了CPU,并且执行了target++,这时就可能出现以上的情况

解决办法:把操作共享数据的代码锁起来,锁默认打开,如果有现成进去之后,锁自动关闭,里面的代码全部执行完毕,线程出来,锁自动打开,这样就可以解决上述问题

5.1 同步代码块

同步代码块是通过关键字synchronized来实现的,括号中需要传入一个锁对象,可以是任意的,但必须是唯一的,通常会使用Thread.class作为锁对象,因为字节码文件对象是唯一的

synchronized (锁对象){}
public class MyThread3 extends Thread {static int ticket = 0;@Overridepublic void run() {while (true) {synchronized (Thread.class) {if (ticket < 100) {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}ticket++;System.out.println("正在卖第" + ticket + "张票");} else {break;}}}}
}

要注意的是,synchronized在这里不能写在while循环外面,不然的话只有线程一就把循环的内容执行完了,然后剩余的线程由于target不满足循环条件,就不会再执行了  

5.2 同步方法

把synchronized加在方法上就是同步方法

格式:修饰符 synchronized 返回类型方法名(方法参数){...};

特点:同步方法是锁住方法里面所有的代码,锁对象不能自己指定,在非静态方法中,锁对象为this所指的对象,在static静态方法中,锁对象指的是当前类的字节码文件的对象

还是上面的例子,这次 实现 Runnable接口,使用同步方法试一下

public class MyRunnable implements Runnable {int ticket = 0;@Overridepublic void run() {while (true) {if (func()) break;}}private synchronized boolean func() {if (ticket == 100) {return true;} else {try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}ticket++;System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票");}return false;}
}

6. 锁

上面的同步代码块和同步方法虽然也是起到了把一段代码锁起来的效果,但是并没有直接看出哪里加上了锁,哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作

Lock中也提供了获得锁和释放锁的方法

void lock() : 获得锁

void unlock() : 释放锁

Lock是一个接口,所以需要通过它的实现类ReentrantLock来实例化对象,然后再调用上面两个方法

以之前创建的MyThread3为例,由于需要创建三个MyThread3的对象,所以在MyThread3中创建的锁对象也会被创建三次,那么就会出现之前超出范围的问题,所以创建的锁对象要用static修饰一下

但这时会出现一个问题,程序最终并没有停止

 这是因为假如线程一抢到了CPU,并执行完毕之后跳出了循环,线程二和线程三还在锁的外面,所以需要改变释放锁的位置,可以利用 finally 来解决这个问题

public class MyThread3 extends Thread {static int ticket = 0;static Lock lock = new ReentrantLock();@Overridepublic void run() {while (true) {//synchronized (Thread.class) {lock.lock();try {if (ticket == 100) {break;} else {Thread.sleep(10);ticket++;System.out.println(getName() + "正在卖第" + ticket + "张票");}} catch (InterruptedException e) {e.printStackTrace();}finally {lock.unlock();}// }}}
}

6.1 死锁

它指的是两个或多个线程在执行过程中,由于竞争资源而造成的一种阻塞现象,若无外力作用,这些线程都将无法向前推进,死锁通常发生在两个或多个线程相互等待对方释放锁的情况,一般就是在锁的嵌套中容易发生,所以要避免这种写法

7. 等待唤醒机制

void wait()当前线程等待,直到被其他线程唤醒
void notify()随机唤醒单个线程
void notifyAll()唤醒所有线程

等待(wait):当一个线程执行到某个对象的wait()方法时,它会释放当前持有的锁(如果有的话),并进入等待状态。此时,线程不再参与CPU的调度,直到其他线程调用同一对象的notify()或notifyAll()方法将其唤醒。

唤醒(notify/notifyAll):

notify: 唤醒在该对象监视器上等待的某个线程,如果有多个线程在等待,那么具体唤醒哪一个是随机的

notifyAll: 唤醒在该对象监视器上等待的所有线程

调用wait方法的线程会释放其持有的锁,被唤醒的线程在执行之前,必须重新获取被释放的锁

public class Cook extends Thread {@Overridepublic void run() {while (true) {synchronized (Desk.lock) {if (Desk.count == 0) {break;} else {if (Desk.foodFlag == 0) {try {Desk.lock.wait();//厨师等待} catch (InterruptedException e) {e.printStackTrace();}} else {Desk.count--;System.out.println("还能再吃" + Desk.count + "碗");Desk.lock.notifyAll();//唤醒所有线程Desk.foodFlag = 0;}}}}}
}

public class Foodie extends Thread {@Overridepublic void run() {while (true) {synchronized (Desk.lock) {if (Desk.count == 0) {break;} else {if (Desk.foodFlag == 1) {try {Desk.lock.wait();} catch (InterruptedException e) {e.printStackTrace();}} else {System.out.println("已经做好了");Desk.foodFlag = 1;Desk.lock.notifyAll();}}}}}
}
public class Desk {public static int foodFlag = 0;public static int count = 10;//锁对象public static Object lock = new Object();
}

这里实现的功能就是,厨师做好事务放在桌子上,美食家开始品尝,如果桌子上没有食物,美食家就等待,有的话,厨师进行等待

7.1 阻塞队列

在Java中,阻塞队列(BlockingQueue)是java.util.concurrent包下的一个接口,它支持两个附加操作的队列。这两个附加的操作是:在元素从队列中取出时,如果队列为空,则等待直到队列中有元素可取;在元素添加到队列时,如果队列已满,则等待直到队列中有空间可用。阻塞队列主要用于生产者-消费者场景,其中生产者线程用于向队列中添加元素,而消费者线程从队列中取出元素。

  • 生产者-消费者场景:在多线程环境下,生产者线程向队列中添加元素,消费者线程从队列中取出元素。阻塞队列能够平衡生产者和消费者的处理能力。

  • 任务队列:在异步处理框架中,将待处理的任务放入阻塞队列中,由线程池中的线程去取任务并执行。

public class Cook1 extends Thread{ArrayBlockingQueue<String> queue;public Cook1(ArrayBlockingQueue<String> queue) {this.queue = queue;}@Overridepublic void run() {while (true){try {queue.put("美食");System.out.println("已经做好了一道美食");} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class Foodie1 extends Thread{ArrayBlockingQueue<String> queue;public Foodie1(ArrayBlockingQueue<String> queue) {this.queue = queue;}@Overridepublic void run() {while (true){try {String food = queue.take();System.out.println(food);} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class ThreadTest6 {public static void main(String[] args) {//必须指定阻塞队列的上限ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);Cook1 cook1 = new Cook1(queue);Foodie1 foodie1 = new Foodie1(queue);cook1.start();foodie1.start();}
}

在Cook1和Foodie1两个类中,发现并没有去使用同步代码块或是锁,因为take()和put()这两个方法在底层已经使用到了锁,如果再使用锁就会发生死锁

此外,由于锁是在方法内部使用的,所以方法外面的打印语句由于CPU抢占的原因,可能发生之前重复打印的情况

8. 多线程的六种状态

多线程的六种状态分别为:新创建,可运行,被阻塞,等待,计时等待,被终止

在Java中是没有定义运行状态的,因为当线程抢夺到CPU的执行权之后,接下来就该交给操作系统了,也没有必要再定义了

9. 线程池

9.1 线程池的概念和使用

在之前我们写的代码中,用到线程就创建,用完之后线程就消失了,这样会浪费操作系统的资源,也存在一些弊端,通过线程池就可以解决这个问题

线程池是一种线程使用模式,它维护着多个线程,等待着监督管理者分配可并发执行的任务

线程池的核心原理:

  1. 创建一个空的线程池

  2. 提交任务时,线程会创建新的线程对象,任务分配完毕,线程归还给线程池,下次再提交任务时,不需要创建新的线程,直接复用已有的线程即可

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

public static ExecutorService newCachedThreadPoll()创建一个没有上限的线程池
public static ExecutorService newCachedThreadPoll(int nThread)创建有上限的线程池

先来正常的获取线程池对象,提交任务,销毁线程

public class ThreadTest7 {public static void main(String[] args) throws InterruptedException {//获取线程池对象ExecutorService pool1 = Executors.newCachedThreadPool();//提交任务pool1.submit(new MyRunable());//销毁线程池pool1.shutdown();}
}

线程复用:

线程池中的线程是复用的,一旦一个线程完成了它的任务,他就会回到线程池中等待下一个任务,如果任务提交的速度不快,或者线程池的配置较小,那么就可能看到同一个线程被用来执行多个任务

例如上面的代码中,每次提交任务,如果线程池中有空闲的线程,就会复用,而不是创建新的线程

9.2 自定义线程池

通过上面介绍的静态方法创建出来的线程池不够灵活,如果说等待的线程过多,阻塞队列中已经排满了线程,这时修改起来就不好操作,使用自定义线程池可以对所有现成进行同一管理和监控,便于及时发现问题,并及时进行配置和调整

创建自定义线程池用到的参数:核心线程的数量,线程池中最大线程池的数量,空闲时间(值),空闲时间(单位),阻塞队列,创建线程的方式,要执行的任务过多时的解决方案

解释:核心线程就是指一直存在于线程池中的线程,两个空闲时间就是值,创建出来的临时线程空闲的时间,超过这个时间就意味着这靠核心线程就足以完成当前提交的任务,就需要销毁临时线程,节约资源,要执行的任务过多时的解决方案指的是,当前线程池中线程的数量已经达到了最大,并且阻塞队列也已经排满了,就需要把多出来的任务踢出去

不断地提交任务,会有以下三个临界点:

1. 当核心线程满了之后,再提交任务就会排队

2. 当核心线程和阻塞队列都满了之后,就会创建临时线程

3. 当核心线程,阻塞队列,临时线程都满了之后,会触发任务的拒绝策略

任务拒绝策略:默认使用丢弃任务并抛出RejectedExecutionException异常

public class MyThreadPoolDemo1 {public static void main(String[] args) {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,//核心线程数量,不能小于06,//最大线程数,不能小于0,大于等于核心线程数60,//最大存活时间TimeUnit.SECONDS,//单位,用TimeUnit指定new ArrayBlockingQueue<>(3),//阻塞队列Executors.defaultThreadFactory(),//创建线程工厂new ThreadPoolExecutor.AbortPolicy()//AbortPolicy是ThreadPoolExecutor的静态内部类);}
}

最大并行数是指计算机系统或软件在处理任务时能够同时执行的最大指令或数据数量。

线程池多大合适:

CPU密集型运算:计算比较多,采用最大线程数 + 1

I/O密集型运算:如果读取本地文件或读取数据库的操作比较多

在这里插入图片描述

这篇关于【Java学习】多线程JUC万字超详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1126769

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06