JUC(4):Java "锁"事一览
一、Lock
1.1 概述
Lock 是 Java.util.concurrent.locks 包下的接口,Lock 实现提供了比 synchronized 关键字更广泛的锁操作,锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象
1.2 Lock 接口
Lock 是一个接口,接口的实现类有 ReentrantLock 和内部类 ReentrantReadWriteLock
public interface Lock
{
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
1.2.1 lock
lock () 方法是平常使用最多的一个方法,就是用来获取锁,如果锁已被其他线程获取,则进行等待。
采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用 Lock 必须在 try-catch 块中进行,并且将释放锁的操作放在 finally 块中进行,以保证锁一定被释放,防止死锁的发生。通常使用 Lock 来进行同步的话,是以下面这种形式去使用的。
Lock lock = ...;
lock.lock();
try{
//处理任务
}
catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
1.2.2 tryLock
该方法表示用来尝试获取锁,但是该方法是有返回值的,如果获取成功,则返回 true,如果获取失败,则返回 false,也就是说这个方法无论如何都会立即返回,在拿不到锁时也不会一直在那等待。
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
if(lock.tryLock()) {
try{
System.out.println("成功获取锁!!");
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
}else {
System.out.println("未获取锁,先干别的");
}
}
1.2.2 newCondition
关键字 synchronized 与 wait\notify 这两个方法可以一起使用实现等待通知功能,lock 锁的 newCondition 方法返回 Condition 对象,Condition 类也可以实现等待\通知模式。
用 notify()通知时,JVM 会随机唤醒某个等待的线程,使用 Condition 类可以进行选择性的通知。它有两个常用方法:
- await()会使当前线程等待,同时会释放锁,当其他线程调用 signal 时,线程会重新获得锁并继续执行。
- signal()用于唤醒一个等待的线程。
1.2.3 ReentrantLock(可重入锁)
ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更多的方法。下面通过一些实例看具体如何使用。
public class LockTest {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public static void main(String[] args) {
LockTest test = new LockTest();
new Thread(()->{test.insert(Thread.currentThread());}).start();
new Thread(()->{test.insert(Thread.currentThread());}).start();
new Thread(()->{test.insert(Thread.currentThread());}).start();
}
public void insert(Thread thread) {
Lock lock = new ReentrantLock(); //注意这个地方
lock.lock();
try {
System.out.println(thread.getName()+"得到了锁");
Thread.sleep(1000);
} catch (Exception e) {
// TODO: handle exception
}finally {
lock.unlock();
System.out.println(thread.getName()+"释放了锁");
}
}
}
ReentrantLock 意为可重入锁,关于可重入锁的概念将在后面详细讲述。它与 synchronized 一样,一个线程可以多次获取同一个锁。然而它比 synchronized 更安全,不会在 trylock 失败的时候发生死锁。
1.2.4 ReadWriteLock
ReadWriteLock 它是一个接口,在它里面只定义了两个方法。
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*/
Lock writeLock();
}
两个方法,一个用来获取写锁,另一个用来获取读锁。
也就是说将文件的读写操作分开,分成 2个锁来分配给线程,从而使得多个线程可以同时读操作,或者一个线程存在写操作,读写不能同时存在。
下面的 ReentrantReadWriteLock 实现了ReadWriteLock 接口。
该实现类里面提供了两个主要的方法:readLock()和 writeLock() 用来获取读锁和写锁;
public class ReadWriteLockTest {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final ReadWriteLockTest test = new ReadWriteLockTest();
new Thread(()->{test.get(Thread.currentThread());}).start();
new Thread(()->{test.get(Thread.currentThread());}).start();
new Thread(()->{test.set(Thread.currentThread());}).start();
}
public void get(Thread thread)
{
rwl.readLock().lock();
try {
System.out.println(thread.getName()+"正在进行读操作");
Thread.sleep(1000);
System.out.println(thread.getName()+"读操作完毕");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally
{
rwl.readLock().unlock();
}
}
public void set(Thread thread)
{
rwl.writeLock().lock();
try {
System.out.println(thread.getName()+"正在进行写操作");
Thread.sleep(2000);
System.out.println(thread.getName()+"写操作完毕");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally
{
rwl.writeLock().unlock();
}
}
}
线程1 和线程2 可以同时读取数据。而线程3写锁则互斥。
- 如果一个线程已经占用了读锁,其他线程要申请写锁,就要等待释放读锁;
- 如果一个线程已经占用了写锁,其他线程如果要申请读锁,就要等待释放写锁;
二、synchronized 与 Lock 的区别
1、首先 synchronized 是 Java 内置关键字,在 JVM 层面,Lock 是个 Java 类;
2、synchronized 无法判断是否获取锁的状态,Lock 可以判断是否获取到锁;
3、synchronized 会自动释放锁(a 线程执行完同步代码会释放锁;b 线程执行过程中发生异常会释放锁)
- Lock 需要在 finally 中手工释放锁(unLock方法释放锁),否则容易造成线程死锁。
4、用 synchronized 关键字的两个线程1 和线程2 ,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。
5、synchronized 是锁可重入、不可中断,非公平,而 Lock 锁可重入,可判断,可公平(两者皆可)
6、Lock 锁适合大量同步的代码的同步问题,synchronized 锁适合代码少量的同步问题。
三、synchronized
synchronized 的作用是保证在同一时刻,被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。
synchronized 是 Java 中解决并发问题的一个常用的方法。
主要作用有三个:
- 原子性:确保先互斥访问同步代码
- 可见性:保证共享变量的修改能够及时可见
- 有序性:解决重排序问题。
3.1 三种应用
1、修饰实例方法,作用于当前实例,进入同步代码前需要先获取实例的锁
2、修饰静态方法,作用于类的 Class 对象,进入修饰的静态方法前需要先获取类的 Class 对象的锁
3、修饰代码块,需要指定加锁对象(记作 lockobj),在进入同步代码块前需要先获取 lockobj 的锁
3.1.1 synchronized 作用于实例对象
synchronize作用于实例方法需要注意:
- 实例方法上加synchronized,线程安全的前提是,多个线程操作的是同一个实例,如果多个线程作用于不同的实例,那么线程安全是无法保证的
- 同一个实例的多个实例方法上有synchronized,这些方法都是互斥的,同一时间只允许一个线程操作同一个实例的其中的一个synchronized方法
3.1.2 synchronized 作用于静态方法
当synchronized作用于静态方法时,锁的对象就是当前类的Class对象。
3.1.3 synchronized 同步代码块
3.2字节码分析
- synchronized 同步代码块,实际使用的是 monitorenter 和 monitorexit 指令;
-
- 一般情况下就是1 个 enter 对应 2个 exit
- synchronized 普通同步方法
-
- javap -v class 文件反编译
- 调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程会将先持有 monitor 锁,然后再执行方法,最后在方法完成时释放 monitor
- synchronized 静态同步方法
-
- 比普通方法多了个 ACC_STATIC 锁,来判断是否静态同步方法
3.3 synchronized 底层原语分析
天生带着 Object 的属性。
四、悲观锁与悲观锁
4.1 悲观锁
认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
synchronized 关键字和 Lock 的实现类都是悲观锁。
- 适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 显式的锁定之后再操作同步资源。
//=============悲观锁的调用方式
public synchronized void m1()
{
//加锁后的业务逻辑......
}
// 保证多个线程使用的是同一个lock对象的前提下
ReentrantLock lock = new ReentrantLock();
public void m2() {
lock.lock();
try {
// 操作同步资源
}finally {
lock.unlock();
}
}
4.2 乐观锁
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有么有别的线程更新了这个数据。
如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再抢
乐观锁一般有两种实现方式:
- 采用版本号机制
- CAS(Compare-and-Swap,即比较并替换)算法实现
//=============乐观锁的调用方式
// 保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
五、八锁案例
原则:能用无锁就不要用锁。能用对象锁,就不用要类锁。
5.1 案例说明
线程操纵资源类
5.1.1 情况1
class Phone{
public synchronized void sendEmail(){
System.out.println("发送邮件!");
}
public synchronized void sendMsg(){
System.out.println("发送短信!");
}
}
public class Lock8 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
new Thread(()->{
phone.sendMsg();
},"b").start();
}
}
//发送邮件!
//发送短信!
5.1.2 情况2
在发送邮件方法中假如暂停 3秒钟,先打印哪个?
- 答:是用的同一个对象,锁的是同一个对象。
class Phone{
public synchronized void sendEmail(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发送邮件!");
}
public synchronized void sendMsg(){
System.out.println("发送短信!");
}
}
public class Lock8 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
new Thread(()->{
phone.sendMsg();
},"b").start();
}
}
======
发送邮件!
发送短信!
5.1.3 情况3
加入一个普通的方法。
答:hello 不用去拿锁
class Phone{
public synchronized void sendEmail(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发送邮件!");
}
public synchronized void sendMsg(){
System.out.println("发送短信!");
}
public void hello(){
System.out.println("hello!");
}
}
public class Lock8 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
new Thread(()->{
phone.hello();
},"b").start();
}
}
======
hello!
发送邮件!
5.1.4 情况4
两个对象
答:此时锁的不是同个对象
class Phone{
public synchronized void sendEmail(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发送邮件!");
}
public synchronized void sendMsg(){
System.out.println("发送短信!");
}
public void hello(){
System.out.println("hello!");
}
}
public class Lock8 {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
new Thread(()->{
phone2.sendMsg();
},"b").start();
}
}
====
发送短信!
发送邮件!
5.1.5 情况5
加上静态方法
答:静态方法锁的是类
class Phone{
public static synchronized void sendEmail(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发送邮件!");
}
public static synchronized void sendMsg(){
System.out.println("发送短信!");
}
public void hello(){
System.out.println("hello!");
}
}
public class Lock8 {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
new Thread(()->{
phone.sendMsg();
},"b").start();
}
}
==
发送邮件!
发送短信!
5.1.6 情况6
class Phone{
public static synchronized void sendEmail(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发送邮件!");
}
public static synchronized void sendMsg(){
System.out.println("发送短信!");
}
public void hello(){
System.out.println("hello!");
}
}
public class Lock8 {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
new Thread(()->{
phone2.sendMsg();
},"b").start();
}
}
====
发送邮件!
发送短信!
5.1.7 情况7
class Phone{
public static synchronized void sendEmail(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发送邮件!");
}
public synchronized void sendMsg(){
System.out.println("发送短信!");
}
public void hello(){
System.out.println("hello!");
}
}
public class Lock8 {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
new Thread(()->{
phone2.sendMsg();
},"b").start();
}
}
=====
发送短信!
发送邮件!
5.1.8 情况8
class Phone{
public static synchronized void sendEmail(){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发送邮件!");
}
public synchronized void sendMsg(){
System.out.println("发送短信!");
}
public void hello(){
System.out.println("hello!");
}
}
public class Lock8 {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
new Thread(()->{
phone.sendMsg();
},"b").start();
}
}
=====
发送短信!
发送邮件!
5.2 总结
由 情况1-2 得知:
一个对象里面如果有多个 synchronized 方法,某一时刻内,只要一个线程去调用其中的一个 synchronized 方法了。
其他的线程都只能等待,换句话说。某一个时刻内,只能有唯一的一个线程去访问这些 synchronized 方法。
锁的是当前对象的 this,被锁定后,其他的线程都不能进入当前对象的其他的 synchronized 方法。
加上静态方法,此时锁的是类。
六、公平锁和非公平锁
公平锁:
- 是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的
- Lock lock = new ReetrantLock(true) true 表示公平锁,先来先得。
非公平锁:
- 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某一个线程一直得不到锁)
- Lock lock = new ReetrantLock(false) 默认就是非公平锁,可以不写
七、可重入锁(递归锁)
可重入锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
如果是1个有 synchronzied 修饰的递归方法,程序第二次进入被自己阻塞了岂不是天大的笑话。所以Java 中 ReetrantLock 和 synchronized 都是可重入锁。
可重入锁的一个优点是可一定程度避免死锁。
7.1可重入锁这四个字分开来解析
可:可以。
重:再次
入:进入
锁:同步锁
一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁
7.2 可重入锁种类
1、隐式锁(synchronized 关键字使用的锁)默认是可重入锁
指的是可重复可递归调用的锁。
在外层使用锁之后,在内层仍然可以使用,并且不发生死锁。
简单的来说就是:在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
同步块:
public class ReEntryLockDemo{
public static void main(String[] args){
final Object objectLockA = new Object();
new Thread(() -> {
synchronized (objectLockA){
System.out.println("-----外层调用");
synchronized (objectLockA){
System.out.println("-----中层调用");
synchronized (objectLockA){
System.out.println("-----内层调用");
}
}
}
},"a").start();
}
}
同步方法:
public class ReEntryLockDemo{
public synchronized void m1(){
System.out.println("-----m1");
m2();
}
public synchronized void m2(){
System.out.println("-----m2");
m3();
}
public synchronized void m3(){
System.out.println("-----m3");
}
public static void main(String[] args){
ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
reEntryLockDemo.m1();
}
}
2、显示锁(Lock)也有 ReetrantLock 这样的可重入锁
public class Demo4 {
private static int num = 0;
private static ReentrantLock lock = new ReentrantLock();
private static void add() {
lock.lock();
lock.lock();
try {
num++;
} finally {
lock.unlock();
lock.unlock();
}
}
public static class T extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
Demo4.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
T t2 = new T();
T t3 = new T();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(Demo4.num);
}
}
上面代码中add()方法中,当一个线程进入的时候,会执行2次获取锁的操作,运行程序可以正常结束,并输出和期望值一样的30000,假如ReentrantLock是不可重入的锁,那么同一个线程第2次获取锁的时候由于前面的锁还未释放而导致死锁,程序是无法正常结束的。ReentrantLock命名也挺好的Re entrant Lock,和其名字一样,可重入锁。
代码中还有几点需要注意:
- lock()方法和unlock()方法需要成对出现,锁了几次,也要释放几次,否则后面的线程无法获取锁了;可以将add中的unlock删除一个事实,上面代码运行将无法结束
- unlock()方法放在finally中执行,保证不管程序是否有异常,锁必定会释放
/**
* @create 2020-05-14 11:59
* 在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的
*/
public class ReEntryLockDemo{
static Lock lock = new ReentrantLock();
public static void main(String[] args){
new Thread(() -> {
lock.lock();
try
{
System.out.println("----外层调用lock");
lock.lock();
try
{
System.out.println("----内层调用lock");
}finally {
// 这里故意注释,实现加锁次数和释放次数不一样
// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
lock.unlock(); // 正常情况,加锁几次就要解锁几次
}
}finally {
lock.unlock();
}
},"a").start();
new Thread(() -> {
lock.lock();
try
{
System.out.println("b thread----外层调用lock");
}finally {
lock.unlock();
}
},"b").start();
}
}
7.3 synchronized 可重入锁的实现机理
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行 monitorenter 时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java 虚拟机会将该所对象的持有线程设置为当前线程,并且将其计数器加1 .
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1 ,否则需要等待,直至持有线程释放该锁。
当执行 monitorexit 时,Java 将锁对象的计数器 减少1,计数器为零代表锁被释放。
八、死锁
死锁是指两个或两个以上线程在执行过程中,因为争抢资源而造成的一种互相等待的现象。
8.1 产生死锁的原因
1、系统资源不足
2、进程运行推进的顺序不合适
3、资源分配不当
public class DeadLockDemo{
public static void main(String[] args){
final Object objectLockA = new Object();
final Object objectLockB = new Object();
new Thread(() -> {
synchronized (objectLockA){
System.out.println(Thread.currentThread().getName()+"\t"+"自己持有A,希望获得B");
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (objectLockB)
{
System.out.println(Thread.currentThread().getName()+"\t"+"A-------已经获得B");
}
}
},"A").start();
new Thread(() -> {
synchronized (objectLockB){
System.out.println(Thread.currentThread().getName()+"\t"+"自己持有B,希望获得A");
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (objectLockA){
System.out.println(Thread.currentThread().getName()+"\t"+"B-------已经获得A");
}
}
},"B").start();
}
}
8.2 如何排查死锁
1、纯命令
jps -l
jstack 进程编号
2、图形化
jconsole
转载自:https://juejin.cn/post/7159890696169029639