多线程操作公共变量

线程不安全问题

当有多个线程同时并发操作一个公共变量,公共变量会在线程的本地内存中存储一个变量副本,线程对公共变量的操作都是基于本地内存的变量副本进行操作的。这种情况下,如果没有对公共变量做锁操作或者CAS操作,就会导致在多个线程处理后的值被覆盖,最终得到的结果会跟预期的结果不一致,造成数据错乱。
image.png

如何保证线程的安全

(1)加锁:synchronized,reentrantlock
(2)公共变量volidate修饰
(3)变量设置为线程私有变量

如何设置线程私有变量

Java哪些线程不共享

Java虚拟机的内存模型

image.png
线程共享区:该区域的所有变量对所有线程都是共享的。
线程隔离区:该区域的所有变量都是线程独有且是隔离的。

局部变量为什么是线程安全的

每一个线程都有独立的栈空间,每次调用一个方法,就会往栈空间存入一个栈帧,相关的变量都会存储到栈帧的局部变量表当中。
image.png
当调用方法时,就会对应的写入一个栈帧,当方法调用完毕,对应的栈帧就会出栈。先入后出。
栈帧存储的内容有:局部变量表,动态链接,操作数栈。
image.png
局部变量分为基本数据类型和对象引用类型。基础数据类型的数据,只存在于栈帧当中,所以就是私有的。
对象引用类型,实质上变量存储的是对象的地址信息,真正的对象存储在线程共享区的堆内存中,所以,每一个线程的局部变量引用的对象,都是基于该线程引用所创建的对象,都是独立的,不会被其他的线程所引用。
image.png

ThreadLocal如何实现线程本地化存储

为解决线程不安全问题,可以让每个线程都拥有自己私有的对象副本,这样就不存在线程之间共享变量的问题。
image.png

一种基于Map实现的线程本地化存储

实现一个Map,key为Thread(线程),value对应每个线程所拥有的变量。
image.png

  1. public class MyThreadLocal<V> {
  2. private final Map<Thread, V> threadLocalMap = new ConcurrentHashMap<>();
  3. public V get() {
  4. return get(Thread.currentThread());
  5. }
  6. private V get(Thread thread) {
  7. return threadLocalMap.get(thread);
  8. }
  9. public void set(V value) {
  10. set(Thread.currentThread(), value);
  11. }
  12. private void set(Thread thread, V value) {
  13. threadLocalMap.set(thread, value);
  14. }
  15. }

该方式的弊端

如果按照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
image.png
整个Thread本地化存储结构,每个线程Thread里的ThreadLocalMap里可以存储多个ThreadLocal本地化对象。每一个ThreadLocal本地对象是通过自己的threadLocalHashCode计算数组下标,分配到下标对应的Entry数组中, 从而可以进行本地化对象的获取和设置操作。

针对ThreadLocal的弱引用关系

ThreadLocalMap内的Entry类是继承弱引用,所以Entry对象为弱引用对象,弱引用对象,会在进行垃圾回收的时候就会被回收。此时,只要Thread对象被垃圾回收,那么相应的,该线程的成员属性ThreadLocalMap就会被回收,ThreadLocalMap下的Entry数组也会被回收,Entry数组下的多个ThreadLocal对象也会被回收。所以最终就不会出现内存泄漏的问题。
image.pngimage.png

ThreadLocal内存泄漏问题

造成内存泄漏的原因

如果是基于线程池去创建线程,那这些线程对应引用的ThreadLocal对象就为强引用的关系。因为线程会一直存在于线程池当中,线程Thread对象不会被回收,导致线程的成员属性ThreadLocalMap对象也不会被回收(Thread对象与ThreadLocalMap的生命周期是相同的),所以根据Thread -> ThreadLocalMap -> Entry -> ThreadLocal + value关系上,完整的Entry对象没办法被回收,但是Entry下的多个ThreadLocal对象因为是WeakReference<ThreadLocal>, 所以会在垃圾回收的时候触发回收,但对应的value值以及Entry数组不会被回收。
ThreadLocalMap存储信息,key-> ThreadLocal对象, value -> value。
image.png
Entry数组中的key是弱引用对象, 会被回收,ThreadLocal=null;

image.png

Entry数组的value并不是弱引用对象,不会被回收,内存泄漏就出现了。
image.png

JDK如何解决ThreadLocal的内存泄漏

ThreadLocal中的get()、set()、remove()方法,会去判断ThreadLocal对象在Entry数组对应的下标是否存在,如果不存在,就会同步设置该ThreadLocal对象对应的Entry以及对应的value为null,去除强引用,有助于后面垃圾回收时回收掉这部分的对象。
核心源码就是expungeStaleEntry()方法的执行,在这个方法内部会把自动垃圾回收的为null的ThreadLocal对象所对应的value和Entry也设置为null。
ThreadLocal的get()、set()、remove()方法会触发expungeStaleEntry()方法的执行。

  1. tab[staleStot].value = null;
  2. tab[staleStot] = null;

