1、介绍
1.
线程回顾
2.
各种所的认识
3. JUC
并发库
4. ThreadLocal
5.
线程池
2、线程回顾
进程:
进程是资源(
CPU
、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系
统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时
候就会为它分配
CPU
时间,程序开始真正运行
。
线程:
线程是程序执行时的最小单位,它是进程的一个执行流,是
CPU
调度和分派的基本单位,一
个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。
线程由
CPU
独立调度执行,在多
CPU
环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。
进程是资源分配的最小单位,线程是程序执行的最小单位。
2.1、实现多线程的三种方式
(
1
)继承
Thread
类
(
2
)实现
Runnable
接口
(
3
)使用
ExecutorService
、
Callable
、
Future
实现有返回结果的多线程
实现
Runnable
和
Callable
接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过
Thread
来调用。可以说任务是通过线程驱动从而执行的。
(
4
)线程池
2.1.1、实现Runnable接口
需要实现
run()
方法。
通过
Thread
调用
start()
方法来启动线程。
2.1.2、实现Callable 接口
与
Runnable
相比,
Callable
可以有返回值,返回值通过
FutureTask
进行封装
2.1.3、继承Thread 类
同样也是需要实现
run()
方法,并且最后也是调用
start()
方法来启动线程
2.1.4、面试题
问:
Thread
和
Runnable
的区别
答:
Thread
和
Runnable
的实质是继承关系,没有可比性。无论使用
Runnable
还是
Thread
,都会
new Thread
,然后执行
run
方法。用法上,如果有复杂的线程操作需求,那就选择继承
Thread
,如果只是简
单的执行一个任务,那就实现
runnable
。
有网友说有这样的区别:
Runnable
更容易实现资源共享,能多个线程同时处理一个资源,而
Thread
不可以。
真的是这样吗?我们来看下面例子:
第一种方式:继承
Thread:
public class ThreadTest {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
static class MyThread extends Thread{
private int ticket = 5;
public void run(){
while(true){
System.out.println("Thread ticket = " + ticket--);
if(ticket < 0){
break;
}
}
}
}
}
运行结果:
第二种方式:实现
Runnable
接口
public class RunnableTest {
public static void main(String[] args) {
MyThread2 mt = new MyThread2();
new Thread(mt).start();
new Thread(mt).start();
}
static class MyThread2 implements Runnable {
private int ticket = 5;
public void run() {
while (true) {
System.out.println("Runnable ticket = " + ticket--);
if (ticket < 0) {
break;
}
}
}
}
}
运行结果:
结果分析
很显然第一种继承
Thread
的方式,出现了票超卖现象,第二种实现
Runnable
接口方式貌似是正常的。所以很多人就得出了结论:
Runnable
更容易可以实现多个线程间的资源共享,而
Thread
不可以!
真的
是这样吗?大错特错!
我们来分析一下:
方式一这个例子结果多卖一倍票的原因根本不是因为
Runnable
和
Thread
的区别,看其中的如下两行代
码:
new MyThread().start();
new MyThread().start();
这里创建了两个
MyThread
对象,每个对象都有自己的
ticket
成员变量,当然会多卖
1
倍。如果把
ticket
定义为
static
类型,就离正确结果有近了一步(因为是多线程同时访问一个变量会有同步问题,加上锁才是最终正确的代码)。
我们再来看下方式二的如下代码:
MyThread2 mt=new MyThread2();
new Thread(mt).start();
new Thread(mt).start();
只创建了一个
Runnable
对象,肯定只卖一倍票(但也会有多线程同步问题,同样需要加锁),根本不是Runnable
和
Thread
的区别造成的。
再来看一个使用
Thread
方式的正确例子:
public class Test001 extends Thread {
private int ticket = 10;
public void run() {
for (int i = 0; i < 10; i++) {
synchronized (this) {
if (this.ticket > 0) {
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "卖掉 一张票,余票:"+(--this.ticket));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] arg) {
Test001 t1 = new Test001();
new Thread(t1, "线程1").start();
new Thread(t1, "线程2").start();
}
}
运行结果:
上例中只创建了一个
Thread
对象(子类
Test001
)效果和
Runnable
一样。
synchronized
这个关键字是必须的,否则会出现同步问题。
最后我们再来看下
Thread
的源码:
public class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
private volatile String name;
private int priority;
private Thread threadQ;
private long eetop;
//........
}
可以看出,
Thread
实现了
Runnable
接口,提供了更多的可用方法和成员而已。
上面讨论下来,
Thread
和
Runnable
没有根本的没区别,只是写法不同罢了,
事实是
Thread
和
Runnable
没有本质的区别,这才是正确的结论,和自以为是的大神所说的
Runnable
更容易实现资源共享,没有半点关系!
2.2、线程操作&生命周期
2.2.1、线程的方法
1. yield
方法:使当前线程从执行状态变为就绪状态。 放弃机会
2. sleep
方法:强制当前正在执行的线程休眠,当睡眠时间到期,则返回到可运行状态。不会放弃锁
资源
3.
join
方法:通常用于在
main()
主线程内,等待其它线程完成再结束
main()
主线程
,
不会放弃锁资源,
线程会等待
4.
deamon
:
守护线程(
deamon
)是程序运行时在后台提供服务的线程,并不属于程序中不可或缺的部分。
当所有非后台线程结束时,程序也就终止,同时会杀死所有后台线程。
main()
属于非后台线程。
使用
setDaemon()
方法将一个线程设置为后台线程。
2.2.2、线程的状态
新建状态
:
使用
new
关键字和
Thread
类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序
start()
这个线程。
就绪状态
:
当线程对象调用了
start()
方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM
里线程调度器的调度。
运行状态
:
如果就绪状态的线程获取
CPU
资源,就可以执行
run()
,此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态
:
如果一个线程执行了
sleep
(睡眠)、
suspend
(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
等待阻塞:运行状态中的线程执行 wait()
方法,使线程进入到等待阻塞状态。
同步阻塞:线程在获取 synchronized
同步锁失败
(
因为同步锁被其他线程占用
)
。
其他阻塞:通过调用线程的 sleep()
或
join()
发出了
I/O
请求时,线程就会进入到阻塞状态。当
sleep()
状态超时,
join()
等待线程终止或超时,或者
I/O
处理完毕,线程重新转入就绪状态。
死亡状态
:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
2.2.3、线程通信
wait() notify() notifyAll()
它们都属于
Object
的一部分,而不属于
Thread
。而
sleep()
是
Thread
的静态方法;
wait()
会在等待时将线程挂起,只有在 notify()
或者
notifyAll()
到达时才唤醒。
sleep() 和
yield()
并没有释放锁,但是
wait()
会释放锁。
实际上,只有在同步控制方法或同步控制块里才能调用
wait()
、
notify()
和
notifyAll()
。
notify()
该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个 wait()
状态的线程来发出通知,并使它等待获取该对象的对象锁。
notifyAll()
使所有原来在该对象上 wait
的线程统统退出
wait
的状态(即全部被唤醒,不再等待
notify
或
notifyAll
,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁。
3、锁的认识和使用
3.1、线程安全概述
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过线程同步、乐观锁等机制保证各个线程都可以正常且正确的执行,不会出现
数据污染等意外情况。
3.2、线程安全的实现方法
线程安全的保证一般使用锁来实现,锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(
Java
中没有哪个
Lock
实现类就叫
PessimisticLock
或
OptimisticLock
),而是在并发情况下
的两种不同策略。
3.2.1、悲观锁
定义:
就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。
这样别人想拿数据就被挡住,直到悲观锁被释放。
互斥同步:互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称
为阻塞同步(
Blocking Synchronization
)。从处理问题的方式上说,互斥同步属于一种悲观的并
发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享数据
是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部
分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操
作
。
举例:
synchronized
,
ReentrantLock
3.2.2、乐观锁
定义:就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,不会上锁!但是如
果
想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修
改过,
则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放
弃操作)。
说到乐观锁,就必须提到一个概念:CAS,
什么是
CAS
呢?
Compare-and-Swap
,即比较并替换,也
有叫 做
Compare-and-Set
的,比较并设置。
非阻塞同步:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用
共
享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施
(最常见
的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需
要把线程挂起,
因此这种同步操作称为非阻塞同步(
Non-Blocking Synchronization
)。
举例:
CAS
,
Atomic
3.2.3、扩展:Java中的锁
解释:
是否要加锁
悲观锁是要加锁的,如
Synchronization
,
ReentrantLock
乐观锁是不加锁的:如对于
Mysql
中的数据而言,可以通过版本号,时间戳等来判断数据是否被并
发修改
是否要阻塞
互斥锁
(
阻塞
)
:如果一个线程尝试获取锁失败,可以进行阻塞等待别人释放锁后再尝试获取锁,如
Synchronization
自旋锁
(
不阻塞
)
:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取
(
占用
)
,那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循
环加锁
->
等待的机制被称为自旋锁
(spinlock)
。
适应性自旋锁
(
不阻塞
)
:在自旋锁的基础上自旋,尝试一定的次数还是获取不到锁就放弃获取锁,
这种模式叫适应性自旋。
是否要排队加锁
公平锁
(
排队加锁
)
:多个线程都在竞锁时是否要按照先后顺序排队加锁,如果是那就是公平锁
非公平锁
(
不排队加锁
)
:多个线程都在竞锁时不需要排队加锁,是为非公平锁
是否可重入
可重入锁:允许同一个线程多次获取同一把锁,是为可重入锁:比如一个递归函数里有加锁操作,
递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也
叫做递归锁)。
Java
里只要以
Reentrant
开头命名的锁都是可重入锁,
Synchronization
也是可重入
的
非可重入锁:一个线程在多个流程中不可用获取到同一把锁,是为非重入锁
可否共享锁
共享锁:多个线程可以共享一把锁,如多个线程同时读,一般是可共享读锁 :如读锁
排他锁:多个线程不可用共享一把锁,比如修改数据时别人是不能修改的的:如写锁
(Mysql)
3.3、线程安全-线程同步synchronized
3.3.1、synchronized的用法
1.
作用于非静态方法,锁住的是对象实例(
this
),每一个对象实例有一个锁。
public synchronized void method() {}
2.
作用于静态方法,锁住的是类的
Class
对象,因为
Class
的相关数据存储在永久代元空间,元空间是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁住所有调用该方法的线程。
public static synchronized void method() {}
3.
作用于
Lock.class
,锁住的是
Lock
的
Class
对象,也是全局只有一个。
synchronized (Lock.class) {}
4.
作用于
this
,锁住的是对象实例,每一个对象实例有一个锁。
synchronized (this) {}
5.
作用于静态成员变量,锁住的是该静态成员变量对象,由于是静态变量,因此全局只有一个。
public static Object monitor = new Object();
synchronized (monitor) {}
3.3.2、synchronized的作用
1. 确保线程互斥的访问同步代码
2. 保证共享变量的修改能够及时可见
3. 有效解决重排序问题。
3.3.3、synchronized原理
synchronized
是基于
JVM
内置锁实现
通过内部对象
Monitor(
监视器锁
)
实现,基于进入与退出
Monitor
对象实现方法与代码块同步,监视
器锁的实现依赖 底层操作系统的
Mutex lock
(互斥锁)实现,它是一个重量级锁性能较低。当
然,
JVM
内置锁在
1.5
之后版本做了重大的优化,如锁粗化(
Lock Coarsening
)、锁消除(
Lock
Elimination
)、轻量级锁(
Lightweight Locking
)、偏向锁(
Biased Locking
)、适应性自旋
(
Adaptive Spinning
)等 技术来减少锁操作的开销,内置锁的并发性能已经基本与
Lock
持平。
Synchronized
关键字
在编译的字节码中加入了两条指令来进行代码的同步
3.3.3.1、monitorenter-加锁
每个对象有一个监视器锁(
monitor
)。当
monitor
被占用时就会处于锁定状态,线程执行
monitorenter
指令时尝试获取
monitor
的所有权,过程如下:
1. 如果
monitor
的进入数为
0
,则该线程进入
monitor
,然后将进入数设置为
1
,该线程即为
monitor
的所有者
2. 如果线程已经占有该
monitor
,只是重新进入,则进入
monitor
的进入数加
1
3.
如果其他线程已经占用了
monitor
,则该线程进入阻塞状态,直到
monitor
的进入数为
0
,再重新
尝试获取
monitor
的所有权
3.3.3.2、monitorexit-释放
1. 执行
monitorexit
的线程必须是
objec tref
所对应的
monitor
的所有者
2. 指令执行时,
monitor
的进入数减
1
,如果减
1
后进入数为
0
,那线程退出
monitor
,不再是这个
monitor
的所有者。其他被这个
monitor
阻塞的线程可以尝试去获取这个
monitor
的所有权
通过这两段描述,我们应该能很清楚的看出
Synchronized
的实现原理,
Synchronized
的语义底层是通过一个
monitor
的对象来完成,
其实
wait/notify
等方法也依赖于
monitor
对象,这就是为什么只有在同步
的块或者方法中才能调用
wait/notify
等方法
,否则会抛出
java.lang.IllegalMonitorStateException
的异
常的原因
3.3.4、扩展:Java虚拟机对synchronized的优化
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级
到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的
降级,关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及
JVM
的其他优化
手段,这里并不打算深入到每个锁的实现和转换过程更多地是阐述
Java
虚拟机所提供的每个锁的核心优
化思想,毕竟涉及到具体过程比较繁琐,如需了解详细过程可以查阅《深入理解
Java
虚拟机原理》。
偏向锁
偏向锁是
Java 6
之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁
(
会涉及到
一些
CAS
操作
,
耗时
)
的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入
偏向模式,此时
Mark Word
的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操
作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没
有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是
对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同
的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即
膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。
轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段
(1.6
之后加入的
)
,此时
Mark Word
的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是
“
对绝大部分的锁,在整个同步周期内都不存在竞争
”
,注意这是经验数据。需要了解的是,轻量级锁所
适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨
胀为重量级锁。
自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能
会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需
要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因
此虚拟机会让当前想要获取锁的线程做几个空循环
(
这也是称为自旋的原因
)
,一般不会太久,可能是
50
个循环或
100
循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就
会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没
办法也就只能升级为重量级锁了。
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,
Java
虚拟机在
JIT
编译时
(
可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译
)
,通过对运行上下文的扫描,去除不可能存在共享资源
竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下
StringBuffer
的
append
是一个同步方法,但是在
add
方法中的
StringBuffer
属于一个局部变量,并且不会被其他线程所
使用,因此
StringBuffer
不可能存在共享资源竞争的情景,
JVM
会自动将其锁消除。
3.4、线程并发库组成(lock/atomic)
其实,显式锁(
Lock
)是属于线程并发库(
java.util.concurrent
)里面一种功能,
java.util.concurrent是
Java
专门为并发设计的编程包。
3.4.1、显式锁
显式锁:需要自己显示的加锁,比如
Synchronized
使用的是
JVM
内置锁实现的,它就不是显示锁
3.4.2、原子变量类 java.util.concurrent.atomic(乐观锁)
Atomic
:为了实现原子性操作提供的一些原子类,使用的是乐观锁实现:
3.4.3、线程池相关
通过线程池操作线程可以增加线程的复用性,防止频繁的创建,销毁线程
3.4.4、并发容器类
并发容器都是线程安全的,比如在多线程中可以使用
ConcurrentHashMap
代替
HashMap
3.4.5、同步工具类
3.5、synchronized与Lock的区别
(
1
)
Lock
的加锁和解锁都是由
java
代码实现的,而
synchronize
的加锁和解锁的过程是由
JVM管理 的
(
2
)
synchronized
能锁住类、方法和代码块,而
Lock
是块范围内的
(
3
)
Lock
能提高多个线程读操作的效率
3.6、线程安全-乐观锁
乐观锁是不加锁的,只是在修改数据的时候先做判断,如果数据没被别人修改即可提交修改,否则不做修改,做出重试或其他的补偿行为,在
Java
中
Atomic
开头的类就是基于
CAS
实现的乐观锁。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
3.6.1、AtomicInteger案例
public class AtomicDemo implements Runnable {
private AtomicInteger number = new AtomicInteger(10);
private String name = "";
AtomicDemo(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("线程:" + this.name + " 执行...");
for (int i = 0; i < 100; i++) {
if (number.get() > 0) {
try {
Thread.sleep(100);
//获取和递减值,内部基于乐观锁保证原子性
System.out.println(Thread.currentThread().getName() + ":卖出:" + number.getAndDecrement());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
AtomicDemo runnableDemo = new AtomicDemo("RunnableDemo");
new Thread(runnableDemo).start();
new Thread(runnableDemo).start();
}
}
3.6.2、使用场景
在数据库层面我们也通常使用乐观锁来保证数据的别发修改问题,通常是在每一行数据增加
version,
或时间戳 ,每次数据修改增加
where version = #{version}
条件,判断数据库中的版本号和修改之间读出
来的版本号是否一致,其实是在判断从读数据,到修改数据的时间段内,别的事物是否修改了该数据,
如果
version
匹配,说明没有问题,可以直接修改,否则可以作出重试,或者回滚事物等处理方式,
案例
如:
select id,version,username from t_user where id = #{id}
User user = userMapper.selectById(1L);
user.setUsername(“zs”);
userMapper.updateById(user);
Update t_user set username = #{username} , version = version + 1 where id = #
{id} and version = #{version}
4、ThreadLocal
4.1、ThreadLocal的认识
ThreadLocal
是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解
决了变量并发访问的冲突问题。
在很多情况下,
ThreadLocal
比直接使用
synchronized
同步机制解决线程安全问题更简单,更方
便,且结果程序拥有更高的并发性。
简单理解就是
ThreadLocal
是一个
key-value
结构,
ThreadLocal
自动把当前线程副本作为
Key
,而
Value就是我们存储的值。
在
A
线程中存储一个元素到
ThreadLocal
,也只能在
A
线程中才能取出这个值,其他线程获取不到,因为ThreadLocal
把
A
线程副本作为
Key
。
示例:
public class ThreadLocalDemo {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
//主线程设置值
threadLocal.set("main-thread-value");
new Thread(()->{
System.out.println("thread线程获取:"+threadLocal.get()); //打印null,新开的线程是获取不到主线程设置的值的
//为新线程设置值
threadLocal.set("new-thread-value");
System.out.println("thread线程获取:"+threadLocal.get()); //打印newthread-value
}).start();
System.out.println("main线程获取:"+threadLocal.get()); //打印 mainthread-value
}
}
很多地方叫做线程本地变量,也有些地方叫做线程本地存储。
ThreadLocal
为变量在每个线程中都
创建了一个副本,那么每个线程可以访问自己内部的副本变量
ThreadLocal
在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线
程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影
响程序执行性能
Synchronized
用于线程间的数据共享
而
ThreadLocal
则用于线程间的数据隔离
4.2、ThreadLocal的原理
首先
ThreadLocal
是一个泛型类,保证可以接受任何类型的对象。
因为一个线程内可以存在多个
ThreadLocal
对象,所以其实是
ThreadLocal
内部维护了一个
Map
,这个
Map
不是直接使用的
HashMap
,而是
ThreadLocal
实现的一个叫做
ThreadLocalMap
的静态内部
类。而我们使用的
get()
、
set()
方法其实都是调用了这个
ThreadLocalMap
类对应的
get()
、
set()
方法。
例如下面的
set
方法:
get()
方法:
createMap()
方法:
ThreadLocalMap
是个静态的内部类:
最终的变量是放在了当前线程的
ThreadLocalMap
中,并不是存在
ThreadLocal
上,
ThreadLocal
可以理解为只是
ThreadLocalMap
的封装,传递了变量值
。
4.3、内存泄露问题
实际上
ThreadLocalMap
中使用的
key
为
ThreadLocal
的弱引用,弱引用的特点是,如果这个对
象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以如果
ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来
ThreadLocalMap
中使用这个
ThreadLocal
的
key
也会被清理掉。但是,
value
是强引用,不会被
清理,这样一来就会出现
key
为
null
的
value
。
ThreadLocalMap
实现中已经考虑了这种情况,在调用
set()
、
get()
、
remove()
方法的时候,会清
理掉
key
为
null
的记录。如果说会出现内存泄漏,那只有在出现了
key
为
null
的记录后,没有手
动调用
remove()
方法,并且之后也不再调用
get()
、
set()
、
remove()
方法的情况下。
建议回收自定义的
ThreadLocal
变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal
变量,可能会影响后续业务逻辑和造成内存泄露等问题。 尽量在代理中使用
try-finally
块进
行回收:
4.4、使用场景
如上文所述,
ThreadLocal
适用于如下两种场景
每个线程需要有自己单独的实例
实例需要在多个方法中共享,但不希望被多线程共享
对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLocal
可以以非常方便的形式满足该需求。
对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。
ThreadLocal
使得代码耦合度更低,且实现更优雅。
4.4.1、存储用户Session
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
4.4.2、解决线程安全的问题
比如
Java7
中的
SimpleDateFormat
不是线程安全的,可以用
ThreadLocal
来解决这个问题:
这里的
DateUtil.formatDate()
就是线程安全的了。
(Java8
里的
java.time.format.DateTimeFormatter
是线程安全的)
4.5、ThreadLocalRandom
ThreadLocalRandom
使用
ThreadLocal
的原理,让每个线程内持有一个本地的种子变量,该种子变量只有在使用随机数时候才会被初始化,多线程下计算新种子时候是根据自己线程内维护的种子变量进行更
新,从而避免了竞争。
用法:
ThreadLocalRandom.current().nextInt(100)
关于线程池的详细介绍,放到了下一篇文章
实现线程的多种方式&锁的介绍&ThreadLocal&线程池 详细总结(下)-CSDN博客