Java中的notify()可以唤醒自己的线程吗?

dced5bon  于 2023-01-29  发布在  Java
关注(0)|答案(2)|浏览(276)

我正在学习多线程,写了一个简单的程序来模拟教室,主要有三个类:白板,学生和教师。
WhiteBoard是一个单例对象,它保存了一个包含教师所写文本的字符串,以及从该字符串写入和读取的方法。教师对象将在WhiteBoard上写入,并等待所有学生读取。当所有学生都完成后,他们将通知教师再次写入。当他写入“end”时,程序结束。
当我测试代码时,我注意到有些学生不止一次地阅读文本。这是一个screenshot with the output。正如您所看到的,巴勃罗连续两次阅读文本。这怎么可能呢?我认为当我们调用方法 notify时(),则在监视器队列中等待的线程之一将被唤醒。这是否包括调用线程本身?如果不包括,为什么会发生这种情况以及如何正确地解决这个问题?
感谢您的帮助!下面是代码:

class WhiteBoard {
    
    // Text written in the whiteboard
    private String texto;
    
    // Total number of students
    private int noOfStudents = 0;
    
    // Counter that keep control of which thread is accessing the whiteboard
    private int counter = 0;

    // Whiteboard instance
    private static WhiteBoard wbInstance;
    // Private constructor
    private WhiteBoard() { }
    
    // Returns the instance of the whiteboard (i'm using a singleton pattern)
    public static WhiteBoard getInstance() {
        if(wbInstance == null)
            wbInstance = new WhiteBoard();
        
        return wbInstance;
    }

    // This method is called when a new student is created
    public void oneMoreStudent() {
        noOfStudents++;
    }

    public synchronized void write(String str) {
        // If it's not the teacher's turn to write, his thread waits
        while(counter != 0) {
            try { 
                wait(); 
            } 
            catch(InterruptedException e) {
                e.printStackTrace(); 
            }
        }
        
        // Write's the text
        texto = str;

        // Increases the counter 
        counter++;

        // Notify students threads
        notifyAll();
    }

    public synchronized String read() {
        // If it's not the students' turn to read, their threads wait
        while(counter == 0) {
            try { 
                wait(); 
            } 
            catch(InterruptedException e) {
                e.printStackTrace(); 
            }
        }

        // Saves current text
        String temp = texto;

        if(counter == noOfStudents)
        {
            // If everyone read, reset counter and notify the teacher

            counter = 0;
            notify();
        }
        else
            // If not, increase counter
            counter++;

        // Return the text
        return temp;
    }
}

// Student class
class Student implements Runnable {
    // Student name
    private String name;

    // Whiteboard instance
    private WhiteBoard wb;

    // Constructor
    public Student(String nome)
    {
        this.name = nome;
        wb = WhiteBoard.getInstance();
        wb.oneMoreStudent();
    }

    @Override
    public void run() {
        
        String txtCopiado;

        while (true) {
            // Reads the text
            txtCopiado = wb.read();

            // If the text is "end", breaks the while loop
            if(txtCopiado.equals("end"))
                break;

            // Print student name and the text he read
            System.out.println(name + ": " + txtCopiado);
        }
    }
}

// Teacher class
class Teacher implements Runnable {

    // This is the array of texts he will write
    private String[] textos = {"Ola amigos", "Java Programming", "Jacare", "end"};
    
    // Whiteboard instance
    private WhiteBoard wb;

    // Constructor 
    public Teacher() {
        wb = WhiteBoard.getInstance();
    }

    @Override
    public void run() {
        
        // Write each text on the whiteboard
        for(String str : textos) {
            wb.write(str);
            
            // Print what the teacher wrote
            System.out.println("Teacher: " + str);
        }
    }
}

// Main class
public class Classroom {

    public static void main(String[] args) {
        
        Thread prof = new Thread(new Teacher());

        Thread alun1 = new Thread(new Student("1. Pablo"));
        Thread alun2 = new Thread(new Student("2. Kiki"));
        Thread alun3 = new Thread(new Student("3. Lucas"));

        prof.start();

        alun1.start();
        alun2.start();
        alun3.start();
    }
}
dffbzjpn

dffbzjpn1#

