Java并发成神之路-精通JUC并发工具十八般武艺——Lock锁(三)

Lock接口

简介、地位、作用

  • 锁是一种工具,用于控制对共享资源的访问
  • Lock和synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又有较大的不同
  • Lock并不是用来代替synchronized的,而是当使用synchronized不合适或不满足要求的时候,来提供高级功能
  • Lock接口最常见的实现类就是ReentrantLock
  • 通常情况下,Lock只允许一个线程来访问这个共享资源,不过有的时候,一些特殊的实现也可以运行并发访问,比如ReadWriteLock里面的ReadLock

为什么synchronized不够用?为什么需要Lock锁?

  • 效率低:锁的释放情况少,试图获得锁时不能设定超时、不能中断一个正在试图获得锁的过程
  • 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
  • 无法知道是否成功获取到锁

方法介绍

  • 在Lock中声明了四个方法来获取锁
  • lock()tryLock()tryLock(long time,TimeUnit unit)lockInterruptibly()

四个方法的区别

lock()

  • lock()就是最普通的获取锁,如果锁已经被其他线程获取,则进行等待
  • Lock不会想synchronized一样在异常时自动释放锁
  • 因此最佳实践是,在finally中释放锁,以保证发生异常时锁一定被释放
  • lock()方法不能被中断,这会带来很大隐患:一旦陷入死锁,lock()就会陷入永远等待

tryLock()

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

tryLock(long time,TimeUnit unit)

  • 相比与tryLock()方法,增加了超时时间,和时间类型两个参数
  • 尝试获取锁,并不会立即返回,而是会等待指定时间,如果超时了就会返回,或者拿到锁了也会返回

lockInterruptibly()

  • 相当于TryLock(long time,TimeUnit unit)把超时时间设置为无限,在等待锁的过程中线程可以被中断

unlock()

  • 解锁

锁分类

  • 这些分类,是从各种不同角度出发去看的
  • 这些分类并不是互斥的,也就是多个类型可以并存:有可能一个锁,同时属于两种类型
  • 比如ReentrantLock即是互斥锁,又是可重入锁

Java中锁分类图

乐观锁和悲观锁

为什么还会诞生非互斥同步锁(乐观锁)——互斥同步锁(悲观锁)

互斥同步锁的劣势

  • 阻塞和唤醒带来的性能劣势(线程状态切换,上下文切换,检查是否有被阻塞线程等,都会带来性能损耗)
  • 永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那几个悲催的线程,将永远也得不到执行
  • 优先级反转(低优先级的线程先拿到了锁,高优先级的线程也只能等待它完成之后才能拿到锁)

什么是乐观锁和悲观锁

悲观锁

  • 如果我不锁柱这个资源,别人就会来争抢,就会造成数据结果错误,所以悲观锁为了确保结果正确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据内容万无一失
  • Java中悲观锁的实现就是synchronized和Lock相关类

乐观锁

  • 认为自己在操作的时候不会有其他线程来干扰,所以并不会锁住被操作对象
  • 在更新的时候,去对比在我修改期间数据有没有被其他人改过:如果没被改变过,就说明真的是只有我自己在操作,那我就正常去执行修改数据
  • 如果数据和我一开始拿到的不一样了,说明其他人在这段时间内修改过数据,那我就不能继续刚才的更新数据过程了,我会选择放弃修改、报错、重试等策略
  • 乐观锁的实现一般都是利用CAS算法来实现的

典型例子

  • 乐观锁的典型例子就是原子类、并发容器等
  • 悲观锁的典型例子就是synchronized和Lock相关类
  • Git:Git就是乐观锁的典型例子,当我们往远程仓库push的时候,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,不一样就会出现更新错误,这就是乐观锁的原理

开销对比

  • 悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响
  • 相反,虽然乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多

两种锁的使用场景

  • 悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:
    • 临界区有IO操作
    • 临界区代码复杂或者循环量大
    • 临界区竞争非常激烈
  • 乐观锁:适合并发写入少,大部分是读取的场景,不加锁能让读取性能大幅度提高

可重入锁和非可重入锁,以ReentrantLock为例(重点)

可重入性质

什么是可重入

  • 当一个线程获取到一把锁之后,可以再次获取这把锁,甚至多次获得这把锁,递归获取
  • ReentrantLock和synchronized都是可重入锁
  • 好处:可以避免死锁,比如需要多次获取当前锁的时候,不可能放弃再重新获取
  • 代码示例如下,递归进行调用,每次不需要重新获取锁
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class RecursionDemo {

    private static ReentrantLock lock = new ReentrantLock();

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

    如图所示,可重入锁,在线程再次获取锁的时候,会判断当前线程是否持有锁,如果有,就在标记上加1
    解锁的时候同理,标记减一,最后判断是否等于0,等于0表示彻底解锁
    不可重入锁则是直接判断锁的状态,已被持有直接返回

可重入锁和不可重入锁源码对比

ReentrantLock的其他方法介绍

  • isHeldByCurrentThread可以看出锁是否被当前线程持有
  • getQueueLength可以返回当前正在等待这把锁的队列有多长,一般这两个方法是开发和调试的时候使用,上线后不会使用

公平锁和非公平锁

什么是公平和非公平

  • 公平值得是按照线程请求的顺序,来分配锁
  • 非公平指的是,不完全按照请求的顺序,在一定情况下可以插队(并不是完全打乱顺序)
  • 注意:非公平也同样不提倡“插队”行为,这里的非公平,指的是“在合适的时机”插队,而不是盲目插队
  • 合适的时机:在被唤醒的空档期,有一个完全清醒的线程来获取锁,可以直接拿到

为什么要有非公平

  • 提高效率
  • 避免唤醒带来的空档期

公平情况(以ReentrantLock为例)

  • 如果创建ReentrantLock对象时,参数填写为true,那么这就是一个公平锁
  • 假设有1234,四个线程去获取锁,1拿到了锁,234就在队列里面等待,2完就是3,然后4

不公平情况(以ReentrantLock为例)

  • 还是上面的例子,假如在1释放锁的时候,线程5恰好去执行lock()
  • 由于ReentrantLock发现此时并没有线程持有lock这把锁(线程2还没有来得及获取,因为唤醒和获取需要时间)
  • 线程5可以插队,直接拿到这把锁,这也是ReentrantLock默认的公平策略,也就是“不公平”

代码示例

如下面代码所示,当ReentrantLock参数为true,设置为公平锁的时候
线程1打印完毕第一次之后,想要再次获取到锁,就会排队到线程10的后面,需要1到10挨个打印完第一伦之后,才会打印第二轮
当ReentrantLock参数为false,设置为非公平锁的时候
线程1打印完之后,就会尝试第二次获取锁,而这个时候线程2唤醒还需要时间,就会出现,线程1连着两次打印,线程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
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
public class FairLock {

public static void main(String[] args) throws InterruptedException {
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();
Thread.sleep(100);
}
}
}

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);
try {
Thread.sleep(duration*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
queueLock.unlock();
}
queueLock.lock();
try {
long duration = (long) (Math.random() * 10000);
System.out.println(Thread.currentThread().getName()+"正在打印,需要"+duration/1000);
try {
Thread.sleep(duration);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
queueLock.unlock();
}
}
}

特例

  • 针对tryLock()方法,它不遵守设定的公平规则(无论公平还是非公平)
  • 例如,当有线程在执行tryLock()的时候,一旦有线程释放了锁,那么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其他线程等待在队列里面了

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

优势 劣势
公平 各线程公平平等,每个线程在等待一段时间后,总有执行的机会 更慢,吞吐量更小
不公平 更快,吞吐量更大 有可能产生线程饥饿,也就是某些线程在长时间内,始终得不到执行

源码对比

