为什么这些代码块的输出结果不同?

4

我刚开始接触线程编程,写了一个简单的程序来测试如何避免竞态条件。我的第一次尝试是使用命名内部类:

/* App1.java */

package ehsan;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class App1{
    private final int poolSize = 10;
    private final int numLoop = 5;
    private int lastThread = 0;

    public App1() {
        ExecutorService taskList = Executors.newFixedThreadPool(poolSize);
        for (int i = 0;i < poolSize;i++) {
            taskList.execute(new Counter());
        }
        taskList.shutdown();
    }

    private class Counter implements Runnable{

        @Override
        public void run() {
            synchronized (this) {
                int currentThread = lastThread;
                System.out.println("Current thread : "+currentThread);
                lastThread = lastThread + 1;
            }
            System.out.println("Thread was executed");
        }
    }

}

以及 App1Test.java :

package ehsan;

import java.io.IOException;

public class Test {
    public static void main(String[] args) throws IOException {
        new App1();
    }
}

因此,它显示了:
Current thread : 0
Thread was executed
Current thread : 1
Thread was executed
Current thread : 1
Thread was executed
Current thread : 3
Thread was executed
Current thread : 4
Thread was executed
Current thread : 5
Thread was executed
Current thread : 6
Thread was executed
Current thread : 7
Thread was executed
Current thread : 6
Current thread : 8
Thread was executed
Thread was executed

我的事情全都搞混了,即使我已经使用了synchronized,我仍然面临竞态条件。

但是我的第二次尝试成功了!:

package ehsan;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class App1 implements Runnable{
    private final int poolSize = 10;
    private final int numLoop = 5;
    private int lastThread = 0;

    public App1() {
        ExecutorService taskList = Executors.newFixedThreadPool(poolSize);
        for (int i = 0;i < poolSize;i++) {
            taskList.execute(this);
        }
        taskList.shutdown();
    }

    @Override
    public void run() {
        synchronized (this) {
            int currentThread = lastThread;
            System.out.println("Current thread : "+currentThread);
            lastThread = lastThread + 1;
            System.out.println("Thread was executed");
        }
    }
}

结果与我预期的一样:

Current thread : 0
Thread was executed
Current thread : 1
Thread was executed
Current thread : 2
Thread was executed
Current thread : 3
Thread was executed
Current thread : 4
Thread was executed
Current thread : 5
Thread was executed
Current thread : 6
Thread was executed
Current thread : 7
Thread was executed
Current thread : 8
Thread was executed
Current thread : 9
Thread was executed

所以我的问题是为什么我的第一次尝试没有成功,而第二次尝试非常成功?谢谢你的帮助,我是多线程编程的初学者!

同步线程的整个run()方法几乎总是一个错误。在您的第二个“工作”示例中,synchronized块防止任何一个线程与其同行同时运行。但是,如果您不允许线程同时运行,那么使用线程的意义何在? - Solomon Slow
3个回答

1
在第一个程序中,您创建了一个不同的Counter实例作为每个线程执行其run()方法的Runnable,因此synchronized (this)为每个线程使用不同的锁,因此代码不是线程安全的。如果您使用相同的Counter实例而不是为每个线程创建新实例,则此程序也将按预期运行。
    Counter counter = new Counter();
    for (int i = 0;i < poolSize;i++) {
        taskList.execute(counter);
    }

在第二个程序中,您使用相同的App1实例作为所有线程执行的Runnablerun()方法,因此synchronized (this)对所有线程使用相同的锁。

0

经过大量的研究,我找到了几种解决方法。

请注意:其中一些解决方案是由Eran、Thomas等人指出的,我感谢他们的帮助,但我只想在一个答案中收集所有可能的解决方案,以便未来的访问者可以轻松地找到答案。

  • 对于命名内部类:

我们可以使用OutterClass实例作为同步锁对象,而不是使用this作为同步锁对象:

 synchronized (App1.this) {
        int currentThread = lastThread;
        System.out.println("Current thread : "+currentThread);
        lastThread = lastThread + 1;
 }
  • 对于分离的类:

解决方案1


在我们从调用者类(包含任务列表的类)接收到的对象上进行同步。以下是一个示例:

public App1 implements Runnable {

    private final Integer shared;/* Not spending time on auto-boxing */

    public App1(Integer sharedNum) {
        shared = sharedNum;
    }

    @Override
    public void run() {
        synchronization(shared){
            //code here
        }
    }
}

public App1Test {
    private final int forSharing = 14;
    public static void main(String[] args) {
        ExecutorService taskList = Executors.newFixedThreadPool(poolSize);
        taskList.execute(new App1(forSharing));
        // and lob lob
    }
}

解决方案2


类对象上的同步:

synchronized (App1.class) { /* As the fields and methods of class App1 are the same for all objects */
    int currentThread = lastThread;
    System.out.println("Current thread : "+currentThread);
    lastThread = lastThread + 1;
}

解决方案3


在静态字段上同步(我喜欢它,因为它真的很创新):
/* Add this field to class definition */
private static Object sharedObject = new Object();

/* Now in `run` method  use the object like this : */
synchronized(sharedObject) {
    //TODO : write your code here :)
}

所以这些是解决这个有点难以调试的狡猾问题的解决方案 :)

我希望这能帮助那些遇到相同问题的同行们 :)

祝好,Ehsan


解决方案4: AtomicInteger。虽然并没有直接回答你的问题,但却解决了你在问题示例中所遇到的问题。 - Fildor
AtomicInteger是什么?基本上就是名字所说的。它是一个带有原子操作的整数(例如"getAndIncrement")。如果您使用其中之一,可以避免同步块,这取决于您的实现方式。因此,在您的“计数器”情况下,使用AtomicInteger,您可以放弃synchronized并且只需一行代码即可完成所有三个操作:System.out.println("Current thread : "+sharedInt.getAndIncrement()); - Fildor
哦,谢谢,我不知道它可以这么容易地使用 :) - user6538026

-1
在 Counter 中,synchronized (this) 在实例上同步,因此如果您将每个线程传递一个新实例(taskList.execute(new Counter())),则根本不会进行任何同步(因为没有 2 个线程使用相同的实例进行同步)。因此,请使用 synchronized(Counter.class) 或其他共享监视器对象(由于 Counter 是 App1 的内部类,因此可以使用 synchronize(App1.this))。
您的第二种方法有效,因为您将相同的实例传递给每个线程:taskList.execute(this)

它解释了为什么在一个实现中它能够工作,但缺少背景说明。 - Konrad
仅作附注:如果我们同步“run”方法,我们仍然可以称之为“并发”吗?拥有预期的输出并不意味着同步已经正确完成。这正是一个例子。执行被序列化,而不是对关键资源(即计数器)进行同步。 - Fildor
@Konrad,您期望有哪些背景说明?同步一般是如何工作的? - Thomas
@Fildor,问题(因此答案)并不是关于并发性的一般性问题,而是关于其中一个特定部分的问题。从一般意义上讲,同步是并发性的一个组成部分 - 在OP的情况下,您将获得顺序执行,但在后台中仍然存在并发性,因为多个线程尝试同时运行同步块(只是不能)。... - Thomas
@Fildor...此外,当前代码与真正的顺序执行之间还有另一个区别:除了按照启动顺序启动线程的顺序外,您无法影响线程运行的顺序,而在真正的顺序代码中,它是确定性的,因为您知道代码将按照定义的顺序执行。不要被看似顺序的输出0、1、2、3等所迷惑——这反映了到目前为止调用的次数,而不是线程运行的顺序。 - Thomas
@Thomas 我知道这一切,我同意你的观点,实际上你的回答回答了具体问题。唯一让我感到不安的是,OP可能会得出这样一个印象,即这是解决这个问题的通用方法。在这个特定的例子中,这完全没问题。但如果除了递增和输出计数器之外还有更多的东西,我们很可能会自食其果。因此,我希望至少提到这一事实。 - Fildor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接