[心得笔记]多线程之间的内存可见性问题


每个线程都有自己的缓存块, 会将主存中的变量缓存到各自的缓冲区中, 每次线程的读取和写入的都是自己缓存区变量, 所以在改变缓冲区变量的时候还有一个过程就是同步主存相应变量的过程, 但是这个过程要看线程是否有多余的时间去同步
使用 volatile可以解决线程内存可见性的问题, 但是它只确保了该对象的可见性, 不能保证一个过程的原子性

下面有个例子: 


一般方法存在的问题
public static void func1() throws InterruptedException {
Person person = new Person();
person.setName("zhazha");
List<Thread> threads = new ArrayList<>();

for (int i = 0; i < 10; i++) {
threads.add(new Thread(() -> {
System.out.println("threadName = " + Thread.currentThread().getName() + " personName = " + person.getName());
person.setName(Thread.currentThread().getName());
}, person.getName() + i));
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
}
打印出来的值是: 
threadName = zhazha6    personName = zhazha
threadName = zhazha2    personName = zhazha
threadName = zhazha9    personName = zhazha
threadName = zhazha8    personName = zhazha
threadName = zhazha5    personName = zhazha
threadName = zhazha3    personName = zhazha
threadName = zhazha0    personName = zhazha
threadName = zhazha4    personName = zhazha
threadName = zhazha1    personName = zhazha
threadName = zhazha7    personName = zhazha
发现问题:
  • 为什么func1会出现打印相同的情况呢?(注意是 func1 不是上面代码cas代码的func2)
for (int i = 0; i < 10; i++) {
threads.add(new Thread(() -> {
System.out.println("threadName = " + Thread.currentThread().getName() + " personName = " + person.getName());
person.setName(Thread.currentThread().getName());
}, person.getName() + i));
}
当时猜测原因很多, 其中一个方式就是在 getName 和 setName 方法上面加上sync 同步代码块方式
public synchronized String getName() {
return name;
}

public synchronized void setName(String name) {
this.name = name;
}
发现问题还是没解决
然后又增加了 for 循环的次数为 1000, 想方法问题的情况
for (int i = 0; i < 1000; i++) {
threads.add(new Thread(() -> {
System.out.println("threadName = " + Thread.currentThread().getName() + " personName = " + person.getName());
person.setName(Thread.currentThread().getName());
}, person.getName() + i));
}
终于发现问题了
threadName = zhazha487    personName = zhazha
threadName = zhazha998    personName = zhazha
threadName = zhazha486    personName = zhazha
threadName = zhazha357    personName = zhazha666
threadName = zhazha349    personName = zhazha888
...
问题就是线程 t 和 线程 t1 在执行
System.out.println("threadName = " + Thread.currentThread().getName() + "	personName = " + person.getName());
这句话的时候, t 执行了这段代码后失去了 cpu时间片 切换到了 t1 也去执行这句话, 所以打印了相同的 "zhazha" 之后总有线程执行的比较快, 设置了Name
person.setName(Thread.currentThread().getName());
此时有个前提就是:
public volatile String name;
这个name字段对多线程是可见的, 不存在编译优化的(volatile)
有了这个前提其他线程才能发现这个线程改变了 name , 否则都从线程自己的内存中读取自己的 name (每个线程都有自己的缓存)
但是有些线程连打印都没开始打印, 直到这些线程开始打印的时候, 发现主存中的name已经改变, 所以打印了 "zhazha666", "zhazha888"

  • 为什么加了 volatile 还是存在上面这个问题呢? 
即使name加上了 volatile , 但是它只能监控这个字段的值的变化, 也就是简单的读和写是原子性的, 但是不能保证 读和写 一起执行时的原子性, 所以就会出现很多线程都在打印, 但是没有线程在执行 setName , 所以打印了很多 "zhazha", 如果这个时候只要有一个线程执行了 setName 那么后面的打印就是那个线程设置的值了

同理在 get 和 set 上面加上 sync 关键字为啥还是不行的原因显而易见了, 所有线程执行完 get 后全部失去时间片, 那么全都得打印 "zhazha"

  • 那么怎么解决呢? 
我们都知道, 多线程在执行一行非常非常简单代码的时候不存在多线程问题, 
比如多个线程执行 这一行代码

person.name
这个够简单了吧, 这个绝对不存在问题
当然不是这么复杂的代码
i++;
上面这行代码就太复杂了

那么解决方案就是把多行代码变成上面那行代码就行了, 也就是线程觉得这一块代码是一行代码的时候, 让线程觉得这块代码就是那么简单的一行代码就行了

所以解决方案就有:
上各种锁 产生 代码块 了

线程在代码块里面执行, 是原子性的, 因为只有一句话, 所以如果一个线程正在执行这个代码块, 那么, 这个线程独占了这个代码块, 其他线程想执行它, 没门

而在这个代码块里面不会出现有序性和可见性的问题(当然有序性不是指令重排序哦)

那么现在又有一个新的解决方案就是 CAS 通俗点就叫, 对比和设置 
它有三个变量需要注意
线程中的值(希望值或者叫预估值, 工作内存中的值)
主存中的值(实际值, 在代码中无法体现)
新的值
他有两个步骤需要注意
对比        -----    对比 希望值 和 实际值
交换        -----    如果上面的对比相同, 则交换, 如果不同则不交换
使用CAS的方法: 
public static void func2() throws InterruptedException {
    Person person = new Person();
    person.setName("zhazha");

    AtomicReferenceFieldUpdater<Person, String> atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Person.class, String.class, "name");

    List<Thread> threads = new ArrayList<>();

    for (int i = 0; i < 10; i++) {
        int finalI = i;
        threads.add(new Thread(() -> {
            System.out.println("threadName = " + Thread.currentThread().getName() + " personName = " + atomicReferenceFieldUpdater.getAndSet(person, Thread.currentThread().getName()));
        }, person.getName() + finalI));
    }

    for (Thread thread : threads) {
        thread.start();
    }
    for (Thread thread : threads) {
        thread.join();
    }
}