这段代码有很多小错误;错误的方式可能永远无法重现,因为Java内存模型(在处理使用这些过时/不明智的策略编写的多线程代码时相当相关)被“可能”填充到了边缘-JVM可能做一件事,也可能**做另一件事。(也许在你的硬件上,在这一天,在你的音乐播放器上有这首歌,它总是走一条路。明天它可能会走不同的路)。
你可能会想:什么鬼,这听起来彻头彻尾的愚蠢,为什么java会被设计成像一个邪恶的猴子的报酬?这个答案不可能是正确的!
遗憾的是,它真的是这样工作的。问题是,试图给予你一吨的保证(如:JVM运行的所有东西总是表现得像是按照某种顺序运行的),那么任何现实的Java应用程序都会运行得非常慢--考虑到现代CPU(“现代”--目前已超过20年)具有多个内核,每个内核都具有多个管道,并且CPU根本无法访问主内存(它们只能访问片上缓存,他们可以要求内存控制器将一个缓存页面交换为另一个缓存页面,但他们不能要求内存控制器读取或写入单个值,CPU几十年来一直无法做到这一点)。由于JVM应该在许多平台上运行,因此实际上只有两种选择:

  1. java规范给出了各种各样的保证,因此JVM很慢。
  2. A规格(JMM -Java内存模型)详细说明了JVM享有的许多“权利”,它可以做各种各样奇怪的事情,因此存在许多JVM实现,它们做着完全不同的事情,因为它们都需要在本地架构和操作系统上"快速工作“。作为一个程序员,你的工作就是保持在这些保证之内。知道线外着色是一个问题,在你的硬件上你可能永远不能测试,哎哟。
    正如您可能猜测的那样,#2是java的实际工作方式。
    这更加突出了为什么你不应该这样做。使用wait()notify()需要非常高水平的专业知识 * 因为 * 它实际上是不可能测试的。获得这种专业知识非常困难:通常,你通过尝试来学习最好的东西,但在这里,尝试只会误导你:你最后写了一些看起来工作可靠的代码,但很久以后才意识到它不工作。只是 * 在你写它的时候 *,不管什么原因,“JVM可能做X或Y”的方面总是走同一条路。或者,在某个CPU上总是走一条路,但在另一个CPU上并不总是这样。
    我举几个例子:

noOfStudents违反了HB/HA(我稍后会解释HB/HA是什么)。

student 1线程启动后没有锁(例如,Studentrun()方法没有同步),然后在Whiteboard示例上调用oneMoreStudent(),然后运行noOfStudents++noOfStudents是一个普通的非易失性字段。

这是JMM违规。JMM规定允许JVM实现对语句重新排序(以任何顺序运行填充),并在任何时间本地缓存任何对象的任何字段(例如,为每个线程提供它自己的noOfStudents字段的小副本),或不。这意味着每个学生线程可以递增它本地缓存的noOfStudents副本,(因此,每个学生线程都会看到noOfStudents为1,即使在所有3个线程都运行之后,从其他证据可以清楚地看出,它们早就通过了oneMoreStudent()调用-请记住,如果您没有建立HBHA,JVM可以自由地重新排序或显示为重新排序)。这会扰乱它的循环,并解释你的代码不工作。这不一定是正确的解释或唯一的方法,只是,这是少数问题之一,可能导致你所看到的,取决于你的CPU,你的操作系统,以及月相。

HB/羟基磷灰石

正如我所说的,JVM可以自由地对语句进行重新排序,并创建具有未定义同步行为的本地缓存(任何本地缓存都可以在任何时候覆盖或不覆盖任何其他缓存,没有任何保证)--除非涉及HB/HA。
HB/HA是一组规则,JMM保证"happens-before“/”happens-after“关系存在。一个操作(例如语句)是相对于另一个操作(另一个语句)的”HB“。
在这种情况下,JVM保证除了计时之外(事情需要多长时间),则不可能从HA行观察任何状态,例如它在HB行运行 * 之前 *。换句话说,“HappensAfter”行保证在HA要到的“HappensBefore”行之后发生,除非您不可能观察到这种情况,除非使用非常狡猾的技巧,例如使用计时,在这种情况下JVM可以随意忽略HB/HA规则。

