多线程学习-Lock锁以及监视器锁详解

news2024/11/28 16:28:10

目录

1. Lock锁

        1.1 Lock锁介绍

        1.2 Lock锁的其他加锁方式

        1.3 Lock和synchronized对比 

2.监视器锁


1. Lock锁

        1.1 Lock锁介绍

        我们知道使用同步方法或同步代码块会自动加锁和解锁,那有没有办法可以自己控制加锁和解锁的时机呢?

        java在JDK1.5之后提供了一个新的锁对象Lock,他有两个常用的方法lock和unlock,一个是手动加锁,另一个是手动解锁。但是Lock本身是一个接口不能实例化对象,通常我们在使用时需要用到它的一个实现类ReentrantLock来实例化对象。也就是:

Lock l=new ReentrantLock();

        并且还能同时设置是否为公平锁。 所谓公平锁和非公平锁,就是看加锁的过程是否公平,公平就是先来先加锁,非公平就是允许后来的线程插队,先获得锁。一般使用非公平锁,比如一个线程先来但是要运行1小时,另一个线程后来只需要运行1秒,如果使用公平锁那么第二个线程需要等1小时才能加锁,而使用非公平锁则允许第二个线程先获得锁,且第一个线程只用等1秒也能获取到锁,总体等待时间由1小时缩短为1秒。可以用以下代码来设置:

//true表示设置为公平锁,false表示设置为非公平锁
Lock l=new ReentrantLock(true);

        默认的锁是非公平锁,可以查看源码:

        使用案例:先用同步代码块来实现卖票功能:

    @Override
    public void run() {

            //模拟卖票过程,总共100张票,售完为止
            while (true){

                //对卖票过程进行加锁
                synchronized (object){
                    if(ticketnum<100) {
                        ticketnum++;
                        System.out.println(Thread.currentThread().getName() + "正在售卖第" + ticketnum + "张票");
                    }else{
                        break;
                    }
                }

                //通常卖票是有时间间隔的,用sleep来模拟
                //由于父类Thread的run方法没有抛异常,所以子类的run方法也不能抛异常,只能使用try-catch
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
        }

    }

        然后使用Lock锁实现,是不是仅需要在同步代码块中要执行的代码的前后分别加上lock和unlock就可以了呢?我们可以试一试:这是在前后加上lock和unclock的方法:

        注意使用继承Thread类的方式实现多线程所创建的lock对象必须是静态的,这个lock就是锁对象,必须唯一,当然如果使用Runnable接口实现的那就可以是普通变量。

public class MyThread extends Thread{

    //ticketnum表示当前正在售卖第几张票
    //使用static将其变为共享的
    public static int ticketnum=0;

    //锁对象,用于加锁操作
    public static Lock lock=new ReentrantLock();;

    @Override
    public void run() {

            //模拟卖票过程,总共100张票,售完为止
            while (true){

                //对卖票过程进行加锁
                lock.lock();
                    if(ticketnum<100) {
                        ticketnum++;
                        System.out.println(Thread.currentThread().getName() + "正在售卖第" + ticketnum + "张票");
                    }else{
                        break;
                    }
                lock.unlock();

                //通常卖票是有时间间隔的,用sleep来模拟
                //由于父类Thread的run方法没有抛异常,所以子类的run方法也不能抛异常,只能使用try-catch
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
        }

    }
}

        运行结果:

        乍一看没有什么问题,但是仔细看就会发现程序并没有停止运行,这是哪里出现了问题呢?

        在上面的代码中,如果判断ticketnum>=100就会break退出循环,但在判断之前执行了加锁操作,break之后循环跳出,unlock解锁操作自然就没有执行,所以此时还处在上锁状态,需要将锁释放程序才能停止运行。

        所以不只是在要执行的代码前后加上lock和unlock那么简单。在上面的例子中我们可以在break前加上一个unlock操作就能解决问题,但这样做unlock就会出现两次,如果有多个break那就需要重复加上多个unlock。那怎样只需要写一个unlock就能保证加的锁最后都能被释放呢?

        保证加的锁最后都能被释放换句话讲就是无论中间执行什么代码,最后解锁的操作都会执行,这和try-catch-finally中的finally部分很契合,finally部分的代码无论任何情况最后都会被执行,所以我们可以使用finally来解决这个问题:

public class MyThread extends Thread{

    //ticketnum表示当前正在售卖第几张票
    //使用static将其变为共享的
    public static int ticketnum=0;

