##要点
- 是否对线程安全有初步的了解(初级)
- 是否对线程安全的产生原因有思考(中级) 优化线程安全要注意什么?
- 是否知道final、volatile关键字的作用(中级)
- 是否清楚1.5之前Java DCL 为什么有缺陷(中级)
- 是否清楚地知道如何编写线程安全的程序(高级)
- 是否对ThreadLocal的使用注意事项有认识(高级)
####是否清楚地知道如何编写线程安全的程序
-
#####什么是线程安全?
-
不安全:资源不同步,脏读脏写; 如多个
线程的工作内存读写主存时的不同步; “进程安全”问题不存在, 因为进程之间内存相互独立,各自独享内存的, 一个进程被杀掉的话,其所有内存都还给物理内存了; 可能共享CPU时间片; 线程是存在于进程当中的, 同一个进程中的线程之间是可以共享内存的; -
线程安全产生的原因:
可变资源(内存)线程间共享(关键词“可变”和“共享”) 线程间不共享的资源不用考虑线程安全了;
-
-
PS:每一个线程都有自己的一个内存副本<Java内存模型>
-
#####如何实现线程安全?
-
不共享资源共享才会产生线程安全问题, 所以尽量不共享; -
共享不可变资源(volatile、final)- 禁止重排序
-
有条件地
共享可变资源- (更改刷新的)可见性 一个线程对共享资源的修改,其他线程能够马上看到! 实现:某个线程对共享资源进行了更新时,要马上刷新到主存!
- 操作原子性
- 禁止重排序
-
##不共享资源
- 可重入函数:
传入一个参数进函数,经过一系列的运算,
再把运算结果返回出去,
中间不会涉及到任何对外部内存的访问、修改,
没有副作用,
像这样没有副作用的函数,
先天就具备线程安全的优势:
####ThreadLocal实现不共享资源
- 虽然说每个线程都会去访问一个
ThreadLocal对象, 但实际上最终访问的 都是自己线程内部的一个副本;
比如下图中的token, 对应的场景如, 一个服务器提供了很多个服务, 每个服务的话, 每个用户进来请求,服务器都会为这个用户 开一个线程 来提供服务, 这个时候, 因为每个用户 就都是属于不同的线程的, 而ThreadLocal便是类似于服务器的设计, 这里每个线程都去访问这个token的时候, 都会有一个自己的 String的 一个副本, 这样线程间便不会互相干扰;
如此便是实现了不共享资源, 也就没有线程安全的问题了; 自己线程之内,不管怎么设置,都不会影响到其他线程; 【UUID,唯一识别码(Universally Unique Identifier),可以由Java工具类生成,用来唯一标注一个元素,如标注线程】 下面是一个用例: - ThreadLocal原理
看一下
ThreadLocal源码的set方法!!!!!!!! 可以看到,ThreadLocal的底层,其实是绑定到线程上的一个ThreadLocalMap, 当前线程没有ThreadLocal时,就先为线程创建一个;【createMap(t, value)】 当前线程有ThreadLocal【t.threadLocals != null】, 则添加值的时候置入键值对map.set(this,value), 使用的key,即this, 即当前调用线程它对应的ThreadLocal类对象引用【t.threadLocals】,value则企图传入的值;也就是说,线程不同,ThreadLocal为线程创建的threadLocals就不同; 访问ThreadLocal.get时, 又是根据线程各自 的 threadLocals【即key】来取value,那 不同的子线程 访问同一个 主线程的ThreadLocal,key不同,它们访问ThreadLocal的set、get时 处理的值,肯定也是不一样的!- ThreadLocal中这个ThreadLocalMap是,储存在、绑定在线程上的:
#总结!!!
- ThreadLocal中这个ThreadLocalMap是,储存在、绑定在线程上的:
- 两个点总结ThreadLocal特性:
唯一 一个
ThreadLocal对象,作为全局变量定义在主线程, 为访问它(set())的N个子线程, 开启(createMap())N个相互独立的ThreadLocalMap, 因此,每一个子线程访问主线程中的这个独一无二的ThreadLocal对象的时候, 总会访问到子线程自身对应的底层数据存储结构ThreadLocalMap;
故
不同的线程,访问同一个ThreadLocal对象的时候, 访问的是(绑定到不同线程的)不同的底层数据结构ThreadLocalMap,读写的是不同的数据;
故
**实现了, 同属主线程的一系列子线程间的,资源不共享,解决的了线程安全问题;
【服务器是一个服务端里边, 操作很多个线程,每个线程服务每个不同的用户;
ThreadLocal是一个ThreadLocal实例里边, 操作很多个ThreadLocalMap, 每个ThreadLocalMap服务不同的子线程】
另外我们可以发现Android的消息机制中,
正是把Looper交给ThreadLocal保管了,
所以同个线程的所有Handler中关联的Looper其实是同一个Looper的副本,
Handler通过Looper找到对应的MessageQueue,
把自己负责的Message加进去:**
实战案例如下:
package test;
public class ThreadLocalTest {
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();
public static class MyRunnable implements Runnable {
@Override
public void run() {
threadLocal.set((int) (Math.random() * 100D));
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(), "A");
Thread t2 = new Thread(new MyRunnable(), "B");
Thread t3 = new Thread(new MyRunnable(), "C");
t1.start();
t2.start();
t3.start();
}
}
运行结果:
- ThreadLocalMap 跟 WeakHashMap 很像:
-
本身对于对象的持有都是弱引用的; 区别是 ThreadLocalMap不用去监听ReferenceQueue, (监听ReferenceQueue还是有一定的开销的) 因,ThreadLocalMap适用于对象较少的场景, 另外, 线程退出时会自动移除;
-
关于Hash冲突的解决方法也是不一样的,
单链表法即传统HashMap解决办法,开放地址法则适合对象比较少的情况, 即线性探测、平方探测、双散列法等等;
-
- ThreadLocal的使用建议:
-
声明为全局静态final成员ThreadLocal在一个主线程中有一个实例就够了, 没必要每次创建子线程都整一个出来, 并且我们set value的时候, 我们是以ThreadLocal的this为key的, ThreadLocal这个对象的引用最好是独一的、不可更改的!
不设置final的话,还有另外的问题, 还要考虑什么时候去初始化它,还要考虑可见性, 这就还要考虑加锁了; -
避免存储大量对象因, 底层数据结构、Hash冲突的解决方案和Hash计算算法, 已经做了限制; -
用完后及时移除对象ThreadLocal自身没有监听机制, 如果你设置的ThreadLocal的存在周期非常的长, 那对应的线程就会一直存在, 其引用不会被回收,有内存泄漏风险
-
##共享不可变资源(加final/volatile,禁止重排序)
####首先普及一下重排序,等下涉及到
什么是重排序?重排序是指令的重排序。 为了提高性能,编译器和处理器常常会对指令做重排序, 重排序就会导致多线程执行的时候有数据不一致问题, 导致程序结果不是理想结果。
重排序分为三类:
- 编译器重排序:不改变单线程程序语义前提下,重新安排执行顺序
- 指令级并行重排序: 指令并行技术可以将多条指令重叠执行, 如果不存在数据依赖性, 处理器会改变语句对应的机器指令执行顺序
- 内存系统重排序
#####**案例:** - 定义一个类: 两个成员,x为final,y不为final; ``` class FinalFieldExample{ final int x; int y;
public FinalFieldExample(){
x = 3;
y = 4;
}
}
**假设Thread1 为 writer线程,初始化了一个FinalFieldExample实例f,
Thread2 为 reader线程,读取实例f 的x、y值,赋值给 i、j;
那么表面上我们是期待结果是 i = 3, j = 4的:**