获得HB/HA关系的一个普通方法是在同一线程中执行连续语句:给定void foo() { a(); b(); }b()相对于a()是HA,原因显而易见。
另一种方式是线程本身:thread.start()是相对于thread结束启动的Runnable的run()方法中的第一行的HB。类似地,thread.yield()的返回是相对于thread运行的最后语句的HA。
建立HB/HA的常用方法是synchronized:如果2个线程都在执行锁定同一监视器的代码(synchronized(x),其中x在两个线程中引用完全相同的对象),一个线程“赢”并首先运行-所有同步块都以某种未定义的顺序“排序”。无论是什么顺序,进入和退出都是HB/HA,正如您所期望的那样-如果一个线程“先运行”,那么它脱离synchronized相对于其他线程进入它是HA。
因为HB/HA保证您不会看到事情乱序运行,您解决了缓存问题-JMM保证不再是每个线程都可以对缓存值进行操作的情况。
因此,将oneMoreStudent设为synchronized可以解决这个特定问题:一根线(随机地,或多或少地,不保证如果你连续启动3个线程,则第一个启动的线程也首先开始运行)首先赢得并传递synchronized {},更新该值,退出synchronized块,并且在这样做时HB相对于“赢得”并进入该块的下一个线程,并且因此,无法观察到第一个线程退出 * 之前 * 的状态:noOfStudents已经递增是状态的改变,因此,第二个线程保证看到noOfStudents是1,并将其递增到2。出于同样的原因,第三个学生线程最终看到2并递增到3,教师线程保证看到它是'3'。当然,除非。

违规:取决于起始顺序

如果教师线程在所有3个学生线程初始化之前被导入(例如递增noOfStudents),然后所有的heck打破松散,因为教师线程将不会等待必要的学生读取量,因此将写更多的所有学生设法读取数据之前。事实上,您启动教师线程第四是无关紧要的,JVM并不保证线程实际上按顺序运行。2这是愚蠢的,线程的目的是尽可能同时执行任务。3因此,所有4个线程同时到达一个需要获取白板对象锁的点(synchronized关键字可以做到这一点),因此一个线程随机获胜。与JMM的惯例一样,您无法通过测试来“证明”这一点。在您的硬件/Java/月相设置中,您很可能确实观察到学生线程首先运行,* 然后 * 教师线程。但这并不能保证,任何不这样做的JVM都不会坏。
让事情变得更加复杂的是,System.out是一个非常大的铁打电话(需要大量时间),并获得各种锁,这意味着试图通过sysout来了解发生了什么,严重改变了应用的运行方式,所以你也不能这样做。并不是说见证任何事情都是有用的-当涉及到JMM时,JVM不必保持一致。

单例冲突

你的getInstance()方法本身就是一个违规--你的代码检查whiteboard字段是否为null,如果是,就创建一个新的白板,这在表面上已经完全错了:如果两个线程同时调用getInstance(),它们都会检查“is it null?”,然后都会说:是的,它们都创建了一个白板示例,都将其赋值给一个静态变量whiteboard,该变量可能是本地缓存,也可能不是本地缓存。现在您有两个白板示例,显然其余代码将完全失败。
更糟糕的是,由于重新排序规则,试图绕过这一点比你想象的要复杂得多。在网上搜索“单例双重检查锁定”以获得更多信息,但请注意,90%的教程都是完全错误的。
解决这个问题的方法是认识到**getInstance()是一个愚蠢的想法**。Java在启动时并不加载所有类,它加载主类,然后开始运行它。如果在运行它的过程中,出现了一些其他尚未卸载的类,则该类将按需加载。然而,一旦类被加载,它将保持加载状态(除非你正在从事类装入器的诡计,这超出了这个答案的范围)。Java本身因此需要进行高级单例锁定,因为JMM保证一个类最多只能加载一次。这意味着这是进行单例操作的唯一正确方法:

class Whiteboard {
  private static final INSTANCE = new Whiteboard();

  public static Whiteboard getInstance() {
    return INSTANCE;
  }
}

这是因为JMM保证类只被加载一次,从而保证INSTANCE只被赋值一次。这不是“不必要的浪费”,因为类根本不会被加载,除非你真的在某个地方提到Whiteboard。在这种情况下...你会需要那个示例。Java's在锁定行为中进行了烘焙,以确保类只加载一次,这是java中最有效的锁定构造。
极不可能的百万分之一的几率下,你可能会不时地引用一个类,而不是获取单例,并且你真的希望它延迟加载,这是正确的方法:

class Whiteboard {
  private static class WhiteboardInstanceHolder {
    static final Whiteboard INSTANCE = new Whiteboard();
  }

  public static Whiteboard getInstance() {
    return WhiteboardInstanceHolder.INSTANCE;
  }
}

