一、进程
进程是操作系统进行资源分配与调度的基本单位,可以理解为操作系统中正在运行的程序,比如:qq、微信、idea工具等,在操作系统中运行的".exe"都可以理解为一个进程。
二、线程
1、概念
线程是进程中的一个执行单位,一个进程中可以有多个线程,每个线程将指令流中的一条一条的指令按照一定的顺序交给CPU去执行
2、并行与并发
(1)并行
在多核CPU中,每个核可以分别调度运行线程,这时多个线程可以并行的执行
(2)并发
在单核CPU下,多个线程是串行执行的。多个线程在执行的过程中通过操作系统中的任务调度器将CPU的时间片分配各不同的线程(不同的线程在轮流使用CPU)。由于CPU的执行速度非常的快,让人感觉不到多个线程在交替执行,从视觉上感受是在并行执行。
3、java线程
(1)创建和运行
java线程使用start方法来启动新的线程(该方法会请求JVM来运行相应的线程去工作),新的线程间接执行run方法中的代码。多个线程start调用顺序并不一定是线程启动的顺序,线程什么启动是由线程调度器(Scheduler)来决定的。
线程的创建有三种方式:
- 继承Thread类创建线程
- 定义继承Thread的线程类
- 重写Thread中的run方法
- 创建自定义的线程对象
- 使用start方法开启线程,执行run方法
代码实现如下:
public class Demo2_Thread {
/**
* 演示多线的创建
*/
public static void main(String[] args) {
MyThread mt = new MyThread(); //4,创建线程的子类对象
//mt.run();
mt.start(); //5,开启线程
for(int i = 0; i < 10000; i ++) {
System.out.println(i + " bb");
}
}
}
class MyThread extends Thread { //1,继承Thread
public void run() { //2,重写run方法
for(int i = 0; i < 10000; i ++){//3,将要执行的代码写在run方法中
System.out.println(i + " aaaaaaaaaaaaaaaa");
}
}
}
效果如下:

注意:在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序是无关的。
- 实现Runnable接口创建线程
- 定义实现Runnable接口的类
- 重写run方法
- 创建Thread对象,传入Runnable实现类
- 调用start方法开启线程,内部调用实现的run方法
public class RunnableTest {
/**
* 测试实现Runnable接口的形式创建线程
* @param args
*/
public static void main(String[] args) {
//3)创建Runnable接口的实现类对象
RunnableThread runnableThread = new RunnableThread();
//4)创建线程对象
Thread thread = new Thread(runnableThread);
//5)开启线程
thread.start();
//当前是main线程
for(int i = 0; i <= 100; i++) {
System.out.println("main -->" + i);
}
//有时调用Thread(Runnable)构造方法时,实参也会传递匿名内部类对象
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i <= 100; i++) {
System.out.println("sub thread2 ---------------->" + i);
}
}
});
thread1.start();
}
}
- 使用Callable和FutureTask创建线程
上面两种创建线程的方式存在一个共同的问题:不能异步的获取执行结果。在java的1.5版本解决了该问题,提供了Callable和FutureTask类相结合的方式来异步获取线程的执行结果。在使用该方式创建线程时有以下需要注意:- 创建多个线程执行同一个FutureTask的run方法时,只会执行一次(只有其中的一个线程来执行)

- 如果多个线程需要同时执行任务,则需要声明多个FutureTask

- 创建多个线程执行同一个FutureTask的run方法时,只会执行一次(只有其中的一个线程来执行)
(2)线程的生命周期

- new: 线程的初始化状态,线程已经被构建,但是还没有执行start方法。
- RUNNABLE: 线程的可运行状态,该状态包括运行中和就绪这两种状态。
- BLOCKED: 线程的阻塞状态,等待其他线程释放资源。
- WATTING: 线程的等待状态,处于该状态的线程需要等待其他线程对其进行唤醒操作,进而进入可运行状态。例如调用了无参数的 Object.wait() 方法,或者调用了无参数的 Thread.join() 方法。
- TIME_WAITING: 线程超时等待状态,可在一定的时间自行返回。例如调用了带有超时参数的 Object.wait() 方法,或者调用了带有超时参数的 Thread.sleep() 方法。
- TERMINATED: 线程终止状态,表示当前线程执行完毕。
(3)常用方法和工具
常用方法:
- sleep
语法:Thread.sleep(mills);让当前线程休眠指定的毫秒数,交出CPU的使用权给其他线程。
package chatpter01;
public class TestSleep {
public static void main(String[] args) {
System.out.println("mian threadname=" + Thread.currentThread().getName()
+ ",begin= " + System.currentTimeMillis());
SubThread4 t4 = new SubThread4();
t4.setName("t4");
t4.start();
System.out.println("mian threadname=" + Thread.currentThread().getName()
+ ",end= " + System.currentTimeMillis());
}
static class SubThread4 extends Thread{
@Override
public void run() {
try {
System.out.println("run threadname=" + Thread.currentThread().getName()
+ ",begin= " + System.currentTimeMillis());
Thread.sleep(2000);
System.out.println("run threadname=" + Thread.currentThread().getName()
+ ",end= " + System.currentTimeMillis());
} catch (InterruptedException e) {
//在子线程的run方法中,如果有受检异常(编译时异常),只有选择捕获处理,不能抛出处理
e.printStackTrace();
}
}
}
}

