目录
一、线程安全的源头问题
二、JMM(Java Memory Model)java内存模型提供的方案
一、线程安全的源头问题
1、可见性
cpu增加了高速缓存,均衡与内存的速度差异,这使得多个线程拿到共享资源后,分别在不同的cpu上执行,每个cpu都拥有自己的高速缓存,每个线程在不同的高速缓存里,互相之间并不可见,如:俩个线程同时拿到i=0,第一个线程在自己的缓存里写i=1,第二个线程并不知道
2、原子性
初始i=0 i++实际上分3步操作,1.获取值2.加一 3.写回内存
这使得第一个线程没有执行完+1的完整操作,就有可能切换的第二个线程操作 此时因为线程1并没有将+1写到内存,则第二个线程读取的仍是0,则最终得到的结果可能是1或2(不可能超过2)
3、有序性:编译程序优化指令的执行顺序,使得能够更加合理的利用缓存
a=b;x=1 有可能真正的执行顺序为x=1先执行
二、JMM(Java Memory Model)java内存模型提供的方案
1、Synchronize可保证原子性,可见性,有序性
三种应用方式
Java中每个对象都可以作为锁,这是synchronized实现同步的基础
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法快,锁是Synchronized括号里配置的对象
2、Volatitle 可保证可见性,有序性,无法保证原子性
对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
- 禁止进行指令重排序。(实现有序性)
- volatile 不能保证原子性。x++ 这种操作不能保证原子性。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由
读取-修改-写入
操作序列组成的组合操作,必须以原子方式执行,而volatile
不能提供必须的原子特性。实现正确的操作需要使x的值在操作期间保持不变(如果一个线程修改x加了1但还写入,另一个拿到后还是原来没加1之前的),而volatile
变量无法实现这点。
保证可见性:
public class ThreadTest {
public static volatile boolean stop = false;
//加上volatile后保证可见性,子线程可以立即读到主线程更改后的值,之后结束,
//不加volatile则有可能线程一直运行着
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while (!stop) {
i++;
}
}
});
thread.start();
Thread.sleep(1000);
stop = true;
}
}
在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令
- lock 前缀的指令在多核处理器下会引发两件事情。
- 1)将当前处理器缓存行的数据写回到系统内存。
- 2)写回内存的操作会使在其他 CPU 里缓存了该内存地址的额数据无效。
JMM提供内存屏障禁止重排序:
为了性能优化,JMM 在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序。JMM 提供了内存屏障阻止这种重排序。volatile 写是在前面和后面分别插入内存屏障,而 volatile 读操作是在后面插入两个内存屏障。
内存屏障 | 说明 |
---|---|
StoreStore 屏障 | 禁止上面的普通写和下面的 volatile 写重排序。 |
StoreLoad 屏障 | 防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。 |
LoadLoad 屏障 | 禁止下面所有的普通读操作和上面的 volatile 读重排序。 |
LoadStore 屏障 | 禁止下面所有的普通写操作和上面的 volatile 读重排序。 |
3、Final关键字
对于final域,编译器和处理器要最受俩个重排序规则
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这俩个操作不能重排序
- 初次读一个包含final域的对象引用,与随后初次读这个final域,这俩个操作不能重排序
4、Atomic保证原子性
多线程下将属性设置为atomic可以保证读取数据的一致性。因为他将保证数据只能被一个线程占用,也就是说一个线程对属性进行写操作时,会使用自旋锁锁住该属性。不允许其他的线程对其进行读取操作了
public class ThreadTest {
public static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(atomicInteger.incrementAndGet());//最后结果一定是1000
}
}).start();
}
}
}
5、ThreadLocal线程局部变量
public class ThreadTest {
public static ThreadLocal<Integer> threadLocal = 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 < 5; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
Integer integer = threadLocal.get();
System.out.println(integer + 5);//运行结果永远是5,r如果公用变量是普通变量肯定会有线程安全问题
}
}, "Thread" + i);
}
for (Thread thread : threads) {
thread.start();
}
}
}
ThreadLocal源码简单分析
每个Thread都维护一个只属于本线程的ThreadLocalMap,当在一个线程中使用ThreadlLocal类型变量则这个变量就被纳入
当前线程ThreadLocalMap管理,之后对此变量的操作,都在属于本线程的ThreadLocalMap里面
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//返回当前线程的ThreadLocalMap
if (map != null)//如果已经被初始化过,则将threadlocal对象作为key,value做为值放入ThreadLocalMap
map.set(this, value);
else//未被初始化过,则初始化ThreadLocalMap
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];//创建一个初始容量为16的Entry数组用来存放所有threadlocal类型变量
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//通过hash值确定位置存放
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
//Thread类里有个全局变量ThreadLocalMap用来存放当前线程的所有ThreadLocal类型变量
ThreadLocal.ThreadLocalMap threadLocals = null;
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {//第二个threadlocal类型的变零
ThreadLocal<?> k = e.get();
if (k == key) {//key相同则覆盖
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);//不存在hash冲突直接存放
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}