Java:堆栈实现中的线程同步

Cod*_*one 1 java multithreading

我正在学习ThreadsJava考虑创建一个简单的堆栈来测试Thread synchronization。这是堆栈类:

public class Stack {

    private int counter;
    private String[] storage;
    
    public Stack(final int number) {
        storage = new String[number];
        counter = 0;
    }
    
    public synchronized void push(final String msg) {
        if(counter == storage.length) {
            System.out.println("Counter full");
        } else {
            storage[counter++] = msg;
        }
    }
    
    public synchronized String pop() {
        if(isEmpty()) {
            System.out.println("There is nothing to pop");
            return null;
        } else {
            String lastElement = storage[--counter];
            storage[counter] = null;
            return lastElement;
        }
    }
    
    public boolean isEmpty() {
        return counter == 0 ? true : false;
    }
    
    public synchronized void printElements() {
        for(int i=0; i < storage.length; i++) {
            System.out.println(storage[i]);
        }
        System.out.println("Number of elements "+counter);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是主要方法:

public static void main(String[] args) {
        Stack s = new Stack(10);
        
        final Thread t1 =  new Thread(() -> {
            for(int i=0; i < 2; i++) {
                s.push("Hello "+i);
            }
            for(int i=0; i < 2; i++) {
                s.push("How are you? "+i);
            }
        });
        
        final Thread t2 = new Thread(() -> {
            for(int i=0; i < 2; i++) {
                s.push("Nice to meet you "+i);
            }
            s.pop();
        });
        
        t1.start();
        t2.start();

        try {
            t2.join();
            t1.join();
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
        
        s.printElements();
    }
Run Code Online (Sandbox Code Playgroud)

使用该词synchronized是使方法一次仅由一个线程执行的基本方法。大多数时候,我收到的序列

Hello 0
Hello 1
How are you? 0
How are you? 1
Nice to meet you 0
null
null
null
null
null
Number of elements 5
Run Code Online (Sandbox Code Playgroud)

这是期望的结果。然而,也有一些情况会返回不同的序列,这就是我的问题出现的地方。使用synchronized将保证一次只有一个线程执行一个方法,即保护critical section. 但这并不意味着在用于创建 的 lambda 表达式中指定的操作序列Runnable将始终在同一流程中执行,或者?(抱歉,如果这是一个虚拟问题)

rzw*_*oot 5

是啊,为什么会这样呢?

线程同时运行,并且它们引入了许多看似随机的东西(尽管它肯定不是您应该依赖的那种随机性!如果您以此作为随机性来源生成加密货币,那么妥协可能是微不足道的例如,您用它生成的任何内容!)。

你的同步门给你的是两个不同的东西:

  • JMM 简化:它建立发生前/发生后 (HBHA) 关系。
  • 门控:您保证不会遇到任何问题,例如 2 个推送操作同时发生,因此,它们都写入 eg storage[5],并且任意一个会覆盖另一个。有时,计数器会增加 2 或仅增加 1。

每个线程都可以随时自由地创建任何对象任何字段本地克隆副本,并且可以自由地仅写入其本地克隆副本。但它没有必要——所以你不能依赖它。如果您编写的代码根据线程是否决定制作该副本而表现不同,那么您就编写了一个错误。阻止这种情况发生的方法是建立 HBHA 关系。您可以在网络上搜索完整列表,但同步是建立它们的一种简单方法。

因此,您的代码有一个错误。您的isEmpty()方法未同步,但正在访问共享状态(该counter字段由多个线程访问和更改 - 因此,实际上有任意数量的counter副本在浮动,如果没有 HBHA,您不知道您可能正在阅读哪个过时的副本)。

此外,一旦你谈论“建立顺序”,就并行化而言,你几乎完全迷失了:重点是事情同时发生,并且每次 2 个线程都必须协调,然后多线程的大部分优点消失。

请注意,在单个线程中,代码按顺序运行(或者至少,您无法观察到只能通过以其他顺序或同时运行的代码来解释的任何效果,尽管热点编译器和 CPU 将协同工作并重新排序代码如果顺序语句根本不互相影响,则(接近)并行运行它们。不过,您无法编写代码来展示这一点,这就是重点 - 如果您可以观察它,优化器将意识到这一点并且不会应用它)。

x.start()启动一个线程会在调用和线程的第一行之间创建 HBHA ,但这就是它结束的地方。所以,当你写:

x = "hello";
t1.start();
t2.start();
Run Code Online (Sandbox Code Playgroud)

这保证了两个线程都会“看到”您对该字段的写入x(通过启动线程建立的 HBHA),但您不能保证 t2 将在 t1 开始运行开始运行。也许 t2 开始得更早,JVM 可以自由地这样做。或不 - 由虚拟机决定;如果您的代码在 t1 比 t2* 更早开始时表现不同,那么您编写了一个错误。

*)这样想确实是错误的。他们同时开始。但这与您观察到的内容有关:根据 JVM 规范,在观察t1 预计在相同时间内执行的操作之前,您观察t2正在执行的任何操作对于 JVM 来说是合法的。t2 是否真的“更早”运行则更加模糊。也许不会,但例如,t2 没有创建本地缓存副本,而 t1 则创建了本地缓存副本。