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

本文详细比较了Java的Lock接口及其ReentrantLock实现与synchronized关键字在加锁、解锁、公平性、中断和等待唤醒机制上的异同,以及如何正确使用Lock确保线程安全和资源释放。
摘要由CSDN通过智能技术生成

目录

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等方法时不会用到监视器锁,自然也就不会抛出异常了。

  • 10
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值