多线程操作公共变量
线程不安全问题
当有多个线程同时并发操作一个公共变量,公共变量会在线程的本地内存中存储一个变量副本,线程对公共变量的操作都是基于本地内存的变量副本进行操作的。这种情况下,如果没有对公共变量做锁操作或者CAS操作,就会导致在多个线程处理后的值被覆盖,最终得到的结果会跟预期的结果不一致,造成数据错乱。
如何保证线程的安全
(1)加锁:synchronized,reentrantlock
(2)公共变量volidate修饰
(3)变量设置为线程私有变量
如何设置线程私有变量
Java哪些线程不共享
Java虚拟机的内存模型

线程共享区:该区域的所有变量对所有线程都是共享的。
线程隔离区:该区域的所有变量都是线程独有且是隔离的。
局部变量为什么是线程安全的
每一个线程都有独立的栈空间,每次调用一个方法,就会往栈空间存入一个栈帧,相关的变量都会存储到栈帧的局部变量表当中。
当调用方法时,就会对应的写入一个栈帧,当方法调用完毕,对应的栈帧就会出栈。先入后出。
栈帧存储的内容有:局部变量表,动态链接,操作数栈。
局部变量分为基本数据类型和对象引用类型。基础数据类型的数据,只存在于栈帧当中,所以就是私有的。
对象引用类型,实质上变量存储的是对象的地址信息,真正的对象存储在线程共享区的堆内存中,所以,每一个线程的局部变量引用的对象,都是基于该线程引用所创建的对象,都是独立的,不会被其他的线程所引用。
ThreadLocal如何实现线程本地化存储
为解决线程不安全问题,可以让每个线程都拥有自己私有的对象副本,这样就不存在线程之间共享变量的问题。
一种基于Map实现的线程本地化存储
实现一个Map,key为Thread(线程),value对应每个线程所拥有的变量。
public class MyThreadLocal<V> {private final Map<Thread, V> threadLocalMap = new ConcurrentHashMap<>();public V get() {return get(Thread.currentThread());}private V get(Thread thread) {return threadLocalMap.get(thread);}public void set(V value) {set(Thread.currentThread(), value);}private void set(Thread thread, V value) {threadLocalMap.set(thread, value);}}
该方式的弊端
如果按照Map方式去存储线程与线程所拥有的变量之间的映射,可能会存在内存泄漏的问题。
因为MyThreadLocal里的map持有线程Thread对象,所以,只要MyThreadLocal对象存活在JVM中,那么map中的线程Thread对象是不会被JVM垃圾回收的,所以就很容易出现内存泄漏。
基于ThreadLocal实现的线程私有化存储
ThreadLocal设计方案
每一个Thread对象都会拥有一个ThreadLocal.ThreadLocalMap的对象成员变量;
ThreadLocalMap存储的是Entry对象数组,Entry数组下的ThreadLocal是弱引用对象WeakReference,垃圾回收的时候就会回收掉ThreadLocal对象:**static class **Entry **extends **WeakReference<ThreadLocal<?>>
Entry对象会存储每一个线程所拥有的变量信息Entry(ThreadLocal<?> k, Object v),根据ThreadLocal计算出对应在Entry数组的下标,数组下标对应的值就是value。
整个顺序的流程就是: Thread -> ThreadLocalMap -> Entry -> ThreadLocal + value。
整个Thread本地化存储结构,每个线程Thread里的ThreadLocalMap里可以存储多个ThreadLocal本地化对象。每一个ThreadLocal本地对象是通过自己的threadLocalHashCode计算数组下标,分配到下标对应的Entry数组中, 从而可以进行本地化对象的获取和设置操作。
针对ThreadLocal的弱引用关系
ThreadLocalMap内的Entry类是继承弱引用,所以Entry对象为弱引用对象,弱引用对象,会在进行垃圾回收的时候就会被回收。此时,只要Thread对象被垃圾回收,那么相应的,该线程的成员属性ThreadLocalMap就会被回收,ThreadLocalMap下的Entry数组也会被回收,Entry数组下的多个ThreadLocal对象也会被回收。所以最终就不会出现内存泄漏的问题。
ThreadLocal内存泄漏问题
造成内存泄漏的原因
如果是基于线程池去创建线程,那这些线程对应引用的ThreadLocal对象就为强引用的关系。因为线程会一直存在于线程池当中,线程Thread对象不会被回收,导致线程的成员属性ThreadLocalMap对象也不会被回收(Thread对象与ThreadLocalMap的生命周期是相同的),所以根据Thread -> ThreadLocalMap -> Entry -> ThreadLocal + value关系上,完整的Entry对象没办法被回收,但是Entry下的多个ThreadLocal对象因为是WeakReference<ThreadLocal>, 所以会在垃圾回收的时候触发回收,但对应的value值以及Entry数组不会被回收。
ThreadLocalMap存储信息,key-> ThreadLocal对象, value -> value。 
Entry数组中的key是弱引用对象, 会被回收,ThreadLocal=null;
Entry数组的value并不是弱引用对象,不会被回收,内存泄漏就出现了。
JDK如何解决ThreadLocal的内存泄漏
ThreadLocal中的get()、set()、remove()方法,会去判断ThreadLocal对象在Entry数组对应的下标是否存在,如果不存在,就会同步设置该ThreadLocal对象对应的Entry以及对应的value为null,去除强引用,有助于后面垃圾回收时回收掉这部分的对象。
核心源码就是expungeStaleEntry()方法的执行,在这个方法内部会把自动垃圾回收的为null的ThreadLocal对象所对应的value和Entry也设置为null。
ThreadLocal的get()、set()、remove()方法会触发expungeStaleEntry()方法的执行。
tab[staleStot].value = null;tab[staleStot] = null;
总结
线程Thread对象包含成员变量ThreadLocalMap,所以ThreadLocalMap与Thread的生命周期是相同的。并且ThreadLocalMap包含了ThreadLocal(作为key使用),所以,在线程池场景下使用ThreadLocal时,可能会导致ThreadLocalMap对应的key被回收了,但是对应的Entry和value并没有被回收,造成了内存泄漏的问题。
所以,应当在ThreadLocal不再使用的时候调用remove方法,避免内存泄漏的问题发生。
实战案例
package com.cmic.test.thread_local;import java.security.NoSuchAlgorithmException;import java.security.SecureRandom;public class ThreadSpecialSecureRandom {private static final ThreadSpecialSecureRandom INSTANCE = new ThreadSpecialSecureRandom();private ThreadSpecialSecureRandom() {}public static ThreadSpecialSecureRandom getInstance() {return INSTANCE;}private static final ThreadLocal<SecureRandom> SECURE_RANDOM_THREAD_LOCAL = new ThreadLocal<SecureRandom>() {@Overrideprotected SecureRandom initialValue() {SecureRandom secureRandom = null;try {secureRandom = SecureRandom.getInstance("SHA1PRNG");} catch (NoSuchAlgorithmException e) {e.printStackTrace();secureRandom = new SecureRandom();}return secureRandom;}};public int nextInt(int bound) {SecureRandom secureRandom = SECURE_RANDOM_THREAD_LOCAL.get();return secureRandom.nextInt(bound);}}
package com.cmic.test.thread_local;import java.util.concurrent.*;import java.util.concurrent.atomic.AtomicInteger;public class UserPasswordSystemManager {private static final UserPasswordSystemManager INSTANCE = new UserPasswordSystemManager();private UserPasswordSystemManager() {}public static UserPasswordSystemManager getInstance() {return INSTANCE;}private static final ExecutorService EXECUTOR = new ThreadPoolExecutor(1,2,60L,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10),new ThreadFactory() {private final AtomicInteger threadNumber = new AtomicInteger(1);@Overridepublic Thread newThread(Runnable r) {int number = threadNumber.getAndIncrement();Thread thread = new Thread(r, "register-thread-pool-thread-" + number);return thread;}},new ThreadPoolExecutor.CallerRunsPolicy());public void register(String loginName, long phoneNumber) {Runnable runnable = new Runnable() {@Overridepublic void run() {ThreadSpecialSecureRandom threadSpecialSecureRandom = ThreadSpecialSecureRandom.getInstance();StringBuilder passwordBuilder = new StringBuilder();for (int i = 0; i < 6; i++) {passwordBuilder.append(threadSpecialSecureRandom.nextInt(10));}String initPassword = passwordBuilder.toString();// 注册用户saveUser(loginName, phoneNumber, initPassword);// 发送短信sendMessage(loginName, phoneNumber, initPassword);}};EXECUTOR.submit(runnable);}private void sendMessage(String loginName, long phoneNumber, String initPassword) {System.out.println("保存登录账号: " + loginName + ", 手机号:" + phoneNumber + ", 密码:" + initPassword + ", 线程名:" + Thread.currentThread().getName());}private void saveUser(String loginName, long phoneNumber, String initPassword) {try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("用户注册完成,登录账号: " + loginName + ", 手机号:" + phoneNumber + ", 密码:" + initPassword + ", 线程名:" + Thread.currentThread().getName());}}
package com.cmic.test.thread_local;public class UserPasswordSystemTest {public static void main(String[] args) {UserPasswordSystemManager userPasswordSystemManager = UserPasswordSystemManager.getInstance();userPasswordSystemManager.register("huangyaoxin", 123123123123L);userPasswordSystemManager.register("oldhuang", 456456456456L);userPasswordSystemManager.register("newhuang", 789789789L);userPasswordSystemManager.register("xxxxxx", 12345678910L);}}

