Java多线程--ThreadLocal

使用案例,定义一个只有一个线程的线程池,提交的第一个任务为设置ThreadLocal的值,提交的第二个任务为打印第一个任务设置的值

	@Test
	public void test01() throws InterruptedException {
		ExecutorService threadPool = Executors.newFixedThreadPool(1);
		threadPool.execute(new Runnable() {
			@Override
			public void run() {
				RequestModel requestModel = new RequestModel();
				requestModel.setRequestId("1");
				RequestModel.setRequestModel(requestModel);
			}
		});
		Thread.sleep(2000);

		threadPool.execute(new Runnable() {
			@Override
			public void run() {
				System.out.println(RequestModel.getRequestModel().getRequestId());
			}
		});
		Thread.sleep(10*1000);
	}
	public static class RequestModel {

		private static final ThreadLocal<RequestModel> REQUEST_MODEL = new ThreadLocal<>();

		private String requestId;

		public String getRequestId() {
			return requestId;
		}

		public void setRequestId(String requestId) {
			this.requestId = requestId;
		}

		public static RequestModel getRequestModel() {
			return REQUEST_MODEL.get();
		}

		public static void setRequestModel(RequestModel requestModel) {
			REQUEST_MODEL.set(requestModel);
		}
	}

利用ThreadLocal实现线程与线程池间数据传递

    @Test
    public void test03() throws InterruptedException {
        String token = "123";
        TokenThreadLocal.setToken(token);
        //业务异步执行
        CUST_THREAD_POOL.execute(new AbstractRunnable() {
            @Override
            public void run() {
                //执行线程打印
                System.out.println("执行业务方法");
                System.out.println("token:" + TokenThreadLocal.getToken());
            }
        });
        Thread.sleep(2000);
        CUST_THREAD_POOL.shutdown();
        while (true) {
            if (CUST_THREAD_POOL.isTerminated()) {
                break;
            }
        }
        System.out.println("run over");
    }

    private static final ThreadPoolExecutor CUST_THREAD_POOL = new ThreadPoolExecutor(4, 8,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>()) {
        @Override
        public void execute(Runnable command) {
            //提交线程数据填充到Runnable中
            if (command instanceof AbstractRunnable) {
                AbstractRunnable abstractRunnable = (AbstractRunnable) command;
                abstractRunnable.setToken(TokenThreadLocal.getToken());
            }
            super.execute(command);
        }

        @Override
        protected void beforeExecute(Thread t, Runnable r) {
            //从Runnable中获取token填充到执行线程中
            if (r instanceof AbstractRunnable) {
                AbstractRunnable abstractRunnable = (AbstractRunnable) r;
                TokenThreadLocal.setToken(abstractRunnable.getToken());
            }
            super.beforeExecute(t, r);
        }

        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            super.afterExecute(r, t);
            //移除执行线程中存储的token
            TokenThreadLocal.remove();
        }
    };

    /**
     * 任务虚基类
     */
    private static abstract class AbstractRunnable implements Runnable {

        private String token;

        public String getToken() {
            return token;
        }

        public void setToken(String token) {
            this.token = token;
        }
    }

    /**
     * token ThreadLocal操作类
     */
    private static class TokenThreadLocal {

        private static final ThreadLocal<String> TOKEN_THREAD_LOCAL = new ThreadLocal<>();

        public static void setToken(String token) {
            TOKEN_THREAD_LOCAL.set(token);
        }

        public static String getToken() {
            return TOKEN_THREAD_LOCAL.get();
        }

        public static void remove() {
            TOKEN_THREAD_LOCAL.remove();
        }
    }

源码解读
ThrealLocal实现的本质是线程内部绑定一个类似于数组的结构,每一个槽存放的是一个个继承自Entry,而这个Entry的继承自WeakReference弱引用,

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

