在 Java 中如何跨多个线程递增一个值?

How does incrementing a value across multiple threads in Java work?

我有两段多线程代码。一个将 Integer 传递给线程,线程递增它,另一个传递一个对象,该对象具有一个递增的内部 Integer。当我 运行 这些输出是不同的,而不是我所期望的。有人可以解释发生了什么以及我如何使第一个行为与第二个行为相似。

实施 1:

public class Main {
    public static void main(String[] args) {
        Integer counter = 0;
        for (int i = 0; i < 3; i++) {
            Threaded t = new Threaded("Thread " + i, counter);
            Thread thread = new Thread(t);
            thread.start();
        }
    }
}

class Threaded implements Runnable {
    private final String name;
    private Integer counter;
    
    public Threaded(String name, Integer counter) {
        this.name = name;
        this.counter = counter;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            counter++;
            System.out.println(name + ": " + counter);
        }
    }
}

输出 1:

Thread 1: 1
Thread 1: 2
Thread 0: 1
Thread 0: 2
Thread 0: 3
Thread 0: 4
Thread 0: 5
Thread 2: 1
Thread 2: 2
Thread 2: 3
Thread 2: 4
Thread 1: 3
Thread 1: 4
Thread 1: 5
Thread 2: 5

实施 2:

public class Main {
    public static void main(String[] args) {
        MyObj obj = new MyObj();
        for (int i = 0; i < 3; i++) {
            Threaded t = new Threaded("Thread " + i, obj);
            Thread thread = new Thread(t);
            thread.start();
        }
    }
}

class MyObj {
    private int count = 0;
    
    public void inc() { count++; }
    
    public int getCount() { return count; }
}

class Threaded implements Runnable {
    
    private final String name;
    private final MyObj obj;
    
    public Threaded(String name, MyObj obj) {
        this.name = name;
        this.obj = obj;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            obj.inc();
            System.out.println(name + ": " + obj.getCount());
        }
    }
}

输出 2:

Thread 1: 2
Thread 1: 3
Thread 1: 4
Thread 1: 5
Thread 1: 6
Thread 0: 1
Thread 0: 7
Thread 2: 8
Thread 0: 9
Thread 2: 10
Thread 2: 11
Thread 0: 12
Thread 2: 13
Thread 2: 15
Thread 0: 14

是否可以让多个线程都直接递增一个Integer却得到类似于实现2的输出?

对于第一个例子,每个线程都有自己的计数器,并且是单独递增的。 Integer 是不可变的,当它递增时,结果是一个新的 Integer 对象。线程获得相同的起始值但没有共享状态。

对于第二个示例,线程共享同一个 MyObject 实例,没有什么可以阻止它们覆盖彼此的工作。第二个例子使用AtomicInteger,线程之间不会互相干扰。

class MyObj {
    private AtomicInteger counter = new AtomicInteger(0);

    public void inc() { 
        counter.incrementAndGet(); 
    }

    public int getCount() { 
        return counter.get();
    }
}

简而言之:您的两个实现之间的区别在于您的 MyObj class 是 可变的,Integer 是不可变的。

特别是,此语句并不像您认为的那样:

counter++;

它的作用是:

  1. 获取 counter 变量的值,它是对不可变 Integer 对象的引用。
  2. 获取Integer对象的int值,
  3. 计算一个新的 int 值,等于 1+ 先前的值,
  4. 用新值构造一个newInteger对象,最后
  5. 将对新对象的引用存储到 counter 变量中。

由于您的每个线程都有自己的 counter,并且每个线程都构建自己的新 Integer 对象序列以分配给 counter,因此线程 运行完全相互独立。


在您的第二个示例中,只有一个 MyObj 实例。每个线程都有自己的 obj 变量,但它们都被初始化为指向同一个实例,并且线程从不分配它们的 obj 变量:线程改为改变一个共享实例。

切勿跨线程共享不受保护的资源

正确。

更改此代码:

new Threaded( "Thread " + i , obj )

… 对此:

new Threaded( "Thread " + i , new MyObject( … ) )

... 获得类似于您的第一个示例的行为。通过将每个新的 Thread 传递给自己的 MyObject 实例,各种 Thread 对象将不再互相干扰。

这里的教训是切勿在未添加保护的情况下跨线程共享资源。正如 Nathan Hughes 所建议的,将 MyObject class 上的 int 替换为 AtomicInteger 是增加保护的一种方法。将 AtomicInteger 标记为 final 以确保其实例永远不会被替换。

执行者服务

另一个问题:在现代 Java 中,我们很少直接处理 Thread class。相反,使用添加到 Java 5.

的 Executors 框架

将您的任务定义为 RunnableCallable。将该任务的实例传递给执行程序服务。

Java 8+ 中的 lambda 语法在定义 Runnable 时通常非常方便。 lambda 中的代码可以引用其周围代码中的变量。

这已在 Stack Overflow 上多次提及。搜索以了解更多信息。

示例代码

这是使用执行程序服务修改后的代码版本。

请注意,inc 方法已更改为 return 新增加的计数 而不是 return 无效。到另一行称为 MyObj#getCount 的代码时,某些其他线程可能已经执行了额外的增量,因此 getCount 的结果不会给出您想要的结果。

package work.basil.threading;

import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class App2
{
    public static void main ( String[] args )
    {
        App2 app = new App2();
        app.demo();
    }

    private void demo ( )
    {
        final MyObj myObj = new MyObj();

        Runnable task = ( ) -> {
            for ( int i = 0 ; i < 5 ; i++ )
            {
                int newValue = myObj.inc();
                // BEWARE: Multi-threaded calls to `System.out` may *not* appear chronologically. Add timestamp to get some idea of chronology.
                System.out.println( Thread.currentThread().getId() + " thread incremented to : " + newValue + " at " + Instant.now() );
            }
        };

        ExecutorService executorService = Executors.newCachedThreadPool();
        for ( int i = 0 ; i < 3 ; i++ )
        {
            executorService.submit( task );
        }
        executorService.shutdown();
        try { executorService.awaitTermination( 1 , TimeUnit.MINUTES ); } catch ( InterruptedException e ) { throw new RuntimeException( e ); }
    }
}

class MyObj
{
    private final AtomicInteger count = new AtomicInteger();

    public int inc ( ) { return this.count.incrementAndGet(); }

    public int getCount ( ) { return count.get(); }
}

示例运行。同样,请注意控制台按时间顺序打印行。

15 thread incremented to : 1 at 2022-03-04T21:54:17.926156Z
17 thread incremented to : 3 at 2022-03-04T21:54:17.926207Z
16 thread incremented to : 2 at 2022-03-04T21:54:17.926219Z
17 thread incremented to : 5 at 2022-03-04T21:54:17.941980Z
15 thread incremented to : 4 at 2022-03-04T21:54:17.941961Z
16 thread incremented to : 6 at 2022-03-04T21:54:17.942027Z
15 thread incremented to : 8 at 2022-03-04T21:54:17.942054Z
17 thread incremented to : 7 at 2022-03-04T21:54:17.942041Z
16 thread incremented to : 9 at 2022-03-04T21:54:17.942067Z
15 thread incremented to : 10 at 2022-03-04T21:54:17.942083Z
17 thread incremented to : 11 at 2022-03-04T21:54:17.942126Z
16 thread incremented to : 12 at 2022-03-04T21:54:17.942164Z
15 thread incremented to : 13 at 2022-03-04T21:54:17.942176Z
16 thread incremented to : 15 at 2022-03-04T21:54:17.942254Z
17 thread incremented to : 14 at 2022-03-04T21:54:17.942219Z