公平锁和非公平锁的源码对比如下
可以看出,公平锁在线程获取锁的时候会先判断,队列中是否有任务,非公平锁则不会判断,直接就去获取锁

公平锁非公平锁源码对比

共享锁和排它锁:以ReentrantReadWriteLock读写锁为例(重点)

什么是共享锁和排他锁

  • 排它锁:又叫独占锁、独享锁
  • 共享锁:又称为读锁,获取共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据
  • 共享锁和排他锁的典型就是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁

读写锁的作用

  • 在没有读写锁之前,我们假设使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题
  • 在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是没有阻塞的,提高了程序的执行效率

读写锁的规则

  • 多个线程只申请读锁,都可以申请到
  • 如果又一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁
  • 如果又一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁
  • 总结:要么是一个或者多个线程同时读锁,要么是一个线程拥有写锁,但是两者不会同时出现(要么多读,要么一写)
  • 换一种理解思路:读写锁只是一把锁,可以通过两种方式锁定:
    • 读锁定和写锁定
    • 读写锁可以同时被一个或多个线程锁定,也可以被单一线程写锁定
    • 但是永远不能同时对这把锁进行读锁定和写锁定

      读写锁代码实现如下所示

      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
      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中的源码可以看出来,公平锁读写锁每次都会判断队列,在决定是否去获取锁
      非公平锁,写锁直接返回false,可以去进行插队,读锁会判断队列第一位是不是排它锁,也就是写锁

ReentrantReadWriteLock公平锁代码实现
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
46
47
48
49
50
51
52
53
54
55
56
57
58
public class NonfairBargeDemo {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static volatile List<String> list = new ArrayList<>();

private static void read() {
list.add(Thread.currentThread().getName() + "开始尝试获取读锁");
readLock.lock();
try {
list.add(Thread.currentThread().getName() + "得到了读锁,正在读取");
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
list.add(Thread.currentThread().getName() + "释放了读锁");
readLock.unlock();
}
}

private static void write() {
list.add(Thread.currentThread().getName() + "开始尝试获取写锁");
writeLock.lock();
try {
list.add(Thread.currentThread().getName() + "得到了写锁,正在写入");
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
list.add(Thread.currentThread().getName() + "释放了写锁");
writeLock.unlock();
}
}

public static void main(String[] args) throws InterruptedException {
new Thread(() -> write(), "Thread1").start();
new Thread(() -> read(), "Thread2").start();
new Thread(() -> read(), "Thread3").start();
new Thread(() -> read(), "Thread4").start();
new Thread(() -> read(), "Thread5").start();
new Thread(() -> write(), "Thread6").start();
new Thread(() -> read(), "Thread7").start();
new Thread(new Runnable() {
@Override
public void run() {
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> read(), "子线程创建的Thread" + i);
}
for (int i = 0; i < 1000; i++) {
threads[i].start();
}
}
}).start();
Thread.sleep(2000);
list.forEach(System.out::println);
}
}

插队演示代码如上所示,先将锁设置为非公平锁
读锁插队:我们先将创建线程获取到一个写锁,写锁完毕之后,会唤醒后面的读锁,后面跟了多个读锁,按照顺序唤醒,需要时间去执行,而这个时候,插队线程会不停的发出插队请求
具体结果如下图
线程1释放读锁之后,线程2被唤醒,然后线程158开始插队并插队成功,之后线程3和4拿到了读锁,然后线程159插队成功,线程5拿到读锁之后,线程5后面的线程6拿的是写锁
队列第一位为写锁的时候读锁不允许插队,所以后面的插队读请求全部插队失败

读锁插队结果示意图

如下图所示,当之前的读锁任务执行完毕之后,并没有其他线程来抢锁,直接由线程7写锁抢锁成功,之后的所有尝试获取读锁的线程都进行等待

写锁插队结果示意图

如果是公平锁,结果如下两图所示,线程1执行完毕之后,线程2、3、4、5开始执行,期间又线程进行尝试,但都在获取锁的时候进行等待
当线程2、3、4、5执行完毕,线程6开始执行,线程6释放锁之后,线程7才开始和其他线程一起执行读锁

