ThreadLocal线程安全示例及其原理

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

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

ThreadLocal线程安全示例及其原理

  • 前言
  • 一、示例
    • ThreadLocal线程安全示例
    • 非线程安全示例
  • 二、ThreadLocal线程安全原理
    • 线程安全原理
    • 内存泄漏
  • 三、其他线程安全的集合
    • Vector(不推荐)
    • HashTable
    • Collections包装方法
    • ConcurrentHashMap(多个桶,锁部分)
    • CopyOnWriteArrayList和CopyOnWriteArraySet
  • 四、Java并发编程12种锁的具体实现方式
  • 总结


前言

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


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

一、示例

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

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();
    }
}

执行结果
ThreadLocal线程安全示例及其原理_第1张图片

非线程安全示例

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线程安全示例及其原理_第2张图片

二、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关键字是用时间换空间。
如何使用,需要根据需要合理使用

你可能感兴趣的:(Java,spring,后端,java,前端,开发语言)