    //锁对象,用于加锁操作
    public static Lock lock=new ReentrantLock();;

    @Override
    public void run() {

            //模拟卖票过程,总共100张票,售完为止
            while (true){

                //对卖票过程进行加锁
                lock.lock();
                
                //使用finally实现
                try {
                    if(ticketnum<100) {
                        ticketnum++;
                        System.out.println(Thread.currentThread().getName() + "正在售卖第" + ticketnum + "张票");
                    }else{
                        break;
                    }
                }finally {
                    lock.unlock();
                }

                //通常卖票是有时间间隔的,用sleep来模拟
                //由于父类Thread的run方法没有抛异常,所以子类的run方法也不能抛异常,只能使用try-catch
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
        }
    }
}

        运行结果为: 

        为了不用考虑使用lock手动加锁后是否最后都能解锁的问题,通常使用如下方式:

lock.lock();
try{
    需要锁住的代码
}finally{
    lock.unlock();
}

        catch块根据实际情况可以加上,用于捕捉异常并处理。

        1.2 Lock锁的其他加锁方式

        Lock除了普通的lock方法获取锁外,还有其他的方式获取锁:

  • trylock()

        这种方式只是尝试获取锁,如果获取不到则不会等待,会立即返回获取锁的结果,成功获取返回true,获取失败则返回false;lock方法则是会先尝试获取锁,如果获取不到则陷入等待,直到成功获取锁。

        使用格式为:

if(lock.trylock()){
    try{
        //要锁住的代码
    }catch(异常){
        //异常处理代码
    }finally{
        lock.unlock();
    }
}
  • trylock(long time,Time.Unit unit) 

        这种方式也是尝试获取锁,但可以设置等待时间,要传入时间的值和单位;所谓等待时间,就是如果当前获取锁失败还可以等待多长时间,如果在这段时间内成功获取到了锁则返回true,反之返回false。  

         使用格式为:

if(lock.trylock(时间的值,时间的单位)){
    try{
        //要锁住的代码
    }catch(异常){
        //异常处理代码
    }finally{
        lock.unlock();
    }
}
  •  lockInterruptibly()

        这个方法可以在获取锁时响应中断,使用时需要使用try-catch包围或者抛出异常。当线程获取不到锁时会进入等待状态,此时若通过线程实例调用interrupt方法可以中断等待过程,而lockInterruptibly()方法允许在获取不到锁时响应interrupt方法的执行,进而中断等待过程。interrupt方法只会中断阻塞过程中的线程,并不会中断正常运行的线程。

        使用格式为:

