Lock接口

简介、地位、作用

锁是一种工具,用于控制对共享资源的访问

Lock和synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是使用上和功能上又有较大的不同,Lock并不是用来代替synchronized的,而是当使用synchronized不合适或不足以满足要求的时候,来提供高级功能的

Lock接口最常见的实现类是ReentrantLock,通常情况下,Lock只允许一个线程来访问这个共享资源,不过有的时候,一些特殊的实现也 可允许并发访问,比如ReadWriteLock里面的ReadLock

为什么synchronizd不够用为什么需要Lock

  1. 效率低

    锁的释放情况少、试图获得锁时不能设定超时,不能中断一个正在试图获得锁的线程

  2. 不够灵活

    加锁和释放锁的时机单一,每个锁仅有单一的条件(某个对象),这是不够的,而读写锁更灵活

  3. 无法知道是否成功 获得到锁

Lock主要方法介绍

在Lock中声明四个方法来获取锁

lock()、tryLock()、tryLock(long time,TimeUnit unit)和lockInterruptibly()

Lock

Lock()是最普通的获取锁,如果锁已经被其他线程获取,则进行等待

Lock不会像synchronized一样在异常时自动释放锁,因此最佳实践是在finally中释放锁,以保证异常时锁一定被释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 描述:
* Lock不会像synchronized一样,异常的时候自动释放锁,所以最佳实践是,finally中释放锁,以便保证发生异常的时候锁一定被释放
*/
public class MustUnlock {

private static Lock lock = new ReentrantLock();

public static void main(String[] args) {
lock.lock();
try{
//获取本锁保护的资源
System.out.println(Thread.currentThread().getName()+"开始执行任务");
}finally {
lock.unlock();
}
}
}

输出结果

1
main开始执行任务

Lock()方法不能被中断,这会带来很大的隐患,一旦陷入死锁,Lock会陷入永久等待

tryLock()

tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,则返回true,否则返回false,代表获取锁失败,相比于Lock,这样的方法显然功能更强大了,我们可以根据是否获取到锁来决定后续程序的行为,该方法会立即返回,即使在拿不到锁的时候也不会一直在等待

tryLock(long time,TimeUnit unit)

超时就放弃

lockInterruptibly()

相当于tryLock(long time,TimeUnit unit)把超时时间设置为无限

unlock()

解锁

可见性保证

Lock的加解锁和synchronized有同样的内存语意,也就是说,下一个线程加锁后可以看到所有前一个线程解锁前发生的所有操作

锁分类

这些分类是从各种不同的角度出发去看的

这些分类并不是互斥的,也就是多个类型可以并存,有可能一个锁,同时属于两种类型

比如ReentrantLock既是互斥锁,又是可重入锁

image-20200216221234030

乐观锁和悲观锁

乐观锁也称为非互斥同步锁,悲观锁也称为互斥同步锁

互斥锁同步锁的劣势

阻塞和唤醒带来的性能劣势

可能会陷入永久阻塞,如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那几个悲催的线程,将永远也得不到执行

会发生优先级反转

什么是乐观锁和悲观锁

从是否锁住资源的角度分类

悲观锁

如果一个线程不锁住资源,别的线程就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正确性,会在每次获取并修改数据的时候把锁锁住,让别人无法访问该数据,这样就可以确保数据内容万无一失

Java中悲观锁的实现就是synchronized和Lock相关类

image-20200217093018777 image-20200217093104111

乐观锁

认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作的对象

在更新的时候,对比用户修改的期间数据有没有被其他人改变过,如果没有改变过,就说明真的是只有用户自己在操作,此时,用户就正常区修改数据,如果数据和用户开始的时候得到的值不一样,说明其他人在这段时间内改变过数据,那就不继续更新数据的过程用户会选择放弃、报错、重试等策略

乐观锁的实现一般是利用CAS算法来实现的

image-20200217094556506 image-20200217094714552 image-20200217094819855 image-20200217094847840

典型例子

Java中悲观锁的实现就是synchronized和Lock相关类

乐观锁的典型例子就是原子类、并发容器

