为什么需要Lock?

为什么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
46
47
48
49
50
51
52
53
public class Test {

static Resource resource1 = new Resource();
static Resource resource2 = new Resource();

public static void main(String[] args) {
new Thread(new task(true)).start();
new Thread(new task(false)).start();
}
}

class Resource{

}

class task implements Runnable{

boolean flag;

public task(boolean flag) {
this.flag = flag;
}

public void run() {
if(flag){
synchronized (Test.resource1){
System.out.println("资源1已被锁定,准备去获取资源2");
try {
//给机会给另一个线程能锁住
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Test.resource2){
System.out.println("资源2已被锁定");
}
}
}else {
synchronized (Test.resource2){
System.out.println("资源2已被锁定,准备去获取资源1");
try {
//给机会给另一个线程能锁住
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Test.resource1){
System.out.println("资源1已被锁定");
}
}
}
}
}
Lock四个获取锁的方法
  • lock():
    • 最普通的获取锁,如果锁已被其他线程获取,则等待
    • Lock不会像synchronized一样在异常时自动释放锁,需要自己释放
    • 所以需要在finally中释放锁,保证发生异常锁一定会释放
    • lock()方法不能被中断,一旦陷入死锁,lock()就会陷入永久等待
  • tryLock():
    • 用来尝试获取锁,如果当前锁没有被其他线程占用,则获取成功返回true,否则返回false
    • 相比lock,功能要强大一点,而且可以根据返回值决定后续程序行为
    • 返回值会立即返回,不会因为拿不到锁而等待
  • tryLock(long time,TimeUnit unit):
    • 超时放弃
  • lockInterruptibly():
    • 相当于把tryLock(long time,TimeUnit unit)中的超时时间设置为无限,在等待锁的过程中线程可以被中断

锁的分类

locks

乐观锁和悲观锁

互斥同步锁的劣势

  • 阻塞和唤醒消耗的性能
  • 可能会永久阻塞,比如遇到无限循环、死锁等问题
  • 优先级反转,即使线程优先级高但是没拿到锁也会在拿到锁的优先级低的线程后执行
悲观锁
  • 认为自己在处理操作的时候有其他线程来干扰,会锁住被操作对象
  • java中悲观锁的实现就是synchronzed和Lock相关的类
  • 适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:
    • 临界区有IO操作
    • 临界区代码复杂或者循环量大
    • 临界区竞争非常激烈
乐观锁
  • 认为自己在处理操作的时候不会有其他线程来干扰,不会锁住被操作对象
  • 在更新时,会检查数据是否被其他线程修改过
  • 一般使用CAS算法实现
  • 典型例子是原子类、并发容器等
  • 适用于并发写入少,大部分是读取的场景,不加锁能让读取性能大幅提高

可重入锁

什么是可重入
  • 同一个线程可以多次获取同一把锁
好处
  • 避免死锁
  • 提高封装性

方法:

  • lock.getHoldCount()获取当前锁的次数

  • lock.isHeldByCurrentThread()可以看出锁是否被当前线程持有

  • lock.getQueueLength()返回当前正在等待这把锁的队列有多长

公平锁和非公平锁

什么是公平锁和非公平锁

公平指的是按照线程请求的顺序来分配锁;非公平锁指的是不完全按照请求的顺序,在一定的情况下可以插队,这样可以避免唤醒的空档期

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
/**
* 演示公平和非公平锁
*/
public class FairLock {

public static void main(String[] args) {
PrintQueue printQueue = new PrintQueue();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(new Job(printQueue));
}

for (int i = 0; i < 10; i++) {
threads[i].start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

}

class Job implements Runnable {

PrintQueue printQueue;

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

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


class PrintQueue {
//创建一个公平锁
//true为公平,false为非公平
private Lock queueLock = new ReentrantLock(false);

public void printJob() {
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();
}

}
}

公平的时候:当线程0释放第一把锁之后,想拿第二把锁,但是后面9个线程已经先排队了,所以第二次执行应该是线程1,第二个轮回线程0才会拿到锁,才能执行完毕

非公平的时候:当线程0释放第一把锁之后本来应该由线程1来拿锁,但是由于唤醒线程1需要时间,所以依旧把锁给线程0使用,线程0直接执行完毕

公平锁:
  • 优势:各线程公平平等,每个线程等待一段时间后都有执行机会
  • 劣势:更慢,吞吐量更小

不公平锁:

  • 更快,吞吐量更大
  • 有可能产生现场饥饿,也就是某些线程在长时间内始终得不到执行

共享锁和排它锁

  • 排它锁,又称独占锁、独享锁,synchronized就是排它锁
  • 共享锁,又称读锁,获得共享锁后,可以查看但无法修改和删除数据,其他线程此时可以获取到共享锁,也是无法修改删除
  • 这两个的典型就是ReentrantReadWriteLock
  • 特点:要么多读,要么一写

例子

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
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

private static ReentrantReadWriteLock.ReadLock readLock;

private static ReentrantReadWriteLock.WriteLock writeLock;

static {
readLock = reentrantReadWriteLock.readLock();
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();

}
读写锁的插队策略
  • 公平锁:不允许插队
  • 非公平锁
    • 写锁可以随时插队
    • 读锁仅在等待队列头结点,不是想获取写锁的线程的时候可以插队,头结点如果是写锁就不会插队
  • 锁的升降级
    • 降级可以提高效率
    • 但是不允许升级,可能导致死锁

自旋锁和阻塞锁

自旋锁

  • 站着cpu不放,一直去检测

  • 如果锁被占用的时间很长,那么自旋只会白浪费处理器资源

  • 适合多核的服务器,并且并发度不能特别高,这种情况比阻塞锁效率高

  • 适合临界区比较短小的情况,临界区大的话线程拿到了锁很久才会释放

简单的自旋锁案例

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
private AtomicReference<Thread> sign = new AtomicReference<>();

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

public void unlock() {
Thread cur = Thread.currentThread();
sign.compareAndSet(cur, 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 {
System.out.println(Thread.currentThread().getName() + "释放了锁");
spinLock.unlock();
}
}
};

Thread thread2 = new Thread(runnable);
Thread thread1 = new Thread(runnable);

thread1.start();
thread2.start();
}

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

  • 缩小同步代码块
  • 尽量不要锁住方法
  • 减少请求锁的次数
  • 锁中不要再包含锁
  • 选择合适的锁类型和合适的工具类