1、Lock
2、synchronized与Lock的区别
- 首先synchronized是java内置关键字(JVM层),Lock是个java类(API层);
- synchronized无法判断是否获取锁的状态(隐式锁),Lock可以判断是否获取到锁(显式锁);
- synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
- synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、公平和非公平(两者皆可);
- Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
3、synchronized的使用方法(8种锁的案例实际体现的3个地方)
- 修饰实例方法,作用于当前实例,进入同步代码前需要先获取实例的锁。
- 修饰静态方法,作用于类的Class对象,进入修饰的静态方法前需要先获取类的Class对象的锁。
- 修饰代码块,需要指定加锁对象(记做lockobj),在进入同步代码块前需要先获取lockobj的锁。
4、悲观锁和乐观锁
4.1、悲观锁
-
在获取数据的时候会先加锁,确保数据不会被别的线程修改。
-
synchronized关键字和Lock的实现类都是悲观锁。
-
适合写操作多的场景,先加锁可以保证写操作时数据正确。
-
显式的锁定之后再操作同步资源。
4.2、乐观锁
-
乐观锁在使用数据时不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据;如果这个数据没有被更新,当前线程将自己修改的数据成功写入;如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作;乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
-
适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
-
乐观锁则直接去操作同步资源,是一种无锁算法。
-
乐观锁一般有两种实现方式:
1. 采用版本号机制 2. CAS(Compare-and-Swap,即比较并替换)算法实现
5、ReentrantLock
5.1、synchronized的局限性
synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由jvm实现,用户不需要显示的释放锁,非常方便,然而synchronized也有一定的局限性,例如:
- 当线程尝试获取锁的时候,如果获取不到锁会一直阻塞,这个阻塞的过程,用户无法控制
- 如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待
JDK1.5之后发布,加入java.util.concurrent包。包内提供了Lock类,用来提供更多扩展的加锁功能。Lock弥补了synchronized的局限,提供了更加细粒度的加锁功能。
5.2、 可重入锁(递归锁):
指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
Synchronized可重入的实现原理:
-
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
-
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
-
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
-
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
5.3、可中断锁
可中断锁是子线程在获取锁的过程中,是否可以相应线程中断操作。synchronized是不可中断的,ReentrantLock是可中断的。
5.4、公平锁和非公平锁
公平锁是指多个线程尝试获取同一把锁的时候,获取锁的顺序按照线程到达的先后顺序获取,而不是随机插队的方式获取。synchronized是非公平锁,而ReentrantLock是两种都可以实现,不过默认是非公平锁。
5.5、ReentrantLock的使用
- 创建锁:ReentrantLock lock = new ReentrantLock()
- 获取锁:lock.lock()
- 释放锁:lock.unlock()
- ReentrantLock锁申请等待限时:tryLock()
synchronized关键字获取锁的过程中,只能等待其他线程把锁释放之后才能够有机会获取到锁。
ReentrantLock提供了获取锁限时等待的方法tryLock()
,可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。 - ReentrantLock其他常用的方法
isHeldByCurrentThread:实例方法,判断当前线程是否持有ReentrantLock的锁。
获取锁的4种方法对比
获取锁的方法 | 是否立即响应(不会阻塞) | 是否响应中断 |
---|---|---|
lock() | × | × |
lockInterruptibly() | × | √ |
tryLock() | √ | × |
tryLock(long timeout, TimeUnit unit) | × | √ |
5.6、总结
- ReentrantLock可以实现公平锁和非公平锁。
- ReentrantLock默认实现的是非公平锁。
- ReentrantLock的获取锁和释放锁必须成对出现,锁了几次,也要释放几次。
- 释放锁的操作必须放在finally中执行。
- lockInterruptibly()实例方法可以相应线程的中断方法,调用线程的interrupt()方法时,lockInterruptibly()方法会触发
InterruptedException
异常。 - 关于
InterruptedException
异常说一下,看到方法声明上带有throws InterruptedException
,表示该方法可以相应线程中断,调用线程的interrupt()方法时,这些方法会触发InterruptedException
异常,触发InterruptedException时,线程的中断中断状态会被清除。所以如果程序由于调用interrupt()
方法而触发InterruptedException
异常,线程的标志由默认的false变为ture,然后又变为false。 - 实例方法tryLock()会尝试获取锁,会立即返回,返回值表示是否获取成功。
- 实例方法tryLock(long timeout, TimeUnit unit)会在指定的时间内尝试获取锁,指定的时间内是否能够获取锁,都会返回,返回值表示是否获取锁成功,该方法会响应线程的中断。
6、死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
6.1、 产生死锁主要原因
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
6.2、 如何排查死锁
- 纯命令
jps -l
jstack 进程编号
- 图形化
jconsole工具