learn-tech/专栏/Java并发编程78讲-完/42AtomicInteger和synchronized的异同点?.md
2024-10-16 00:20:59 +08:00

182 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
42 AtomicInteger 和 synchronized 的异同点?
在上一课时中,我们说明了原子类和 synchronized 关键字都可以用来保证线程安全,在本课时中,我们首先分别用原子类和 synchronized 关键字来解决一个经典的线程安全问题,给出具体的代码对比,然后再分析它们背后的区别。
代码对比
首先,原始的线程不安全的情况的代码如下所示:
public class Lesson42 implements Runnable {
static int value = 0;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Lesson42();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(value);
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
value++;
}
}
}
在代码中我们新建了一个 value 变量并且在两个线程中对它进行同时的自加操作每个线程加 10000 然后我们用 join 来确保它们都执行完毕最后打印出最终的数值
因为 value++ 不是一个原子操作所以上面这段代码是线程不安全的具体分析详见第 6 所以代码的运行结果会小于 20000例如会输出 14611 等各种数字
我们首先给出方法一也就是用原子类来解决这个问题代码如下所示
public class Lesson42Atomic implements Runnable {
static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Lesson42Atomic();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(atomicInteger.get());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
atomicInteger.incrementAndGet();
}
}
}
用原子类之后我们的计数变量就不再是一个普通的 int 变量了而是 AtomicInteger 类型的对象并且自加操作也变成了 incrementAndGet 由于原子类可以确保每一次的自加操作都是具备原子性的所以这段程序是线程安全的所以以上程序的运行结果会始终等于 20000
下面我们给出方法二我们用 synchronized 来解决这个问题代码如下所示
public class Lesson42Syn implements Runnable {
static int value = 0;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Lesson42Syn();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(value);
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (this) {
value++;
}
}
}
}
它与最开始的线程不安全的代码的区别在于 run 方法中加了 synchronized 代码块就可以非常轻松地解决这个问题由于 synchronized 可以保证代码块内部的原子性所以以上程序的运行结果也始终等于 20000是线程安全的
方案对比
下面我们就对这两种不同的方案进行分析
第一点我们来看一下它们背后原理的不同
在第 21 课时中我们详细分析了 synchronized 背后的 monitor 也就是 synchronized 原理同步方法和同步代码块的背后原理会有少许差异但总体思想是一致的在执行同步代码之前需要首先获取到 monitor 执行完毕后再释放锁
而我们在第 39 课时中介绍了原子类它保证线程安全的原理是利用了 CAS 操作从这一点上看虽然原子类和 synchronized 都能保证线程安全但是其实现原理是大有不同的
第二点不同是使用范围的不同
对于原子类而言它的使用范围是比较局限的因为一个原子类仅仅是一个对象不够灵活 synchronized 的使用范围要广泛得多比如说 synchronized 既可以修饰一个方法又可以修饰一段代码相当于可以根据我们的需要非常灵活地去控制它的应用范围
所以仅有少量的场景例如计数器等场景我们可以使用原子类而在其他更多的场景下如果原子类不适用那么我们就可以考虑用 synchronized 来解决这个问题
第三个区别是粒度的区别
原子变量的粒度是比较小的它可以把竞争范围缩小到变量级别通常情况下synchronized 锁的粒度都要大于原子变量的粒度如果我们只把一行代码用 synchronized 给保护起来的话有一点杀鸡焉用牛刀的感觉
第四点是它们性能的区别同时也是悲观锁和乐观锁的区别
因为 synchronized 是一种典型的悲观锁而原子类恰恰相反它利用的是乐观锁所以我们在比较 synchronized AtomicInteger 的时候其实也就相当于比较了悲观锁和乐观锁的区别
从性能上来考虑的话悲观锁的操作相对来讲是比较重量级的因为 synchronized 在竞争激烈的情况下会让拿不到锁的线程阻塞而原子类是永远不会让线程阻塞的不过虽然 synchronized 会让线程阻塞但是这并不代表它的性能就比原子类差
因为悲观锁的开销是固定的也是一劳永逸的随着时间的增加这种开销并不会线性增长
而乐观锁虽然在短期内的开销不大但是随着时间的增加它的开销也是逐步上涨的
所以从性能的角度考虑它们没有一个孰优孰劣的关系而是要区分具体的使用场景在竞争非常激烈的情况下推荐使用 synchronized而在竞争不激烈的情况下使用原子类会得到更好的效果
值得注意的是synchronized 的性能随着 JDK 的升级也得到了不断的优化synchronized 会从无锁升级到偏向锁再升级到轻量级锁最后才会升级到让线程阻塞的重量级锁因此synchronized 在竞争不激烈的情况下性能也是不错的不需要谈虎色变”。