- **实际上的情况可能会不如我们期待的那样子,
由于虚拟机的实现或者CPU架构的特征,
指令是可能发生重排序的,
重排序会把非final的变量赋值指令 排序到构造方法之外,**<br>
这样的结果自然是,
x因为是final的所以自然会在构造方法之内进行赋值,
但y是非final的,
有可能构造方法执行完了,
y的赋值指令还没有走完,<br>
这个时候因为构造方法走完,
reader读的时候发现f 是不等于null的,
就会把未完成赋值的y 的值给读出来,
那结果j的值就是0了:

>####所以,各单位请注意!<br>`final`啊,它还有一个`禁止重排序`的作用,<br>即,禁止`被final修饰的代码`的`对应的指令`被重排序
>###补充:volatile
>**`volatile`除了能保证线程间的`可见性`,
也能`禁止重排序`!!**
>- **从1.5开始,其语义被增强了,明确了`禁止重排序`的作用;
1.4以前,即便使用双重校验锁的单例模式,也是有问题的;**
>**单例模式案例(两种加volatile的情况,正常):**
>**如果不加volatile,就可能会出现类似重排序的问题了:
>有可能重排序之后,
构造方法的调用的指令被排到了后面,
这时候程序 还没等`构造方法` `执行完毕`,
就把分配好内存的`实例`赋值给了`引用`,<br>
这时候这个引用因为没有经过构造方法,
所以还没有被初始化,
此时Thread1解锁,
Thread2直接把这个没有初始化完的引用拿去使用了,
就可能出现问题了!**
>###所以千万注意,使用单例模式的时候<br>一定要为单例加上`volatile`关键字!
##有条件地共享可变资源
#####保证可见性的方法
- **使用final关键字**
- **使用volatile关键字**
- **加锁,锁释放时会强制将缓存刷新到主内存**
不过加锁要注意,
`加锁只是 对另外跟你这个线程 同样使用一个锁 的那些线程,`
才能保证可见性,
如果某个线程没有加锁,它就不一定能够看到了;<br>
加了锁的,
锁释放时会强制将缓存刷新到主内存,
**为什么刚说,`其他线程加锁 才能看到 本线程 访问的主内存的对应值,`
因为资源只有加锁,
才会去主内存刷新,
才会跟其他 同样对本资源 加了锁的线程 保持同步!
不对共享资源加锁的线程 可能拿着 自己运行内存的数据副本 就去读、写、运算、更新操作了;
如此便可能造成文首所说的,脏读脏写等线程不安全的情况!**
>>>
#####保证原子性
- **加锁,保证操作的互斥性,
实现执行控制,
加锁的代码会实现原子性;**
- **使用`CAS`指令(`Unsafe.compareAndSwapInt`)
不过`Unsafe`不是公开的,
需要用到反射才能用得到它;**
- **使用原子数值类型(如`AtomicInteger`)**
- **使用原子属性更新器(`AtomicReferenceFieldUpdater`)**
**经典案例,`a++`,
++操作符不是原子性的,
任何编程语言在进行a++操作的时候,
都会先把值从a中读出来,给到一个临时变量如tmp中,
tmp加一,
之后再把tmp写回到a中,
全程经过了三步操作,不是一个不可拆分的运算单元,
即,非原子性!**
>>>**如下图,两个线程同时进行a++,
因为a++非原子性操作,
由此可能造成脏读脏写:**

<br><br><br>
--------
- 参考自[慕课网_大厂资深面试官 带你破解Android高级面试](https://coding.imooc.com/learn/list/317.html)