ThreadLocal线程安全示例及其原理

19 篇文章 1 订阅
18 篇文章 0 订阅

提示:多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的

ThreadLocal是用空间换取时间,synchronized关键字是用时间换空间。


前言

线程安全是多线程编程时的计算机程序代码中的一个概念。 在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。


提示:以下是本篇文章正文内容,下面案例可供参考

一、示例

以下示例说明了两个线程操作同一对象的过程中,线程安全和线程不安全的两种结果

ThreadLocal线程安全示例

package com.mabo;

import java.util.concurrent.TimeUnit;

public class TheadLocalTest {
    private static ThreadLocal<Integer> threadLocalStudent = new ThreadLocal<>();
    private static int a=0;
    static {
        threadLocalStudent.set(a);
    }
    public static void main(String[] args) {
        // 简单写一个测试线程隔离的例子
        // 原料: 1个ThreadLocal类型的变量 2个线程
        // 期望结果:线程一set的变量 线程二get不到!
        new Thread(()->{
            a=2;
            threadLocalStudent.set(a);
            System.out.println(Thread.currentThread()+"线程保存的对象:"+threadLocalStudent.get());
            try {
                // 细节!!! 先睡一会再get避免误差。
                // 可见这是一个严谨性很高的测试Demo
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread()+"的值为"+threadLocalStudent.get());
        }).start();
        new Thread(()->{
            a=6;
            threadLocalStudent.set(a);
            System.out.println(Thread.currentThread()+"线程保存的对象:"+threadLocalStudent.get());
            try {
                // 细节!!! 先睡一会再get避免误差。
                // 可见这是一个严谨性很高的测试Demo
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread()+"的值为"+threadLocalStudent.get());
        }).start();
    }
}

执行结果
在这里插入图片描述

非线程安全示例

package com.mabo;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

public class Test {
    private static int a=0;
    /**
     * @Description : 测试
     * 当一个线程操作的过程中被另一个线程操作了当前对象,线程不安全
     * @Author : mabo
    */

    public static void main(String[] args) {
        new Thread(()->{
            a=2;
            System.out.println(Thread.currentThread()+"线程保存的对象:"+a);
            try {
                // 细节!!! 先睡一会再get避免误差。
                // 可见这是一个严谨性很高的测试Demo
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread()+"的值为"+a);

        }).start();
        new Thread(()->{
            a=6;
            System.out.println(Thread.currentThread()+"线程保存的对象:"+a);
            try {
                // 细节!!! 先睡一会再get避免误差。
                // 可见这是一个严谨性很高的测试Demo
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread()+"的值为"+a);
        }).start();
    }
}

执行结果
在这里插入图片描述

二、ThreadLocal线程安全原理

线程安全原理

可以看到ThreadLocal操作值的时候是取得当前线程的ThreadLocalMap对象,然后把值设置到了这个对象中,这样对于不同的线程得到的就是不同的ThreadLocalMap,那么向其中保存值或者修改值都只是会影响到当前线程,这样就保证了线程安全。

源码如下

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

内存泄漏

由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。

但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal正确的使用方法
每次使用完ThreadLocal都调用它的remove()方法清除数据
将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

三、其他线程安全的集合

Vector(不推荐)

Vector和ArrayList类似,是长度可变的数组,与ArrayList不同的是,Vector是线程安全的,它几乎给所有的public方法都加上了sychronized关键字。由于加锁倒是性能降低,在不需要并发访问时,这种强制性的同步就显得多余,所以现在几乎没有什么人在使用。

HashTable

HashTable和HashMap类似,不同的是HashTable是线程安全的,它也是几乎的给所有的public方法都加上了sychronized关键字,还有一个不同点是:HashTable的K, V都不能是null,但是HashMap可以。

Collections包装方法

由于ArrayList和HashMap性能好,但是不是线程安全,所以Collections工具类中提供了相应的包装方法将他们包装成相应的线程安全的集合:

List<E> synchronizedList = Collections.sychronizedList(new ArrayList<E>());

Set<E> sychronizedSet = Collections.sychronizedSet(new HashSet<E>());

Map<K, V> synchronizedMap = Collections.sychronizedMap(new HashMap<K, V>());

Collections针对每种集合都声明了一个线程安全的包装类,在原集合的基础上添加了锁对象,集合中的每个方法都通过这个锁对象实现同步

ConcurrentHashMap(多个桶,锁部分)

ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。HashTable的加锁方法是给每个方法加上synchronized关键字,这样锁的是整个对象。而ConcurrentHashMap是有更细粒度的锁。
在JDK1.8之前,ConcurrentHashMap加的是分段锁,即Segment,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响。
JDK1.8之后对此进行了进一步的改进,取消了Segment,直接在table元素上加锁,实现对每一行加锁,进一步减小了并发冲突的概率。

CopyOnWriteArrayList和CopyOnWriteArraySet

针对涉及到数据修改的部分,都会使用ReentrantLock锁住操作,并将修改或添加的元素,通过拷贝的方式,加入数组中,最后修改数组的引用为新复制的数组。读取时直接读取数组,不需要获取锁。

四、Java并发编程12种锁的具体实现方式

点击如下链接查看
Java并发编程12种锁的具体实现方式–点击此处跳转

总结

ThreadLocal是用空间换取时间,synchronized关键字是用时间换空间。
如何使用,需要根据需要合理使用

  • 4
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadLocalJava中的一个线程局部变量,它提供了一种线程安全的方式来存储每个线程的独立副本。每个线程都可以独立地访问和修改自己的副本,而不会影响其他线程的副本。 ThreadLocal原理是通过一个ThreadLocalMap来存储每个线程的变量副本。在ThreadLocalMap中,以ThreadLocal对象作为key,以线程的变量副本作为value。当使用ThreadLocal的get()方法获取变量值时,会先获取当前线程,然后从ThreadLocalMap中根据当前ThreadLocal对象获取对应的变量副本。当使用ThreadLocal的set()方法设置变量值时,会先获取当前线程,然后将变量值存储到ThreadLocalMap中对应的位置。 下面是一个简单的示例代码,演示了如何使用ThreadLocal来实现线程局部变量: ```java public class ThreadLocalExample { private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { // 创建并启动两个线程 Thread thread1 = new Thread(() -> { threadLocal.set(1); System.out.println("Thread 1: " + threadLocal.get()); threadLocal.remove(); }); Thread thread2 = new Thread(() -> { threadLocal.set(2); System.out.println("Thread 2: " + threadLocal.get()); threadLocal.remove(); }); thread1.start(); thread2.start(); } } ``` 在上面的代码中,我们创建了一个ThreadLocal对象`threadLocal`,并在两个线程中分别设置和获取变量值。每个线程都有自己独立的变量副本,互不干扰。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值