Java知识体系Java并发编程进阶,多线程和锁底层原理探究

Posted 未来村村长

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java知识体系Java并发编程进阶,多线程和锁底层原理探究相关的知识,希望对你有一定的参考价值。



||To Up||

未来村村长正推出一系列【To Up】文章,该系列文章重要是对Java开发知识体系的梳理,关注底层原理和知识重点。”天下苦八股文久矣?吾甚哀,若学而作苦,此门无缘,望去之。“该系列与八股文不同,重点在于对知识体系的构建和原理的探究。


文章目录

  • ​​||To Up||​​
  • ​​一、Java多线程????​​
  • ​​1、进程与线程​​
  • ​​(1)进程​​
  • ​​(2)线程​​
  • ​​(3)线程与进程的区别​​
  • ​​2、线程的创建​​
  • ​​(1)继承Thread类​​
  • ​​(2)实现Runnable接口​​
  • ​​(3)使用Callable和FutureTask​​
  • ​​(4)使用线程池创建​​
  • ​​3、线程的状态与相关操作​​
  • ​​(1)相关状态与操作​​
  • ​​(2)守护线程​​
  • ​​(3)线程组​​
  • ​​① 创建​​
  • ​​② 使用​​
  • ​​③ 线程组的枚举​​
  • ​​④ main线程组的获取​​
  • ​​4、线程间通信​​
  • ​​5、线程池的架构设计​​
  • ​​(1)线程池架构设计​​
  • ​​(2)Executors的使用与弊端​​
  • ​​5、线程池标准创建方式与相关原理​​
  • ​​(1)ThreadPoolExecutor​​
  • ​​① 核心和最大线程数量​​
  • ​​② BlockingQueue​​
  • ​​③ keepAliveTime​​
  • ​​(2)向线程池提交任务的两种方式​​
  • ​​(3)任务调度流程​​
  • ​​(4)线程工厂ThreadFactory​​
  • ​​(5)任务阻塞队列​​
  • ​​(6)线程池的拒绝策略​​
  • ​​(7)线程池的状态与关闭​​
  • ​​6、ThreadLocal:线程安全解决​​
  • ​​(1)ThreadLocal原理​​
  • ​​(2)与Synchnized区别​​
  • ​​(3)ThreadLocal成员方法​​
  • ​​二、Java内置锁????​​
  • ​​1、synchronized关键字​​
  • ​​(1)synchronized同步方法​​
  • ​​(2)synchronized同步块​​
  • ​​(3)Synchronized的两个推论​​
  • ​​2、Java内置锁​​
  • ​​(1)Java对象与内置锁​​
  • ​​(2)内置锁的状态​​
  • ​​① 无锁状态​​
  • ​​② 偏向锁状态​​
  • ​​③ 轻量级锁状态​​
  • ​​④ 重量级锁状态​​
  • ​​3、synchronized执行原理​​
  • ​​4、CAS​​
  • ​​(1)原理​​
  • ​​(2)使用​​
  • ​​(3)CAS操作的弊端和规避​​
  • ​​三、JUC显式锁????​​
  • ​​1、显式锁Lock接口​​
  • ​​2、可重入锁ReentrantLock​​
  • ​​3、显式锁的使用​​
  • ​​(1)使用lock阻塞抢锁​​
  • ​​(2)使用tryLock非阻塞抢锁​​
  • ​​(4)使用tryLock限时抢锁​​
  • ​​4、LockSupport​​
  • ​​5、锁的类型​​
  • ​​(1)可重入锁和不可重入锁​​
  • ​​(2)悲观锁和乐观锁​​
  • ​​(3)公平锁和非公平锁​​
  • ​​(4)可中断锁和不可中断锁​​
  • ​​(5)共享锁和独占锁​​
  • ​​(6)读写锁​​
  • ​​四、AQS:显式锁的原理????​​
  • ​​1、AQS的组成​​
  • ​​(1)状态标志位​​
  • ​​(2)队列节点类​​
  • ​​(3)FIFO双向同步队列​​
  • ​​(4)钩子方法​​
  • ​​2、AQS实现锁原理​​
  • ​​(1)自己实现一个简单锁​​
  • ​​(2)AQS锁抢占原理​​
  • ​​① 锁抢占执行过程​​
  • ​​② acquire​​
  • ​​③ addWaiter()​​
  • ​​④ acquireQueued()​​
  • ​​(3)AQS释放锁原理​​
  • ​​① 锁释放执行过程​​
  • ​​② release()​​
  • ​​② tryRelease()​​
  • ​​③ unparkSuccessor(h)​​
  • ​​3、AQS条件队列​​
  • ​​(1)Condition基本原理​​
  • ​​(2)await等待方法原理​​
  • ​​(3)signal唤醒方法原理​​
  • ​​五、JUC原子类????️​​
  • ​​(1)基本类型原子操作​​
  • ​​(2)引用类型原子操作​​
  • ​​六、volatile????​​
  • ​​1、并发三大问题​​
  • ​​(1)原子性问题​​
  • ​​(2)可见性问题​​
  • ​​(3)有序性问题​​
  • ​​2、volatile原理​​

