赞
踩
目录
什么是线程安全? 原子性,可见性,有序性
怎么去解决安全性问题? ->
方式一:锁(synchronized)
方式二:ThreadLocal:提供了一个线程范围的局部变量,线程级别隔离
我们在了解一个技术的时候我们先了解这个技术的使用
举个例子:
线程不安全的情况:
- private static int num = 0;
-
- public static void main(String[] args) {
- Thread[] threads = new Thread[5];
- for (int i = 0; i < threads.length; i++) {
- threads[i] = new Thread(() -> {
- num += 5;
- System.out.println(Thread.currentThread().getName() + ":" + num);
- }, "thread-" + i);
- }
- for (Thread thread : threads) {
- thread.start();
- }
- }
输出结果:
- thread-0:10
- thread-2:15
- thread-3:20
- thread-1:10
- thread-4:25
每个线程拿到的num值是不确定的 -> 线程不安全
如何保证线程安全?1)使用synchronized同步锁来解决线程并发安全问题 2)使用ThreadLocal,提供了一个线程范围的局部变量,线程级别隔离。
使用ThreadLocal之后:
- static ThreadLocal<Integer> num = new ThreadLocal<Integer>() {
- @Override
- protected Integer initialValue() {
- return 0;
- }
- };
-
- public static void main(String[] args) {
- Thread[] threads = new Thread[5];
- for (int i = 0; i < threads.length; i++) {
- threads[i] = new Thread(() -> {
- int localNum = num.get();
- localNum += 5;
- num.set(localNum);
- System.out.println(Thread.currentThread().getName() + ":" + num.get());
- });
- }
- for (Thread thread : threads) {
- thread.start();
- }
- }
输出结果:
- Thread-0:5
- Thread-1:5
- Thread-2:5
- Thread-3:5
- Thread-4:5
提出疑问:
1)每个线程的变量副本是如何存储的?
2)ThreadLocal是什么时候设置初始化的?
我们从ThreadLocal的get()方法看起,
进入get()方法之后,我们可以看到在get方法中首先调用了一个getMap()方法。
getMap()方法中返回了一个ThreadLocalMap类型的成员变量threadLocals
因为返回的threadLocals的值为null,我们回到get方法中,调用setInitialValue()方法;
在setInitialValue方法中调用了我们在声明ThreadLocal的时候重写的initialValue()方法;
我们可以看到ThreadLocal本身的initialValue方法返回的是null,我们通过对这个方法的重写,返回了0;
然后,此时value值为0,继续调用getMap方法,依然返回threadLocals,值为null;
进入createMap;
在createMap中将当前ThreadLocal对象作为ThreadLocalMap的key,将初始值0作为ThreadLocalMap的value值,传给ThreadLocalMap的构造函数;
在ThreadLocalMap的构造函数中初始化了一个大小为16的Entry数组用来存放键值对,对于下标的存取使用了一个小算法(斐波那契数列,也叫黄金分割数列),
firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
总是能产生一个0至(容量-1)的一个随机数,并且不重复;
我们可以看到源码中对于threadLocalHashCode进行初始化的时候调用了nextHashCode函数;
注意是nextHashCode()而不是hashCode(),因为hashCode()函数是存在重复值的,比如“通话”和“重地”;
nextHashCode()函数中使用了这样一个常量--HASH_INCREMENT;
由于nextHashCode()函数中的方法封装比较复杂,我们自己手写一个算法来感受一些这个算法的魅力。
斐波那契数列例子:
- public class Demo {
- private static final int HASH_INCREMENT = 0x61c88647;
-
- public static void main(String[] args) {
- magicHash(16);
- magicHash(32);
- }
-
- private static void magicHash(int size) {
- int hashCode = 0;
- for (int i = 0; i < size; i++) {
- hashCode = i * HASH_INCREMENT + HASH_INCREMENT;
- System.out.print((hashCode & (size - 1)) + " ");
- }
- System.out.println();
- }
- }
执行结果:
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0
下面我们回到setInitialValue方法,可以看到方法最后返回了初始值0;
将0赋值给了localNum变量;
我们将localNum加5之后再set的时候调用ThreadLocal的set方法,传递值为5的参数;
可以看到在set方法中再次调用了getMap方法;
返回了当前ThreadLocal对象中的threadLocals成员变量,
由于在之前的代码中已经为threadLocals成员变量进行了赋值,所以此时返回了一个ThreadLocalMap对象。
因此,在Thread Local的set方法中直接调用了ThreadLocalMap的set方法;
在ThreadLocalMap的set方法中,我们可以看到依然使用了特殊算法来获取Entry数组的下标,
然后对ThreadLocalMap的Entry数组进行了遍历,如果当前ThreadLocal对象对应的ThreadLocalMap中的key值存在的话,就直接更新key对应的value值,如果不存在的话就new一个新的Entry键值对放到table中,并且如果数量超过16就会进行扩容;
补充:k == null的判断是因为Entry继承了WeakReference,是一个弱应用类型,有可能会为null
set值之后,我们再次get方法获取我们之前的set 的值,
TreadLocal整体的结构图如下所示:
引用类型分为:WeakReference弱引用,强引用,软引用,虚引用
我们平时一般使用的都是强应用,
什么是弱应用呢?
举个例子:
A a = new A();
B b = new B(a);
这两个就是属于强引用类型,当a=null的时候,这时垃圾回收器(GC)无法去回收a应用类型占用的响应的堆空间,因为这个a引用和b引用有一种强以来关系,这时我们就需要把B类型设置成WeakReference弱引用类型,这样垃圾回收器就可以正常回收a的对象空间。
ThreadLocal的局限性是,当ThreadLocal中存放的是引用类型的话,我们使用ThreadLocal无法保证并发线程中该对象的同步线程安全。
举个例子:
- public class Test {
- static App app = new App();
- static ThreadLocal<App> num = ThreadLocal.withInitial(() -> app);
-
- public static void main(String[] args) {
- Thread[] threads = new Thread[5];
- for (int i = 0; i < threads.length; i++) {
- threads[i] = new Thread(() -> {
- App localApp = num.get();
- localApp.inc();
- num.set(localApp);
- System.out.println(Thread.currentThread().getName() + ":" + num.get().getNum());
- });
- }
- for (Thread thread : threads) {
- thread.start();
- }
- }
- }
- class App {
- private Integer num = 0;
-
- public void inc() {
- num++;
- }
-
- public Integer getNum() {
- return num;
- }
- }
输出结果:
- Thread-1:1
- Thread-0:2
- Thread-2:3
- Thread-3:4
- Thread-4:5
因此,ThreadLocal对于引用类型的并发线程安全是不能保证的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。