五、多线程锁

5.1 Synchronized的8种锁的情况

5.1.1代码演示

package lock;

import java.util.concurrent.TimeUnit;

/**
* @author LWJ
* @date 2023/6/18
*
* 演示锁的 8 种情况
*/

class Phone{
    public static synchronized void sendSms() throws Exception{
        //Thread.sleep(4000);
        System.out.println("------------sendSms");
    }

    public synchronized void sendEmail() throws Exception{

        System.out.println("------------sendEmail");
    }

    public  void getHello() {

        System.out.println("------------getHello");
    }
}

public class Lock8 {

    public static void main(String[] args) throws Exception {
        Phone phone = new Phone();

        new Thread(() -> {
            try{
                phone.sendSms();
            }catch (Exception e){
                e.printStackTrace();
            }
        },"thread-A").start();

        Thread.sleep(100);

        new Thread(() -> {
            try{
                phone.sendEmail();
            }catch (Exception e){
                e.printStackTrace();
            }
        },"thread-A").start();

    }
}

/*
1 标准访问,先打印短信还是邮件
------sendSMS
------sendEmail
2 停 4 秒在短信方法内,先打印短信还是邮件
------sendSMS
------sendEmail
3 新增普通的 hello 方法,是先打短信还是 hello
------getHello
------sendSMS
4 现在有两部手机,先打印短信还是邮件
------sendEmail
------sendSMS
5 两个静态同步方法, 1 部手机,先打印短信还是邮件 ==== 锁的是Phone.class
------sendSMS
------sendEmail
6 两个静态同步方法, 2 部手机,先打印短信还是邮件 ==== 锁的是Phone.class
------sendSMS
------sendEmail
7 1 个静态同步方法,1 个普通同步方法, 1 部手机,先打印短信还是邮件
------sendEmail
------sendSMS
8 1 个静态同步方法,1 个普通同步方法, 2 部手机,先打印短信还是邮件
------sendEmail
------sendSMS
*/

5.1.2 总结

synchronized 实现同步的基础: Java 中的每一个对象都可以作为锁。具体表现为以下 3 种形式。

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的 Class 对象。
  • 对于同步方法块,锁是 Synchonized 括号里配置的对象

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
所有的静态同步方法用的也是同一把锁——类对象本身,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!

5.2 公平锁和非公平锁

