ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

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

2020-03-24 10:02:05  阅读:277  来源: 互联网

标签:Thread personName getName 打印 person 线程 内存 多线程 心得



每个线程都有自己的缓存块, 会将主存中的变量缓存到各自的缓冲区中, 每次线程的读取和写入的都是自己缓存区变量, 所以在改变缓冲区变量的时候还有一个过程就是同步主存相应变量的过程, 但是这个过程要看线程是否有多余的时间去同步使用 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() + "\tpersonName = " + 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 = zhazhathreadName = zhazha2    personName = zhazhathreadName = zhazha9    personName = zhazhathreadName = zhazha8    personName = zhazhathreadName = zhazha5    personName = zhazhathreadName = zhazha3    personName = zhazhathreadName = zhazha0    personName = zhazhathreadName = zhazha4    personName = zhazhathreadName = zhazha1    personName = zhazhathreadName = 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() + "\tpersonName = " + 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() + "\tpersonName = " + person.getName());
person.setName(Thread.currentThread().getName());
}, person.getName() + i));
}
终于发现问题了threadName = zhazha487    personName = zhazhathreadName = zhazha998    personName = zhazhathreadName = zhazha486    personName = zhazhathreadName = zhazha357    personName = zhazha666threadName = zhazha349    personName = zhazha888...问题就是线程 t 和 线程 t1 在执行
System.out.println("threadName = " + Thread.currentThread().getName() + "\tpersonName = " + 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() + "\tpersonName = " + atomicReferenceFieldUpdater.getAndSet(person, Thread.currentThread().getName()));
        }, person.getName() + finalI));
    }

    for (Thread thread : threads) {
        thread.start();
    }
    for (Thread thread : threads) {
        thread.join();
    }
}
打印了: threadName = zhazha0    personName = zhazhathreadName = zhazha2    personName = zhazha0threadName = zhazha8    personName = zhazha2threadName = zhazha6    personName = zhazha5threadName = zhazha1    personName = zhazha9threadName = zhazha9    personName = zhazha6threadName = zhazha5    personName = zhazha8threadName = zhazha3    personName = zhazha7threadName = zhazha7    personName = zhazha4threadName = 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 = zhazha5threadName = zhazha1    personName = zhazha9明明应该在 zhazha5 打印完毕后再打印的 zhazha6 (有前提的, 假设线程按循序启动)但是却打印了 zhazha9 就是因为本来能打印的 zhazha6 但是就是设置 newValue 失效了, 失去了打印 zhazha6 的资格, 错过了就是一辈子
好了跳出扩展现在举个例子int i = 0;i++;众所周知这是线程不安全的, 那么 CAS 让其安全的方法有啥? 1) 先读取 i 的值为 N2) 让其和主存中的 i 进行比较3) 不相等, 设置失败, 继续读取再次循环, 相等设置成功, 跳出循环


来自为知笔记(Wiz)





标签:Thread,personName,getName,打印,person,线程,内存,多线程,心得
来源: https://www.cnblogs.com/bangiao/p/12557111.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有