。调用sleep方法会让当前线程从Running状态进入TIME_WAITING状态。
。当其他线程使用interrupt方法打断正在睡眠的线程时,sleep方法会抛出interruptedException异常。
。睡眠结束后线程不一定立即执行,可能进入就绪状态。
- yield
调用yield会让当前线程从Running进入Runnable就绪状态(放弃当前的CPU资源),然后调度执行其他线程,也有可能再次调度到该线程。具体的实现依赖于操作系统的任务调度器。
package chatpter01;
public class TestYield {
public static void main(String[] args) {
SubThread6 t6 = new SubThread6();
t6.start();
long begin = System.currentTimeMillis();
long sum = 0;
for(int i = 0; i <= 100000; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "用时: " + (end - begin));
}
static class SubThread6 extends Thread{
@Override
public void run() {
long begin = System.currentTimeMillis();
long sum = 0;
for(int i = 0; i <= 100000; i++) {
sum += i;
Thread.yield();
}
long end = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + "用时: " + (end - begin));
}
}
}

- join
使当前线程处于等待状态,直到指定的线程执行结束,当前线程再继续执行,也可以使用join(time)等待指定的时间后再继续执行。
package chatpter01;
public class TestJoin {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "执行完毕!");
}
});
thread.setName("join-test");
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "执行完毕!");
}
}

- interrupt
中断线程. 注意调用interrupt()方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程。sleep方法会抛出中断异常。
package chatpter01;
public class TestInterrupt {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "执行完毕!");
}
});
thread.setName("join-test");
thread.start();
thread.interrupt();
System.out.println(Thread.currentThread().getName() + "执行完毕!");
}
}

-
currentThread
Thread.currentThread()方法可以获得当前线程。同一代码段可以被多个线程来执行,Thread.currentThread()返回值就是在代码实际运行时候的线程对象。 -
setName
设置线程名称,通过设置线程名称可以提高代码的可测试性和可读性。 -
getName
返回线程名称 -
isAlive
判断当前线程是否处于活跃状态,活跃线程表示线程已经启动并且没有终止。 -
getId
获取线程的唯一标识,需要注意的是某个标号的线程运行结束后,该编号可能会被后续创建的线程使用。 -
setPriority
设置线程的优先级,取值范围为1-10(如果超出这个范围会抛出异常IllegalArgumentException)。它仅仅起到一个提示作用,调度器可以忽略。 -
setDaemon
java中的线程分为用户线程与守护线程,守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程。守护线程不能单独运行,当JVM中没有其他用户线程,只有守护线程时,守护线程会自动销毁,JVM会退出
(4)查看进程、线程的方法
-
windows
- 任务管理器(ctrl+alt+delete)
- tasklist

- taskkill杀死线程
-
linux
- ps -ef 查看所有进程
- ps -ft -p 查看某个进程的所有线程
- kill 杀死进程
- top按大写H切换是否显示线程
- top -H -p 查看某个进程的所有线程
-
java
- jps命令查看所有java进程
- jstack 查看某个java进程的所有线程状态
三、进程和线程对比
进程是线程的容器,一个进程至少有一个线程,一个进程中也可以有多个线程。在操作系统中是以进程为单位分配资源,如虚拟存储空间,文件描述符等。每个线程都有各自的线程栈,自己的寄存器环境,自己的线程本地存储。
四、线程安全
介绍线程安全前先来介绍下什么是非线程安全。非线程安全指的是在并发访问同一个对象中的实例变量时,可能发生脏读的情况(读取到了其他线程修改后的值)。而线程安全不会发生脏读的情况,多个线程对同一个对象的实例变量的操作是经过同步处理的。
1、背景(导致线程安全的根本原因)
现代CPU架构带来的可见性问题。

