为什么这个类不是线程安全的?

96
class ThreadSafeClass extends Thread
{
     private static int count = 0;

     public synchronized static void increment()
     {
         count++;
     }

     public synchronized void decrement()
     {
         count--;
     }
}

有人可以解释一下为什么上面的类不是线程安全的吗?


6
我对Java不是很了解,但看起来这些方法每个都是独立的线程安全的,但同时可能会在每个方法中有一个线程。也许如果你有一个单一的方法,它需要带一个布尔值(increment)参数,那么它就是线程安全的。或者你可以使用一些锁定对象。正如我所说,我不知道Java - 我的评论基于C#知识。 - Wai Ha Lee
请参见https://dev59.com/Cmsz5IYBdhLWcg3wlYwn。 - Tagir Valeev
4
只要不创建该类的实例,它就是线程安全的。 - Benjamin Gruenbaum
1
你认为它为什么不是线程安全的? - Raedwald
为什么增量是静态的而减量不是? - Kerem Baydoğan
显示剩余2条评论
7个回答

134

increment方法是static,它会在ThreadSafeClass类的类对象上同步。而decrement方法不是静态的,它将在调用它的实例上同步。也就是说,它们会在不同的对象上同步,因此两个不同的线程可以同时执行这些方法。由于++--操作不是原子操作,所以该类不是线程安全的。

此外,由于countstatic的,从decrement中修改它是不安全的,因为它可以在不同的实例上调用并以这种方式并发地修改count


12
由于count是静态的,即使没有static increment()方法,拥有一个实例方法decrement()也是错误的,因为两个线程可以在不同的实例上调用decrement()并同时修改相同的计数器。 - Holger
1
这可能是一个很好的理由,通常更喜欢使用synchronized块(甚至在整个方法内容上)而不是在方法上使用修饰词,即 synchronized(this) { ... } synchronized(ThreadSafeClass.class) { ... } - Bruno
“++”和“--”即使在“volatile int”上也不是原子的。 “Synchronized”解决了“++” /“--”的读取/更新/写入问题,但“static”关键字在这里非常重要。好答案! - Chris Cirefice
在一个同步的实例方法中修改静态字段是错误的:从实例方法中访问静态变量本身并没有什么问题,从synchronized实例方法中访问它也没有什么问题。只是不要期望实例方法上的"synchronized"提供对静态数据的保护。这里唯一的问题就是你在第一段中所说的:这些方法使用不同的锁来尝试保护相同的数据,这当然根本没有提供任何保护。 - Solomon Slow

23

你有两个同步方法,但其中一个是静态的,另一个不是。当访问同步方法时,根据它的类型(静态或非静态),将锁定不同的对象。对于静态方法,将在Class对象上放置锁,而对于非静态块,则将在运行该方法的类的实例上放置锁。由于你有两个不同的锁定对象,因此你可以同时有两个线程修改相同的对象。


14
有人能解释一下为什么上面的类不是线程安全的吗?
由于 increment 是静态的,同步将在类本身上进行。 decrement 不是静态的,同步将在对象实例化上进行,但这并不能保证任何东西,因为 count 是静态的。
我想补充一点,声明一个线程安全的计数器,我认为最简单的方法是使用 AtomicInteger 而不是原始的 int。
让我引导您到 java.util.concurrent.atomic 包信息。

8

其他回答已经很好地解释了原因,我只是补充一些总结synchronized的内容:

public class A {
    public synchronized void fun1() {}

    public synchronized void fun2() {}

    public void fun3() {}

    public static synchronized void fun4() {}

    public static void fun5() {}
}

A a1 = new A();

fun1fun2上的synchronized是在实例对象级别上同步的。fun4上的synchronized是在类对象级别上同步的。这意味着:

  1. 当两个线程同时调用a1.fun1()时,后一个调用将被阻塞。
  2. 当线程1调用a1.fun1()且线程2调用a1.fun2()时同时发生,后一个调用将被阻塞。
  3. 当线程1调用a1.fun1()且线程2调用a1.fun3()时同时发生,则不会阻塞,这两个方法将同时执行。
  4. 当线程1调用A.fun4()时,如果其他线程同时调用A.fun4()A.fun5(),后一个调用将被阻塞,因为fun4上的synchronized是类级别的。
  5. 当线程1调用A.fun4()时,线程2同时调用a1.fun1(),则不会阻塞,这两个方法将同时执行。

6
  1. 递减 锁定的对象与 递增 不同,因此它们不会相互阻止运行。
  2. 在一个实例上调用 递减 锁定的对象与在另一个实例上调用 递减 不同,但它们影响同一件事。

第一个意味着重叠调用 递增递减 可能导致相互抵消(正确),递增或递减。

第二个意味着两个重叠的调用 递减 在不同的实例上可能导致双重递减(正确)或单个递减。


4

由于有两种不同的方法,一种是实例级别的,另一种是类级别的,因此您需要锁定2个不同的对象以使其线程安全。


1
如其他答案所解释的那样,您的代码不是线程安全的,因为静态方法increment()锁定了类监视器,而非静态方法decrement()锁定了对象监视器。

对于此代码示例,存在更好的解决方案,无需使用synchronized关键字。 您必须使用AtomicInteger来实现线程安全。

使用AtomicInteger实现线程安全:

import java.util.concurrent.atomic.AtomicInteger;

class ThreadSafeClass extends Thread {

    private static AtomicInteger count = new AtomicInteger(0);

    public static void increment() {
        count.incrementAndGet();
    }

    public static void decrement() {
        count.decrementAndGet();
    }

    public static int value() {
        return count.get();
    }

}

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