所谓并发编程是指在一台处理器上 “同时” 处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。
并发编程,从程序设计的角度来说,是希望通过某些机制让计算机可以在一个时间段内,执行多个任务。从计算机 CPU 硬件层面来说,是一个或多个物理 CPU 在多个程序之间多路复用,提高对计算机资源的利用率。从调度算法角度来说,当任务数量多于 CPU 的核数时,并发编程能够通过操作系统的任务调度算法,实现多个任务一起执行。
对于一个 Java 程序员而言,能否熟练掌握并发编程是判断他优秀与否的重要标准之一。因为并发编程是 Java 语言中最为晦涩的知识点,它涉及操作系统、内存、CPU、编程语言等多方面的基础能力,更为考验一个程序员的内功。
并发编程有三大特性:
定义: 并发编程是指在一台处理器上 “同时” 处理多个任务。并发是在同一实体上的多个事件,多个事件在同一时间间隔发生。
意义:开发者通过使用不同的语言,实现并发编程,充分的利用处理器(CPU)的每一个核,以达到最高的处理性能,提升服务器的资源利用率,提升数据的处理速度。
下图展示了最简单的 CPU 核心通过缓存与主存进行通讯的模型。
在缓存出现后不久,系统变得越来越复杂,缓存与主存之间的速度差异被拉大,由于 CPU 的频率太快了,快到主存跟不上,这样在线程处理器时钟周期内,CPU 常常需要等待主存,这样就会浪费资源。从我们的感官上,计算机可以同时运行多个任务,但是从 CPU 硬件层面上来说,其实是 CPU 执行线程的切换,由于切换频率非常快,致使我们从感官上感觉计算机可以同时运行多个程序。
为了避免长时间的线程等待,我们一方面提升硬件指标(如多级高速缓存的诞生,这里不做讨论),另一方面引入了并发概念,充分的利用处理器(CPU)的每一个核,减少 CPU 资源等待的时间,以达到最高的处理性能。
操作系统是包含多个进程的容器,而每个进程又是容纳多个线程的容器。
a. 什么是进程?
官方定义: 进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
Tips:操作系统分配的资源和调度对象其实就是 CPU 时间片。
b. 什么是线程?
官方定义: 线程是操作系统能够进行资源调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,每个线程执行的都是进程代码的某个片段,特定的线程总是在执行特定的任务。
c. 线程与进程的区别?
诞生起源:先有进程,后有线程。进程由于资源利用率、公平性和便利性诞生,线程则是为了提高 CPU 的利用率、提高程序的执行效率而诞生。
概念:进程是资源分配的最小单位。 线程是程序执行的最小单位(线程是操作系统能够进行资源调度的最小单位,同个进程中的线程也可以被同时调度到多个 CPU 上运行),线程也被称为轻量级进程;
内存共享:默认情况下,进程的内存无法与其他进程共享(进程间通信通过 IPC 进行)。 线程共享由操作系统分配给其父进程的内存块。
串行:顺序执行,按步就搬。在 A 任务执行完之前不可以执行 B。
并行:同时执行,多管齐下。指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同 CPU 核心上同时执行。
并发:穿插执行,减少等待。指多个线程轮流穿插着执行,并发的实质是一个物理 CPU 在若干道程序之间多路复用,其目的是提高有限物理资源的运行效率。
Java 内存模型(即 Java Memory Model,简称 JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
下图展示了Java 的内存模型。
工作内存(私有):由于JVM 运行程序的实体是线程,而每个线程创建时 JVM 都会为其创建一个工作内存(栈区和PC寄存器),用于存储线程私有的数据。线程私有的数据只能供自己使用,其他线程不能够访问到当前线程私有的内存空间,保证了不同的线程在处理自己的数据时,不受其他线程的影响。
主内存(共享):Java 内存模型中规定所有变量都存储在主内存(堆区和方法区),主内存是共享内存区域,所有线程都可以访问。从上图中可以看到,Java 的并发内存模型与操作系统的 CPU 运行方式极其相似,这就是 Java 的并发编程模型。通过创建多条线程,并发的进行操作,充分利用系统资源,达到高效的并发运算。
关于工作内存和主内存,详见JVM运行时数据区。
位于 java.lang 包下的 Thread 类是非常重要的线程类。学习 Thread 类的使用是学习多线程并发编程的基础。它实现了 Runnable 接口,其包集成结构如下图所示。
Thread 类的常用方法介绍:
方法 | 作用 |
---|---|
start() | 启动当前的线程,调用当前线程的 run () |
run() | 通常需要重写 Thread 类中的此方法,将创建要执行的操作声明在此方法中。 |
currentThread() | 静态方法,返回代码执行的线程。 |
getName() | 获取当前线程的名字。 |
setName() | 设置当前线程的名字。 |
sleep(long millitime) | 让当前进程睡眠指定的毫秒数,在指定时间内,线程是阻塞状态。 |
isAlive() | 判断进程是否存活。 |
wait() | 线程等待。 |
notify() | 线程唤醒。 |
Java 多线程有 3 种创建方式如下:
实例:
/**
* 方式一:继承Thread类的方式创建线程
*/
public class ThreadExtendTest extends Thread{ //步骤 1
@Override
public void run() { //步骤 2
//run方法内为具体的逻辑实现
System.out.println("create thread by thread extend");
}
public static void main(String[] args) {
new ThreadExtendTest(). start();
}
}
Tips:由于 Java 是面向接口编程,且可进行多接口实现,相比 Java 的单继承特性更加灵活,易于扩展,所以相比方式一,更推荐使用方式二进行线程的创建。
实例:
/**
* 方式二:实现java.lang.Runnable接口
*/
public class ThreadRunnableTest implements Runnable{//步骤 1
@Override
public void run() {//步骤 2
//run方法内为具体的逻辑实现
System.out.println("create thread by runnable implements");
}
public static void main(String[] args) {
new Thread(new ThreadRunnableTest()). start();
}
}
Tips:方式一与方式二的创建方式都是复写 run 方法,都是 void 形式的,没有返回值。但是对于方式三来说,实现 Callable 接口,能够有返回值类型。
实例:
/**
* 方式三:实现Callable接口
*/
public class ThreadCallableTest implements Callable<String> {//步骤 1
@Override
public String call() throws Exception { //步骤 2
//call 方法的返回值类型是 String
//call 方法是线程具体逻辑的实现方法
return "create thread by implements Callable";
}
public static void main(String[] args) throws ExecutionException, InterruptedException{
FutureTask<String> future1 = new FutureTask<String>(new ThreadCallableTest());
Thread thread1 = new Thread(future1);
thread1. start();
System.out.println(future1.get());
}
}
首先确认,这并不是线程创建的第四种方式,先来看如何创建。
实例:
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("通过匿名内部类创建Thread");
}
});
从代码中可以看出,还是进行了一个 Runnable 接口的使用,所以这并不是新的 Thread 创建方式,只不过是通过方式二实现的一个内部类创建。
实验目的:对 Thread 的创建方式进行练习,巩固本节重点内容,并在练习的过程中,使用常用的 start 方法和 sleep 方法以及 线程的 setName 方法。
实验步骤:
实现:
public class ThreadTest implements Runnable{
@Override
public void run() {
System.out.println("线程:"+Thread.currentThread()+" 正在执行...");
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new ThreadTest());
t1.setName("ThreadOne");
Thread t2 = new Thread(new ThreadTest());
t2.setName("ThreadTwo");
t1. start();
t1.sleep(5000);
t2. start();
t1.sleep(1000);
System.out.println("线程执行结束。");
}
}
执行结果:
线程:Thread[ThreadOne,5,main] 正在执行...
线程:Thread[ThreadTwo,5,main] 正在执行...
线程执行结束。
方法定义:多线程环境下,如果需要确保某一线程执行完毕后才可继续执行后续的代码,就可以通过使用 join 方法完成这一需求设计。
在项目实践中经常会遇到一个场景,就是需要等待某几件事情完成后主线程才能继续往下执行, 比如多个线程加载资源, 需要等待多个线程全部加载完毕再汇总处理。
Thread 类中有一个 join 方法就可以做这个事情,join 方法是 Thread 类直接提供的。join 是无参且返回值为 void 的方法。
如上图所示,假如有 3 个线程执行逻辑,线程 1 需要执行5秒钟,线程 2 需要执行10 秒钟,线程 3 需要执行 8 秒钟。 如果我们的开发需求是:必须等 3 条线程都完成执行之后再进行后续的代码处理,这时候我们就需要使用到 join 方法。
使用 join 方法后:
join 方法是 Thread 类中的方法,为了了解该方法的异常处理,我们先来简要的看下 join 方法的 JDK 源码:
public final void join() throws InterruptedException {
join(0);
}
从源代码中我们可以看到, join 方法抛出了异常,所以,我们在使用 join 方法的时候,需要对 join 方法的调用进行 try catch 处理或者从方法级别进行异常的抛出。
try-catch 处理示例:
public class DemoTest implements Runnable{
@Override
public void run() {
System.out.println("线程:"+Thread.currentThread()+" 正在执行...");
}
public static void main(String[] args) {
Thread t1 = new Thread(new DemoTest());
t1. start();
try {
t1.join();
} catch (InterruptedException e) {
// 异常捕捉处理
}
}
}
throws 异常处理示例:
public class DemoTest implements Runnable throws InterruptedException {
@Override
public void run() {...}
public static void main(String[] args) {
Thread t1 = new Thread(new DemoTest());
t1. start();
t1.join();
}
}
为了更好的了解 join 方法的使用,我们首先来设计一个使用的场景。
场景设计:
需求:我们需要等 3 个线程都执行完毕后,再进行后续代码的执行。3 个线程执行完毕后,打印执行时间。
期望结果: 10 秒执行时间。
实现:
public class DemoTest{
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() { //线程 1
@Override
public void run() {
try {
Thread.sleep (5000 ); //线程 1 休眠 5 秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println ("线程 1 休眠 5 秒钟,执行完毕。");
}
});
Thread threadTwo = new Thread(new Runnable() { //线程 2
...
Thread.sleep (10000 ); //线程 2 修眠 10 秒钟
...
System.out.println ("线程 2 修眠 10 秒钟,执行完毕。");
}
});
Thread threadThree = new Thread(new Runnable() {//线程 3
...
Thread.sleep (8000 ); //线程 3 修眠 8 秒钟
...
System.out.println ("线程 3 修眠 8 秒钟,执行完毕。");
}
});
Long startTime = System.currentTimeMillis();
threadOne. start();threadTwo. start();threadThree. start();
System.out.println("等待三个线程全部执行完毕再继续向下执行,我要使用 join 方法了。");
threadOne.join(); //线程 1 调用 join 方法
threadTwo.join(); //线程 2 调用 join 方法
threadThree.join(); //线程 3 调用 join 方法
Long endTime = System.currentTimeMillis();
System.out.println("三个线程都执行完毕了,共用时: "+ (endTime - startTime) + "毫秒");
}
}
执行结果验证:
等待三个线程全部执行完毕再继续向下执行,我要使用 join 方法了。
线程 1 休眠 5 秒钟,执行完毕。
线程 3 修眠 8 秒钟,执行完毕。
线程 2 修眠 10 秒钟,执行完毕。
三个线程都执行完毕了,共用时: 10002毫秒
从执行的结果来看,与我们对 join 方法的理解和分析完全相符。
除了无参的 join 方法以外, Thread 类还提供了有参 join 方法如下:
public final synchronized void join(long millis)
throws InterruptedException
该方法的参数 long millis 代表的是毫秒时间。
方法作用:等待 millis 毫秒终止线程,假如这段时间内该线程还没执行完,也不会再继续等待。
上面的代码里,我们都是调用的无参 join 方法,现在对上一个知识点代码进行如下调整:
threadOne.join(); //线程 1 调用 join 方法
threadTwo.join(3000); //线程 2 调用 join 方法
threadThree.join(); //线程 3 调用 join 方法
可以看到,线程 2 使用 join 方法 3000 毫秒的等待时间,如果 3000 毫秒后,线程 2 还未执行完毕,那么主线程则放弃等待线程 2,只关心线程 1 和线程 3。
查看执行结果:
等待三个线程全部执行完毕再继续向下执行,我要使用 join 方法了。
线程 1 休眠 5 秒钟,执行完毕。
线程 3 修眠 8 秒钟,执行完毕。
三个线程都执行完毕了,共用时: 8000毫秒
线程 2 修眠 10 秒钟,执行完毕。
从执行结果来看, 总用时 8000 毫秒,因为线程 2 被放弃等待了,所以只考虑线程 1 和线程 3 的执行时间即可。
我们知道操作系统是为每个线程分配一个时间片来占有 CPU 的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,这里所说的 “自己占有的时间片” 就是 CPU 分配给线程的执行权。
进一步探究,何为让出 CPU 执行权呢?
当一个线程通过某种可行的方式向操作系统提出让出 CPU 执行权时,就是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,主动放弃剩余的时间片,并在合适的情况下,重新获取新的执行时间片。
方法介绍:Thread 类中有一个静态的 yield 方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己的 CPU 使用权。
public static native void yield();
Tips:从这个源码中我们能够看到如下两点要点:
作用:暂停当前正在执行的线程对象(及放弃当前拥有的 cup 资源),并执行其他线程。yield () 做的是让当前运行线程回到就绪状态,以允许具有相同优先级的其他线程获得运行机会。
目的:yield 即 “谦让”,使用 yield () 的目的是让具有相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证 yield () 达到谦让目的,因为放弃 CPU 执行权的线程还有可能被线程调度程序再次选中。
为了更好的了解 yield 方法的使用,我们来设计一个使用场景。
场景设计:
期望结果: 未加入 yield 方法之前打印的时间 < 加入 yield 方法之后的打印时间。因为 yield 方法在执行过程中会放弃 CPU 执行权并从新获取新的 CPU 执行权。
代码实现 - 正常执行:
public class DemoTest extends Thread {
@Override
public void run() {
Long start = System.currentTimeMillis();
int count = 0;
for (int i = 1; i <= 10000000; i++) {
count = count + i;
}
Long end = System.currentTimeMillis();
System.out.println("总执行时间: "+ (end-start) + " 毫秒, 结果 count = " + count);
}
public static void main(String[] args) throws InterruptedException {
DemoTest threadOne = new DemoTest();
threadOne. start();
}
}
执行结果验证:
总执行时间: 6 毫秒.
代码实现 - yield 执行:
public class DemoTest extends Thread {
@Override
public void run() {
Long start = System.currentTimeMillis();
int count = 0;
for (int i = 1; i <= 10000000; i++) {
count = count + i;
this.yield(); // 加入 yield 方法
}
Long end = System.currentTimeMillis();
System.out.println("总执行时间: "+ (end-start) + " 毫秒. ");
}
public static void main(String[] args) throws InterruptedException {
DemoTest threadOne = new DemoTest();
threadOne. start();
}
}
执行结果验证:
总执行时间: 5377 毫秒.
从执行的结果来看,与我们对 yield 方法的理解和分析完全相符,当加入 yield 方法执行时,线程会放弃 CPU 的执行权,并等待再次获取新的执行权,所以执行时间上会更加的长。
定义:当前线程使用完时间片后,就会处于就绪状态并让出 CPU,让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。
问题点解析:让出 CPU 的线程等下次轮到自己占有 CPU 时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场, 当再次执行时根据保存的执行现场信息恢复执行现场。
线程上下文切换时机: 当前线程的 CPU 时间片使用完或者是当前线程被其他线程中断时,当前线程就会释放执行权。那么此时执行权就会被切换给其他的线程进行任务的执行,一个线程释放,另外一个线程获取,就是我们所说的上下文切换时机。
定义:死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
如上图所示死锁状态,线程 A 己经持有了资源 2,它同时还想申请资源 1,可是此时线程 B 已经持有了资源 1 ,线程 A 只能等待。
反观线程 B 持有了资源 1 ,它同时还想申请资源 2,但是资源 2 已经被线程 A 持有,线程 B 只能等待。所以线程 A 和线程 B 就因为相互等待对方已经持有的资源,而进入了死锁状态。
互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待;
不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放,如 yield 释放 CPU 执行权);
请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放;
循环等待条件:指在发生死锁时,必然存在一个线程请求资源的环形链,即线程集合 {T0,T1,T2,…Tn}中的 T0 正在等待一个 T1 占用的资源,T1 正在等待 T2 占用的资源,以此类推,Tn 正在等待己被 T0 占用的资源。如下图所示:
为了更好的了解死锁是如何产生的,我们首先来设计一个死锁争夺资源的场景。
场景设计:
期望结果:发生死锁,线程 threadA 和 threadB 互相等待。
实现:
public class DemoTest{
private static Object resourceA = new Object();//创建资源 resourceA
private static Object resourceB = new Object();//创建资源 resourceB
public static void main(String[] args) throws InterruptedException {
//创建线程 threadA
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread().getName() + "获取 resourceA。");
try {
Thread.sleep(1000); // sleep 1000 毫秒,确保此时 resourceB 已经进入run 方法的同步模块
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "开始申请 resourceB。");
synchronized (resourceB) {
System.out.println (Thread.currentThread().getName() + "获取 resourceB。");
}
}
}
});
threadA.setName("threadA");
//创建线程 threadB
Thread threadB = new Thread(new Runnable() { //创建线程 1
@Override
public void run() {
synchronized (resourceB) {
System.out.println(Thread.currentThread().getName() + "获取 resourceB。");
try {
Thread.sleep(1000); // sleep 1000 毫秒,确保此时 resourceA 已经进入run 方法的同步模块
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "开始申请 resourceA。");
synchronized (resourceA) {
System.out.println (Thread.currentThread().getName() + "获取 resourceA。");
}
}
}
});
threadB.setName("threadB");
threadA. start();
threadB. start();
}
}
代码讲解:
执行结果验证:
threadA 获取 resourceA。
threadB 获取 resourceB。
threadA 开始申请 resourceB。
threadB 开始申请 resourceA。
由结果可知已经出现死锁,threadA 申请 resourceB,threadB 申请 resourceA,但均无法申请成功,死锁得以实验成功。
要想避免死锁,只需要破坏掉至少一个形成死锁的必要条件即可。
破坏互斥条件:该条件无法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件:一次性申请所有的资源。
破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。
感兴趣的话还可以了解一下避免死锁的经典算法——银行家算法。
Java 中的线程分为两类,分别为 daemon 线程(守护线程〉和 user 线程(用户线程)。
在 JVM 启动时会调用 main 函数, main 函数所在的线程就是一个用户线程,其实在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。
守护线程定义:所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程。比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。
当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
用户线程定义:某种意义上的主要用户线程,只要有用户线程未执行完毕,JVM 虚拟机不会退出。
区别: 在本质上,用户线程和守护线程并没有太大区别,唯一的区别就是当最后一个非守护线程结束时,JVM 会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响 JVM 的退出。
创建方式:将线程转换为守护线程可以通过调用 Thread 对象的 setDaemon (true) 方法来实现。
创建细节:
守护线程创建示例:
public class DemoTest {
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
//代码执行逻辑
}
});
threadOne.setDaemon(true); //设置threadOne为守护线程
threadOne. start();
}
}
为了更好的了解守护线程与 JVM 是否退出的关系,我们首先来设计一个守护线程正在运行,但用户线程执行完毕导致的 JVM 退出的场景。
场景设计:
期望结果:
Tips:main 函数就是一个用户线程,main 方法执行时,只有一个用户线程,如果 main 函数执行完毕,用户线程销毁,JVM 退出,此时不会考虑守护线程是否执行完毕,直接退出。
代码实现 - 不加入 join 方法:
public class DemoTest {
public static void main(String[] args){
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum = sum + i;
}
System.out.println("守护线程,最终求和的值为: " + sum);
}
});
threadOne.setDaemon(true); //设置threadOne为守护线程
threadOne. start();
System.out.println("main 函数线程执行完毕, JVM 退出。");
}
}
执行结果验证:
main 函数线程执行完毕, JVM 退出。
从结果上可以看到,JVM 退出了,守护线程还没来得及执行,也就随着 JVM 的退出而消亡了。
代码实现 - 加入 join 方法:
public class DemoTest {
public static void main(String[] args){
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum = sum + i;
}
System.out.println("守护线程,最终求和的值为: " + sum);
}
});
threadOne.setDaemon(true); //设置threadOne为守护线程
threadOne. start();
try {
threadOne.join(); // 加入join 方法
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main 函数线程执行完毕, JVM 退出。");
}
}
执行结果验证:
守护线程,最终求和的值为: 5050
main 函数线程执行完毕, JVM 退出。
从结果来看,守护线程不决定 JVM 的退出,除非强制使用 join 方法使用户线程等待守护线程的执行结果,但是实际的开发过程中,这样的操作是不允许的,因为守护线程,默认就是不需要被用户线程等待的,是服务于用户线程的。
诞生:早在 JDK 1.2 的版本中就提供 java.lang.ThreadLocal,ThreadLocal 为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
概述:ThreadLocal 很容易让人望文生义,想当然地认为是一个 “本地线程”。其实,ThreadLocal 并不是一个 Thread,而是 Thread 的局部变量,也许把它命名为 ThreadLocalVariable 更容易让人理解一些。
当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
总体概括:从线程的角度看,目标变量就象是线程的本地变量,这也是类名中 “Local” 所要表达的意思。
作用: ThreadLocal 是 JDK 包提供的,它提供了线程本地变量,也就是如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。
ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。
使用场景: 如为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B 线程正在使用的 Connection。还有 Session 管理等问题。
方法介绍:set 方法用于设置 ThreadLocal 变量的值,设置成功后,该变量只能够被当前线程访问,其他线程不可直接访问操作改变量。
实例:
public class DemoTest{
public static void main(String[] args){
ThreadLocal<String> localVariable = new ThreadLocal<> () ;
localVariable.set("Hello World");
}
}
Tips: set 方法可以设置任何类型的值,无论是 String 类型 ,Integer 类型,Object 等类型,原因在于 set 方法的 JDK 源码实现是基于泛型的实现,此处只是拿 String 类型进行的举例。
实例:
public void set(T value) { // T value , 泛型实现,可以 set 任何对象类型
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
方法介绍:get 方法用于获取 ThreadLocal 变量的值,get 方法没有任何入参,直接调用即可获取。
实例:
public class DemoTest{
public static void main(String[] args){
ThreadLocal<String> localVariable = new ThreadLocal<> () ;
localVariable.set("Hello World");
System.out.println(localVariable.get());
}
}
结果验证:
Hello World
探究-观察如下程序:
public class DemoTest{
public static void main(String[] args){
ThreadLocal<String> localVariable = new ThreadLocal<> () ;
localVariable.set("Hello World");
localVariable.set("World is beautiful");
System.out.println(localVariable.get());
System.out.println(localVariable.get());
}
}
探究解析:
从程序中来看,我们进行了两次 set 方法的使用。
第一次 set 的值为 Hello World ;第二次 set 的值为 World is beautiful。接下来我们进行了两次打印输出 get 方法,那么这两次打印输出的结果都会是 World is beautiful。 原因在于第二次 set 的值覆盖了第一次 set 的值,所以只能 get 到 World is beautiful。
结果验证:
World is beautiful
World is beautiful
总结: ThreadLocal 中只能设置一个变量值,因为多次 set 变量的值会覆盖前一次 set 的值,我们之前提出过,ThreadLocal 其实是使用 ThreadLocalMap 进行的 value 存储,那么多次设置会覆盖之前的 value,这是 get 方法无需入参的原因,因为只有一个变量值。
方法介绍:remove 方法是为了清除 ThreadLocal 变量,清除成功后,该 ThreadLocal 中没有变量值。
实例:
public class DemoTest{
public static void main(String[] args){
ThreadLocal<String> localVariable = new ThreadLocal<> () ;
localVariable.set("Hello World");
System.out.println(localVariable.get());
localVariable.remove();
System.out.println(localVariable.get());
}
}
Tips: remove 方法同 get 方法一样,是没有任何入参的,因为 ThreadLocal 中只能存储一个变量值,那么 remove 方法会直接清除这个变量值。
结果验证:
Hello World
Null
对 ThreadLocal 的常用方法我们已经进行了详细的讲解,而多线程下的 ThreadLocal 才是它存在的真实意义,那么问了更好的学习多线程下的 ThreadLocal,我们来进行场景的创建,通过场景进行代码实验,更好的体会并掌握 ThreadLocal 的使用。
场景设计:
预期结果:在 threadOne 设置成功后进入了 5000 毫秒的休眠状态,此时由于只有 threadTwo 调用了 remove 方法,不会影响 threadOne 的 get 方法打印,这体现了 ThreadLocal 变量的最显著特性,线程私有操作。
实例:
public class DemoTest{
static ThreadLocal<String> local = new ThreadLocal<>();
public static void main(String[] args){
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
local.set("threadOne's local value");
try {
Thread.sleep(5000); //沉睡5000 毫秒,确保 threadTwo 执行 remove 完成
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(local.get());
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
local.set("threadTwo's local value");
System.out.println(local.get());
local.remove();
System.out.println("local 变量执行 remove 操作完毕。");
}
});
threadTwo. start();
threadOne. start();
}
}
结果验证:
threadTwo's local value
local 变量执行 remove 操作完毕。
threadOne's local value
从以上结果来看,在 threadTwo 执行完 remove 方法后,threadOne 仍然能够成功打印,这更加证明了 ThreadLocal 的专属特性,线程独有数据,其他线程不可侵犯。
ThreadLocal 是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal 比直接使用 synchronized 同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
ps:以上内容来自对慕课教程的学习与总结。