Java多线程
程序,进程,线程的基本概念:
程序:是为了完成特定的任务,使用某种语言编写的一组指令的集合,是一段静态的代码,静态对象,如 Excel,World等。
进程:是程序的一次执行多次,或者是正在运行的一个程序,是一个动态的过程,有自身的产生,存在和消亡的过程,即存在生命周期。
线程:进程可以进一步分为线程,是一个程序内部的一条执行路径。
百科解释
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。
线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
简单理解
线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。
线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
进程 : 指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
线程: 系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。是程序执行的最小单位。
并行与并发
并行与并发的主要区别在于:1.处理任务不同;2.存在不同;3.CPU资源不同;
两者处理任务不同
并发(Concurrent)
并发是一个CPU处理器同时处理多个线程任务。(宏观上是同时处理多个任务,微观上其实是CPU在多个线程之间快速的交替执行CPU把运行时间划分成若干个(微小)时间段,公平的分配给各个线程执行,在一个时间段的线程运行时,其他线程处于挂起状态,这种就称之为并发。)
并行(parallel)
并行是多个CPU处理器同时处理多个线程任务。(当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这就被称之为并行。)
两者存在不同
并发(Concurrent)
并发可以在一个 CPU 处理器和多个 CPU 处理器系统中都存在(多个CPU处理器系统其中的一个CPU也可以进行并发操作)
并行(parallel)
并行之能在多个 CPU 处理器中存在。
两者的 CPU 资源不同
并发(Concurrent)
并发过程中,线程之间会抢占 CPU 资源,轮流使用,(其实CPU会多个各个线程公平的分配时间片和进行执行)。
并行(parallel)
在并行过程中,线程之间不会抢占 CPU 资源。(因为是多个 CPU 处理器,各自做各自的)
串行
是指多个任务按照顺序依次来执行,一个任务完成后才能执行下一个任务,例如:单线程程序就是串行执行的。
并发编程的三要素
原子性:是指一个或者多个操作要么全部执行成功,要么全部执行失败
可见性:一个线程对共享变量的修改,另一个线程能够立刻看到
有序性:程序的执行顺序按照代码的先后顺序执行(处理器可能会对指令进行重排序)
Java 中多线程的实现方式
继承 Thread 类
实现 Runable 接口
使用 Callable 和 Future
使用线程池
线程存在的几种状态
Java 中的线程的状态被定义在了 java.lang.Thread.State 枚举类中,其源码如下:
public class Thread {
public enum State {
/* 新建 */
NEW ,
/* 可运行状态 */
RUNNABLE ,
/* 阻塞状态 */
BLOCKED ,
/* 无限等待状态 */
WAITING ,
/* 计时等待 */
TIMED_WAITING ,
/* 终止 */
TERMINATED;
}
// 获取当前线程的状态
public State getState() {
return jdk.internal.misc.VM.toThreadState(threadStatus);
}
}
每种线程的状态含义如下:
NEW : 一个尚未启动的线程的状态,称为初始状态,开始状态;线程刚被创建,但是并没有启动,还没有调用 start() 方法。
Runnable :当调用线程对象的 start() 方法,此时线程对象进入了 RUNNABLE 状态,即可执行状态,此时才是真正在 JVM 进程中创建了一个线程,线程一经启动并不是立即得到执行的,线程的运行与否需要听令与 CPU 的调度,因此这个中间状态称为可执行状态(RUNNABLE),线程具备执行的资格,但是并没有真正执行起来而是在等待 CPU 的调度。
BLOCKED :当一个线程试图获取一个对象锁,而该对象锁被其他线程持有,则该线程进入 Blocked 状态;当该线程持有锁的时候,该线程将变成 Runnable 状态。
WAITING :一个正在等待的线程的状态,称为等待状态,造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。
TIMED_WAITING :一个在限定时间内等待的线程状态,也称为限时等待状态,造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。
TERMINATED :一个完全运行完成的线程的状态,也称为终止状态,结束状态。
继承 Thread 类
- 创建一个继承 Thread 类的子类;
- 重写 Thread 类的 run() 方法;
- 创建 Thread 子类的对象;
- 通过此对象调用 start() 方法。
start与run方法的区别
start方法的作用:1.启动当前线程 2.调用当前线程的重写的run方法(在主线程中生成子线程,有两条线程)
调用start方法以后,一条路径代表一个线程,同时执行两线程时,因为时间片的轮换,所以执行过程随机分配,且一个线程对象只能调用一次start方法。
run方法的作用:在主线程中调用以后,直接在主线程一条线程中执行了该线程中run的方法。(调用线程中的run方法,只调用run方法,并不新开线程)
https://www.cnblogs.com/hyzs/gallery/image/488298.html
public class Test01 extends Thread {
private static int ticket = 100;
@Override
public void run(){
while (true){
if(ticket <= 0){
return;
}
getTicket();
}
}
private static void getTicket(){
if(ticket == 0){
return;
}
System.out.println(Thread.currentThread().getName() + "-->售出第:" + (101 - ticket) + "票");
ticket--;
try{
sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
Test01 test01 = new Test01();
Test01 test02 = new Test01();
Test01 test03 = new Test01();
test01.start();
test02.start();
test03.start();
}
}
实现 Runable 接口方式
1.创建一个实现了 Runable 接口的类;
2.实现类去上西安 Runable 中的抽象方法:run();
3.创建实现类的对象;
4.将此对象作为参数传递到 Thread 类中的构造器中,创建 Thread 类对象;
5.通过 Thread 类的对象调用 start() 方法;
import static java.lang.Thread.activeCount;
import static java.lang.Thread.sleep;
/**
-
ClassName : Test02
-
Package : PACKAGE_NAME
-
Description :
-
Author : hyu
-
Create : 2024/4/27 13:17
-
Version : 1.0
*/
public class Test02 {public static void main(String[] args) {
window1 w = new window1();Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start();
}
}
class window1 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true){
if(ticket > 0){
try{
sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"当前售出第"+(101 - ticket)+"张票");
ticket--;
}else{
break;
}
}
}
}
实现 callable 接口的方式
与使用runnable方式相比,callable功能更强大些:
runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回结果
callable 是一个接口,相当于将线程封装了一个返回指,方便以多线程的方式计算结果。
- 创建一个实现 callable 的实现类
- 实现 call() 方法,此线程需要执行的操作声明在 call() 中;
- 创建 callable 实现类的对象;
- 将 callable 接口实现类的对象作为传递到 FutureTask 的构造器中,创建 FutureTask 的对象;
- 将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start 方法启动(通过FutureTask的对象调用方法get获取线程中的call的返回值)
import javax.annotation.processing.SupportedSourceVersion;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
-
ClassName : Test03
-
Package : PACKAGE_NAME
-
Description :
-
Author : hyu
-
Create : 2024/4/27 13:38
-
Version : 1.0
*/
public class Test03 {public static void main(String[] args) {
NumThread numThread = new NumThread();FutureTask futureTask = new FutureTask(numThread); Thread t1 = new Thread(futureTask); t1.setName("线程1"); t1.start(); try{ Object sum = futureTask.get(); System.out.println(Thread.currentThread().getName() + " : " +sum); }catch (ExecutionException e){ e.printStackTrace(); }catch (InterruptedException e){ e.printStackTrace(); }
}
}
class NumThread implements Callable {
private int sum = 0;
@Override
public Object call() throws Exception {
for(int i=0;i<=100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + " : " +i);
sum += 1;
}
}
return sum;
}
}
使用线程池的方式
提前创建好需要的线程,放入线程池中,在使用的时候直接获取,使用完放回线程池中,可以避免频繁的创建销毁,实现重复利用。
线程池的设计思路
准备一个任务容器;
一次性启动多个消费者线程;
在刚开始的时候任务容器是空的,所有的线程都在等待(wait)
直到一个外部线程向这个任务容器中放了一个“任务”,就会有一个消费者线程被唤醒;
这个消费者线程取出“任务”,并且执行这个任务,在执行完毕后,继续等待下一次任务的到来;
线程池的处理流程
当调用线程池的时候首先判断线程是否空闲,若空闲则直接使用该线程执行逻辑,否则创建核心线程在执行逻辑;
当核心线程数满后,将新的任务放到工作队列中,遵循先进先出的原则;
当工作队列满后,将创建新的线程执行任务,直到达到最大线程数;
当最大线程数,工作队列都达到上限后,开始采用指定的饱和策略。
核心线程 -> 工作队列 -> 最大线程 -> 饱和策略
当工作队列满后才会创建最大线程
饱和策略是在队列和线程都满了之后的策略
程序运行饱和策略说明工作队列会最大线程数等参数设置不合理或者程序死锁造成卡死,需要对代码逻辑进行完善
线程池的饱和策略
AbortPolicy : 终止,这种拒绝策略在拒绝任务的时候,会直接抛出一个类型是 RejectedExecutionException 的 RuntimeException。让我们感知到任务被拒绝了,可以根据业务逻辑选择重试或者放弃提交等策略。
CallerRunsPolicy : 调用者线程执行,当有了新任务后,如果线程池没被关闭且没有能力执行,则将这个任务交给提交任务的线程执行,即谁提交任务,谁就负责执行任务。
DiscardPolicy : 丢弃,当新任务被提交后直接被丢弃掉,没有任何通知,存在一定的风险。
DiscardOldestPolicy : 丢弃最老的任务,如果线程池没有被关闭且没有能力执行,则会丢弃任务队列中的位于头结点的任务,通常是存活时间最长的任务,同样存在丢失数据的风险。
线程池 Executors 默认线程池
JDK 对线程池进行了相关的实现,在真实的企业开发中,很少去自定义线程池,而是使用 JDK 中自带的线程池。
使用 Executors 中提供的静态方法来创建线程池
static ExecutorService newCachedThreadPool() //创建一个默认的线程池
static newFixedThreadPool(int nThreads) //创建一个指定最多线程数量的线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
-
ClassName : MyThreadPoolDemo
-
Package : PACKAGE_NAME
-
Description :
-
Author : hyu
-
Create : 2024/4/27 14:01
-
Version : 1.0
*/
public class MyThreadPoolDemo {public static void main(String[] args) throws InterruptedException{
// 创建一个默认的线程池对象,默认是空的,默认最多可以容纳int类型的最大值 ExecutorService executorService = Executors.newCachedThreadPool(); // Executors 可以创建线程池对象 // ExecotorService 可以控制线程池 // 向线程池中提交任务 executorService.submit(() ->{ System.out.println(Thread.currentThread().getName() + "在执行任务了!!!"); }); executorService.submit(() -> { System.out.println(Thread.currentThread().getName() + "在执行任务了!!!"); }); executorService.submit(() -> { System.out.println(Thread.currentThread().getName() + "在执行任务了!!!"); }); executorService.shutdown();
}
}
Executors创指定上限的线程池
static ExecutorService newFixedThreadPool(int nThreads) : 创建一个指定最多线程数量的线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
-
ClassName : MyThreadPoolDemo2
-
Package : PACKAGE_NAME
-
Description :
-
Author : hyu
-
Create : 2024/4/27 14:10
-
Version : 1.0
*/
public class MyThreadPoolDemo2 {public static void main(String[] args) {
// 创建一个指定线程最大数量的线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); ThreadPoolExecutor poolExecutor = (ThreadPoolExecutor) executorService; // 此时线程池的线程数量是 0 System.out.println("线程池的线程数量: " + poolExecutor.getPoolSize()); // 向线程池中提交任务 executorService.submit(() -> { System.out.println(Thread.currentThread().getName() + "在执行任务了!!!"); }); executorService.submit(() -> { System.out.println(Thread.currentThread().getName() + "在执行任务了!!!"); }); executorService.submit(() -> { System.out.println(Thread.currentThread().getName() + "在执行任务了!!!"); }); executorService.submit(() -> { System.out.println(Thread.currentThread().getName() + "在执行任务了!!!"); }); // 此时线程池中的线程数量是提交的任务数 System.out.println("线程池的线程数量: " + poolExecutor.getPoolSize());
}
}
自定义线程池 ThreadPoolExecutor
注意:明确线程池对多可执行的任务数 = 队列容量 + 最大线程数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize: 核心线程的最大值,不能小于0
maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
keepAliveTime: 空闲线程最大存活时间,不能小于0
unit: 时间单位
workQueue: 任务队列,不能为null
threadFactory: 创建线程工厂,不能为null
handler: 任务的拒绝策略,不能为null
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
-
ClassName : ThreadPoolExecutorDemo01
-
Package : PACKAGE_NAME
-
Description :
-
Author : hyu
-
Create : 2024/4/27 16:55
-
Version : 1.0
*/
public class ThreadPoolExecutorDemo01 {public static void main(String[] args) {
/*
核心线程数量是 1,最大线程池数量是 3, 任务容器数量是 1, 空闲线程的最大存在时间是 20s
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,3,20, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());// 提交任务 for(int x=0;x<5;x++){ threadPoolExecutor.submit(() -> { System.out.println(Thread.currentThread().getName() + "---->> 执行了任务"); }); }
}
}
Java 标准库线程池的几种创建方式
import java.util.concurrent.*;
public class ThreadPool {
public static void main(String[] args) {
// 1. 用来处理大量短时间工作任务的线程池,如果池中没有可用的线程将创建新的线程,如果线程空闲60秒将收回并移出缓存
ExecutorService cachedThreadPool=Executors.newCachedThreadPool();
// 2. 创建一个操作无界队列且固定大小线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 3. 创建一个操作无界队列且只有一个工作线程的线程池
ExecutorService singleThreadExecutor=Executors.newSingleThreadExecutor();
// 4. 创建一个单线程执行器,可以在给定时间后执行或定期执行。
ScheduledExecutorService singleThreadScheduleExecutor=Executors.newSingleThreadScheduledExecutor();
// 5. 创建一个指定大小的线程池,可以在给定时间后执行或定期执行。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
// 6. 创建一个指定大小(不传入参数,为当前机器CPU核心数)的线程池,并行地处理任务,不保证处理顺序
Executors.newWorkStealingPool();
// 7. 自定义线程池
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(3,10,1000,TimeUnit.MICROSECONDS,new LinkedBlockingQueue
}
}
线程中的死锁即处理机制
什么是死锁?
在 Java 中 死锁是指两个或者多个线程互相持有对方所需要的锁,并且在无法继续执行的情况下永久的等待对方释放锁,这种情况下,所有这几的线程都无法继续执行,程序被卡住,无法正常终止;死锁通常发生在多线程并发执行的时候,当线程之间相互竞争获取资源的时候,典型的死锁场景包括以下四个条件的同时满足:
互质:至少有一个资源同时只能被一个线程占用;
占有并等待:一个线程已经占有一个资源,同时还等待另一个线程占有的资源;
不可剥夺:一个线程已经占有的资源不能被其他线程强制性剥夺;
环路等待:多个线程之间形成等待资源的环路,如 a,b,c 三个线程分别占有锁,但同时其中a线程等待b线程释放锁,b线程等待c线程释放锁,而c线程又等待a线程释放锁。
Java 中的锁机制
概念
锁指多线程编程中的机制,用于控制对共享资源的访问,可以防止多个线程同时修改或读取共享资源,从而确保线程安全;锁用于实现线程之间的互斥协调,确保在多线程环境下对共享资源的访问顺序和正确性。
锁分类
按照性质划分
乐观锁:认为一个线程获取共享数据的时候,不会存在其他线程修改该共享数据的情况,所以不会上锁;例如:CAS机制,版本号机制等。
悲观锁:认为一个线程在获取共享数据的时候,一定会存在其他线程修改该共享数据的情况,因此获取共享数据的时候会进行加锁,例如:Synchronize, ReentrantLock 锁。