例子一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PessimismOptimismLock {

int a;

public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
}

public synchronized void testMethod() {
a++;
}

}

例子二

Git:Git就是乐观锁的典型例子,当向远端仓库push的时候,git就会检查远端仓库的版本是不是领先于现在的版本,如果远程仓库的版本号和本地的不一样,就表示有其他人修改了远端代码,这次提交就失败了,如果远端和本地版本号一致,就可以顺利提交版本到远端仓库

Git不适用悲观锁

例子三

select for update就是悲观锁,用version控制数据库就是乐观锁

添加一个字段lock_version(专门用来记录版本号的)

先查询这个更新语句的version:select * from table

然后update set num = 2,version = version + 1 where version = 1 and id = 5

如果version被更新了等于2,不一样就会出现更新错误,就是是乐观锁的原理

开销对比

悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁的时间很长,也不会对互斥锁造成影响

乐观锁一开始的开销笔悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多

使用场景

悲观锁

适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自选等消耗,典型情况:

  1. 临界区有IO操作
  2. 临界区代码复杂或循环量大
  3. 临界区竞争非常激烈

乐观锁

适合并发写入少,大部分是读写的场景,不加锁能让读取性能大幅提高

可重入锁和非可重入锁

ReentrantLock使用案例

案例一

image-20200217110015522 image-20200217110219956 image-20200217110300124
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
/**
* 描述: 演示多线程预定电影院座位
*/
public class CinemaBookSeat {

private static ReentrantLock lock = new ReentrantLock();

private static void bookSeat() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "开始预定座位");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "完成预定座位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

public static void main(String[] args) {
new Thread(() -> bookSeat()).start();
new Thread(() -> bookSeat()).start();
new Thread(() -> bookSeat()).start();
new Thread(() -> bookSeat()).start();
}
}

输出结果

1
2
3
4
5
6
7
8
Thread-0开始预定座位
Thread-0完成预定座位
Thread-1开始预定座位
Thread-1完成预定座位
Thread-2开始预定座位
Thread-2完成预定座位
Thread-3开始预定座位
Thread-3完成预定座位
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
/**
* 描述: 演示ReentrantLock的基本用法,演示被打断
*/
public class LockDemo {

public static void main(String[] args) {
new LockDemo().init();
}

private void init() {
final Outputer outputer = new Outputer();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
outputer.output("悟空");
}

}
}).start();

new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
outputer.output("大师兄");
}

}
}).start();
}

static class Outputer {

Lock lock = new ReentrantLock();

//字符串打印方法,一个个字符的打印
public void output(String name) {

int len = name.length();
lock.lock();
try {
for (int i = 0; i < len; i++) {
System.out.print(name.charAt(i));
}
System.out.println("");
} finally {
lock.unlock();
}
}
}
}

什么是可重入

可重入锁意味着:若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。

通俗来说:当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。

好处

避免死锁

可重入的性质

案例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class GetHoldCount {
private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
//getHoldCount()方法会显示出当前锁被获取几次
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.lock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
lock.unlock();
System.out.println(lock.getHoldCount());
}
}

输出结果

1
2
3
4
5
6
7
0
1
2
3
2
1
0

案例一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RecursionDemo {

private static ReentrantLock lock = new ReentrantLock();

private static void accessResource() {
lock.lock();
try {
System.out.println("已经对资源进行了处理");
if (lock.getHoldCount()<5) {
System.out.println(lock.getHoldCount());
accessResource();
System.out.println(lock.getHoldCount());
}
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
accessResource();
}
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
已经对资源进行了处理
1
已经对资源进行了处理
2
已经对资源进行了处理
3
已经对资源进行了处理
4
已经对资源进行了处理
4
3
2
1

源码对比:可重入锁ReentrantLock以及非可重入锁ThreadPoolExcutor的Worker类

image-20200217120854960

ReentrantLock的其他方法介绍

isHeldByCurrentThread

可以看出锁是否被当前线程持有

getQueueLength

可以返回当前正在等待这把锁的队列有多长

一般这两个方法是开发和调试时候使用

公平锁和非公平锁

什么是公平和非公平

公平指的是按照线程请求的顺序来分配锁

非公平指的是不完全按照请求的顺序,在一定情况下可以插队

注意:非公平也同样不提倡“插队”行为,这里的非公平指的是在“合适的时机”插队,而不是盲目的插队

为什么要有非公平锁

Java这样设计的目的,是为了提高效率,避免唤醒带来的空档期

在大多数情况下,由于唤醒的过程开销是比较大的,在这个期间为了增加吞吐量,来把这段唤醒开销时间利用出去,这就是非公平锁的根本原因

公平情况(以ReentrantLock为例)

ReentrantLock默认是非公平锁,如果在创建ReentrantLock对象时,参数填写为true,那么这就是一个公平锁

案例

假设线程1234是按顺序调用lock()的

后续等待的线程会到wait queue里,按照顺序依次执行

image-20200217132628631

在线程1执行unlock()释放锁之后,由于此时线程2的等待时间最久,所以线程2先得到锁,然后是线程3和线程4

image-20200217132907762

不公平的情况(以ReentrantLock为例)

如果在线程1释放锁的时候,线程5恰好去执行lock()

由于ReentrantLock发现此时并没有线程持有lock这把锁(线程2还没来得及获取到,因为获取需要时间)

线程5可以插队,直接拿到这把锁,这也是ReentrantLock默认的公平策略,也就是“不公平”

image-20200217133241108

代码案例:演示公平和非公平的效果

公平锁

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
/**
* 描述: 演示公平和不公平两种情况
*/
public class FairLock {

public static void main(String[] args) {
PrintQueue printQueue = new PrintQueue();
Thread thread[] = new Thread[10];
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Job(printQueue));
}
for (int i = 0; i < 10; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

class Job implements Runnable {

PrintQueue printQueue;

public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始打印");
printQueue.printJob(new Object());
System.out.println(Thread.currentThread().getName() + "打印完毕");
}
}

class PrintQueue {

private Lock queueLock = new ReentrantLock(true);

public void printJob(Object document) {
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}

queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration+"秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}

输出结果

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
Thread-0开始打印
Thread-0正在打印,需要4
Thread-1开始打印
Thread-2开始打印
Thread-3开始打印
Thread-4开始打印
Thread-5开始打印
Thread-6开始打印
Thread-7开始打印
Thread-8开始打印
Thread-9开始打印
Thread-1正在打印,需要6
Thread-2正在打印,需要4
Thread-3正在打印,需要2
Thread-4正在打印,需要4
Thread-5正在打印,需要1
Thread-6正在打印,需要6
Thread-7正在打印,需要1
Thread-8正在打印,需要10
Thread-9正在打印,需要4
Thread-0正在打印,需要4秒
Thread-0打印完毕
Thread-1正在打印,需要10秒
Thread-1打印完毕
Thread-2正在打印,需要9秒
Thread-2打印完毕
Thread-3正在打印,需要2秒
Thread-3打印完毕
Thread-4正在打印,需要6秒
Thread-4打印完毕
Thread-5正在打印,需要7秒
Thread-5打印完毕
Thread-6正在打印,需要4秒
Thread-6打印完毕
Thread-7正在打印,需要9秒
Thread-7打印完毕
Thread-8正在打印,需要6秒
Thread-8打印完毕
Thread-9正在打印,需要4秒
Thread-9打印完毕

非公平锁

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
/**
* 描述: 演示公平和不公平两种情况
*/
public class FairLock {

public static void main(String[] args) {
PrintQueue printQueue = new PrintQueue();
Thread thread[] = new Thread[10];
for (int i = 0; i < 10; i++) {
thread[i] = new Thread(new Job(printQueue));
}
for (int i = 0; i < 10; i++) {
thread[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

class Job implements Runnable {

PrintQueue printQueue;

public Job(PrintQueue printQueue) {
this.printQueue = printQueue;
}

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始打印");
printQueue.printJob(new Object());
System.out.println(Thread.currentThread().getName() + "打印完毕");
}
}

class PrintQueue {

private Lock queueLock = new ReentrantLock(false);

public void printJob(Object document) {
queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration);
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}

queueLock.lock();
try {
int duration = new Random().nextInt(10) + 1;
System.out.println(Thread.currentThread().getName() + "正在打印,需要" + duration+"秒");
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
queueLock.unlock();
}
}
}

输出结果

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
Thread-0开始打印
Thread-0正在打印,需要8
Thread-1开始打印
Thread-2开始打印
Thread-3开始打印
Thread-4开始打印
Thread-5开始打印
Thread-6开始打印
Thread-7开始打印
Thread-8开始打印
Thread-9开始打印
Thread-0正在打印,需要6秒
Thread-1正在打印,需要6
Thread-0打印完毕
Thread-1正在打印,需要10秒
Thread-1打印完毕
Thread-2正在打印,需要2
Thread-2正在打印,需要1秒
Thread-2打印完毕
Thread-3正在打印,需要10
Thread-3正在打印,需要3秒
Thread-3打印完毕
Thread-4正在打印,需要6
Thread-4正在打印,需要7秒
Thread-4打印完毕
Thread-5正在打印,需要2
Thread-5正在打印,需要3秒
Thread-5打印完毕
Thread-6正在打印,需要3
Thread-6正在打印,需要2秒
Thread-6打印完毕
Thread-7正在打印,需要8
Thread-7正在打印,需要6秒
Thread-7打印完毕
Thread-8正在打印,需要6
Thread-8正在打印,需要5秒
Thread-8打印完毕
Thread-9正在打印,需要2
Thread-9正在打印,需要5秒
Thread-9打印完毕

特例

针对tryLock()方法,它不遵循设定的公平的规则

当有线程执行tryLock()的时候,一旦有线程释放了锁,那么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其他线程在等待队列里

对比公平和非公平的优缺点

image-20200217162838514

源码分析

image-20200217162916201

先去判断是否前面有线程在队列中排队,如果没有则再次获取锁,非公平锁中没有这样的判断,也就是说不管有没有线程在队列中,都尝试获取锁

共享锁和排它锁

以ReentrantReadWriteLock读写锁为例

什么是共享锁和排它锁

排它锁

又称独占锁、独享锁,获取锁之后既能读也能写,此时其他线程再也没有办法获得排它锁,只能有获取排它锁的线程修改数据,保证了线程安全,synchronized就是排它锁

共享锁

又称为读锁,在获取共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据

共享锁和排它锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是排它锁

读写锁的作用

在没有读写锁之前,假设使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费一定的资源,因为多个读操作同时进行,并没有线程安全问题

在读的地方使用读锁,写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,提高了程序的执行效率

读写锁的规则

  1. 多个线程只申请读锁,都可以申请到
  2. 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
  3. 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁

总结

一句话总结,要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两个不会同时出现(要么多读,要么一写)

读写锁只是一把锁,可以通过两种方式锁定:读锁定和写锁定。读写锁可以同时被一个或者多个线程读锁定,也可以被单一线程写锁定,但是永远不能同时对这把锁进行读锁定和写锁定

ReentrantReadWriteLock具体用法

之前的情况,未使用读写锁

image-20200217182506087

现在用了读写锁,线程1和线程2可以同时用读锁,提高了效率

image-20200217182619470

当线程1和线程2都释放锁以后,线程3和线程4就可以写入了,但是只能有一个线程持有写锁

image-20200217182746211

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
/**
* 描述: TODO
*/
public class CinemaReadWrite {

private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

private static void read() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放读锁");
readLock.unlock();
}
}

private static void write() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放写锁");
writeLock.unlock();
}
}

public static void main(String[] args) {
new Thread(()->read(),"Thread1").start();
new Thread(()->read(),"Thread2").start();
new Thread(()->write(),"Thread3").start();
new Thread(()->write(),"Thread4").start();
}
}

输出结果

1
2
3
4
5
6
7
8
Thread1得到了读锁,正在读取
Thread2得到了读锁,正在读取
Thread2释放读锁
Thread1释放读锁
Thread3得到了写锁,正在写入
Thread3释放写锁
Thread4得到了写锁,正在写入
Thread4释放写锁

读锁和写锁的交互方式

插队策略

插队哪一个线程去执行,读线程可以插队,可以多个线程同时去读,这样提高了效率

升降级

读锁和写锁不是平等的,实际上写锁更加厉害,读锁相对没有那么厉害,可不可以直接升降级,可不可以在不释放写锁的情况下直接升级获取读锁,或者可不可以在持有读锁的时候可不可以升级成写锁

ReentrantReadWriteLock的实现

不允许读锁插队

允许降级,不允许升级

读锁插队策略

对于公平锁而言,如果是公平的就不要想插队了

对于非公平锁,假设线程2和线程4正在同时读取,线程3想要写入,拿不到锁,于是进入等待队列,线程5不在队列里,现在想要读取

策略1

读可以插队,效率高,但容易造成饥饿

image-20200217200730026

策略2

避免饥饿

image-20200217200947738

image-20200217201005490

策略的选择取决于锁的具体实现,ReentrantReadWriteLock的实现是选择策略2,是很明智的

总结

公平锁

不允许插队

非公平锁

写锁可以随时插队

读锁仅在等待队列头结点不是获取写锁的线程的时候,才可以插队

源码分析

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
/**
* Nonfair version of Sync
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
}

/**
* Fair version of Sync
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}

锁的升降级

为什么需要升降级

支持锁的降级,不支持升级

代码演示

共享锁和排它锁总结

  1. ReentrantReadWriteLock实现了ReadWriteLock接口,最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁

  2. 锁申请和释放策略

    • 多个线程只能申请读锁,都可以申请到

    • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁

    • 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放锁

    • 要么是一个或多个线程同时有读锁,要么是一个线程有些锁,但是两者不能同时出现

      总结:要么多读,要么一写

  3. 插队策略

    为了防止饥饿,读锁不能插队

  4. 升降级策略

    只能降级,不能升级

  5. 适用场合

    相比于ReentrantLock适用于一般场合,ReentrantReadWriteLock适用于读多写少的情况,合理使用可以进一步提高并发效率

自旋锁和阻塞锁

概念

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要消耗处理器时间,如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长,在许多场景中,同步资源锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失

如果物理机器有多个处理器能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁

而为了让当前线程去检测,需要让当前线程进行自旋,如果在自旋完成后,前面锁定同步资源已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁

阻塞锁和自旋锁相反,阻塞锁如果遇到没有得到锁的情况,会直接把线程阻塞,直到被唤醒

缺点

如果锁被占用的时间过长,那么自旋锁的线程只会白浪费处理器的资源,在自旋的过程中,一直消耗CPU,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋锁时间增加,开销也是线性增长的,开销也是线性增长的

原理和源码分析

在Java1.5版本及以上的并发框架java.util.concurrent的atomic包下的类基本都是使用自旋锁实现的

AtomicInteger的实现

自旋锁的实现原理是CAS,AtomicInteger中调用unsafe进行自增操作的源码中,do-while循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没有修改成功,就在while中死循环,直到修改成功

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
/**
* 描述: 自旋锁
*/
public class SpinLock {

private AtomicReference<Thread> sign = new AtomicReference<>();

public void lock() {
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null, current)) {
System.out.println("自旋获取失败,再次尝试");
}
}

public void unlock() {
Thread current = Thread.currentThread();
sign.compareAndSet(current, null);
}

public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
spinLock.lock();
System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
spinLock.unlock();
System.out.println(Thread.currentThread().getName() + "释放了自旋锁");
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
}
}

输出结果

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
Thread-0开始尝试获取自旋锁
Thread-0获取到了自旋锁
Thread-1开始尝试获取自旋锁
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
自旋获取失败,再次尝试
Thread-0释放了自旋锁
Thread-1获取到了自旋锁
Thread-1释放了自旋锁

适用场景

自旋锁一般用于多核的服务器,在并发不是特别高的情况下,比阻塞锁的效率高

