java 线程安全解决方案-java怎么解决线程安全问题
ThreadLocal本地存储保证并发安全前言介绍
多线程由于并发执行带来了性能优势,同时,多线程之间的数据竞争也带来了线程安全问题。 之前提到过,不可变类Immutability可以用来解决线程安全问题。 该方法的本质是让Threads不直接修改属性值,保证线程安全。 其实还有一种方式就是线程不共享,读写自己线程的变量。 没有分享,就没有伤害。 这就是本地存储方案ThreadLocal的优势所在。
什么是 ThreadLocal
ThreadLocal其实是一种线程关闭的思想。 本质上有点像局部变量。 方法入栈时,所有的局部变量都存储在栈帧的局部变量表中。 它们是当前线程独有的,不会与其他线程共享。 这样保证了线程安全,对于ThreadLocal也是如此。 主要功能是数据隔离。 填充的数据只属于当前线程。 变量数据与其他线程相对隔离,保证多线程场景不会被其他线程篡改。 .
如何使用线程本地
/**
* 类的作用是给每一个线程分配一个id
*/
class ThreadId{
static final AtomicLong nextId = new AtomicLong(0);
// withInitial 创建线程局部变量
static final ThreadLocal tl = ThreadLocal.withInitial(()->{
return nextId.getAndIncrement();
});
//返回当前线程的局部变量的副本中的值。 如果变量没有当前线程的值,则首先将其初始化
//为调用initialValue()方法返回的值
static long get(){
return tl.get();
}
}
方法执行结果对比
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i <5 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"===="+ThreadId.get());
},"T"+i).start();
}
Thread.sleep(3000);
System.out.println("=======================");
for (int i = 0; i <5 ; i++) {
System.out.println(Thread.currentThread().getName()+"===="+ThreadId.get());
}
}
从上面可以看出,单个线程多次调用tl.get会返回相同的nextId,而不同的线程需要重新调用ThreadLocal.withInitial方法来赋值一个nextId值。 nextId值在多线程下不重复,说明nextId不在线程间共享变量。
ThreadLocal使用场景
线程隔离的ThreadLocal在实际项目中用的并不多,但是我们可以简单举几个常用但被忽略的业务场景
弹簧事务管理
为了保证单个线程中的数据库操作使用同一个数据库链接,spring可以通过事务传播来管理多个事务配置之间的切换,只要知道数据库链接是由ThreadLock管理的即可。
public abstract class TransactionSynchronizationManager {
//线程上下文中保存着【线程池对象:ConnectionHolder】的Map对象。线程可以通过该属性获取到同一个Connection对象。
private static final ThreadLocal
正确使用 SimpleDataFormat
SimpleDataFormat之前在线程过多的情况下存在线程安全问题,如下
public class ThreadLockDemo2 {
public static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws ParseException {
for (int i = 0; i <10 ; i++) {
int finalI = i;
new Thread(()->{
String format1 = simpleDateFormat.format(new Date(finalI *1000));
System.out.println(Thread.currentThread().getName()+"=="+format1);
},"T"+i).start();
}
}
}
预期的结果应该是这样的
实际结果是这样的
原因分析如下。 在SimpleDateFormat类的格式实现类中,有如下操作
public class SimpleDateFormat extends DateFormat {
// DateFormat继承过来的
protected Calendar calendar;
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
// 多个线程之间共享变量calendar,并修改calendar
calendar.setTime(date);
// 代码省略
}
}
怎么解决呢?其实有很多方法如下
public class ThreadLockDemo2 {
public static void main(String[] args) throws ParseException {
/**
* 注意点,在创建ThreadLocal对象时 如果赋值只是如下这种方式赋值,那么只有当前线程
* 调用threadLocal.get()能够获取到,其它线程一律是空!!!!
* ThreadLocal threadLocal = new ThreadLocal();
* threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
*/
ThreadLocal threadLocal = ThreadLocal.withInitial(()->{
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
});
for (int i = 0; i <10 ; i++) {
int finalI = i;
new Thread(()->{
SimpleDateFormat simpleDateFormat = threadLocal.get();
String format1 = simpleDateFormat.format(new Date(finalI *1000));
System.out.println(Thread.currentThread().getName()+"=="+format1);
},"T"+i).start();
}
}
}
上下文参数传递
如果项目中有一个线程需要跨多个方法调用java 线程安全解决方案,那么需要传递的对象就是上下文(context)。 如果采用责任链的形式java 线程安全解决方案,会很麻烦,需要为每个方法添加上下文参数,使用ThreadLock可以轻松解决。
class Test{
// before
public void work(){
getInfo(user);
checkInfo(user);
setSomeThing(user);
log(user);
}
// now
public void work(){
try{
threadLocal.set(user);
// 方法内部采用 user = threadLocal.get()即可获取
getInfo(user);
checkInfo(user);
setSomeThing(user);
log(user);
}finally {
threadLocal.remove();
}
}
}
ThreadLocal 的工作原理 存储结构
了解了ThreadLocal的使用和应用场景之后,如果我们自己设计一个ThreadLocal应该怎么办呢?
存储容器应该是Map类型,key是线程,value是对象。 施工图应如下所示。
代码语义如下
public class MyThreadLock {
// 容器
private Map map = new ConcurrentHashMap<>();
public void set(T object){
Thread thread = Thread.currentThread();
map.put(thread,object);
}
public Object get(Thread thread){
return map.get(thread);
}
public void remove(){
map.clear();
}
}
但是JAVA真的是这样实现的吗? 显然不是,虽然JAVA中也有一个名为ThreadLocalMap的Map,但是持有者不是ThreadLocal类,而是Thread类。 Thread类有属性threadLocals,其类型为ThreadLocalMap,ThreadLocalMap的key类型为ThreadLocal
结构图如下
源码说明
class Thread {
// 内部持有ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
class ThreadLocal{
// 获取线程Thread类的属性threadLocals 也就是 ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 省略其它逻辑
public T get() {
Thread t = Thread.currentThread();
// 获取根据当前线程ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// this为当前调用get的ThreadLocal对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 初始化ThreadLocalMap对象
return setInitialValue();
}
static class ThreadLocalMap{
// 内部是数组而不是Map
Entry[] table;
// 根据ThreadLocal查找Entry
Entry getEntry(ThreadLocal key){
//省略查找逻辑
}
//Entry定义
static class Entry extends WeakReference{
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
}
}
源码也能解释为什么ThreadLocal是线程安全的,因为ThreadLocal的值保存在当前线程Thread类的ThreadLocalMap类型的threadLocals属性中,每个线程都有自己的ThreadLocalMap,互不干扰。
同时这里需要对WeakReference弱引用进行说明。 什么是 WeakReference 弱引用?
顾名思义,当垃圾回收线程扫描其管辖的内存区域时,一旦发现只有弱引用的对象,无论当前内存空间是否足够,都会回收其内存。
内存泄漏风险
因为ThreadLocalMap和Thread同生共死,ThreadLocalMap永远不会被回收,ThreadLocal是弱引用。 当ThreadLock对象没有被外界强引用时,Entry对象中的key会被回收,但Entry中的value会被回收。 这时候,即使值的生命周期已经结束,值对象也无法被回收,从而导致内存泄漏。
既然有内存泄漏的风险,那怎么解决呢?
JVM不能自动释放值对象,我们可以手动释放值对象。 一般使用try{}finally{}的方式来释放资源,ThreadLocal同样适用。 使用ThreadLocal后,手动清除当前线程的Entry对象。 能
ThreadLocal threadLocal = new ThreadLocal();
try {
threadLocal.set("test");
Object o = threadLocal.get();
}finally {
// 手动移除 原理很简单就是将当前对象在Entry[]数组中查找,做数组元素移除操作
threadLocal.remove();
}
如何共享 ThreadLock 数据
ThreadLock实现了线程关闭的思想,但是如果我想指定ThreadLock中的数据是线程间共享的,怎么处理呢?用InheritableThreadLocal实现,先上传代码
public static void main(String[] args) {
ThreadLocal threadLocal = new InheritableThreadLocal<>();
try {
threadLocal.set("牛逼");
new Thread(()->{
System.out.println("牛逼不牛逼:"+threadLocal.get());
new Thread(()->{
System.out.println("子线程牛逼波:"+threadLocal.get());
}).start();
}).start();
}finally {
threadLocal.remove();
}
}
在主线程中创建ThreadLocal对象后,在主线程内部创建的所有线程(即子线程)都可以获得该值。
源码分析
// ThreadLocal 类
public void set(T value) {
Thread t = Thread.currentThread();
// 调用InheritableThreadLocal类的getMap 获取的是
// Thread类的属性 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 重点在这里 在第一次没有获取到ThreadLocalMap 创建map
// 调用InheritableThreadLocal类的createMap
createMap(t, value);
}
public class InheritableThreadLocal extends ThreadLocal {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
// 重写createMap方法
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
public class Thread{
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// inheritThreadLocals=true
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// 省略无数代码
// parent指创建子线程的线程 从示例中看就是主线程main
Thread parent = currentThread();
// parent.inheritableThreadLocals 在threadLocal.set("牛逼");时已经不为空
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 子线程的属性
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// 省略无数代码
}
}
需要注意的是,在生产中,尽量不要使用InheritableThreadLocal,不仅有内存泄漏的风险,还有线程池中的线程是动态创建的问题,容易造成继承关系的混乱。 如果业务逻辑依赖于InheritableThreadLocal,可能会导致业务问题。 逻辑计算错误比内存泄漏更难解决。