//或者直接抛出异常
try {
            l.lockInterruptibly();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
try{
        //要锁住的代码
    }catch(异常){
        //异常处理代码
    }finally{
        lock.unlock();
    }

        1.3 Lock和synchronized对比 

        使用Lock加锁和使用synchronized加锁有什么不同呢?

  • synchronized是java的一个内置关键字,而Lock则是一个接口。
  • synchronized是自动加锁,如果线程1已经获取到了锁,线程2再要获取这个锁时会自动陷入等待,并且整个过程不可见,无法判断当前是否成功获得了锁;而Lock可以通过trylock()方法尝试获取锁,该方法不会陷入等待并且会立即返回结果,可以通过返回值判断当前是否获取到了锁。
  • synchronized会自动解锁,而Lock必须要手动释放锁,如果不释放则线程就不会停止运行。
  • synchronized的锁是可重入、非公平的锁,并且获取不到锁后陷入的等待过程不可中断(因为加锁过程是自动的,我们无法控制);而Lock的锁是可重入的锁,但可以选择是否是非公平的锁,也可以使用lockInterruptibly方法响应中断。
  • synchronized一般用于对少量代码加锁,Lock一般用于对大量代码加锁。少量代码一般所需的运行时间短,对应的加锁时间就短,当有其他线程也获取这个锁时等待时间就短,就没必要考虑等待过程不可中断的问题,反正等不了多长时间锁就释放了;而大量代码通常执行的时间较长,加锁时间也就越长,这意味着其他线程需要等待很长时间,此时如果想要中断等待过程只能用Lock。
  • synchronized有内置监视器锁,而Lock需要手动添加监视器锁,可以选择Condition监视器锁。

2.监视器锁

        上面说到在使用Lock手动加锁和解锁时要使用try-catch-finally包围,保证加上的锁最后都能被释放,但这样做是否也能保证代码正常运行呢?

        如果不使用等待唤醒机制,程序能够正常运行,但如果使用了wait和notify,则程序会报错。就像下面的例子,实现A、B、C、D四个线程依次执行两次:

//资源类
public class Example1 {
    //标志位,初始值设为1,让A先执行
    private int flag=1;
    //创建锁对象
    Lock lock=new ReentrantLock();

    public void A(){
        for(int i=0;i<2;i++) {
            //加锁
            lock.lock();
            try {
                while (flag != 1) {
                    lock.wait();
                }
                System.out.println("线程A正在运行中...");
                flag = 2;
                lock.notifyAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    public void B(){
        for(int i=0;i<2;i++) {
            //加锁
            lock.lock();
            try {
                while (flag != 2)
                    lock.wait();
                System.out.println("线程B正在运行中...");
                flag = 3;
                lock.notifyAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    public synchronized void C(){
        for(int i=0;i<2;i++) {
            //加锁
            lock.lock();
            try {
                while (flag != 3)
                    lock.wait();
                System.out.println("线程C正在运行中...");
                flag = 4;
                lock.notifyAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    public synchronized void D(){
        for(int i=0;i<2;i++) {
            //加锁
            lock.lock();
            try {
                while (flag != 4)
                    lock.wait();
                System.out.println("线程D正在运行中...");
                flag = 1;
                lock.notifyAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

//实现
public class Main {
    public static void main(String[] args) {

        //创建资源类对象
        Example1 example1=new Example1();
        
        //创建A、B、C、D四个线程
        Thread ta=new Thread(()->{
                example1.A();
        });
        Thread tb=new Thread(()->{
                example1.B();
        });
        Thread tc=new Thread(()->{
                example1.C();
        });
        Thread td=new Thread(()->{
                example1.D();
        });

        //启动四个线程
        ta.start();
        tb.start();
        tc.start();
        td.start();
    }
}

        运行结果:

        可以看到虽然运行了四次,但异常抛出了八次,并且都是IllegalMonitorStateException异常,说明每次线程在运行时都出现了同样的异常。

        在Java中wait、notify 或notifyAll 方法必须在同步块或同步方法中调用,否则将抛出IllegalMonitorStateException 异常,这是因为这些方法是与对象的内部锁(也称为监视器锁)关联的。当一个线程调用一个对象的wait 方法时,它必须已经通过synchronized关键字获得了该对象的监视器锁。如果没有获得锁,也就是当前线程并不是锁对象的监视器锁的持有者,就会抛出 IllegalMonitorStateException 异常。同样,调用notify或notifyAll也需要线程已经拥有该对象的监视器锁。

        那什么是监视器锁呢,监视器锁和锁对象又有什么关系呢?

        内置监视器锁也称为JVM锁或者同步锁,是Java语言提供的一种基本锁机制,内置监视器锁通常通过synchronized关键字实现。每个Java对象都有一个与之相关联的监视器锁(也称为内置锁,由JVM生成并管理),当线程要访问被锁定的代码块时,它必须先获得与该对象相关联的监视器锁。当代码块使用synchronized关键字加锁时会自动获取锁对象的内置监视器锁,执行完该代码块后会自动释放锁。如果另一个线程已经持有了这个锁,则当前线程就会阻塞等待,直到该锁被释放。由于锁对象是唯一的,所以获取的监视器锁也是同一个锁,只有持有这个锁的线程才能够访问被锁住的资源。

        回到上面的例子,只加了Lock锁并不会获取锁对象的监视器锁,这就导致当前线程不是锁对象的监视器锁的持有者,所以运行时抛出了异常,同时这也意味着wait、notify以及notifyAll只能用于同步代码块或者同步方法中,只有synchronized才能获取到监视器锁。所以代码可以改为:

//资源类
public class Example1 {
    //标志位,初始值设为1,让A先执行
    private int flag=1;

    public synchronized void A() throws IOException {
        for(int i=0;i<2;i++) {
            try {
                while (flag != 1) {
                    //由于加锁的对象flag是非静态的,所以同步方法的锁对象使用的是this
                    this.wait();
                }
                System.out.println("线程A正在运行中...");
                flag = 2;
                this.notifyAll();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void B(){
        for(int i=0;i<2;i++) {
            try {
                while (flag != 2)
                    this.wait();
                System.out.println("线程B正在运行中...");
                flag = 3;
                this.notifyAll();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void C(){
        for(int i=0;i<2;i++) {
            try {
                while (flag != 3)
                    this.wait();
                System.out.println("线程C正在运行中...");
                flag = 4;
                this.notifyAll();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void D(){
        for(int i=0;i<2;i++) {
            try {
                while (flag != 4)
                    this.wait();
                System.out.println("线程D正在运行中...");
                flag = 1;
                this.notifyAll();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

//实现
public class Main {
    public static void main(String[] args) throws IOException {

        //创建资源类对象
        Example1 example1=new Example1();

        //创建A、B、C、D四个线程
        Thread ta=new Thread(()->{
            try {
                example1.A();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
        Thread tb=new Thread(()->{
                example1.B();
        });
        Thread tc=new Thread(()->{
                example1.C();
        });
        Thread td=new Thread(()->{
                example1.D();
        });

        //启动四个线程
        ta.start();
        tb.start();
        tc.start();
        td.start();
    }
}

        运行结果:

        其实还有一个问题没有回答,那就是使用Lock锁后,为什么不使用等待唤醒机制时,如果添加了try-catch-finally之后就能正常运行呢(前提是业务代码没问题)?

        这是因为虽然手动添加了Lock锁不会获取到监视器锁,但是只有在调用wait、notify以及notifyAll时才需要监视器锁,当使用sleep、join等方法时不会用到监视器锁,自然也就不会抛出异常了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1581778.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

BoostCompass(数据准备预处理模块)

阅读导航 一、网页数据下载二、编写数据去标签与数据清洗的模块 Parser✅boost 开发库的安装1. 基本思路2. 详细讲解&#xff08;1&#xff09;程序递归遍历目录&#xff0c;收集所有HTML文件的路径&#xff08;2&#xff09;对每个HTML文件进行解析&#xff0c;提取出文档标题…

政安晨:【深度学习神经网络基础】(五)—— 霍普菲尔德神经网络和玻尔兹曼机

目录 简述 霍普菲尔德神经网络 训练霍普菲尔德神经网络 Hopfield-Tank神经网络 玻尔兹曼机 总之 政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: 政安晨的机器学习笔记 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&am…

Python学习笔记10 - 程序的组织结构

1. if 语句 2. pass语句 3. 内置函数range() 4. while 循环语句 5. for-in 循环语句 6. 流程控制语句 break 7. 流程控制语句 continue 8. else 语句 9. 嵌套循环

申请GeoTrust证书

GeoTrust是全球知名的数字证书颁发机构&#xff08;CA&#xff09;和安全解决方案提供商。它成立于1999年&#xff0c;后来成为DigiCert旗下的一部分。GeoTrust专注于提供SSL证书和其他安全产品&#xff0c;以保护网站流量、电子邮件通信和企业身份的安全。 GeoTrust的SSL证书在…

Vue+node.js蔬菜水果农产品网上商城管理系统

用户能够及时掌握最新的数据&#xff0c;并能有效地提升用户的服务水平。本制度的优势在于&#xff1a; &#xff08;1&#xff09;该系统将蔬菜信息及时地提供给使用者。使用者可以在任何时候登陆该网站查询相关资讯&#xff0c;所有资讯均真实可信&#xff0c;并严肃处理各项…

test4102

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和…

YUDAO源码中的正序倒序表格ElmentUI的实现,与后端的配合?

前端展示和实现&#xff1a; 1. elmentUI表格的定义 2. JS请求参数改造 <!-- 列表 --><el-table v-loading"loading" :data"list" sort-change"handleSortChange"><el-table-column label"Expiry Date" prop"…

人体跟随小车(旭日x3派、yolov5、目标检测)

人体跟随小车&#xff08;yolov5、目标检测&#xff09; 前言最终结果接线实现注意 前言 上板运行的后处理使用cython封装了&#xff0c;由于每个版本的yolo输出的形状不一样&#xff0c;这里只能用yolov5-6.2这个版本。 ①训练自己的模型并部署于旭日x3派参考&#xff1a; ht…

直播系统的短视频直播源码,带有多功能后台系统的直播短视频平台 APP 源码。

内容目录 一、详细介绍二、效果展示1.部分代码2.效果图展示 三、学习资料下载 一、详细介绍 此源码是一个直播系统&#xff0c;集直播、短视频等功能&#xff0c;根据市场趋势开发并推出思乐直播APP&#xff0c;APP功能丰富且可在后台管理系统进行配置&#xff0c;做到按需求来…

【算法一则】贪心加双指针-盛水最多的容器

目录 题目题解贪心算法双指针解题思路暴力破解法双指针 总结 题目 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器…

设计模式学习笔记 - 设计模式与范式 -行为型:7.责任链模式(下):框架中常用的过滤器、拦截器是如何实现的?

概述 上篇文章《6.责任链模式&#xff08;上&#xff09;&#xff1a;原理与实现》&#xff0c;学习了职责链模式的原理与实现&#xff0c;并且通过一个敏感词过滤框架的例子&#xff0c;展示了职责链模式的设计意图。本质上来说&#xff0c;它跟大部分设计模式一样&#xff0…

不到6毛钱的I2C总线实时时钟日历芯片LK8563

前言 8563实时时钟芯片&#xff0c;国内外均有多家生产&#xff0c;今推荐一个性价比极高的RTC芯片&#xff0c;LK8563&#xff0c;一片不到6毛钱. 特点 基于32.768kHz晶体的秒&#xff0c;分&#xff0c;小时&#xff0c;星期&#xff0c;天&#xff0c;月和年的计时 带有世…

【堡垒机】堡垒机的介绍

目前&#xff0c;常用的堡垒机有收费和开源两类。 收费的有行云管家、纽盾堡垒机&#xff1b; 开源的有jumpserver&#xff1b; 这几种各有各的优缺点&#xff0c;如何选择&#xff0c;大家可以根据实际场景来判断 什么是堡垒机 堡垒机&#xff0c;即在一个特定的网络环境下&…

第十届蓝桥杯大赛个人赛省赛(软件类) CC++ 研究生组-RSA解密

先把p&#xff0c;q求出来 #include<iostream> #include<cmath> using namespace std; typedef long long ll; int main(){ll n 1001733993063167141LL, sqr sqrt(n);for(ll i 2; i < sqr; i){if(n % i 0){printf("%lld ", i);if(i * i ! n) pri…

Matlab|【防骗贴】【免费】基于主从博弈的主动配电网阻塞管理

目录 1 主要内容 程序亮点 2 部分代码 3 程序结果 4 下载链接 1 主要内容 《基于主从博弈的主动配电网阻塞管理》文献介绍&#xff1a;主要采用一种配电网节点边际电价统一出清的主从博弈双层调度框架。上层框架解决用户在负荷聚合商引导下的用电成本最小化问题&#xff0…

电脑更新到win11后不能上网,更新win11后无法上网

越来越多的用户升级了win11系统使用&#xff0c;然而有些用户发现电脑更新到win11后不能上网了&#xff0c;这是怎么回事呢?而且奇怪的是&#xff0c;网络状态显示已连接&#xff0c;但就是无法上网&#xff0c;原本以为重置网络就能搞定&#xff0c;但结果相反。针对这一情况…

DRF的认证、权限、限流、序列化、反序列化

DRF的认证、权限、限流、序列化、反序列化 一、认证1、直接用&#xff0c;用户授权2、认证组件源码 二、权限1. 直接使用&#xff0c;用户权限2.权限组件源码 三、序列化1. 序列化1.1 自定义Serailizer类序列化1.2 在视图APIView中使用1.3 自定义ModelSerializer类序列化1.4 不…

vue3 +Taro 页面实现scroll-view 分页功能

需求 现在分页列表 后端只给你一个分页的数据列表 没有总页数 没有当前的分页 页数 只有这么一个list 、、、 如何去分页 我这使用的是scroll-view 组件 滑动到底部的事件 根据你当前设定的每页的数据数量和后端返回给你的数据列表数量 当某一次分页 两个数量不相等了以后 就…

ActiveMQ介绍及linux下安装ActiveMQ

ActiveMQ介绍 概述 ActiveMQ是Apache软件基金下的一个开源软件&#xff0c;它遵循JMS1.1规范&#xff08;Java Message Service&#xff09;&#xff0c;是消息队列服务&#xff0c;是面向消息中间件&#xff08;MOM&#xff09;的最终实现&#xff0c;它为企业消息传递提供高…

目标检测——YOLO系列学习(一)YOLOv1

YOLO可以说是单阶段的目标检测方法的集大成之作&#xff0c;必学的经典论文&#xff0c;从准备面试的角度来学习一下yolo系列。 YOLOv1 1.RCNN系列回顾 RCNN系列&#xff0c;无论哪种算法&#xff0c;核心思路都是Region Proposal&#xff08;定位&#xff09; classifier&am…