-
CPU(中央处理器)是计算机的核心部件,负责执行计算机指令和处理数据。其组成部分主要包括以下几个关键部分:
- 算术逻辑单元(ALU, Arithmetic Logic Unit):
- ALU是CPU中的基本处理单元,用于执行算术和逻辑运算。
- 它能执行加、减、乘、除等基本的数学运算,以及位操作、比较等操作。
- 控制单元(CU, Control Unit):
- CU负责从内存或高速缓存中取出指令,并对指令进行解码,然后发出控制信号,指挥整个CPU自动、协调地工作。
- CU还负责异常和中断的处理。
- 寄存器(Registers):
- 寄存器是CPU内部的高速存储单元,用于存储数据和指令的地址。
- 常见的寄存器包括通用寄存器、指令寄存器(IR)、程序计数器(PC)、地址寄存器(AR)等。
- 寄存器直接参与CPU的运算,因此其存取速度非常快。
- 高速缓存(Cache Memory):
- 虽然不是CPU的核心组成部分,但高速缓存对于CPU的性能至关重要。
- 高速缓存用于存储CPU经常访问的数据和指令,以减少对主存的访问次数,从而提高CPU的运算速度。
- 总线(Bus):
- 总线是CPU与其他部件(如内存、I/O设备等)进行通信的通道。
- 常见的总线包括数据总线、地址总线和控制总线。
- 其他辅助部件:
- 时钟发生器(Clock Generator):用于产生时钟信号,控制CPU的工作节奏。
- 电源管理单元(PMU, Power Management Unit):负责CPU的电源管理和节能控制。
- 算术逻辑单元(ALU, Arithmetic Logic Unit):
此外,随着技术的发展,现代CPU还集成了许多其他功能,如浮点运算单元(FPU)、指令集扩展(如SSE、AVX等)、多核处理器(Multi-Core Processor)和并行处理技术(如超线程技术)等,以提高CPU的处理能力和效率。
- 现代CPU3级缓存架构

由于CPU、内存和I/O设备的速度有极大的差异,为了合理利用CPU的高性能,平衡这三者的速度差异,计算机体系结构、操作系统和编译程序都做出了各自的贡献:
- CPU增加的缓存,以均衡CPU与内存的速度差异(在多核CPU下会出现【可见性】问题)
- 操作系统增加了线程分时复用CPU,进而均衡CPU与I/O设备的速度差异(会导致【原子性】问题)
- 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用(会导致【有序性】问题)
2、并发问题的根源
并发问题根源:并发三要素:
- 原子性
线程切换带来原子性问题。所谓的原子操作,就是“不可中断的一个或一系列操作”,是指不会被线程调度机制打断的操作。这种操作一旦开始,就一直运行到结束,中间不会有任何线程的切换(也就是说其他线程看不到当前线程操作的中间结果)。
案例:
非原子性带来的问题测试
package chatpter01;
public class TestAtomic {
public static Integer num = 0;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
});
thread.start();
for (int i = 0; i < 10000; i++) {
num ++;
}
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(num);
}
}

使用原子类测试:
package chatpter01;
import java.util.concurrent.atomic.AtomicInteger;
public class TestAtomic {
public static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
num.getAndIncrement();
}
}
});
thread.start();
for (int i = 0; i < 10000; i++) {
num.getAndIncrement();
}
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(num);
}
}

- 可见性
缓存导致可见性问题。可见性问题通俗的描述:在多线程环境中,一个线程对某个共享变量更新之后,后续其他的线程无法立即读取到该共享变量的更新结果,这样就产生了可见性问题。
案例,可见性带来的问题测试:
package chatpter01;
public class TestVolatile {
private boolean numFlag = false;
private int num = 0;
private static int flag = 0;
// private static volatile int flag = 0;
public static void main(String[] args) throws Exception {
final TestVolatile volatileDemo = new TestVolatile();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for (int i = 0; i < 10000; i++) {
volatileDemo.num ++;
}
volatileDemo.numFlag = true;
System.out.println("num: " + volatileDemo.num);
}
}).start();
while(!volatileDemo.numFlag) {
int a = 0;
int b =a + 1;
flag = b;
}
System.out.println("main线程执行结束");
}
}

现象描述:
虽然其他的线程将 volatileDemo.numFlag设置为true,但是main线程一直无法跳出while循环,这就说明了main线程无法读取到其他线程对volatileDemo.numFlag的更新结果,这样就产生了可见性问题。
- 有序性
指令重排序:编译优化带来有序性问题。在不影响单线程程序执行结果的前提下,计算机为了最大限度的发挥机器的性能,会优化指令的执行次序。重排序会遵循as-if-serial与happens-before原则:
3、Java如何解决线程安全问题的
(1)Java内存模型(JMM)概念
- JMM定义了一组规则和规范,该规范定义了一个线程对共享变量写入时,如何确保对另一个线程是可见的。实际上,JMM提供了合理的禁用缓存以及禁止重排序的方法,所以其核心的价值在于解决可见性和有序性。
- JMM的另外一个大价值在于能屏蔽各种硬件和操作系统的访问差异,保证Java程序在各种平台下对内存的访问最终都是一致的。
(2)JMM如何解决有序性问题:
- JMM提供了自己的内存屏障指令,要求JVM编译器实现这些指令,禁止特定类型的编译器和CPU重排序
- JMM内存屏障
- 由于不同的CPU硬件实现内存屏障的方式不同,JMM屏蔽了这种底层CPU硬件平台的差异,定义了不对应任何CPU的JMM逻辑层内存屏障,由JVM在不同的硬件平台生成对应的内存屏障机器码
- JMM内存屏障主要有Load和Store两类,具体如下:
- Load Barrier(读屏障)
在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主存加载数据 - Store Barrier(写屏障)
在写指令之后插入写屏障,能让写入缓存的最新数据写回主存
- Load Barrier(读屏障)
- 在实际使用时,会对Load Barrier和Store Barrier两类屏障进行组合,组合成LoadLoad(LL)、StoreStore(SS)、LoadStore(LS)、StoreLoad(SL)四个屏障,用于禁止特定类型的CPU重排序:
- LoadLoad(LL)屏障
- StoreStore(SS)屏障
- LoadStore(LS)屏障
- StoreLoad(SL)屏障
(3)Java内存模型定义的两个概念
- 主存:主要存储的是Java实例对象,所有线程创建的实例对象都存放在主存中,无论该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括共享的类信息、常量、静态变量。由于是共享数据区域,因此多条线程对同一个变量进行访问可能会发现线程安全问题
- 工作内存:主要存储当前方法的所有本地变量信息(工作内存存储着主存中的变量副本),每个线程只能访问自己的工作内存,即线程中的本地变量对其他线程是不可见的,即使两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括字节码行号指示器、相关Native方法的信息。注意,由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