一、Java多线程????

1、进程与线程

(1)进程

一个进程是一个程序的一次启动和执行,一个进程一般由程序段、数据段、进程控制块组成:

  • 程序段:代码段,需要执行的指令集合
  • 数据段:需要操作的数据和程序运行时产生的数据
  • 进程控制块:进程的描述信息和控制信息,是进程存在的唯一标志
  • 进程的描述信息:进程ID和进程名称
  • 进程的调度信息:程序的起始地址和通信信息
  • 进程的资源信息:内存信息和文件句柄
  • 进程上下文:CPU寄存器的值、当前程序计数器的值

每当使用Java命令启动一个Java应用程序时,就会启动一个JVM进程,所有的Java程序代码都是以线程运行,JVM找到程序的入口main()方法,然后运行main方法产生一个线程,同时还会启动另外一个GC线程用于垃圾回收。

(2)线程

线程是指“进程代码段”的一次顺序执行流程,线程是CPU调度的最小单位,而进程是操作系统资源分配的最小单位,线程之间共享进程的内存空间、系统资源。

线程的组成如下:

  • 线程ID:线程的唯一标识,同一个进程内的线程ID不会重复
  • 线程名称:用于用户识别,若没有显式命名系统会自动分配
  • 线程优先级:表示线程调度的优先级,优先级越高获得CPU的执行机会越大
  • 线程状态:分别为新建(NEW)、可执行(RUNNABLE)、阻塞(BLOCKED)、无限期等待(WAITING)、限时等待(TIMED_WAITING)、结束(TERMINATED)。
  • 线程类型:是否为守护线程

(3)线程与进程的区别

线程是“进程代码段”的一次顺序执行流程,一个进程由多个线程组成,一个进程至少有一个线程。线程是CPU调度的最小单位,进程是操作系统分配资源的最小单位。进程之间相互独立,但进程内部的各个线程之间并不完全相互独立。各个线程之间共享进程的方法区内存、堆内存以及系统资源。

2、线程的创建

(1)继承Thread类

Thread类是Java多线程编程的基础,通过继承Thread类创建线程类可以实现线程的创建:

  • 继承Thread类,创建新的线程类
  • 重写run()方法,将需要并发执行的业务代码编写在run()方法中
  • 调用Thread实例的start()方法启动线程
  • 线程启动后,线程的run方法将被JVM执行

Thread类的源码量较大,不作展示分析。我们只需要知道Thread定义了线程的状态以及操作线程的相关方法即可。

(2)实现Runnable接口

Thread也实现了Runnable接口,且Thread中有以下构造方法可通过传入Runnable接口实现对象参数来实现线程的创建。

//系统定义名称
public Thread(Runnable target)
init(null, target, "Thread-" + nextThreadNum(), 0);

//自定义名称创建
public Thread(Runnable target, String name)
init(null, target, name, 0);

则通过实现Runnable接口来创建线程的步骤如下:

  • 定义一个类实现Runnable接口
  • 实现Runnable接口中的run()抽象方法【业务处理逻辑】
  • 通过Thread的构造方法创建线程对象,传入Ruunable实例作为参数
  • 调用Thread实例的start()方法启动线程
  • 线程启动后,线程的run方法将被JVM执行,Thread的run方法将会调用Runnable实例的run方法

这里Thread的run()方法先判断target是否为null,这里的target类型就是Runnable,即我们传入的参数。

public void run() 
if (target != null)
target.run();

