为什么同步功能无法正常工作?

5

这是我的代码:

private int count = 0;

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

 public void doWork() throws InterruptedException {

    Thread t1 = new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < 5; i++) {
                increment();
                System.out.println(count+"  "+Thread.currentThread().getName());
            }}});

    Thread t2 = new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < 5; i++) {
                increment();
                System.out.println(count+"  "+Thread.currentThread().getName());
            }}});

    t1.start();
    t2.start();
}

这是我的输出结果:
2  Thread-1
2  Thread-0
3  Thread-1
5  Thread-1
6  Thread-1
4  Thread-0
8  Thread-0
9  Thread-0
7  Thread-1
10  Thread-0

我的理解是,increment是同步的。因此,它应该首先递增一个数字,然后释放锁,并将锁交给线程t1t2。所以,它应该一次递增一个数字,对吗?
但为什么我的代码每次增加两个或三个数字?我做错了什么吗(我是新手)?

9
increment()方法是同步的,但count不是。increment()方法和System.out.println()语句都不是原子操作。 - bradimus
5个回答

5

虽然count++;是同步的,但System.out.println(count+" "+Thread.currentThread().getName());不是,但它访问了count变量。

即使你同步访问,也没有用,因为下一个场景仍然可能发生:

  • 线程1增加
  • 线程2增加
  • 线程1打印值2
  • 线程2打印值2

要解决这个问题,您需要在同步部分中进行增量和打印。例如,您可以将System.out.println(count+" "+Thread.currentThread().getName());放入increment方法中。


1
或者将 count 声明为 volatile。 - chrylis -cautiouslyoptimistic-
@chrylis 这将解决第一个问题,但不是第二个问题。 - talex
1
将“count”标记为“volatile”不会防止线程2在线程1打印之前进行增量。 - bradimus

1

increment 方法在返回后,可以在另一个线程上运行,但在获取 count 进行连接之前。

count+"  "+Thread.currentThread().getName()

您可以通过在一个同步块中修改和检索count来解决这个问题:
public synchronized int incrementAndGet() {
    count++;
    return count; // read access synchronized
}

for (int i = 0; i < 5; i++) {
    System.out.println(incrementAndGet()+"  "+Thread.currentThread().getName());
}

或者使用专门设计用于此目的的标准库中的类

private final AtomicInteger counter = new AtomicInteger(0);

public void doWork() throws InterruptedException {

    Thread t1 = new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(counter.incrementAndGet() + "  " + Thread.currentThread().getName());
            }
        }
    });

    Thread t2 = new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println(counter.incrementAndGet() + "  " + Thread.currentThread().getName());
            }
        }
    });

    t1.start();
    t2.start();
}

当然,这并不一定会导致数字1到10按顺序打印,只是没有重复获取任何数字。以下输出可能会发生:
2  Thread-0
3  Thread-0
4  Thread-0
1  Thread-1
5  Thread-0
6  Thread-1
7  Thread-0
8  Thread-1
9  Thread-1
10  Thread-1

@Hemlata 噢,它确实有效,但是如果没有显式地进行同步,您不能指望打印语句也能同步,而且您在问题中没有提到这样的限制。(没有数字出现超过一次,没有数字丢失,在每个线程上按升序打印数字。)此外,在那种情况下使用多个线程是没有意义的... - fabian
也许是因为我是新手,所以做错了什么导致它不起作用。 - user6857832

0

实际发生的是,您的线程正在快照(也许另一个词更好)变量count当前值并显示它。您可以将其视为有一个带有数字零的蓝色桶,两个Threads以相同的颜色和数字获取相同的桶。现在,他们分别在这些桶上工作。

如果您希望它们在同一个桶上工作,则必须使它们成为原子性,例如使用AtomicIntegervolatile或Java并发包中的任何其他工具。


0

解决方案1:由fabian提供。 提供单个函数incrementAndGet()

解决方案2:使用synchronized块而不是synchronized方法(如果可能):

完整代码如下:

private int count = 0;
private Object dummyObject = new Object();

public void increment() {
    count++;
}

public int getCount() {
    return count;
}

public void doWork() throws InterruptedException {

    Thread t1 = new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < 5; i++) {
                synchronized (dummyObject) {
                    increment();
                    System.out.println(count + "  " + Thread.currentThread().getName());
                }
            }
        }
    });

    Thread t2 = new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < 5; i++) {
                synchronized (dummyObject) {
                    increment();
                    System.out.println(count + "  " + Thread.currentThread().getName());
                }
            }
        }
    });

    t1.start();
    t2.start();
}

那没有帮助,因为在调用increment和调用getCount之间,该值仍然可以更改,因为在这种情况下监视器被获取两次。 - fabian

0

一种不使用 synchronized 的替代解决方案。

由于您的用例很简单(只需增加计数器并打印值),AtomicInteger 是更好的选择。

import java.util.concurrent.atomic.AtomicInteger;

public class TestCounter{
    private AtomicInteger count = new AtomicInteger(0);

    public void doWork() throws InterruptedException {

        Thread t1 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(""+Thread.currentThread().getName()+":"+count.incrementAndGet());
                }}});

        Thread t2 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(""+Thread.currentThread().getName()+":"+count.incrementAndGet());
                }}});

        t1.start();
        t2.start();
    }

    public static void main(String args[]) throws Exception{
        TestCounter tc = new TestCounter();
        tc.doWork();
    }
}

输出:

Thread-0:1
Thread-0:3
Thread-0:4
Thread-0:5
Thread-0:6
Thread-1:2
Thread-1:7
Thread-1:8
Thread-1:9
Thread-1:10

请参考@fabian的答案,了解为什么这些数字不是按顺序打印出来的。

如果您期望以1-10递增的数字序列,那么不需要使用线程。


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