【并发】保证共享变量在多线程并发时的线程安全
Code:
public class AdderTest {
static int i;
static CountDownLatch latch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
Runnable task = new Runnable() {
@Override
public void run() {
int x = 0;
while (x++ < 100000){
i++;
}
latch.countDown();
}
};
Thread adder1 = new Thread(task, "Adder-1");
Thread adder2 = new Thread(task,"Adder-2");
//开启两个加法器线程
adder1.start();
adder2.start();
//主线程阻塞,等待两个加法器线程完成操作
latch.await();
System.out.println(i);
}
}
? ? ? ? 两个Adder线程执行同样的操作:累加共享变量i,执行这个操作十万次;使用CountDownLatch是为了让主线程等待两个Adder线程执行完累加操作,最后打印共享变量i,看共享变量i的最终值是否是我们预期的二十万。
? ? ? ? 共享变量i的最终值:
? ? ? ? 每次执行的结果都不相同,但这些结果有一个共同点:都小于二十万。为什么会出现这种情况呢?因为在字节码中,i++可不仅仅是一步操作。
0: getstatic #2 // 获取静态变量i的值
3: iconst_1 // 将常量1推送到栈顶
4: iadd // 将栈顶两个int型数值相加
5: putstatic #2 // 将相加后的值存储到静态变量i
8: return // 返回
? ? ? ? 可以看到,一个简单的i++代码在字节码中包含四步,这四步即包含读又包含写,而多线程场景下,对共享变量进行读、写操作无疑会产生线程安全问题。因为线程对共享变量执行读、写操作时无法保证这两个操作的原子性。
? ? ? ? 依照上图,假设静态变量i的值一开始是1,线程Adder-1先获取了静态变量i的值,执行+1操作,准备赋值给静态变量i时,线程Adder-1的CPU时间片用完了,线程被挂起;随后线程Adder-2也来执行相同的操作,由于它的CPU时间片充足,它能完整的执行所有操作。
? ? ? ? 假设线程Adder-2在线程Adder-1挂起的这段时间,执行了三次完整操作,那么静态变量i的值就是4。此时线程Adder-1又获得了CPU的时间片,能够继续执行赋值操作,将静态变量i的值修改成2,这就相当于将线程Adder-2之前三次操作的结果给覆盖掉了,这也是为什么最终结果总是小于预期值的原因。
解决方案:
????????(1)悲观锁
? ? ? ? synchronized:
public class AdderTest {
static int i;
static CountDownLatch latch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Runnable task = new Runnable() {
@Override
public void run() {
int x = 0;
synchronized (lock){
while (x++ < 100000) {
i++;
}
}
latch.countDown();
}
};
Thread adder1 = new Thread(task, "Adder-1");
Thread adder2 = new Thread(task,"Adder-2");
//开启两个加法器线程
adder1.start();
adder2.start();
//主线程阻塞,等待两个加法器线程完成操作
latch.await();
System.out.println(i);
}
}
? ? ? ? 为了减少锁带来的性能影响,我们希望锁粒度尽可能小,一般只会对存在共享变量读、写的临界区代码块上锁(这里临界区代码块是i++),但这里为了减少多次获得锁、释放锁带来的性能影响,我们应该把锁放在while循环外。
? ? ? ? ReentrantLock:
public class AdderTest {
static int i;
static CountDownLatch latch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Runnable task = new Runnable() {
@Override
public void run() {
int x = 0;
try {
lock.lock();
while (x++ < 100000){
i++;
}
}finally {
lock.unlock();
}
latch.countDown();
}
};
Thread adder1 = new Thread(task, "Adder-1");
Thread adder2 = new Thread(task,"Adder-2");
//开启两个加法器线程
adder1.start();
adder2.start();
//主线程阻塞,等待两个加法器线程完成操作
latch.await();
System.out.println(i);
}
}
? ? ? ? 使用ReentrantLock实现线程互斥时,一般搭配try、finally一块使用,避免死锁现象产生。对finally机制感兴趣的读者可以看我的另一篇博客:【异常】浅析异常体系及为什么一定会执行finally块-CSDN博客
? ? ? ? 测试:
? ? ? ? (2)乐观锁
? ? ? ? AtomicInteger原子类:
public class AdderTest {
static AtomicInteger i = new AtomicInteger(0);
static CountDownLatch latch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
Runnable task = new Runnable() {
@Override
public void run() {
int x = 0;
while (x++ < 100000) {
i.getAndIncrement();
}
latch.countDown();
}
};
Thread adder1 = new Thread(task, "Adder-1");
Thread adder2 = new Thread(task,"Adder-2");
//开启两个加法器线程
adder1.start();
adder2.start();
//主线程阻塞,等待两个加法器线程完成操作
latch.await();
System.out.println(i);
}
}
? ? ? ? 将静态变量i的实现改为AtomicInteger原子类。多线程操作AtomicaInteger对象时,线程安全由AtomicaInteger内部的Unsafe类基于CAS(乐观锁思想的一种体现)来保证。
? ? ? ? getAndIncrement()方法实现:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
? ? ? ? 测试:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!