image.png

总结

线程Thread对象包含成员变量ThreadLocalMap,所以ThreadLocalMap与Thread的生命周期是相同的。并且ThreadLocalMap包含了ThreadLocal(作为key使用),所以,在线程池场景下使用ThreadLocal时,可能会导致ThreadLocalMap对应的key被回收了,但是对应的Entry和value并没有被回收,造成了内存泄漏的问题。
所以,应当在ThreadLocal不再使用的时候调用remove方法,避免内存泄漏的问题发生。

实战案例

  1. package com.cmic.test.thread_local;
  2. import java.security.NoSuchAlgorithmException;
  3. import java.security.SecureRandom;
  4. public class ThreadSpecialSecureRandom {
  5. private static final ThreadSpecialSecureRandom INSTANCE = new ThreadSpecialSecureRandom();
  6. private ThreadSpecialSecureRandom() {
  7. }
  8. public static ThreadSpecialSecureRandom getInstance() {
  9. return INSTANCE;
  10. }
  11. private static final ThreadLocal<SecureRandom> SECURE_RANDOM_THREAD_LOCAL = new ThreadLocal<SecureRandom>() {
  12. @Override
  13. protected SecureRandom initialValue() {
  14. SecureRandom secureRandom = null;
  15. try {
  16. secureRandom = SecureRandom.getInstance("SHA1PRNG");
  17. } catch (NoSuchAlgorithmException e) {
  18. e.printStackTrace();
  19. secureRandom = new SecureRandom();
  20. }
  21. return secureRandom;
  22. }
  23. };
  24. public int nextInt(int bound) {
  25. SecureRandom secureRandom = SECURE_RANDOM_THREAD_LOCAL.get();
  26. return secureRandom.nextInt(bound);
  27. }
  28. }
  1. package com.cmic.test.thread_local;
  2. import java.util.concurrent.*;
  3. import java.util.concurrent.atomic.AtomicInteger;
  4. public class UserPasswordSystemManager {
  5. private static final UserPasswordSystemManager INSTANCE = new UserPasswordSystemManager();
  6. private UserPasswordSystemManager() {
  7. }
  8. public static UserPasswordSystemManager getInstance() {
  9. return INSTANCE;
  10. }
  11. private static final ExecutorService EXECUTOR = new ThreadPoolExecutor(
  12. 1,
  13. 2,
  14. 60L,
  15. TimeUnit.SECONDS,
  16. new ArrayBlockingQueue<>(10),
  17. new ThreadFactory() {
  18. private final AtomicInteger threadNumber = new AtomicInteger(1);
  19. @Override
  20. public Thread newThread(Runnable r) {
  21. int number = threadNumber.getAndIncrement();
  22. Thread thread = new Thread(r, "register-thread-pool-thread-" + number);
  23. return thread;
  24. }
  25. },
  26. new ThreadPoolExecutor.CallerRunsPolicy());
  27. public void register(String loginName, long phoneNumber) {
  28. Runnable runnable = new Runnable() {
  29. @Override
  30. public void run() {
  31. ThreadSpecialSecureRandom threadSpecialSecureRandom = ThreadSpecialSecureRandom.getInstance();
  32. StringBuilder passwordBuilder = new StringBuilder();
  33. for (int i = 0; i < 6; i++) {
  34. passwordBuilder.append(threadSpecialSecureRandom.nextInt(10));
  35. }
  36. String initPassword = passwordBuilder.toString();
  37. // 注册用户
  38. saveUser(loginName, phoneNumber, initPassword);
  39. // 发送短信
  40. sendMessage(loginName, phoneNumber, initPassword);
  41. }
  42. };
  43. EXECUTOR.submit(runnable);
  44. }
  45. private void sendMessage(String loginName, long phoneNumber, String initPassword) {
  46. System.out.println("保存登录账号: " + loginName + ", 手机号:" + phoneNumber + ", 密码:" + initPassword + ", 线程名:" + Thread.currentThread().getName());
  47. }
  48. private void saveUser(String loginName, long phoneNumber, String initPassword) {
  49. try {
  50. Thread.sleep(200);
  51. } catch (InterruptedException e) {
  52. e.printStackTrace();
  53. }
  54. System.out.println("用户注册完成,登录账号: " + loginName + ", 手机号:" + phoneNumber + ", 密码:" + initPassword + ", 线程名:" + Thread.currentThread().getName());
  55. }
  56. }
  1. package com.cmic.test.thread_local;
  2. public class UserPasswordSystemTest {
  3. public static void main(String[] args) {
  4. UserPasswordSystemManager userPasswordSystemManager = UserPasswordSystemManager.getInstance();
  5. userPasswordSystemManager.register("huangyaoxin", 123123123123L);
  6. userPasswordSystemManager.register("oldhuang", 456456456456L);
  7. userPasswordSystemManager.register("newhuang", 789789789L);
  8. userPasswordSystemManager.register("xxxxxx", 12345678910L);
  9. }
  10. }