我们也可以看看Runnable的源码,可以看到Runnable是一个函数式接口,即只有一个方法的接口,其与Thread都来自java.lang包。

package java.lang;
@FunctionalInterface
public interface Runnable
public abstract void run();

(3)使用Callable和FutureTask

在使用Callable和FutureTask之前我们先来看看它们的源码,来认识一下他们。首先是Callable,同样是一个函数式接口,其中的call与run类似,但是其具有返回值,可通过泛型来定义。

package java.util.concurrent;
@FunctionalInterface
public interface Callable<V>
V call() throws Exception;

但是我们知道要创建线程离不开Thread类,所以这里使用了FutureTask进行牵线搭桥。我们可以看到FutureTask类的声明和构造器。

public class FutureTask<V> implements RunnableFuture<V> 
public FutureTask(Callable<V> callable)
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable

FutureTask继承了RunnableFuture,其源码如下。所以我们可以想到,通过FutureTask构造器可构造一个Runnable实例,这样就可以传入Thread代理执行。

package java.util.concurrent;
public interface RunnableFuture<V> extends Runnable, Future<V>
void run();

使用Callable和FutureTask创建线程的步骤如下:

  • 创建一个Callable接口的实现类,实现call()方法【编写业务逻辑并设置返回值】
  • 使用Callable实现类的实例构造一个FutureTask实例
  • 使用FutureTask实例作为Thread构造器的参数target创建一个线程
  • 调用Thread实例的start()方法启动新线程,Thread的run方法会执行FutureTask的run方法,最终会调用Callable的call方法

(4)使用线程池创建

可以通过Executors工厂类构建线程池,然后通过其execute()【没有返回值,只接收Runnable实例和Thread实例】方法和submit()【可接收有返回值的Callable实例,或Runnable实例和Thread实例】方法实现线程的创建和执行。但生产环境不允许通过Executors创建线程池,需要通过调用ThreadPoolExecutor的构造方法完成。

线程池具体原理与操作后续会进行说明。

3、线程的状态与相关操作

(1)相关状态与操作

在Thread源码中使用enum枚举了Thread的六种状态:

  • NEW(新建):线程创建,未调用start方法启动,这里会调用相应的init方法进行创建【new】
  • RUNNABLE(可运行):线程处于可执行状态,已经在Java虚拟机执行,但还在等其它操作系统资源【start()】
  • 就绪状态:调用start()、CPU时间片用完、sleep()操作结束、join()操作结束、抢到对象锁、调用yield()方法
  • BLOCKED(阻塞):线程处于阻塞,线程在等待一个监控锁,特别是多线程下的场景等待另一个线程同步块的释放。【synchronized】
  • WAITING(等待):线程处于等待状态,指的是该线程正在等待另一个线程执行某些特定的操作【wait()、join()】
  • TIMED_WAITING(调校时间的等待):与时间相关的等待,调用了设定等待时长参数的方法【sleep(xx)、wait(xx)、LockSupport.parkNanos(xx)/parkUntil(xx)】
  • TERMINATED(终止):线程执行完毕的状态或执行过程发生了异常

我们可以通过getState()方法获取线程的执行状态,或者通过isAlice()方法判断一个线程是否还存活。

【Java知识体系】Java并发编程进阶,多线程和锁底层原理探究_多线程

(2)守护线程

JVM进程中的GC线程就是一个守护线程,守护线程的使用有以下要点:

  • 守护线程必须在启动前通过setDaemon()方法将状态设置为true,启动后就不能进行设置,否则报InterruptedException异常
  • 守护线程存在被JVM强制终止的风险,所以在守护线程中尽量不去访问系统资源
  • 守护线程中创建的线程也是守护线程

(3)线程组

一组线程或线程组的集合,在多线程情况下,对线程进行分组管理。直接在main方法中运行的线程或线程组,都属于main线程组,在main方法中运行的代码上一级为System线程组,其中线程的上一级为main线程组。

① 创建
ThreadGroup threadGroup01 = new ThreadGroup()
② 使用
Thread thread01 = new Thread(threadGroup01,new ThreadImplentsRunnable(),"thread-01");
Thread thread02 = new Thread(threadGroup01,new ThreadImplentsRunnable(),"thread-02")
③ 线程组的枚举
Thread[] threadList = new Thread[10];
threadGroup.enumerate(threadList);
④ main线程组的获取
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup()

