并发编程的三大挑战之原子性及其解决方案
目录
一、原子性问题
1、带来原子性问题的原因
2、如何解决线程切换带来的原子问题
2.1、使用synchronized关键字来保证
2.2、使用CAS来保证原子性
2.3、使用lock锁来保证
一、原子性问题
1、带来原子性问题的原因
线程切换是带来原子的根本原因,java的并发程序是基于多线程的,自然就会涉及到任务切换。而任务切换的时机是可以发生cpu的时间片结束时,由于目前我们使用的编程语言都是高级语言,一条高级语言往往是需要多条CPU指令完成的,例如count++,至少需要三条CPU指令。
- 指令1:首先需要把变量count从主内存中加载cpu的寄存器中
- 指令2:在寄存器中执行+1操作
- 指令3:将结果写入内存(缓存机制可能导致写入的是cpu的缓存而不是内存)
如下图所示,两个线程如果在执行count++的时候,过程如果发生了线程切换,会导致得不到预期的结果2,可能会出现意向不到结果,两个线程对count执行++操作后,在主内存中值为1.
原子性的定义:原子性指一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到他
private static int count;public static void increment(){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}count++;}public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 100; i++) {increment();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 100; i++) {increment();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
2、如何解决线程切换带来的原子问题
本质就是保存这块有非原子的操作语句,同一个时刻只能被一个线程访问到,并且对修改后的值,保证后续线程可见。通常的做法有:
2.1、使用synchronized关键字来保证
之前的increment()方法修改为如下方式:
public static synchronized void increment(){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}count++;}
2.2、使用CAS来保证原子性
使用CAS来解决的时候,如下所示:
private static AtomicInteger atomicInteger = new AtomicInteger();public static void increment(){try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}atomicInteger.incrementAndGet();}
2.3、使用lock锁来保证
当我们使用锁来保证原子问题时,其示例代码如下:
private static int count = 0;public static void increment(){ReentrantLock reentrantLock = new ReentrantLock();try {reentrantLock.lock();Thread.sleep(10);count++;} catch (InterruptedException e) {e.printStackTrace();}finally {reentrantLock.unlock();}}