高并发下常见问题

并发中变量可见性问题

造成线程安全, 变量可见性问题的原因

volatile 关键字的用途, 使用场景

什么是并发中的变量可见性问题

  • 问题 1. 变量分为哪几类?

    • 全局变量
    • 局部变量
    • 属性 (静态的, 非静态的)
    • 本地变量
    • 参数
  • 问题 2, 如何在多个线程间共享数据?

    • 用全局变量: 静态变量, 或共享对象
  • 问题 3, 一个变量在线程 1 中被改变值了, 在线程 2 中能看到该变量的最新值吗?

什么是并发中的变量可见性问题

public class VisibilityDemo {

    // 状态标识
    //private static volatile boolean flag = true;

    private static boolean flag = true;

    public static void main(String [] args){

        new Thread(new Runnable() {
            @Override public void run() {
                int i= 0;
                while (VisibilityDemo.flag){
// synchronized (this){
// i++;
// }
                   // i++;
                }
                System.out.println(i);
            }
        }).start();


        try{
            TimeUnit.SECONDS.sleep(2);
        }catch(InterruptedException e){
            e.printStackTrace();
        }


        VisibilityDemo.flag= false;
        System.out.println("设置为 false 了");
    }

}
  • 并发的线程能不能看到变量的最新值, 这就是并发中的变量可见性问题

    • 为什么会不可见?
    • 怎么才能可见?
怎么才能可见?
  • 方式一: synchronized

  • 方式二: volatile

为什么可见了呢?

JAVA 内存模型

JAVA 内存模型及操作规范
  1. 共享变量必须存放在主内存中
  2. 线程有自己的工作内存, 线程只可操作自己的工作内存;
  3. 线程要操作共享变量, 需从主内存中读取到工作内存, 改变值后需从工作内存同步到主内存中。
JAVA 内存模型会带来什么问题?
请思考, 有变量 A, 多线程并发对其累加会有什么问题?如三个线程并发操作变量 A, 大家读取 A 时都读到 A=0, 都对 A+1, 再讲值同步回主内存。结果是多少?

如何解决线程安全问题?

  • 内存模型也产生了变量可见性问题。

如何让线程 2 使用 A 是看到最新值?

  • 线程 1 修改 A 后必须立马同步回主内存;
  • 线程 2 使用 A 前需要重新从主内存读取到工作内存。

疑问 1: 使用前不会重新从主内存读取到工作内存吗?

疑问 2: 修改后不会立马同步回主内存吗?

JAVA 内存模型同步交互协议, 规定了 8 种原子操作:

  1. lock(锁定): 将主内存中的变量锁定, 为一个线程所独占
  2. unclock(解锁): 将 lock 加的锁定解除, 此时其它的线程可以有机会访问此变量
  3. read(读取): 作用于主内存变量, 将主内存中的变量值读取到工作内存当中
  4. load(载入): 作用于工作内存变量, 将 read 读取的值保持到工作内存中的变量副本中
  5. use(使用): 作用于工作内存变量, 将值传递给线程的代码执行引擎
  6. assign(赋值): 作用于工作内存变量, 将执行性引擎处理返回的值重新复制给变量副本
  7. store(存储): 作用于工作内存变量, 将变量副本的值传送到主内存中
  8. write(写入): 作用于主内存变量, 将 store 传送过来的值写入到主内存的共享变量中
JAVA 内存模型 - 同步交互协议, 操作规范

如果要把一个变量从主内存复制到工作内存, 那就要按顺序执行 read 和 load 操作, 如果要把变量从工作内存同步回主内存, 就要按顺序执行 store 和 write 操作。注意,Java 内存模型只要求上述两个操作必须按顺序执行, 而没有保证是连续执行。也就是说 read 与 load 之间,sotre 与 write 之间是可插入其他指令, 如对主内存中的变量 a,b 进行访问时, 一种可能出现顺序是 read a,read b,load b,load a。除此之外,Java 内存模型还规定了在执行上述八种基本操作时必须满足如下规则:

  • 不允许 read 和 load,store 和 write 操作之一单独出现, 即不允许一个变量从主内存读取了但工作内存不接受, 或者从工作内存发起回写了但主内存不接受的情况出现。
  • 不允许一个线程丢弃它的最近的 assin 操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

synchronized 怎么做到可见性

  • Synchronized 语义规范:

    • 进入同步块前, 先清空工作内存中的共享变量, 从主内存中重新加载。
    • 解锁前必须把修改的共享变量同步回主内存
  • Synchronized 是如何做到线程安全的?

    • 锁机制保护共享资源, 只有获得锁的线程才可操作共享资源;
    • Synchronized 语义规范保证了修改共享资源后, 会同步回主内存, 就做到了线程安全

volatile 的使用场景

  • volatile 的使用范围

    • volatile 只可修饰成员变量 (静态的, 非静态的)。Why?
    • 多线程并发下, 才需要使用它。
  • volatile 典型的应用场景

    • 只有一个修改者, 多个使用者, 要求保证可见性的场景
    • 转态标识, 如示例中的介绍标识。
    • 数据定期发布,多个获取者。