打印了: 
threadName = zhazha0    personName = zhazha
threadName = zhazha2    personName = zhazha0
threadName = zhazha8    personName = zhazha2
threadName = zhazha6    personName = zhazha5
threadName = zhazha1    personName = zhazha9
threadName = zhazha9    personName = zhazha6
threadName = zhazha5    personName = zhazha8
threadName = zhazha3    personName = zhazha7
threadName = zhazha7    personName = zhazha4
threadName = zhazha4    personName = zhazha1

cas算法中它在不断的判断是否能够跳出那个循环, 只要是跳出循环的都是正确的
所以就会出现上面的打印
原因就在这段代码
public V getAndSet(T obj, V newValue) {
V prev;
do {
prev = get(obj);
} while (!compareAndSet(obj, prev, newValue));
return prev;
}
线程 t 设置值发现失败, 返回继续在 while 中执行循环, 再次获取 prev 发现还是和主存的值不同, 还是在这个循环里面执行 ... 持续 N 次, 最终把自己的 newValue 设置进了主存, 返回 prev, 这个时候这个 prev 会出现不同循环的打印
threadName = zhazha6    personName = zhazha5
threadName = zhazha1    personName = zhazha9
明明应该在 zhazha5 打印完毕后再打印的 zhazha6 (有前提的, 假设线程按循序启动)
但是却打印了 zhazha9 就是因为本来能打印的 zhazha6 但是就是设置 newValue 失效了, 失去了打印 zhazha6 的资格, 错过了就是一辈子

好了跳出扩展现在举个例子
int i = 0;
i++;
众所周知这是线程不安全的, 那么 CAS 让其安全的方法有啥? 
1) 先读取 i 的值为 N
2) 让其和主存中的 i 进行比较
3) 不相等, 设置失败, 继续读取再次循环, 相等设置成功, 跳出循环





原文地址:https://www.cnblogs.com/bangiao/p/12557111.html