4、线程间通信

线程的通信可以定义为:当多个线程共同操作共享资源时,线程间通过某种方式互相告知自己的状态,避免无效的资源争夺。通信方式有:等待-通知、共享内存、管道流。其中[等待-通知]是使用较普遍的通信方式。

Java内置锁可以使用wait()和notify()来实现”等待-通知“的通信方式。使用wait()方法以后,JVM会将当前线程加入该锁监视器的等待集合(WaitSet)。使用notify()后,JVM会唤醒该锁监视器等待集合中的第一条线程,若使用notifyAll会唤醒监视器等待集合的所有线程。

5、线程池的架构设计

Java线程的创建和销毁代价都比较高,频繁的创建和销毁线程非常低效,所以出现了线程池。线程池的好处:

  • 提升性能:不需要自己创建线程,将任务交给线程池去执行,线程池能尽可能使用空闲的线程执行异步任务,对创建的线程实现复用
  • 线程管理:线程池可以对线程进行有效管理,使得异步任务得到高效调度执行

(1)线程池架构设计

【Java知识体系】Java并发编程进阶,多线程和锁底层原理探究_java_02

① Executor:Executor是Java异步目标任务”执行者“接口,其只包含一个方法execute(Runnable command)。

② ExecutorService:ExecutorService继承Executor,其新增了submit和invoke系列方法,对外提供了异步任务的接收服务。

③ AbstractExecutorService:AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。

④ ThreadPoolExecutor:JUC线程池的核心实现类,线程池预先提供了指定数量的可重用线程,并对每个线程池都维护了一些基础数据统计,方便线程的管理和监控。

⑤ ScheduledExecutorService:继承于ExecutorService,用于完成”延时“和周期性任务的调度线程接口。

⑥ ScheduledThreadPoolExecutor:它提供了ScheduledExecutorService中的”延时执行“和”周期执行“等抽象调度方法的具体实现。

⑦ Executors是一个静态工厂类,提供了快速创建线程池的方法。

(2)Executors的使用与弊端

Java通过Executors工厂类提供了4中快捷创建线程池的方法。

方法名

功能简介

newSingleThreadExecutor()

创建只有一个线程的线程池

newFixedThreadPool(int nThreads)

创建固定大小的线程池

newCachedThreadPool()

创建一个不限制线程数量的线程池,任何提交的任务都立即执行,但空闲线程会得到及时回收

newScheduledThreadPool()

创建一个可定期或延时执行任务的线程池

使用Executors工厂类创建线程池有以下潜在问题:

  • 通过newFixedThreadPool(int nThreads)创建固定大小的线程池或通过newSingleThreadExecutor()创建只有一个线程的线程池,若任务提交速度持续大于任务处理速度,会造成大量的任务等待,等待队列过大会造成内存溢出异常
  • 通过newCachedThreadPool()创建一个不限制线程数量的线程池,若大量任务被启动,则需要创建大量的线程,也可能导致内存溢出异常(OOM,Out Of Memory)
  • 通过newScheduledThreadPool()创建一个可定期或延时执行任务的线程池,同样会因为线程数不设限制,从而导致OOM。

5、线程池标准创建方式与相关原理

(1)ThreadPoolExecutor

企业开发规范会要求使用标准的ThreadPoolExecutor构造工作线程池,其中会使用到其较重要的构造器如下:

public ThreadPoolExecutor(
int corePoolSize,//核心线程数,空闲也不会回收
int maximumPoolSize,//最大线程数
long keepAliveTime,TimeUnit unit,//线程最大空闲时长
BlockingQueue<Runnable> workQueue,//任务的排队队列
ThreadFactory threadFactory,//新线程的产生方式
RejectedExecutionHandler handler//拒绝策略
)
① 核心和最大线程数量

线程池执行器根据corePoolSize和maximumPoolSize来自动维护线程池的工作线程,当maximumPoolSize被设置为Integer.MAX_VALUE时,线程池可以接收任意数量的并发任务。corePoolSize和maximumPoolSize可以在运行过程中通过set方法动态更改。

使用线程池可以降低资源消耗,提高相应速度和线程的管理性。但是线程数配置不合理会适得其反。对于不同的任务类型将配置不同的线程数: