java基础之多线程总结二(CAS和各种常用锁)

CAS

在java中,对很多常见的需要加锁的操作进行了封装,例如Atomic开头的一些类,这些类是线程安全的,但是内部却不是用synchronized加锁实现,而是CAS。
例如AtomicInteger的’incrementAndGet()’方法最终调用的实际是下边这个方法

1
2
3
4
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);

这个本地方法在一个比较特殊的类中,即Unsafe,这个类可以直接进行内存管理等操作,且源码可以看出来这是一个典型的单例模式。
CAS是compare and set的缩写,即比较并设值的意思,CAS之所以能保证线程安全,是因为这类操作是cpu原语支持的,执行过程中不能被打断。
CAS操作会有ABA问题,意思就是一个线程在操作的时候,另一个线程把数据修改了,然后又改了回去,单纯比较值似乎没有变化。
ABA问题对于基础数据类型的数据其实没有太大影响,如果不是基础类型,并且必须处理ABA问题,可以考虑增加版本号管理,在compare的时候连版本号一起比较。
就像AtomicInteger类,如果要处理ABA问题可以考虑使用AtomicStampedReference类。

Atomic这一类操作的CAS是无锁的,所以有的时候比synchronized的效率要高,而针对数据自增这种操作,jdk还自带了一些其他的类,例如LongAdder,LongAdder采用的也是CAS,但是是分段的,所以一些应用场景下可能性能更好,例如有这样一段测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* @Author tuzongxun
* @Date 2022/2/23
*/
public class LongAddrDemo {
private static Long count1 = 0L;

private static AtomicLong count2 = new AtomicLong(0);

private static LongAdder count3 = new LongAdder();

public static void main(String[] args) {
Thread[] threads = new Thread[1000];

Object o = new Object();
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
synchronized (o) {
count1++;
}
}
});
}

Long t1 = System.currentTimeMillis();
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
try {
threads[i].join();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
Long t2 = System.currentTimeMillis();
System.out.println("count1-synchronized:" + count1 + ":" + (t2 - t1));
//############################################
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
count2.incrementAndGet();
}
});
}

Long t11 = System.currentTimeMillis();
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
try {
threads[i].join();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
Long t12 = System.currentTimeMillis();
System.out.println("count2-Atomic:"+count2 + ":" + (t12 - t11));

//############################################
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
count3.increment();
}
});
}

Long t21 = System.currentTimeMillis();
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
try {
threads[i].join();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
Long t22 = System.currentTimeMillis();
System.out.println("count3-LongAdder:"+count3 + ":" + (t22 - t21));
}
}

上述代码运行后结果如下:

1
2
3
count1-synchronized:100000000:6060
count2-Atomic:100000000:2149
count3-LongAdder:100000000:499

很明显,针对当前的测试,LongAdder的性能高于AtomicLong,AtomicLong的性能要高于synchronized。
但是需要注意的是,上边的结论只是针对于当前场景,并不是说什么时候都是这样,具体应用的时候还需要进行实际分析和测试确定。

各种锁

