多线程操作公共变量
线程不安全问题
当有多个线程同时并发操作一个公共变量,公共变量会在线程的本地内存中存储一个变量副本,线程对公共变量的操作都是基于本地内存的变量副本进行操作的。这种情况下,如果没有对公共变量做锁操作或者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>() {
@Override
protected 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);
@Override
public 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() {
@Override
public 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);
}
}