线程的工作内存
读写主存
时的不同步
;
“进程安全”问题不存在,
因为进程之间内存相互独立,各自独享内存的,
一个进程被杀掉的话,其所有内存都还给物理内存了;
可能共享CPU时间片;
线程是存在于进程当中的,
同一个进程中的线程之间是可以共享内存的;可变
资源(内存)线程间共享
(关键词“可变”和“共享”)
线程间不共享的资源不用考虑线程安全了;不共享资源
共享才会产生线程安全问题,
所以尽量不共享;共享不可变资源
(volatile、final)
共享可变资源
不共享资源
,
也就没有线程安全
的问题了;
自己线程之内,不管怎么设置,都不会影响到其他线程:
ThreadLocalMap
,
添加值的时候置入键值对map.set(this,value)
,
使用的key,实际上就是this,即ThreadLocal类对象引用,
value则企图传入的值;
既然是数据结构是绑定到线程上的,
也就是说,
假设,两个访问ThreadLocal的引用 它们所处的线程 是不一样的话,
那么,它们访问ThreadLocal的set、get时 处理的值,肯定也是不一样的!
ThreadLocal对象
,作为全局变量
定义在主线程
,
为访问它(set()
)的N
个子线程
,
开启(createMap()
)N
个相互独立
的ThreadLocalMap
,
因此,每一个子线程
访问主线程
中的这个独一无二的ThreadLocal对象
的时候,
总会访问到子线程自身
对应的底层数据存储结构
ThreadLocalMap
;
故不同
的线程
,访问同
一个ThreadLocal对象
的时候,
访问的是(绑定
到不同
线程的)不同
的底层数据结构ThreadLocalMap
,
读写
的是不同的数据
;
故
实现了,
同属主线程
的一系列子线程
间的,
资源不共享
,解决的了线程安全
问题;实战案例如下:
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();
}
}
运行结果:
声明为全局静态final成员
ThreadLocal在一个主线程中有一个实例就够了,
没必要每次创建子线程都整一个出来,
并且我们set value的时候,
我们是以ThreadLocal的this为key的,
ThreadLocal这个对象的引用最好是独一的、不可更改的!
不设置final的话,还有另外的问题,
还要考虑什么时候去初始化它,还要考虑可见性,
这就还要考虑加锁了;避免存储大量对象
因,
底层数据结构、Hash冲突的解决方案和Hash计算算法,
已经做了限制;用完后及时移除对象
ThreadLocal自身没有监听机制,
如果你设置的ThreadLocal的存在周期非常的长,
那对应的线程就会一直存在,
其引用不会被回收,有内存泄漏风险首先普及一下重排序,等下涉及到
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的:
所以,各单位请注意!
final
啊,它还有一个禁止重排序
的作用, 即,禁止被final修饰的代码
的对应的指令
被重排序补充:volatile
volatile
除了能保证线程间的可见性
, 也能禁止重排序
!!
禁止重排序
的作用;
1.4以前,即便使用双重校验锁的单例模式,也是有问题的;
单例模式案例(两种加volatile的情况,正常):
如果不加volatile,就可能会出现类似重排序的问题了:
有可能重排序之后,
构造方法的调用的指令被排到了后面,
这时候程序 还没等构造方法
执行完毕
,
就把分配好内存的实例
赋值给了引用
,
这时候这个引用因为没有经过构造方法,
所以还没有被初始化,
此时Thread1解锁,
Thread2直接把这个没有初始化完的引用拿去使用了,
就可能出现问题了!
所以千万注意,使用单例模式的时候
一定要为单例加上volatile
关键字!
加锁只是 对另外跟你这个线程 同样使用一个锁 的那些线程,
才能保证可见性,
如果某个线程没有加锁,它就不一定能够看到了;
加了锁的,
锁释放时会强制将缓存刷新到主内存,
为什么刚说,其他线程加锁 才能看到 本线程 访问的主内存的对应值,
因为资源只有加锁,
才会去主内存刷新,
才会跟其他 同样对本资源 加了锁的线程 保持同步!
不对共享资源加锁的线程 可能拿着 自己运行内存的数据副本 就去读、写、运算、更新操作了;
如此便可能造成文首所说的,脏读脏写等线程不安全的情况!CAS
指令(Unsafe.compareAndSwapInt
)
不过Unsafe
不是公开的,
需要用到反射才能用得到它;AtomicInteger
)AtomicReferenceFieldUpdater
)经典案例,a++
,
++操作符不是原子性的,
任何编程语言在进行a++操作的时候,
都会先把值从a中读出来,给到一个临时变量如tmp中,
tmp加一,
之后再把tmp写回到a中,
全程经过了三步操作,不是一个不可拆分的运算单元,
即,非原子性!
如下图,两个线程同时进行a++, 因为a++非原子性操作, 由此可能造成脏读脏写: