同步方法未按预期工作

5

我有一个变量,由两个线程共享。这两个线程会在它上面进行一些操作。但每次执行程序时,共享变量的结果不同,我不知道为什么。

public class Main
{
    public static int sharedVar = 0;
    public static void main(String[] args) 
    {
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        mt1.start();
        mt2.start();

        try
        {
            // wait for the threads
            mt1.join();
            mt2.join();
        }
        catch (InterruptedException e1)
        {
            e1.printStackTrace();
        }

        System.out.println(sharedInt); // I expect this value to be 20000, but it's not
    }
}

以下是 "MyThread" 类:
public class MyThread extends Thread
{
    private int times = 10000;
    private synchronized void addOne()
    {
        for (int i = 0; i < times; ++i)
        {
            Main.sharedVar ++;
        }
    }

    @Override
    public void run()
    {
        addOne();
    }
}

有时候,sharedVar的最终结果会是13735、12508或18793,但从未达到我期望的20000。另一个有趣的现象是当times=1000时,最终结果总是2000。
有人能解释这种现象吗?

尝试将sharedVar声明为volatile。 - Benjy Kessler
在 sharedVar 上添加 volatile 后,结果接近 20000,但仍低于 20000。 - Brian
volatile 单独使用无法生效的原因是 Main.sharedVar++ 是一个复合操作(读取-增加-写入)。Volatile 对此没有保护作用。 - Kayaman
我明白了,那AtomicLong就可以使用。 - Benjy Kessler
如果您的代码像这样工作,我想指出它可能仍然不是您想要的。您的代码将确保首先一个线程执行10000次增量,然后另一个线程执行10000次增量。它不会让两个线程交替地增加(在调度程序的自由裁量下更多或更少)。 - Silly Freak
5个回答

6

同步方法保护资源this,这意味着您的代码等效于:

private void addOne()
{
    synchronized(this)
    {
        for (int i = 0; i < times; ++i)
        {
            Main.sharedVar ++;
        }
    }
}

但是你有两个对象需要调用addOne方法。这意味着mt1.addOnethismt2.addOnethis不同,因此你没有一个共同的同步资源。

尝试将你的addOne代码更改为:

private void addOne()
{
    synchronized(MyThread.class)
    {
        for (int i = 0; i < times; ++i)
        {
            Main.sharedVar ++;
        }
    }
}

您会看到预期的行为。正如下面的评论建议的那样,最好使用与MyThread.class不同的对象进行同步,因为类对象可以从许多点访问,并且很容易出现其他代码尝试使用相同的对象进行同步的情况。


1
不要锁定类。使用其他对象作为锁。无论如何加1 :) - TheLostMind
@TheLostMind 是的,你说得对,最好锁定其他对象,但我想编写一些草稿代码,其中一个公共对象用作同步资源。 - Pablo Francisco Pérez Hidalgo
将“int sharedVar = 0;”改为“Integer sharedVar = new Integer(0);”,然后您可以通过键入“synchronized(Main.sharedVar)”锁定sharedVar。 - Brian
@PabloFranciscoPérezHidalgo - 你说得没错。只是在类对象上加锁是一个不良实践。 :) - TheLostMind
1
@SillyFreak - 我的意思是在多线程环境中应该使用AtomicInteger而不是Integer。另外,如果你将Integer作为锁,请不要对其进行递增操作。 :) - TheLostMind
显示剩余5条评论

3
当您在非静态方法上使用synchronized时,您使用当前对象作为监视器。
当您在静态方法上使用synchronized时,您使用类的当前对象(ClassName.class静态字段)作为监视器。
在您的情况下,您在Thread的对象上使用synchronized(2个不同的实例),因此两个不同的线程将同时修改您的sharedVar静态字段。
您可以用不同的方式来解决这个问题。
addOne方法移动到Main并使其成为static
private static synchronized void addOne(int times)
{
    for (int i = 0; i < times; ++i)
    {
        sharedVar++;
    }
}

您可以创建一个名为SharedVar的类,其中包含字段private int var;和方法synchronized void addOne(int times),然后将单个SharedVar实例传递给您的线程。
public static void main(String[] args) 
{
    SharedVar var = new SharedVar();
    MyThread mt1 = new MyThread(var);
    MyThread mt2 = new MyThread(var);
    mt1.start();
    mt2.start();

    try
    {
        // wait for the threads
        mt1.join();
        mt2.join();
    }
    catch (InterruptedException e1)
    {
        e1.printStackTrace();
    }

    System.out.println(var.getVar()); // I expect this value to be 20000, but it's not
}

但是如果你需要在多个线程中修改只有一个整数的情况,你可以使用来自java.til.concurrent.*的类,如AtomicLongAtomicInteger


0

在 start 方法之后立即加入线程。从这个线程-1开始,然后进入死亡状态,之后线程-2将开始并进入死亡状态。因此,它将始终打印您期望的输出。

将代码更改如下所示:

public class Main{

    public static int sharedVar = 0;

    public static void main(String[] args)

        {
            MyThread mt1 = new MyThread();
            MyThread mt2 = new MyThread();

            try

                {
                    mt1.start();
                    mt1.join();
                    mt2.start();
                    mt2.join();
                }

            catch (InterruptedException e1)

                {
                    e1.printStackTrace();
                }

            System.out.println(sharedVar);

        }
}

class MyThread extends Thread
{
    private int times = 1000000;

    private synchronized void addOne()
        {
            for (int i = 0; i < times; ++i)
                {
                    Main.sharedVar++;
                }
        }

    @Override
    public void run()
        {
            addOne();
        }
}

0

sharedVar定义为AtomicLong而不是int。使函数synchronized也可以工作,但效率较低,因为您只需要增量同步。


0

当一个线程即将执行一个'synchronized' 实例方法时,它会获取该对象的锁(准确地说是该对象监视器)。

因此,在您的情况下,Thread mt1在Object mt1上获取锁,而Thread mt2在Object mt2上获取锁,它们不会相互阻塞,因为两个线程正在使用两个不同的锁。

当两个线程同时并发修改共享变量(非同步方式),结果是不可预测的。

关于值为1000的情况,对于较小的输入,交错执行可能会产生正确的结果(幸运的是)。

解决方法:从addOne方法中删除synchronized关键字,并将sharedVal设置为 'AtomicInteger' 类型。


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