jdk中有很多使用CAS实现的锁,例如ReentrantLock,ReenTrantLock相比synchronized更加灵活,不过从实现上来说可能稍微麻烦些,其中有一点就是需要手动解锁,例如如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @Author tuzongxun
* @Date 2022/2/23
*/
public class LockDemo {
private static int count=0;

public static void main(String[] args) {
ReentrantLock lock=new ReentrantLock();
Thread t1=new Thread(()->{
lock.lock();
for (int i = 0; i < 1000000; i++) {
count++;
}
lock.unlock();
});
Thread t2=new Thread(()->{
lock.lock();
for (int i = 0; i < 1000000; i++) {
count++;
}
lock.unlock();
});
try {
t1.start();
t2.start();
t1.join();
t2.join();
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}

上边的代码总能输出2000000,是线程安全的。需要注意的是,这里的unlock最后必须手动调用,上边的代码比较简单,所以没有过多处理,正常来说应该加入到finally代码块中以保证一定被调用。
之所以说ReentrantLock灵活,是因为它可以被打断,使用lockInterruptibly(),在创建lock对象的时候,还可以选择使用公平锁还是非公平锁,默认是非公平的,如果要公平,则可以在后边参数中传true,例如:

1
ReentrantLock lock=new ReentrantLock(true);

所谓的公平锁可以简单地理解为先来后到,而不是来了就直接·抢。

上边的代码,在main线程中使用了join方法等待两个线程结束,然后输出最终结果,实际上还可以用CountDownLatch替换这种写法,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @Author tuzongxun
* @Date 2022/2/24
*/
public class CountDownLatchDemo {
private static int count=0;

public static void main(String[] args) {
ReentrantLock lock=new ReentrantLock(true);
CountDownLatch cd=new CountDownLatch(2);
new Thread(()->{
lock.lock();
for (int i = 0; i < 1000000; i++) {
count++;
}
lock.unlock();
cd.countDown();
}).start();
new Thread(()->{
lock.lock();
for (int i = 0; i < 1000000; i++) {
count++;
}
lock.unlock();
cd.countDown();
}).start();
try {
cd.await();
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}

需要注意的是,CountDownLatch关键在于创建对象的时候定义的数量以及调用countDown方法,所以实际上可以在一个线程里多次调用countDown把数量减到零,这是需要写程序的时候自己控制的。

上边用到的ReentrantLock和synchronized比较类似,都是排他锁,这种锁在读多写少需要读写分离的场景中就有些不够用,相对来说效率也不够高,例如如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* @Author tuzongxun
* @Date 2022/2/24
*/
public class ReentrantLockDemo2 {
private static int count=0;
private static ReentrantLock reentrantLock=new ReentrantLock();
public static void readCount(Lock lock){
try{
lock.lock();
Thread.sleep(1000);
System.out.println(new Date() +"----"+count);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}

private static void writeCount(Lock lock){
try{
lock.lock();
Thread.sleep(1000);
count++;
System.out.println(new Date() +"----"+count);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}

public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
readCount(reentrantLock);
}).start();
}
for (int i = 0; i < 5; i++) {
new Thread(()->{
writeCount(reentrantLock);
}).start();
}
}
}

上述代码运行结果如下:

1
2
3
4
5
6
7
8
9
10
Thu Feb 24 20:32:38 CST 2022----0
Thu Feb 24 20:32:39 CST 2022----0
Thu Feb 24 20:32:40 CST 2022----0
Thu Feb 24 20:32:41 CST 2022----0
Thu Feb 24 20:32:42 CST 2022----0
Thu Feb 24 20:32:43 CST 2022----1
Thu Feb 24 20:32:44 CST 2022----2
Thu Feb 24 20:32:45 CST 2022----3
Thu Feb 24 20:32:46 CST 2022----4
Thu Feb 24 20:32:47 CST 2022----5

可以看到这里不论是读还是写,都会独自占用一秒时间,总共花费10秒。
实际上,有一种读写分离的锁可以使的读锁共享,写锁排他,从而在适当的应用场景下提升效率,例如上边代码可以改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* @Author tuzongxun
* @Date 2022/2/24
*/
public class ReentrantLockDemo2 {
private static int count=0;
private static ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
private static Lock readLock=reentrantReadWriteLock.readLock();
private static Lock writeLock=reentrantReadWriteLock.writeLock();

public static void readCount(Lock lock){
try{
lock.lock();
Thread.sleep(1000);
System.out.println(new Date() +"----"+count);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}

private static void writeCount(Lock lock){
try{
lock.lock();
Thread.sleep(1000);
count++;
System.out.println(new Date() +"----"+count);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}

public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
readCount(readLock);
}).start();
}
for (int i = 0; i < 5; i++) {
new Thread(()->{
writeCount(writeLock);
}).start();
}
}
}

上述代码运行结果如下:

1
2
3
4
5
6
7
8
9
10
Thu Feb 24 20:36:18 CST 2022----0
Thu Feb 24 20:36:18 CST 2022----0
Thu Feb 24 20:36:18 CST 2022----0
Thu Feb 24 20:36:18 CST 2022----0
Thu Feb 24 20:36:18 CST 2022----0
Thu Feb 24 20:36:19 CST 2022----1
Thu Feb 24 20:36:20 CST 2022----2
Thu Feb 24 20:36:21 CST 2022----3
Thu Feb 24 20:36:22 CST 2022----4
Thu Feb 24 20:36:23 CST 2022----5

可以看到这里实际上只花费了5秒,写的操作每个占用了1秒,所有读的操作都在同一秒内完成了。这里的代码和上边的相比,只是用了不同的锁。

jdk中还有一个类,可以实现指定数量的线程都到齐之后再开始运行,这个类就是CyclicBarrier,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* @Author tuzongxun
* @Date 2022/2/24
*/
public class CyclicBarrierDemo {

public static void main(String[] args) {
CyclicBarrier cb=new CyclicBarrier(20,()->{
System.out.println("数量凑齐,开始运行-------------------:"+new Date());
});

for (int i = 0; i <41 ; i++) {
System.out.println("创建线程:"+(i+1)+new Date());
new Thread(()->{
try {
cb.await();
System.out.println("开始运行:"+new Date());
}
catch (InterruptedException e) {
e.printStackTrace();
}
catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

上述代码输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
创建线程:1Thu Feb 24 19:52:27 CST 2022
创建线程:2Thu Feb 24 19:52:28 CST 2022
创建线程:3Thu Feb 24 19:52:29 CST 2022
创建线程:4Thu Feb 24 19:52:30 CST 2022
创建线程:5Thu Feb 24 19:52:31 CST 2022
创建线程:6Thu Feb 24 19:52:32 CST 2022
创建线程:7Thu Feb 24 19:52:33 CST 2022
创建线程:8Thu Feb 24 19:52:34 CST 2022
创建线程:9Thu Feb 24 19:52:35 CST 2022
创建线程:10Thu Feb 24 19:52:36 CST 2022
创建线程:11Thu Feb 24 19:52:37 CST 2022
创建线程:12Thu Feb 24 19:52:38 CST 2022
创建线程:13Thu Feb 24 19:52:39 CST 2022
创建线程:14Thu Feb 24 19:52:40 CST 2022
创建线程:15Thu Feb 24 19:52:41 CST 2022
创建线程:16Thu Feb 24 19:52:42 CST 2022
创建线程:17Thu Feb 24 19:52:43 CST 2022
创建线程:18Thu Feb 24 19:52:44 CST 2022
创建线程:19Thu Feb 24 19:52:45 CST 2022
创建线程:20Thu Feb 24 19:52:46 CST 2022
数量凑齐,开始运行-------------------:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
开始运行:Thu Feb 24 19:52:46 CST 2022
创建线程:21Thu Feb 24 19:52:47 CST 2022
创建线程:22Thu Feb 24 19:52:48 CST 2022
创建线程:23Thu Feb 24 19:52:49 CST 2022
创建线程:24Thu Feb 24 19:52:50 CST 2022
创建线程:25Thu Feb 24 19:52:51 CST 2022
创建线程:26Thu Feb 24 19:52:52 CST 2022
创建线程:27Thu Feb 24 19:52:53 CST 2022
创建线程:28Thu Feb 24 19:52:54 CST 2022
创建线程:29Thu Feb 24 19:52:55 CST 2022
创建线程:30Thu Feb 24 19:52:56 CST 2022
创建线程:31Thu Feb 24 19:52:57 CST 2022
创建线程:32Thu Feb 24 19:52:58 CST 2022
创建线程:33Thu Feb 24 19:52:59 CST 2022
创建线程:34Thu Feb 24 19:53:00 CST 2022
创建线程:35Thu Feb 24 19:53:01 CST 2022
创建线程:36Thu Feb 24 19:53:02 CST 2022
创建线程:37Thu Feb 24 19:53:03 CST 2022
创建线程:38Thu Feb 24 19:53:04 CST 2022
创建线程:39Thu Feb 24 19:53:05 CST 2022
创建线程:40Thu Feb 24 19:53:06 CST 2022
数量凑齐,开始运行-------------------:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
开始运行:Thu Feb 24 19:53:06 CST 2022
创建线程:41Thu Feb 24 19:53:07 CST 2022

从上述结果可以看出,只有20个线程都准备好了之后才会开始运行,并且这个程序如果不手动关闭,则会一直处理运行等待状态。

jdk中还有一个线程相关的类,可以实现类似限流的操作,可以设定允许同时运行的线程数量,这个类就是Semaphore,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @Author tuzongxun
* @Date 2022/2/24
*/
public class SemaphoreDemo {

public static void main(String[] args) {
Semaphore sd=new Semaphore(2);
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
sd.acquire();
System.out.println(new Date());
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}finally {
sd.release();
}
}).start();
}
}
}

这里创建Semaphore对象时传入了参数2,意思就是同时运行有2个线程运行,上述代码执行结果如下:

1
2
3
4
5
6
7
8
9
10
Thu Feb 24 20:48:57 CST 2022
Thu Feb 24 20:48:57 CST 2022
Thu Feb 24 20:48:58 CST 2022
Thu Feb 24 20:48:58 CST 2022
Thu Feb 24 20:48:59 CST 2022
Thu Feb 24 20:48:59 CST 2022
Thu Feb 24 20:49:00 CST 2022
Thu Feb 24 20:49:00 CST 2022
Thu Feb 24 20:49:01 CST 2022
Thu Feb 24 20:49:01 CST 2022

可以看到,每秒只有两个结果是一样的。如果把上边对象的2改成5,则运行结果如下:

1
2
3
4
5
6
7
8
9
10
Thu Feb 24 20:53:21 CST 2022
Thu Feb 24 20:53:21 CST 2022
Thu Feb 24 20:53:21 CST 2022
Thu Feb 24 20:53:21 CST 2022
Thu Feb 24 20:53:21 CST 2022
Thu Feb 24 20:53:22 CST 2022
Thu Feb 24 20:53:22 CST 2022
Thu Feb 24 20:53:22 CST 2022
Thu Feb 24 20:53:22 CST 2022
Thu Feb 24 20:53:22 CST 2022

即同一秒有5个线程在运行。
需要注意的是,这个类也是支持公平锁和非公平锁的,默认是非公平,如果要使用公平锁,则可以这样增加第二个参数,设置为true,例如:

1
Semaphore sd=new Semaphore(5,true);

除了上述加锁用法,还有一个也比较常用的锁相关的类LockSupport,用法示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @Author tuzongxun
* @Date 2022/2/24
*/
public class LockSupportDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 10; i++) {
if (i == 3) {
LockSupport.park();
}
System.out.println(i + ":" + new Date());
}
});
t.start();
try {
Thread.sleep(3000);
LockSupport.unpark(t);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}

上述代码输出结果如下:

1
2
3
4
5
6
7
8
9
10
0:Thu Feb 24 23:58:50 CST 2022
1:Thu Feb 24 23:58:50 CST 2022
2:Thu Feb 24 23:58:50 CST 2022
3:Thu Feb 24 23:58:53 CST 2022
4:Thu Feb 24 23:58:53 CST 2022
5:Thu Feb 24 23:58:53 CST 2022
6:Thu Feb 24 23:58:53 CST 2022
7:Thu Feb 24 23:58:53 CST 2022
8:Thu Feb 24 23:58:53 CST 2022
9:Thu Feb 24 23:58:53 CST 2022

可以很明显的看到线程在park加锁后就进入了阻塞状态,在调用了unpark之后才开始继续运行。
线程锁相关的用法很多,各种锁都有自己的适用场景,没有绝对的哪个更好,甚至有的时候可能用哪个都差不多,这些都需要具体需要的时候分析以及测试。

推荐文章