有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java多线程中的线程安全

我找到了关于线程安全性的代码,但没有给出示例的人的任何解释。我想理解为什么如果我不在“count”之前设置“synchronized”变量,那么计数值将是非原子的(始终=200是期望的结果)。谢谢

    public class Example {

     private static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    for (int i = 0; i < 100; i++) {
                       //add synchronized
                        synchronized (Example.class){
                        count++;
                    }
                }
            }).start();
        }

        try{
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }
      }

共 (3) 个答案

  1. # 1 楼答案

    ++不是原子的

    count++操作不是原子操作。这意味着这不是一次单独的行动。{}实际上是三个操作:加载、增量、存储

    首先,变量中存储的值被加载(复制)到CPU核心的寄存器中

    第二,核心寄存器中的值是递增的

    第三也是最后一点,新的递增值从核心寄存器写入(复制)到内存中变量的内容。然后,内核的寄存器可以自由地为其他工作分配其他值

    两个或多个线程完全有可能读取变量的相同值,例如42。然后,这些线程中的每一个都将继续将该值增加到相同的新值43。然后,它们将各自向同一个变量写回43,无意中反复存储43

    添加synchronized可以消除这种竞争条件。当第一个线程获得锁时,第二个和第三个线程必须等待。因此,第一个线程保证能够单独读取、递增和写入新值,从42到43。完成后,该方法退出,从而释放锁。争夺锁的第二个线程获得了许可,获得了锁,并且能够在不受干扰的情况下读取、递增和写入新值44。等等,线程安全

    另一个问题是可见性

    然而,这个代码仍然被破坏

    这段代码存在可见性问题,不同的线程可能会读取缓存中保存的过时值。但这是另一个话题。搜索以了解有关volatile关键字、AtomicInteger类和Java内存模型的更多信息

  2. # 2 楼答案

    I would like to understand why if I don't set the "synchronized" variable before "count" that the count value will be non-atomic.

    简单的回答是:因为JLS这么说

    如果使用synchronized(或volatile或类似的东西),那么Java语言规范(JLS)不能保证主线程将看到子线程写入count的值

    这在JLS的Java内存模型部分有详细说明。但是规格非常技术化

    简化的版本是,如果在连接写入和读取的(HB)关系之前没有发生,则不能保证读取变量时看到之前写入的值。然后有一系列规则说明何时存在HB关系。其中一条规则是,释放互斥锁的线程与获取互斥锁的不同线程之间存在HB

    另一种直观(但不完整且技术上不准确)的解释是count的最新值可能会缓存在寄存器或芯片组的内存缓存中。synchronized构造将值刷新为内存

    这是一个不准确的解释,因为JLS没有提到任何关于寄存器、缓存等的内容。相反,内存可见性保证了JLS指定的通常由Java编译器实现,该编译器插入指令,将寄存器写入内存、刷新缓存、或硬件平台所需的任何内容


    另一点需要注意的是,这并不是关于count++是原子还是不是1。它是关于对count的更改的结果是否对其他线程可见

    1-它不是原子的,但是对于原子操作,你会得到与简单赋值相同的效果

  3. # 3 楼答案

    让我们通过一个华尔街的例子回到基本面

    比方说,你(打T1)和你的朋友(打T2)决定在华尔街的一家咖啡馆见面。你们俩是同时开始的,比如说从华尔街的南端开始(尽管你们不是一起走的)。你们在人行道的一边醒来,你们的朋友在华尔街人行道的另一边走,你们都往北走(方向相同)

    现在,假设你来到一家咖啡馆前,你以为这就是你和你朋友决定见面的咖啡馆,所以你走进咖啡馆,点了一杯冷咖啡,开始一边等一边啜饮

    但是,在路的另一边,类似的事情发生了,你的朋友遇到一家咖啡店,点了一块热巧克力,正在等你

    过了一会儿,你们都决定另一个人不来了,放弃了会面的计划

    你们都错过了目的地和时间。为什么会这样?不用说,但是,因为你没有决定确切的地点

    密码

    synchronized(Example.class){
     counter++;
    }
    

    解决你和你朋友刚刚遇到的问题

    从技术上讲,操作计数器++实际上分三步进行

    Step 1: Read the value of counter (lets say 1)
    Step 2: Add 1 in to the value of counter variable.
    Step 3: Write the value of the variable counter back to memory.
    

    如果两个线程同时处理计数器变量,计数器的最终值将是不确定的。例如,Thread1可以将计数器的值读取为1,同时thread2可以将变量的值读取为1。两个线程最后都将计数器的值增加到2。这被称为竞赛条件

    为了避免这个问题,操作计数器++必须是原子的。要使其原子化,需要同步线程的执行。每个线程都应该以有组织的方式修改计数器

    我建议您在实践中阅读Java并发这本书,每个开发人员都应该阅读这本书