(4)Java内存模型的规定
- 所有变量存储在主存中
- 每个线程都有自己的工作内存,且对变量的操作都是在工作内存中进行的
- 不同线程之间无法直接访问彼此工作内存中的变量,要想访问只能通过主存来传递
(5)JMM的8个操作
-
JMM定义了一套自己的主存与工作内存之间的交互协议,即一个变量如何从主存拷贝到工作内存,又如何从工作内存写入主存,该协议包含8种操作,并且要求JVM具体实现必须保证其中每一种操作都是原子的、不可再分的
- lock
锁定,把变量标识为线程独占,作用于主内存变量 - unlock
解锁,把锁定的变量释放,别的线程才能使用 - read
读取,把变量值从主内存读取到工作内存 - load
载入,把read读取到的值放入工作内存的变量副本中 - use
使用,把工作内存中一个变量的值传递给执行引擎 - assign
赋值,把从执行引擎接收到的值赋给工作内存里面的变量 - store
存储,把工作内存中一个变量的值传递到主内存中 - write
写入,把store进来的数据存放入主内存的变量中

- lock
-
Java内存模型还规定了执行上述8种基本操作时必须满足如下规则
- 不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store和write之间是可以插入其他指令的。不允许read和load、store和write操作之一单独出现,意味着有read就有load,不能读取了变量而不予加载到工作内存中;有store就有write,也不能存储了变量值而不写到主内存中
- 不允许一个线程丢弃它最近的assign操作,也就是说当线程使用assign操作对私有内存的变量副本进行变更时,它必须使用write操作将其同步到主存中
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主存中
- 一个新的变量只能从主存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assin)的变量,换句话说,就是对一个变量实施use和store操作之前,必须先执行load和store操作
- 一个变量在同一个时刻只允许一个线程对其执行lock操作,但lock操作可以被同一个线程重复执行对此,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值
- 如果一个变量实现没有被lock操作锁定,就不允许对它执行unlock操作,也不允许unlock一个被其他线程锁定的变量
- 对一个变零执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)
(6)总结
Java内存模型规范了Java虚拟机(JVM)如何提供按需禁用缓存和编译优化的方法:

这些方法包括:volatile、synchronized和final关键字,以及Java内存模型中的Happens-before规则:

- volatile关键字
volatile关键字在JVM中,相当于是一种轻量级同步机制,具有如下特点:- 保证可见性
- 对一个volatile修饰变量的读操作,总是能够读取到该变量最新的值。
- 一个线程修改了volatile修饰的变量,这个变量的新值会立即刷新回主内存中。
- 一个线程读取volatile修饰的变量,该变量在工作内存中无效,需要重新到主内存中去读取。
- 案例
- 保证可见性
package chatpter01;
public class TestVolatile {
private boolean numFlag = false;
private int num = 0;
//private static int flag = 0;
private static volatile int flag = 0;
public static void main(String[] args) throws Exception {
final TestVolatile volatileDemo = new TestVolatile();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for (int i = 0; i < 10000; i++) {
volatileDemo.num ++;
}
volatileDemo.numFlag = true;
System.out.println("num: " + volatileDemo.num);
}
}).start();
while(!volatileDemo.numFlag) {
int a = 0;
int b =a + 1;
flag = b;
}
System.out.println("main线程执行结束");
}
}