这是因为,正如我所说的,java在真正需要的时候才会加载类,这里直到有人调用getInstance()才需要它。这个代码段要复杂得多,唯一的优点是你可以将Whiteboard作为一个类型来提及,而 * 不用 * 调用.getInstance(),从而避免了加载。这通常是不相关的。所以您不应该为这个通常不必要的复杂示例而烦恼。
结束语-你真正应该做的是什么
以上任何一个都可以解释你所看到的。我不会对你写的代码感到惊讶。如果你修复了所有3个,代码工作可靠,你真的不知道你是否真的修复了所有的错误,或者仍然存在的错误只是一个性质,不会发生在你的硬件/操作系统组合。
因此,这是火箭科学。你不应该这样做。有不同的编程模型,是非常上级的,更容易推理:
1.大范围并发--通过一个专门为它设计的媒介发送所有线程间的通信,比如一个数据库(设置一个事务,瞧),或者一个消息队列。
1.使用java.util.concurrent包中的一些东西。在这里,CyclicBarrier可以工作,或者可能是它拥有的许多集合中的一个。

qmb5sa22

qmb5sa222#

下面是对该主题感兴趣的人的更新:)
我使用Executor框架修复了这个问题,我从@rzwitserloot的回答中得到了一些提示,并使用线程池和CompletableFuture类编写了一个更干净、更稳定的代码。
Student类的对象不再依赖于WhiteBoard类中的计数器,相反,它们只是读取文本并将其打印到终端上--如果它不是“end”字符串的话。
Teacher类现在有了一个内部计数器,可以跟踪它必须在白板上写什么文本; WhiteBoard类现在有了一个适当的Lock对象,可以避免read()方法中的争用条件; write()方法不需要锁,因为只有Teacher线程才能访问它。
该get()方法在到达字符串“end”时将返回 false。我用它来退出while循环。如果你想知道我为什么使用get()方法调用学生任务(因为这基本上返回“void”)。因为这将使主线程等待所有学生线程完成任务。否则,程序可能会突然结束。下面是新输出的屏幕截图:

正如你所看到的,学生们随机地访问和打印白板上的文字,但是老师总是会先写,这就是他们的意图:)
如果需要,可以使用thenRunAsync(Runnable action)方法定义学生线程的顺序。
下面是新代码:

class WhiteBoard {
    // Text written in the whiteboard
    private volatile String texto;
    
    // Read lock
    private Lock readLock = new ReentrantLock();

    // Private constructor
    private WhiteBoard() { }
    // Whiteboard instance
    private final static WhiteBoard wbInstance = new WhiteBoard();
    
    // Returns the instance of the whiteboard (i'm using a singleton pattern)
    public static WhiteBoard getInstance() {
        return wbInstance;
    }

    public void write(String str) {
        // Write's the text
        texto = str;
    }

    public String read() {
        // Lock to avoid race condition
        readLock.lock();

        try {
            return texto;
        } 
        finally {
            readLock.unlock();
        }
    }
}

// Student class
class Student implements Runnable {
    // Student name
    private String name;

    // Whiteboard instance
    private WhiteBoard wb;

    // Constructor
    public Student(String name)
    {
        this.name = name;
        wb = WhiteBoard.getInstance();
    }

    @Override
    public void run() {
        // Reads the text
        String txtCopiado = wb.read();

        // If the text is "end", returns
        if(txtCopiado.equals("end"))
            return;
        
        // Print student name and the text he read
        System.out.println(name + ": " + txtCopiado + " | Thread -> " + Thread.currentThread().getName());
    }
}

// Teacher class
class Teacher implements Supplier<Boolean> {

    // This is the array of texts he will write
    private String[] textos = {"Ola amigos", "Java Programming", "Jacare", "end"};

    // Index of the text he is writing
    private int index = 0;
    
    // Whiteboard instance
    private WhiteBoard wb;

    // Constructor 
    public Teacher() {
        wb = WhiteBoard.getInstance();
    }

    @Override
    public Boolean get() {
        // Write each text on the whiteboard
        wb.write(textos[index]);
        
        // Print what the teacher wrote
        System.out.println("Teacher: " + textos[index] + " | Thread -> " + Thread.currentThread().getName());

        return !textos[index++].equals("end");
    }
}

// Main class
public class Classroom {

    public static void main(String[] args) {
        var prof = new Teacher();

        var alun1 = new Student("1. Pablo");
        var alun2 = new Student("2. Kiki");
        var alun3 = new Student("3. Lucas");

        // Creates thread pool with 4 trheads
        ExecutorService es = Executors.newFixedThreadPool(4);

        try {
            boolean flag = true;
            while(flag)
            {
                // Check if the teacher has more texts to write
                flag = CompletableFuture.supplyAsync(prof, es).get();

                // Call students tasks
                CompletableFuture.allOf(
                        CompletableFuture.runAsync(alun1, es),
                        CompletableFuture.runAsync(alun2, es),
                        CompletableFuture.runAsync(alun3, es))
                    .get();
            }

            // Shutdown thread pool
            es.shutdown();
        } 
        catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

相关问题