entry的key为ThreadLocal,如果ThreadLocal置为了空,下一次执行gc时会被回收,那么key指向对象为空(详细查看https://blog.csdn.net/BtWangZhi/article/details/104274731),
每一个ThreadLocal在线程中的数组(其实如果数组的最后一个位置存放数据,是可以放到数组的头部的,应该是循环数组)中存放的位置的索引时跟ThreadLocal的threadLocalHashCode有关,
每一个ThreadLocal的threadLocalHashCode值通过原子更新类累加

private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

累加的数为0x61c88647转位十进制1640531527=(int) ((1L << 32) * (Math.sqrt(5) - 1)/2)=((1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1)))
=2^32*黄金比例,这样做保证了数据均匀分散。
参考https://blog.csdn.net/y4x5M0nivSrJaY3X92c/article/details/81124944

每一个entry在数组中存放的位置我们称之为槽,而槽的定位使用的hashCode与数组长度进行并运算,

int i = key.threadLocalHashCode & (table.length - 1);

如 果 该 槽 处 存 放 数 据 , 那 么 放 到 下 一 个 , 如 果 下 一 个 依 然 已 经 有 值 了 , 那 么 存 放 到 下 下 一 个 , 以 此 内 推 , 这 种 算 法 叫 线 性 探 测 法 。 \color{#FF0000}{如果该槽处存放数据,那么放到下一个,如果下一个依然已经有值了,那么存放到下下一个,以此内推,这种算法叫线性探测法。} 线
在这里插入图片描述
设置值的核心代码在静态内部类ThreadLocalMap中。

        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            //threadLocalHashCode为ThreadLocal初始化时通过AtomicInteger原子递增后得到的数据。
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
				
                if (k == key) {
                    //覆盖之前的值
                    e.value = value;
                    return;
                }

                if (k == null) {
                    //在后续的槽中找到等于key的槽,然后将找到的数据填充到槽i中。清理从i之前第一个entry的key为空至找到的entry这段entry
                    //如果key为空,那么将槽置为空,不为空,那么将entry移动到指定的槽或后面的槽中。expungeStaleEntry
                    //最后执行清理整个数组 log2 length次 ,cleanSomeSlots
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			//设置值
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

			//从staleSlot开始找已经被gc回收的Entry的key。
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    //如果在staleSlot向前不存中entry的key被回收的,那么就从i开始清理。
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //整理后续的entry
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }
				
                if (k == null && slotToExpunge == staleSlot)
                    //slotToExpunge == staleSlot说明前置不存中已经被回收的ThreaLocal,更新回收开始的索引为当前i。
                    slotToExpunge = i;
            }

			//没有发现entry的key为当前key,那么将入参赋值到槽位空的位置。
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);
			//发现从staleSlot开始后面,存在已经被gc回收的entry,那么执行整理。
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

主 线 逻 辑 : 利 用 T h r e a d L o c a l 传 入 的 t h r e a d L o c a l H a s h C o d e 值 与 T h r e a d 中 的 M a p 的 长 度 进 行 取 并 操 作 得 到 数 组 索 引 值 , − 》 如 果 该 槽 处 没 有 数 据 , 设 置 值 到 该 索 引 中 。 如 果 有 , 那 么 尝 试 放 到 下 下 个 位 置 , − 》 如 果 存 在 已 经 被 g c 回 收 的 E n t r y 的 k e y , 那 么 执 行 整 理 工 作 − 》 阈 值 判 断 − 》 扩 容 。 \color{#FF0000}{主线逻辑:利用ThreadLocal传入的threadLocalHashCode值与Thread中的Map的长度进行取并操作得到数组索引值,-》如果该槽处没有数据,设置值到该索引中。如果有,那么尝试放到下下个位置,-》如果存在已经被gc回收的Entry的key,那么执行整理工作-》阈值判断-》扩容。} 线ThreadLocalthreadLocalHashCodeThreadMapgcEntrykey

get方法核心逻辑

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                //索引位置的数据还没被GC回收。从环形数组中继续找。
                return getEntryAfterMiss(key, i, e);
        }

置顶槽中为空,从槽后面继续搜索。

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    //存在已经被回收的entry的key,执行整理逻辑
                    expungeStaleEntry(i);
                else
                    //该处槽位已经被其他Entry占用,继续搜索下一个
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