image.png
image.png
image.png
ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(简称AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态,马上我们会看到,这个volatile变量是ReentrantLock内存语义实现的关键。
image.png
ReentrantLock的类图 (来自《Java并发编程的艺术》)

5.2.1 公平锁获取

ReentrantLock分为公平锁和非公平锁,我们首先分析公平锁。使用公平锁时,加锁方法lock()调用轨迹如下。

1ReentrantLock:lock()2FairSync:lock()3AbstractQueuedSynchronizer:acquire(int arg)4ReentrantLock:tryAcquire(int acquires)

image.png
从上面源代码中我们可以看出,加锁方法首先读volatile变量state

5.2.2 公平锁释放锁

在使用公平锁时,解锁方法unlock()调用轨迹如下。

1ReentrantLock:unlock()2AbstractQueuedSynchronizer:release(int arg)3Sync:tryRelease(int releases)

image.png

公平锁在释放锁的最后写volatile变量state,在获取锁时首先读这个volatile变量。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后立即变得对获取锁的线程可见。
----《Java并发编程的艺术》

5.2.3 非公平锁的释放

非公平锁的释放和公平锁完全一样,所以这里仅仅分析非公平锁的获取。

5.2.3 非公平锁的获取

使用非公平锁时,加锁方法lock()调用轨迹如下。

1ReentrantLock:lock()2NonfairSync:lock()3AbstractQueuedSynchronizer:compareAndSetState(int expect,int update) 

5.3 可重入锁

当一个线程获取到锁后,在索取到锁的业务代码中,继续请求锁L,那么这个时候,应该能直接获取到锁L,这就是锁的可重入性。

5.3.1 Synchronized是隐式可重入锁

  1. 代码
/**
 * @author LWJ
 * @date 2023/6/18
 */
public class Demo1 {

    public static void main(String[] args) {
        Object o = new Object();
        new Thread(() -> {
            synchronized (o){
                System.out.println("外层");
                synchronized (o){
                    System.out.println("中层");
                    synchronized (o){
                        System.out.println("内层");
                    }
                }
            }
        },"thread-A").start();
    }
}

干点坏事

  1. 输出

外层
中层
内层

5.3.2 Lock(ReentrantLock)显示可重入锁代码

  1. 代码
/**
 * @author LWJ
 * @date 2023/6/18
 *
 * Lock 显示可重入锁演示
 */
public class Demo2 {

    public static void main(String[] args) {

        Lock lock = new ReentrantLock();

        new Thread(() -> {
            try{
                lock.lock();
                System.out.println("外层锁");
                try{
                    lock.lock();
                    System.out.println("内层锁");
                }finally {
                    lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        },"thread-A").start();
    }

}

  1. 输出

外层锁
内层锁

  1. 干点坏事儿,如代码
package reentrylock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author LWJ
 * @date 2023/6/18
 * <p>
 * Lock 显示可重入锁演示
 */
public class Demo2 {

    public static void main(String[] args) {

        Lock lock = new ReentrantLock();

        new Thread(() -> {
            try {
                lock.lock();
                System.out.println("外层锁");
                try {
                    lock.lock();
                    System.out.println("内层锁");
                } finally {
                    //干坏事儿
                    //lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        }, "thread-A").start();


        //干坏事儿后再来个线程
        new Thread(() -> {
            try {
                lock.lock();
                System.out.println("干坏事儿了!");
            } finally {
                lock.unlock();
            }
        }, "thread-B").start();
    }
}
  1. 干点坏事儿,输出
    :::danger
    image.png
    还在运行,坏事儿没输出,锁没有完全释放!
    :::

5.4 死锁

5.4.1死锁产生四大必要条件:

  1. 互斥

  2. 请求保持

  3. 不可剥夺

  4. 循环等待

  5. 互斥:

互斥很好理解。举个例子,A进程和B进程都需要操作一个map,而这个map是唯一的。那么对这个资源的使用就是互斥的。

  1. 请求保持

这个条件是说,当某个进程在请求新资源时,它不放弃原有的资源。

  1. 不可剥夺

这个条件的意思是,A进程在使用完某个资源前,这个资源是不会被其他进程所使用的,除非A主动释放。

  1. 循环等待

发生死锁的一个必要条件是当前进程队列产生了闭环。 很好理解,当有进程{p1,p2…pn},p2等待p1释放一个互斥资源,Pn等待Pn-1释放一个互斥资源,而P1等待Pn,那么久产生了闭环。

5.4.2 一些可能的原因

  • 系统资源不足
  • 进程运行推进顺序不合适
  • 资源分配不当

5.4.3 死锁代码

/**
 * @author LWJ
 * @date 2023/6/18
 */
public class DeadLockDemo {

    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
    	//线程1
        new Thread(() -> {
            synchronized (lock1){
                System.out.println("获取到lock1,常识获取lock2");
                // sleep 1秒
                try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
                synchronized (lock2){
                    System.out.println("获取lock2");
                }
            }
        },"thread-A").start();
    	//线程2
        new Thread(() -> {
            synchronized (lock2){
                System.out.println("获取到lock2,常识获取lock1");
                // sleep 1秒
                try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
                synchronized (lock1){
                    System.out.println("获取lock2");
                }
            }
        },"thread-B").start();
    }
}

image.png

5.4.4 死锁验证

为啥要验证?有时候出现这种等待情况,并不一定是由死锁造成的。可能由于微服务调用延迟,网络波动等原因。那么如何验证?----两个命令jps,jstack

刚学了JVM,说实在的真的没有第一时间想到用这两个命令查看,相关实践还是少!

  1. jps

image.png

  1. jstack
PS D:\CodeWorkpace\idea-workspace\juc-study> jstack 4936

Java stack information for the threads listed above:
===================================================
"thread-B":
        at deadlock.DeadLockDemo.lambda$main$1(DeadLockDemo.java:32)
        - waiting to lock <0x00000007167512c8> (a java.lang.Object)
        - locked <0x00000007167512d8> (a java.lang.Object)
        at deadlock.DeadLockDemo$$Lambda$2/990368553.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:750)
"thread-A":
        at deadlock.DeadLockDemo.lambda$main$0(DeadLockDemo.java:21)
        - waiting to lock <0x00000007167512d8> (a java.lang.Object)
        - locked <0x00000007167512c8> (a java.lang.Object)
        at deadlock.DeadLockDemo$$Lambda$1/2003749087.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:750)

Found 1 deadlock.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值