另外,自旋锁适用于临界区比较小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放),则不合适

可中断锁

在Java中,synchronized就不是可中断锁,而Lock是可中断锁,因为tryLock(time)和lockInterruptibly都能响应中断

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,用户就可以中断它,这就是可中断锁

锁优化

Java虚拟机对锁的优化

自旋锁和自适应

在锁的临界区很小的情况下,可以提高效率

自适应是在尝试自旋的时候不是盲目尝试100万遍,而是尝试10次、20次,如果得不到锁,就转为阻塞锁

锁消除

有一些场景不必要加锁,某段代码就在方法内部的,并且所有同步的代码都在方法内部,此时不会有外部用户访问这个方法的内部资源,此时虚拟机会分析出来,认为这些资源是私有的,无需加锁,就把该锁消除了

锁粗化

在写代码的时候如何优化锁和提高并发性能

  1. 缩小同步代码块

  2. 尽量不要锁住方法

  3. 减少请求锁的次数

  4. 避免人为制造“热点”

  5. 锁中尽量不要再包含锁

  6. 选择合适的锁类型或合适的工具类

文章目录
  1. 1.
    1. 1.1. Lock接口
      1. 1.1.1. 简介、地位、作用
      2. 1.1.2. 为什么synchronizd不够用为什么需要Lock
      3. 1.1.3. Lock主要方法介绍
        1. 1.1.3.1. Lock
        2. 1.1.3.2. tryLock()
        3. 1.1.3.3. tryLock(long time,TimeUnit unit)
        4. 1.1.3.4. lockInterruptibly()
        5. 1.1.3.5. unlock()
      4. 1.1.4. 可见性保证
    2. 1.2. 锁分类
    3. 1.3. 乐观锁和悲观锁
      1. 1.3.1. 互斥锁同步锁的劣势
      2. 1.3.2. 什么是乐观锁和悲观锁
      3. 1.3.3. 典型例子
        1. 1.3.3.1. 例子一
        2. 1.3.3.2. 例子二
        3. 1.3.3.3. 例子三
      4. 1.3.4. 开销对比
      5. 1.3.5. 使用场景
    4. 1.4. 可重入锁和非可重入锁
      1. 1.4.1. 什么是可重入
      2. 1.4.2. 可重入的性质
        1. 1.4.2.1. 源码对比:可重入锁ReentrantLock以及非可重入锁ThreadPoolExcutor的Worker类
        2. 1.4.2.2. ReentrantLock的其他方法介绍
    5. 1.5. 公平锁和非公平锁
      1. 1.5.1. 什么是公平和非公平
      2. 1.5.2. 为什么要有非公平锁
      3. 1.5.3. 公平情况(以ReentrantLock为例)
      4. 1.5.4. 不公平的情况(以ReentrantLock为例)
      5. 1.5.5. 代码案例:演示公平和非公平的效果
      6. 1.5.6. 特例
      7. 1.5.7. 对比公平和非公平的优缺点
      8. 1.5.8. 源码分析
    6. 1.6. 共享锁和排它锁
      1. 1.6.1. 什么是共享锁和排它锁
      2. 1.6.2. 读写锁的作用
      3. 1.6.3. 读写锁的规则
      4. 1.6.4. ReentrantReadWriteLock具体用法
      5. 1.6.5. 读锁和写锁的交互方式
        1. 1.6.5.1. ReentrantReadWriteLock的实现
      6. 1.6.6. 读锁插队策略
        1. 1.6.6.1. 总结
      7. 1.6.7. 源码分析
      8. 1.6.8. 锁的升降级
      9. 1.6.9. 共享锁和排它锁总结
    7. 1.7. 自旋锁和阻塞锁
      1. 1.7.1. 概念
      2. 1.7.2. 缺点
      3. 1.7.3. 原理和源码分析
      4. 1.7.4. 适用场景
    8. 1.8. 可中断锁
    9. 1.9. 锁优化
      1. 1.9.1. Java虚拟机对锁的优化
      2. 1.9.2. 在写代码的时候如何优化锁和提高并发性能
|