文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

详谈ThreadLocal-单例模式下高并发线程安全

2024-04-02 19:55

关注

ThreadLocal-单例模式下高并发线程安全

在多例的情况下,每个对象在堆中声明内存空间,多线程对应的Java栈中的句柄或指针指向堆中不同的对象,对象各自变量的变更只会印象到对应的栈,也就是对应的线程中,不会影响到其它线程。所以多例的情况下不需要考虑线程安全的问题,因为一定是安全的。

而在单例的情况下却完全不一样了,在堆中只有一个对象,多线程对应的Java栈中的句柄或指针指向同一个对象,方法的参数变量和方法内变量是线程安全的,因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享的资源。但是成员变量只有一份,所有指向堆中该对象的句柄或指针都可以随时修改和读取它,所以是非线程安全的。

为了解决线程安全的问题,我们有3个思路:

主程序:


public static void main(String[] args) {
		//Mirror是个单例的,只构建了一个对象
		Mirror mirror = new Mirror("魔镜");
		
		//三个线程都在用这面镜子
		MirrorThread thread1 = new MirrorThread(mirror,"张三");
		MirrorThread thread2 = new MirrorThread(mirror,"李四");
		MirrorThread thread3 = new MirrorThread(mirror,"王二");
		
		thread1.start();
		thread2.start();
		thread3.start();
	}

很好理解,创建了一面镜子,3个人一起照镜子。

MirrorThread:


public class MirrorThread extends Thread{
	private Mirror mirror;	
	private String threadName;	
	public MirrorThread(Mirror mirror, String threadName){
		this.mirror = mirror;
		this.threadName = threadName;
	}
		
	//照镜子
	public String lookMirror() {
		return threadName+" looks like "+ mirror.getNowLookLike().get();
	}
	
	//化妆
	public void makeup(String makeupString) {
		mirror.getNowLookLike().set(makeupString);
	}
	
	@Override
    public void run() {
		int i = 1;//阈值
		while(i<5) {
			try {
				long nowFace = (long)(Math.random()*5000);
				sleep(nowFace);
				StringBuffer sb = new StringBuffer();
				sb.append("第"+i+"轮从");
				sb.append(lookMirror());
				makeup(String.valueOf(nowFace));
				sb.append("变为");
				sb.append(lookMirror());
				System.out.println(sb);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			i++;
		}
	}
}

也很好理解,就是不断的更新自己的外貌同时从镜子里读取自己的外貌。

重点是Mirror:


public class Mirror {
	private String mirrorName;	
	//每个人要看到自己的样子,所以这里要用ThreadLocal
	private ThreadLocal<String> nowLookLike;	
	public Mirror(String mirrorName){
		this.mirrorName=mirrorName;
		nowLookLike = new ThreadLocal<String>();
	}
 
	public String getMirrorName() {
		return mirrorName;
	}
 
	public ThreadLocal<String> getNowLookLike() {
		return nowLookLike;
	}
}

对每个人长的样子用ThreadLocal类型来表示。

先看测试结果:

第1轮从张三 looks like null变为张三 looks like 3008

第2轮从张三 looks like 3008变为张三 looks like 490

第1轮从王二 looks like null变为王二 looks like 3982

第1轮从李四 looks like null变为李四 looks like 4390

第2轮从王二 looks like 3982变为王二 looks like 1415

第2轮从李四 looks like 4390变为李四 looks like 1255

第3轮从王二 looks like 1415变为王二 looks like 758

第3轮从张三 looks like 490变为张三 looks like 2746

第3轮从李四 looks like 1255变为李四 looks like 845

第4轮从李四 looks like 845变为李四 looks like 1123

第4轮从张三 looks like 2746变为张三 looks like 2126

第4轮从王二 looks like 758变为王二 looks like 4516

OK,一面镜子所有人一起照,而且每个人都只能看的到自己的变化,这就达成了单例线程安全的目的。

我们来细看下它是怎么实现的。

先来看Thread:

Thread中维护了一个ThreadLocal.ThreadLocalMapthreadLocals = null; ThreadLocalMap这个Map的key是ThreadLocal,value是维护的成员变量。现在的跟踪链是Thread->ThreadLocalMap-><ThreadLocal,Object>,那么我们只要搞明白Thread怎么跟ThreadLocal关联的,从线程里找到自己关心的成员变量的快照这条线就通了。


ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

再来看ThreadLocal:它里面核心方法两个get()和set(T)


public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

方法里通过Thread.currentThread()的方法得到当前线程,然后做为key存储到当前线程对象的threadLocals中,也就是TreadLocalMap中。

OK,这样整个关系链已经建立,真正要去访问的成员变量在一个map中,key是线程号,值是属于该线程的快照。

ThreadLocal里还有map的创建createMap(t, value)、取值时对象的初始值setInitialValue()、线程结束时对象的释放remove()等细节,有兴趣的可以继续跟进了解下。

ThreadLocal应用其实很多,例如Spring容器中实例默认是单例的,transactionManager也一样,那么事务在处理时单例的manager是如何控制每个线程的事务要如何处理呢,这里面就应用了大量的ThreadLocal。

多线程中的ThreadLocal

1.ThreadLocal概述

多线程的并发问题主要存在于多个线程对于同一个变量进行修改产生的数据不一致的问题,同一个变量指的值同一个对象的成员变量或者是同一个类的静态变量。之前我们常听过尽量不要使用静态变量,会引起并发问题,那么随着Spring框架的深入人心,单例中的成员变量也出现了多线程并发问题。Struts2接受参数采用成员变量自动封装,为此在Spring的配置采用多例模式,而SpringMVC将Spring的容器化发挥到极致,将接受的参数放到了注解和方法的参数中,从而避免了单例出现的线程问题。今天,我们讨论的是JDK从1.2就出现的一个并发工具类ThreadLocal,他除了加锁这种同步方式之外的另一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。我们先看一下官方是怎么解释这个变量的?

大致意思是:此类提供了局部变量表。这些变量与普通变量不同不同之处是,每一个通过get或者set方法访问一个线程都是他自己的,将变量的副本独立初始化。ThreadLocal实例通常作用于希望将状态与线程关联的类中的私有静态字段(例如,用户ID或交易ID)。

只要线程是活动的并且可以访问{@code ThreadLocal}实例, 每个线程都会对其线程局部变量的副本保留隐式引用。 线程消失后,其线程本地实例的所有副本都将进行垃圾回收(除非存在对这些副本的其他引用)。也就是说,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。而每个线程的副本全部放到ThreadLocalMap中。

2. ThreadLocal简单实用


public class ThreadLocalExample {
    public static class MyRunnable implements Runnable {
        private ThreadLocal<Double> threadLocal = new ThreadLocal();
        private Double variable;
 