整理数组的expungeStaleEntry需要特别说一下,好多位置都用到了,
在尝试获取数据或添加数据的时候,如果发现部分槽中entry的key为空,则说明已经被gc回收的,那么需要整理,
做了两件事,一件是如果entry的key为空,那么将槽置为空,如果不为空,那么将该槽中的entry通过并运算重新发到合适处或合适处的后面

        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            //清空索引staleSlot处的数据
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            //对整个环形数组中的entry数据位置进行重新放置。同时entry的k被回收的则将数组索引处置为空。
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    //数组槽位置为空
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;
						//将数据放到合适的后续槽位中。线性探测法。
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

关于ThreadLocal会造成内存泄漏,写一点个人的理解,
如果一个线程在执行的过程中,向ThreadLocal添加了一个大的对象,线程执行完成,但是没有执行threadLocal的remove方法,大对象挂载在线程下,线程不被GC回收,存在引用链Thread.threadLocals-》ThreadLocalMap.table-》Entry.value,虽然Entry.key为弱引用,会被回收,但是value不会这样有潜在内存泄漏的风险,不过ThreadLocal内部有一套清理机制,如果ThreadLocal置为了空,那么对应的Entry.key为活不过下一次gc,如果还有其他ThreadLocal,执行get、set、remove会整理线程下的Entry数组中Entry.key为空对应的value,置为null,让gc回收。
最 保 险 的 是 在 线 程 一 次 执 行 完 后 执 行 r e m v e 方 法 , \color{#FF0000}{最保险的是在线程一次执行完后执行remve方法,} 线remve

两个线程存在父子关系,ThreadLoca就无法实现数据共享了,这个时候就需要InheritableThreadLocal

    @Test
    public void test02() throws InterruptedException {
        InheritableThreadLocal<String> threadLocal=new InheritableThreadLocal<>();
        threadLocal.set("1");

        new Thread(){
            @Override
            public void run() {
                System.out.println(threadLocal.get());
            }
        }.start();

        Thread.sleep(5000);
        System.out.println("run over");
    }

在这里插入图片描述
子线程也共享了父线程的中的数据,
new 子线程的时候,把父线程的inheritableThreadLocals复制给子线程而已。代码在Thread-》init中。这样就得到了父线程中的数据。

参考:
https://www.cnblogs.com/micrari/p/6790229.html
https://blog.csdn.net/zzg1229059735/article/details/82715741
关于threadLocalHashCode值累加参考:
https://blog.csdn.net/y4x5M0nivSrJaY3X92c/article/details/81124944

Random 生成随机数

        int min = 0;
        int max = 100;
        Random random = new Random();
        int s = random.nextInt(max) % (max - min + 1) + min;
        System.out.println(s);

Random初始时根据当前时间生成一个其实随机数即种子,
后面一直再上面进行计算然后返回,同时会计算下一次的种子,通过CAS算法存放到AtomicLong类型的变量中,在并发的场景下会出现频繁自旋。

    protected int next(int bits) {
        long oldseed, nextseed;
        AtomicLong seed = this.seed;
        do {
            oldseed = seed.get();
            nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));
        return (int)(nextseed >>> (48 - bits));
    }

ThreadLocalRandom
和Random一样,初始化的时候会生成一个种子,通过UnSafe设置到Thread中,

    static final void localInit() {
        int p = probeGenerator.addAndGet(PROBE_INCREMENT);
        int probe = (p == 0) ? 1 : p; // skip 0
        long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
        Thread t = Thread.currentThread();
        UNSAFE.putLong(t, SEED, seed);
        UNSAFE.putInt(t, PROBE, probe);
    }

后面每次生成随机数都生成下一个种子存放到Thread的threadLocalRandomSeed变量中。

    final long nextSeed() {
        Thread t; long r; // read and update per-thread seed
        UNSAFE.putLong(t = Thread.currentThread(), SEED,
                       r = UNSAFE.getLong(t, SEED) + GAMMA);
        return r;
    }

参考:https://mp.weixin.qq.com/s/Ke1iZ3Ec2wf06BfqrNxbJQ

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值