公平锁抢锁结果示意图
公平锁抢锁结果示意图2

锁的升降级

  • 支持降级,不支持升级
  • 假设又两个线程同时持有读锁,其中一个想要进行升级,那么等另外一个线程执行完毕就可以进行升级,但是如果两个线程都想要升级,那么就会互相等待,陷入死锁(锁可以支持升级,只不过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
    46
    public class Upgrading {

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

    private static void readUpgrading(){
    readLock.lock();
    try {
    System.out.println(Thread.currentThread().getName()+"得到了读锁,正在读取");
    Thread.sleep(1000);
    System.out.println("升级会带来堵塞");
    writeLock.lock();
    System.out.println(Thread.currentThread().getName()+"成功获取到了写锁,升级成功");
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    System.out.println(Thread.currentThread().getName()+"释放了读锁");
    readLock.unlock();
    }
    }

    private static void writeUpgrading(){
    writeLock.lock();
    try {
    System.out.println(Thread.currentThread().getName()+"得到了写锁,正在写入");
    Thread.sleep(1000);
    readLock.lock();
    System.out.println("在不释放写锁的情况下直接获取读锁成功,锁降级成功");
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    readLock.lock();
    System.out.println(Thread.currentThread().getName()+"释放了写锁");
    writeLock.unlock();
    }
    }

    public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> writeUpgrading(), "Thread1");
    thread.start();
    thread.join();
    System.out.println("-----------------------");
    new Thread(()->readUpgrading(),"Thread2").start();
    }
    }

总结

  • ReentrantReadWriteLock实现了ReadWriteLock接口,最主要的有来给两个方法:readLock()writeLock()用来获取读锁和写锁
  • 锁的申请和释放策略
    • 多个线程只申请读锁,都可以申请到
    • 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请的线程会一直等待释放读锁
    • 如果有一个线程已经占用了写锁,则此时其他线程如果要申请读锁,则申请的线程会一直等待释放写锁
    • 要么是一个或者多个线程同时持有读锁,要么是一个线程持有写锁,但是两者不会同时出现
  • 插队策略:为了防止饥饿,读锁不能插队
  • 升降级策略:只能降级,不能升级
  • 使用场景:相比于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
public class SpinLock {

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

public void lock() {
Thread thread = Thread.currentThread();
while (!sign.compareAndSet(null, thread)) {
}
}

public void unlock() {
Thread thread = Thread.currentThread();
sign.compareAndSet(thread, 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()+"释放了自旋锁");
}
}
};
new Thread(runnable).start();
new Thread(runnable).start();
}
}

自旋锁的适用场景

  • 自旋锁一般用于多核服务器,在并发度不是特别高的情况下,比阻塞锁的效率高
  • 另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久之后才会释放),那也是不合适的

可中断锁:顾名思义,就是可以相应中断的锁

  • 在Java中synchronized就是不可中断锁,而Lock是可中断锁,因为tryLock(time)lockInterruptibly都能响应中断
  • 如果某一线程A正在执行锁中的代码,另一个线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以中断它,这种就是可中断锁

锁优化

Java虚拟机对锁的优化

  • 自旋锁和自适应(在自旋的时候会自动判断自旋的时间,也许自旋几次之后就会转为阻塞,会根据上次获取锁的情况进行判断)
  • 锁消除(jvm会判断对锁的使用,假设是在一个方法内部,根本不会出现并发情况,JVM会自动帮忙消除锁)
  • 锁粗化(虚拟机检测到在很短的代码内对同一个多不停的加锁解锁,就会将锁的范围粗化,减少加锁解锁的次数)

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

  • 缩小同步代码块
  • 尽量不要锁着方法
  • 减少请求锁的次数
  • 避免认为制造“热点”
  • 锁中尽量不要再包含锁
  • 选择合适的锁类型或合适的工具类