CAS自旋锁与synchronized关键字的使用与区别

Posted 小鹏说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CAS自旋锁与synchronized关键字的使用与区别相关的知识,希望对你有一定的参考价值。

问题引入

本例子想要表达的是,count++这行代码在汇编级别不是原子操作
要想解决这个问题,有两种方式:
1、可以在count++外面加上synchronized代码块
2、利用Java内部提供的AtomicInteger类

 1 package com.lzp.juc.cas;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 import java.util.concurrent.CountDownLatch;
 6 
 7 /**
 8  * @Author LZP
 9  * @Date 2021/6/25 21:46
10  * @Version 1.0
11  */
12 public class ProblemTest {
13 
14     /**
15      * 本例子想要表达的是,count++这行代码在汇编级别不是原子操作
16      * 要想解决这个问题,有两种方式:
17      * 1、可以在count++外面加上synchronized代码块
18      * 2、利用Java内部提供的AtomicInteger类
19      */
20     int count;
21 
22     void m() {
23         for (int i = 0; i < 10000; i++) {
24             count++;
25         }
26     }
27 
28     public static void main(String[] args) {
29         final int num = 10;
30 
31         CountDownLatch cdl = new CountDownLatch(num);
32 
33         ProblemTest v = new ProblemTest();
34 
35         List<Thread> threads = new ArrayList<>();
36 
37         for (int i = 0; i < num; i++) {
38             threads.add(new Thread(() -> {
39                 v.m();
40                 cdl.countDown();
41             }, "thread-" + i));
42         }
43 
44         // 启动线程
45         threads.forEach(Thread::start);
46 
47         try {
48             cdl.await();
49         } catch (InterruptedException e) {
50             e.printStackTrace();
51         }
52 
53         System.out.println(v.count);
54     }
55 
56 }

方式一:加synchronized关键字

 1 package com.lzp.juc.cas;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 import java.util.concurrent.CountDownLatch;
 6 
 7 /**
 8  * @Author LZP
 9  * @Date 2021/6/25 22:01
10  * @Version 1.0
11  *
12  * 示例一
13  * 10个线程
14  * 100000
15  * 总耗时:22ms
16  *
17  * 示例二
18  * 1000个线程
19  * 10000000
20  * 总耗时:502ms
21  *
22  * 示例三
23  * 10000个线程
24  * 100000000
25  * 总耗时:1602ms
26  *
27  * 通过三个数量级的测试与比较,不难发现:当线程数量少时,用CAS(自旋锁)效率较高,而当线程数量达到10000时,
28  * 即此时线程数量已经很多了,发现加了synchronized代码块的代码竟然比用AtomicInteger的效率更高
29  * 总结:
30  * 当线程数量较少,且线程任务执行时间也较短时,用CAS更好
31  * 当线程数量较多时(即竞争比较激烈),且线程任务执行时间也较长时,直接用synchronized关键字会更好,因为
32  * synchronized底层其实是从用户态切换到内核态,去向操作系统申请一把重量级锁,这时,没有抢到锁的其他所有
33  * 线程就只能进入等待队列,等当前拿到这把锁的线程执行完,并将该锁释放之后,才会被再次唤醒激活,去再次竞争
34  * 这把锁。
35  *
36  */
37 public class Solution1 {
38 
39     int count;
40 
41     void m() {
42         for (int i = 0; i < 10000; i++) {
43             synchronized (this) {
44                 count++;
45             }
46         }
47     }
48 
49     public static void main(String[] args) {
50         final int num = 10000;
51 
52         CountDownLatch cdl = new CountDownLatch(num);
53 
54         Solution1 v = new Solution1();
55 
56         List<Thread> threads = new ArrayList<>();
57 
58         for (int i = 0; i < num; i++) {
59             threads.add(new Thread(() -> {
60                 v.m();
61                 cdl.countDown();
62             }, "thread-" + i));
63         }
64 
65         long start = System.currentTimeMillis();
66 
67         // 启动线程
68         threads.forEach(Thread::start);
69 
70         try {
71             cdl.await();
72         } catch (InterruptedException e) {
73             e.printStackTrace();
74         }
75 
76         System.out.println(v.count);
77 
78         long end = System.currentTimeMillis();
79 
80         System.out.println("总耗时:" + (end - start));
81     }
82 
83 }

方式二:使用Java中的AtomicInteger原子类操作API,底层实现:CAS

 1 package com.lzp.juc.cas;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 import java.util.concurrent.CountDownLatch;
 6 import java.util.concurrent.atomic.AtomicInteger;
 7 
 8 /**
 9  * @Author LZP
10  * @Date 2021/6/25 22:01
11  * @Version 1.0
12  *
13  * 使用AtomicInteger类
14  * CAS 自旋锁、乐观锁
15  *
16  * 示例一
17  * 10个线程
18  * 100000
19  * 总耗时:6ms
20  *
21  * 示例二
22  * 1000个线程
23  * 10000000
24  * 总耗时:216ms
25  *
26  * 示例三
27  * 10000个线程
28  * 100000000
29  * 总耗时:2929ms
30  */
31 public class Solution2 {
32 
33     AtomicInteger atomicInteger = new AtomicInteger(0);
34 
35     void m() {
36         for (int i = 0; i < 10000; i++) {
37             atomicInteger.incrementAndGet();
38         }
39     }
40 
41     public static void main(String[] args) {
42         final int num = 10000;
43 
44         CountDownLatch cdl = new CountDownLatch(num);
45 
46         Solution2 v = new Solution2();
47 
48         List<Thread> threads = new ArrayList<>();
49 
50         for (int i = 0; i < num; i++) {
51             threads.add(new Thread(() -> {
52                 v.m();
53                 cdl.countDown();
54             }, "thread-" + i));
55         }
56 
57         long start = System.currentTimeMillis();
58 
59         // 启动线程
60         threads.forEach(Thread::start);
61 
62         try {
63             cdl.await();
64         } catch (InterruptedException e) {
65             e.printStackTrace();
66         }
67 
68         System.out.println(v.atomicInteger.get());
69 
70         long end = System.currentTimeMillis();
71 
72         System.out.println("总耗时:" + (end - start) + "ms");
73     }
74 
75 }

发现:

通过三个数量级的测试与比较,不难发现:当线程数量少时,用CAS(自旋锁)效率较高,而当线程数量达到10000时,即此时线程数量已经很多了,发现加了synchronized代码块的代码竟然比用AtomicInteger的效率更高

总结:

当线程数量较少,且线程任务执行时间也较短时,用CAS更好
当线程数量较多,且线程任务执行时间也较长时,直接用synchronized关键字会更好,因为synchronized底层其实是从用户态切换到内核态,去向操作系统申请一把重量级锁,这时,没有抢到锁的其他所有线程就只能进入等待队列,等当前拿到这把锁的线程执行完,并将该锁释放之后,才会被再次唤醒激活,去再次竞争这把锁。

以上是关于CAS自旋锁与synchronized关键字的使用与区别的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程常见面试题-第一节:锁策略CAS和Synchronized原理

synchronized的锁升级(偏向锁,自旋锁(cas),重量级锁)

Java并发专题之三Java线程同步

Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

乐观锁与悲观锁