- 不保证原子性
保证原子性需要借助synchronized这样的锁机制
- 禁止指令重排
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。
- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
- volatile内存语义
- volatile写的内存语义
写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量的值刷新到主内存中 - volatile读的内存语义
读一个volatile变量时,JMM会把线程对应的工作内存中的共享变量数据设置为无效的,然后会从主内存中去读取共享变量最新的数据 - 禁止指令重排序
用volatile修饰的变量在硬件层面上会通过在指令前后加入内存屏障来实现,编译器级别是通过下面的规则实现为了实现这些volatile内存语义,JMM对于volatile变量会有特殊的约束:- 使用volatile修饰的变量其read、load、use都是连续出现的,所以每次使用变量的时候都要从主存读取最新的变量值,替换私有内存的变量副本值(如果不同的话)。
- 其对同一个变量assign、store、write操作都是连续出现的,所以每次对变量的改变都会立马同步到主存中。
- 虽然volatile修饰的变量可以强制刷新内存,但是其并不具备原子性。虽然其要求对变量的(read、load、use)、(assign、store、write)必须是连续出现,但是在不同的CPU内核上并发执行的线程还是有可能出现脏读数据。
- volatile写的内存语义
- volatile缓存可见性实现原理
- 底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并写回到主内存。
- IA-32和Intel-64架构软件开发者手册对lock指令的解释:
- 会将当前处理器缓存行的数据立即写回到系统内存
- 这个写回内存的操作会引起在其他CPU缓存了该内存地址的数据无效(MESI协议)
- 提供内存屏蔽功能,使lock前后的指令不能重排序
- Java程序汇编代码查看
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VolatileVisibilityTest.prepareData
- synchronized关键字
- 概念
每个Java对象都隐含一把锁,称为Java内置锁(或者对象锁、隐式锁)。使用synchronized(syncObject)调用相当于获取syncObject的内置锁,所以可以使用内置锁对临界区代码段进行排他性保护 - 特性
- 为非公平锁
- 为可重入锁
- 为独占锁
- 为不可中断锁
- 异常自动释放锁
- 应用
- 同步方法
- 同步静态方法
- 同步代码块
- 概念
package chatpter01;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
public class TestSynchronized {
/**
* 测试同步代码块
*/
@Test
public void Test01() {
final CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(new Runnable() {
@Override
public void run() {
mm(); //使用的锁对象this
countDownLatch.countDown();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
mm(); //使用的锁对象this
countDownLatch.countDown();
}
}).start();
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
/**
* 同步代码块
*/
private void mm() {
synchronized (this) { //经常使用this当前对象作为锁对象
for(int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
}
- As-if-Serial规则
无论如何重排序,都必须保证代码在单线程下运行正确 - Happens-Before规则
JMM定义了一套自己的规则:Happens-Before(先行发生)规则,并且确保只要两个Java语句之间必须存在Happens-Before关系,JMM尽量确保这两个Java语句之间的内存可见性和指令有序性- 程序顺序执行规则(as-if-serial规则)
一个线程中的每个操作,happens-before于该线程的任意后续操作(单个线程中的代码顺序无论怎么重排序,对于结果是不变的)。 - volatile变量规则
对一个volatile修饰变量的写,happens-before于任意后续对这个volatile变量的读 - 传递性规则
如果A happens-Before B,且B happens-Before C,那么A happens-Before C - 监视锁规则(Monitor Lock Rule)
对一个锁的解锁,happens-before 于随后对这个锁的加锁 - start规则
如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作 - join规则
如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回 - 线程中断规则
对线程interrupt()方法的调用Happens-Before于被中断线程的代码检测到中断事件的发生
例如:在线程A中中断线程B之前,将共享变量x的值修改为100,则当线程B检测到中断事件时,访问到的x变量的值为100 - 对象终结原则
一个对象的初始化完成Happens-Before于它的finalize()方法的开始
- 程序顺序执行规则(as-if-serial规则)
- final关键字
五、线程间同步
1、介绍
线程同步机制是一套用于协调线程之间的数据访问的机制,该机制可以保障线程安全。Java平台提供的线程同步机制包括:锁,volatile关键字,final关键字,CAS,以及相关的API,如Object.wait()/Object.notity()等。
2、锁
(1)介绍
线程安全问题的产生前提是多个线程并发访问共享数据。将多个线程的共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问,锁就是复用这种思想来保障线程安全的。在使用上,一个锁只能被一个线程持有(这种锁被称为排他锁或互斥锁: Mutex)。

其中被锁保护的区域称为临界区。此外在实现形式上JVM把锁分为内部锁(synchronized关键字实现)和显示锁(java.concurrent.locks.Lock接口的实现类实现)。
(2)锁的作用
保障线程的原子性、可见性和有序性。
(3)锁在使用形式上的分类
- 可重入性(Reentrancy):一个线程可以多次申请该锁。如果一个线程持有一个锁的时候还能够继续成功申请该锁,称该锁是可重入的,否则就称该锁为不可重入的。
(4)锁在争用和调度上的分类
- 公平锁:若干个线程按照锁的申请顺序来获取锁。相当于线程会进入队列,按照队列中线程的顺序获取锁。
- 非公平锁:多个线程去获取锁的时候,会直接尝试去获取该锁,如果获取不到就进入队列排队等待获取锁。
其中内部锁synchronized关键字属于非公平锁,显示锁Lock支持公平锁和非公平锁两种形式。
(5)锁的粒度
一个锁可以保护的共享数据的数量大小称为锁的粒度。锁保护共享数据量大,称该锁的粒度粗,否则就称该锁的粒度细。锁的粒度过粗会导致线程在申请琐时会进行不必要的等待,锁的粒度过细会增加锁调度的开销。
六、线程间通信
1、什么是线程间通信
当多个线程共同操作共享的资源时,线程间通过某种方式相互告知自己的状态,以避免无效的资源竞争。
2、线程间通信方式
(1)等待-通知
该方式是java中普遍的线程间通信方式,经典的案例就是“生产者-消费者”模式
- java关键字synchronized与wait()/notify()这两个方法一起使用可以实现等待/通知模式。
案例:
package chapter04;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class WaitNotify {
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) throws Exception {
Thread waitThread = new Thread(new Wait(), "WaitThread");
Thread notifyThread = new Thread(new Notify(), "NotifyThread");
waitThread.start();
TimeUnit.SECONDS.sleep(1);
notifyThread.start();
}
static class Wait implements Runnable {
public void run() {
//加锁,拥有lock的monitor
synchronized (lock) {
//当条件不满足时,继续wait,同时释放lock的锁
while (flag) {
try {
System.out.println(Thread.currentThread() + " flag is true. wait@ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//条件满足时,完成工作
System.out.println(Thread.currentThread() + " flag is false. running@ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
}
}
}
static class Notify implements Runnable {
public void run() {
//加锁,拥有lock的monitor
synchronized (lock) {
//获取lock的锁,然后进行通知,通知时不会释放lock的锁
//直到当前线程释放了lock后,WaitThread才能从wait方法中返回
System.out.println(Thread.currentThread() + " hold lock. notify @ " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
lock.notify();
flag = false;
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread() + " notify 唤醒 wait线程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

注意点:(1)调用wait方法时会释放锁;(2)调用notify方法时不会释放锁,直到当前线程执行完释放锁,其他线程才能从wait方法中返回。
- Lock锁的newCondition()方法返回Condition对象,Condition类也可以实现等待/通知模式。
- LockSupport中的park()和unpark()的作用分别是阻塞线程和接触阻塞线程。
(2)共享内存
(3)管道流
package chapter04;
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
public class Piped {
public static void main(String[] args) throws IOException {
PipedWriter pipedWriter = new PipedWriter();
PipedReader pipedReader = new PipedReader();
//将输出流和输入流进行连接,否则在使用时会抛出IOException
pipedWriter.connect(pipedReader);
Thread printThread = new Thread(new Print(pipedReader), "PrintThread");
printThread.start();
int receive = 0;
try {
while ((receive = System.in.read()) != -1) {
pipedWriter.write(receive);
}
} catch (Exception e) {
e.printStackTrace();
}
}
static class Print implements Runnable {
private PipedReader in;
public Print(PipedReader in) {
this.in = in;
}
public void run() {
int receive = 0;
try {
System.out.println("请输入:");
while ((receive = in.read())!= -1) {
System.out.println("输出:" + (char)receive);
System.out.println("请输入:");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

七、CAS
1、什么是CAS
- 全称是:Compare-And-Swap,是CPU并发原语;功能是判断内存中某个位置值是否为预期值,如果是则改为新值,这个过程是原子操作(也就是说CAS是线程安全的)。
- CAS是一种乐观锁,避免了悲观锁独占锁对象的情况,进而提高了并发性。通常用于并发编程中解决多线程并发访问共享资源的同步和数据一致性问题。
- JDK5所增加的JUC(java.util.concurrent)并发包对操作系统的底层CAS原子操作进行了封装,为上层Java程序提供了CAS操作的API
- CAS并发原语体现在Java语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮助我们实现CAS汇编指令。操作系统层面的CAS是一条CPU的原子指令(lock cmpxchg指令),正是由于该指令具备原子性,因此使用CAS操作数据时不会造成数据不一致的问题,Unsafe提供的CAS方法直接通过native方式(封装C++代码)调用了底层CPU指令 lock cmpxchg。
- lock 指令最终实现比较复杂,可以锁缓存、锁总线。

下面用一个简单的Java应用示例来演示CAS的使用(多线程累加操作):
package chatpter01;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class TestCasExample {
private static AtomicInteger counter = new AtomicInteger(0);
private static final CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
int oldValue, newValue;
do {
oldValue = counter.get();
newValue = oldValue + 1;
} while (!counter.compareAndSet(oldValue, newValue));
}
countDownLatch.countDown();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
int oldValue, newValue;
do {
oldValue = counter.get();
newValue = oldValue + 1;
} while (!counter.compareAndSet(oldValue, newValue));
}
countDownLatch.countDown();
});
t1.start();
t2.start();
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final counter value: " + counter.get());
}
}
结果:

2、Java API层面
CAS并发操作的原子性,是靠Java中sun.misc.Unsafe来保障的。通过该类native可以直接操作指定的内存数据。

其中compareAndSwapInt是一个本地方法。
3、底层实现层面
(1)完成Java应用层的CAS操作主要涉及Unsafe方法的调用
- 获取Unsafe实例
- 调用Unsafe提供的CAS方法,这些方法主要封装了底层CPU的CAS原子操作
- 调用Unsafe提供的字段偏移量方法,这些方法用于获取对象中的字段(属性)偏移量,此偏移量值需要作为参数提供给CAS操作。
(2)Unsafe类
- Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全的底层操作,如直接方法系统内存资源、自主管理内存资源等,Unsafe大量的方法都是原生(native)方法,基于C++实现,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。
- Unsafe类的权限定名为sun.misc.Unsafe,对普通程序员来说是“危险”的,一般的应用开发都不会涉及此类,Java官方也不建议直接在应用程序中使用。
(3)CAS在java中的应用
Java中的很多并发类库都应用了CAS(Compare and Swap)机制来实现高效的线程安全操作。下面是一些常见的Java并发类库及其应用CAS的API:
- java.util.concurrent.atomic包:提供了一些原子类,如AtomicInteger、AtomicLong、AtomicReference等,这些类都是通过CAS来实现原子性操作的。
- java.util.concurrent.ConcurrentHashMap类:这个类是一个线程安全的哈希表实现,其中的很多方法(如putIfAbsent(K key, V value)和remove(Object key, Object value)等)都应用了CAS机制。
- java.util.concurrent.locks包:这个包提供了一些锁的实现,如ReentrantLock、ReadWriteLock、StampedLock等,其中的很多方法(如tryLock()和compareAndSetState(int expect, int update)等)也应用了CAS机制。
- java.nio包:这个包提供了Java非阻塞I/O的实现,其中的Selector类就应用了CAS机制来实现高效的多路复用。
- java.util.concurrent.atomic.LongAdder类:这个类是Java 8新增的,用于实现高并发的累加器,其中也应用了CAS机制来实现原子性操作。
这只是一些常见的Java并发类库及其应用CAS的API,实际上,Java中还有很多其他的类库也应用了CAS机制来实现高效的线程安全操作。
八、ThreadLocal
1、什么是ThreadLocal
-
提供了线程本地变量:是一个以ThreadLocal对象为键、任意对象为值的存储结构,可以有效的避免线程安全问题。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
-
提供了线程局部变量的功能:每个线程都可以通过 ThreadLocal 创建自己独立的变量副本,在不同线程之间互不影响。其主要作用如下:
- 实现线程封闭(线程安全):ThreadLocal可以将数据和线程关联起来,保证了每个线程访问到的数据都是独立的,实现线程的封闭性从而避免了线程安全性问题。
- 保存线程的上下文信息:使用ThreadLocal可以保存线程的上下文信息,例如用户会话、数据库连接、事务状态等,这些信息可以在线程的生命周期内进行访问。
- 提高性能:可以减少线程间的同步操作,避免了加锁的开销从而提高了程序的并发性。
- 避免传参:可以避免变量在方法调用链中频繁的传递,使代码变得更加的简洁和易读。
使用ThreadLocal时有一下需要注意的地方:
- 需要及时清理资源:使用完 ThreadLocal 后,需要手动调用 remove() 方法清理资源,避免内存泄漏。
- 不适合在线程池等需要复用线程的情况下使用:因为 ThreadLocal 中的值会伴随线程的生命周期,如果线程被重用,可能导致数据混乱。
2、ThreadLocal中的成员方法
- set(T value)
设置当前线程在“线程本地变量”实例中绑定的本地值。 - T get()
获得当前线程在“线程本地变量”实例中绑定的本地值。 - remove()
移除当前线程在“线程本地变量”实例中绑定的本地值。
3、ThreadLocal源码解析
通过阅读ThreadLocal源码可知,ThreadLocal本身并不存储任何数据的,数据是通过每个线程的ThreadLocalMap来进行存储的。

(1)ThreadLocalMap介绍
- ThreadLocalMap中key表示ThreadLocal对象本身,value表示要存储在当前线程本地副本中的值。
- ThreadLocalMap是ThreadLocal中的一个静态内部类。

(2)ThreadLocal中的set(T value)方法

- 首先通过Thread.currentThread()方法获取当前线程实例对象。
- 然后通过getMap方法获取当前线程的ThreadLocalMap对象。
- 最后判断map是否为null
- 不为null:将当前ThreadLocal作为key,value作为值存入map中。
- 为null:通过createMap方法创建当前线程的ThreadLocalMap对象,并将 当前ThreadLocal作为key,value作为值存入到该对象中。
(3)ThreadLocal中的get()方法

- 通过Thread.currentThread()方法获取当前线程实例对象。
- 通过getMap方法获取当前线程的ThreadLocalMap对象。
- 如果map不为null,从map中获取key为当前ThreadLocal实例的entry。
- 如果当前entry不为null,返回对应的value值。
- 如果map为null或entry为null,进行初始化设置并返回value值。
(4)ThreadLocal中的remove()方法
用于移除当前线程的局部变量。

- 通过getMap方法获取当前线程的ThreadLocalMap对象。
- 如果map不为null,从map中移除当前ThreadLocal对象对应的键值对。
4、ThreadLocal是如何实现隔离的
- 每个线程对象都有自己的ThreadLocal.ThreadLocalMap变量。
- 当调用ThreadLocal对象set方法时,将数据存储到了线程对象对应的ThreadLocalMap中。
- 当调用ThreadLocal对象get方法时,实际上是从线程对象对应的ThreadLocalMap中获取数据。
- 在多线程环境下,通过ThreadLocal设置值或获取值最终都是线程对象各自的ThreadLocalMap进行操作,从而实现了线程隔离。
5、ThreadLocal内存泄漏
有了前面的ThreadLocal基础知识的学习,可知Thread有如下结构:

代码演示ThreadLocal的用法:
package chatpter01;
public class TestThreadLocal {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
private static ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
int num = i;
new Thread(() -> {
threadLocal.set(num);
threadLocal1.set(num + 5);
System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + ": " + threadLocal1.get());
}).start();
}
}
}

下面分别来介绍下不会出现内存泄漏和出现内存泄漏的场景。
(1)不会出现内存泄漏的场景
当我们开启线程并将变量通过ThreadLocal对象存储在Thread对象对应的ThreadLocalMap中。当线程在运行中Thread与ThreadLocalMap的引用关系就一直存在(强引用,垃圾回收机制不会对其进行回收)。当线程任务行完成并退出后Thread与ThreadLocalMap的引用关系就不存在了,垃圾回收器会对ThreadLocalMap进行回收,这种情况是不会发生内存泄漏的。

(2)出现内存泄漏的场景
下面我们来分析下线程池中的线程与ThreadLocalMap的关系,图例如下所示,可以看出每个线程都拥有自己的ThreadLocalMap对象。由于在线程池中核心线程任务执行完成后是不会退出的,会等待接收下一个任务来执行,那么ThreadLocalMap和核心线程就会一直保持了强引用关系,垃圾回收器不会对ThreadLocalMap对象进行回收,存在内存泄漏的情况。

详细分析:
- ThreadLocalMap中key为WeakReference对象即为弱引用,会被垃圾回收器进行回收。
- ThreadLocalMap中value为强引用,只要线程不终止,value就不会被回收。
6、ThreadLocal中的TTL问题
ThreadLocal 是 Java 中提供的一个类,它提供了线程本地的变量。这些变量与其他普通变量的区别在于,每一个访问这个变量的线程都有其自己独立初始化的变量副本。ThreadLocal 实例通常作为静态私有的字段在类中定义,用于关联线程和线程上下文数据。
然而,ThreadLocal 的使用可能会带来内存泄漏的问题,尤其是在 Web 应用中,如使用 Servlet 容器时。这是因为 ThreadLocal 的生命周期实际上是与线程的生命周期绑定的,而不是与对象的生命周期绑定的。如果在线程池中使用 ThreadLocal,并且没有在线程退出前正确清除 ThreadLocal 中的值,那么即使对象不再被引用,由于 ThreadLocal 持有对对象的引用,这些对象也无法被垃圾收集器回收,从而可能导致内存泄漏。
TTL(Time To Live)在这里可能指的是 ThreadLocal 中变量的“存活时间”。在 Web 应用中,由于线程通常是由线程池管理的,并且可能会被重用,所以如果不正确地管理 ThreadLocal,这些变量可能会比预期存活得更久。
要解决这个问题,你可以采取以下措施:
- 显式清除:在每次使用完 ThreadLocal 后,显式调用 remove() 方法来清除线程局部变量。这通常在 finally 块中完成,以确保即使发生异常也能被清除。
try {
// 使用 ThreadLocal 变量
Object value = threadLocal.get();
// ... 其他代码 ...
} finally {
// 清除 ThreadLocal 变量
threadLocal.remove();
}
- 使用 try-with-resources(Java 9+):从 Java 9 开始,ThreadLocal 实现了 AutoCloseable 接口,因此可以使用 try-with-resources 语句自动管理 ThreadLocal 的生命周期。
try (ThreadLocal<MyType> threadLocal = new ThreadLocal<>()) {
// 使用 threadLocal 变量
threadLocal.set(new MyType());
// ... 其他代码 ...
} // 在这里,threadLocal 会自动调用 remove()
- 使用框架提供的工具:某些框架(如 Spring)提供了管理 ThreadLocal 的工具类,可以更方便地管理 ThreadLocal 的生命周期。
- 避免在 Servlet 中使用 ThreadLocal:由于 Servlet 的生命周期与线程池中的线程并不完全一致,因此在 Servlet 中使用 ThreadLocal 容易导致内存泄漏。尽量避免在 Servlet 中使用 ThreadLocal,或者确保在使用完后正确清除。
总之,管理 ThreadLocal 的关键是确保在线程不再需要它时,及时清除其值,以避免潜在的内存泄漏问题。

5471

被折叠的 条评论
为什么被折叠?



