在并发环境下,为了保证并发安全问题,通常我们会进行加锁操作,比如加上synchronized关键字。但是很多情况下,我们不需要这样的重量级锁,比如说多个线程对某个int类型变量i
进行++操作,但是不加锁吧,又怕影响结果,因为i++不是一个原子操作,会出现并发问题,我们来看个案例。
1 public class NonAtomicIncrementDemo { 2 // 共享变量 3 private static int i = 0; 4 5 public static void main(String[] args) throws InterruptedException { 6 // 线程数量 7 int threadCount = 10; 8 // 每个线程执行的操作次数 9 int incrementCount = 1000; 10 11 // 创建线程数组 12 Thread[] threads = new Thread[threadCount]; 13 14 // 创建并启动线程 15 for (int j = 0; j < threadCount; j++) { 16 threads[j] = new Thread(() -> { 17 for (int k = 0; k < incrementCount; k++) { 18 // 非原子操作 i++ 19 i++; 20 } 21 }); 22 threads[j].start(); 23 } 24 25 // 等待所有线程执行完毕 26 for (Thread thread : threads) { 27 thread.join(); 28 } 29 30 // 预期结果 31 int expectedResult = threadCount * incrementCount; 32 // 实际结果 33 int actualResult = i; 34 35 System.out.println("预期结果: " + expectedResult); 36 System.out.println("实际结果: " + actualResult); 37 } 38 }
执行的结果
问题原因:由于 i++ 不是原子操作,多个线程可能会同时读取 i 的值,然后对其进行加 1 操作,最后再写回。这样就可能会导致某些线程的加 1 操作被覆盖,从而使最终结果小于预期值
解决方法:可以使用 Java 提供的原子类(如 AtomicInteger)来保证 i++ 操作的原子性,或者使用 synchronized 关键字来对 i++ 操作进行同步。
以下是使用 AtomicInteger 解决该问题的示例代码:
1 import java.util.concurrent.atomic.AtomicInteger; 2 3 public class AtomicIncrementDemo { 4 // 原子整数 5 private static AtomicInteger atomicInteger = new AtomicInteger(0); 6 7 public static void main(String[] args) throws InterruptedException { 8 // 线程数量 9 int threadCount = 10; 10 // 每个线程执行的操作次数 11 int incrementCount = 1000; 12 13 // 创建线程数组 14 Thread[] threads = new Thread[threadCount]; 15 16 // 创建并启动线程 17 for (int j = 0; j < threadCount; j++) { 18 threads[j] = new Thread(() -> { 19 for (int k = 0; k < incrementCount; k++) { 20 // 原子操作 incrementAndGet() 21 atomicInteger.incrementAndGet(); 22 } 23 }); 24 threads[j].start(); 25 } 26 27 // 等待所有线程执行完毕 28 for (Thread thread : threads) { 29 thread.join(); 30 } 31 32 // 预期结果 33 int expectedResult = threadCount * incrementCount; 34 // 实际结果 35 int actualResult = atomicInteger.get(); 36 37 System.out.println("预期结果: " + expectedResult); 38 System.out.println("实际结果: " + actualResult); 39 } 40 }
在这个示例中,使用 AtomicInteger 的 incrementAndGet() 方法来保证 i++ 操作的原子性,从而避免了线程安全问题。
incrementAndGet底层就是用了CAS操作。
CAS 的基本思路就是, 如果这个地址上的值和期望的值相等, 则给其赋予新 值, 否则不做任何事儿,但是要返回原值是多少。 自然 CAS 操作执行完成时, 在 业务上不一定完成了, 这个时候我
们就会对 CAS 操作进行反复重试, 于是就有了 循环 CAS。很明显, 循环 CAS 就是在一个循环里不断的做 cas 操作, 直到成功为 止。 Java 中的 Atomic 系列的原子操作类的实现则是利用了循环 CAS来实现。