        @Override
        public void run() {
            threadLocal.set(Math.floor(Math.random() * 100D));
            variable = Math.floor(Math.random() * 100D);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
 
            System.out.println("ThreadValue==>"+threadLocal.get());
            System.out.println("Variable==>"+variable);
        }
    }
    
    public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();
        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);
        thread1.start();
        thread2.start();
    } 
}

通过上面的例子,我们发现将Double放入ThreadLocal中,不会出现多线程并发问题,而成员变量variable却发生了多线程并发问题。

3.ThreadLocal的内部原理

通过源码我们发现ThreadLocal主要提供了下面五个方法:



public T get() { }
 

public void set(T value) { }
 

public void remove() { }
 

protected T initialValue(){ }

3.1 get方法


 public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //通过当前线程获取ThreadLocalMap
    //Thread类中包含一个ThreadLocalMap的成员变量
    ThreadLocalMap map = getMap(t);
    //如果不为空,则通过ThreadLocalMap中获取对应value值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //如果为空,需要初始化值
    return setInitialValue();
}
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        //如果为空,则创建
        createMap(t, value);
    return value;
}

首先是取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是 this,而不是当前线程t。 如果获取成功,则返回value值。如果map为空,则调用setInitialValue方法返回value。

在setInitialValue方法中,首先执行了initialValue方法(我们上面提到的最后一个方法),接着通过当前线程获取ThreadLocalMap,如果不存在则创建。创建的代码很简单,只是通过ThreadLocal对象和设置的Value值创建ThreadLocalMap对象。

3.2 set方法


public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

这个方法和setInitialValue方法的业务逻辑基本相同,只不过setInitialValue调用了initialValue()的钩子方法。这里代码简单,我们就不做过多解释。

3.3 remove方法


public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

这个方法是从jdk1.5才出现的。处理逻辑也很很简单。通过当前线程获取到ThreadLocalMap对象,然后移除此ThreadLocal。

3.4 initialValue方法


protected T initialValue() {
    return null;
}

是不是感觉简单了,什么也没有处理,直接返回一个null,那么何必如此设计呢?当我们发现他的修饰符就会发现,他应该是一个钩子方法,主要用于提供子类实现的。追溯到源码中我们发现,Supplier的影子,这就是和jdk8的lamda表达式关联上了。


static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
    private final Supplier<? extends T> supplier; 
    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }
    
    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

4. 总结

在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。之所以这里是一个map,是因为通过线程会存在多个类中定义ThreadLocal的成员变量。初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals; 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

5. ThreadLocalMap引发的内存泄漏

ThreadLocal属于一个工具类,他为用户提供get、set、remove接口操作实际存放本地变量的threadLocals(调用线程的成员变量),也知道threadLocals是一个ThreadLocalMap类型的变量。下面我们来看看ThreadLocalMap这个类的一个entry:


static class Entry extends WeakReference<ThreadLocal<?>> {
    
    Object val
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
 
public WeakReference(T referent) {
    super(referent); //referent:ThreadLocal的引用
}
 
//Reference构造方法     
Reference(T referent) {
    this(referent, null);//referent:ThreadLocal的引用
}
 
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

在上面的代码中,我们可以看出,当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。

考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

总结:THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