JUC基础02——Lock接口

47 阅读5分钟

Lock 接口

Synchronized 关键字回顾

概述

synchronized 是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}
    括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用
    的对象是调用这个方法的对象;
    • 虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定
      义的一部分,因此, synchronized 关键字不能被继承。如果在父类中的某个方
      法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这
      个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上
      synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方
      法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,
      子类的方法也就相当于同步了。
  3. 修改一个静态的方法:其作用的范围是整个静态方法,作用的对象是这个类的
    所有对象;
  4. 修改一个类:其作用的范围是 synchronized 后面括号括起来的部分,作用主
    的对象是这个类的所有对象

优点:

  • 使用synchronized修饰临界资源,从而避免了线程并发安全问题。
  • 和Object类的 wait / notify组合使用,从而避免了死锁问题。
  • 自动加解锁。

缺点:

  • 同步锁,多线程间只能排队执行。
  • 无法手动释放锁,当线程出现异常或阻塞时,其他线程只能不知期限地等待,影响程序效率。

代码示例

package com.avgrado.demo.thread;

class Ticket {
    private int nums =30;

    public synchronized void saleticket(){
        if(nums>0){
            System.out.println(Thread.currentThread().getName()+":卖出:"+(nums--)+"剩余:"+nums);
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(()->{
           for (int i = 0; i <15 ; i++) {
               ticket.saleticket();
           }
       },"A").start();
        new Thread(()->{
            for (int i = 0; i <19 ; i++) {
                ticket.saleticket();
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i <25 ; i++) {
                ticket.saleticket();
            }
        },"C").start();
    }
}

什么是Lock

概述

Lock接口位于java.util.concurrent.locks包中,是Java并发编程的一部分。Lock接口提供了比使用synchronized关键字更灵活的锁定机制。使用Lock接口,可以更好地控制锁的获取和释放,以及处理尝试获取锁时的异常情况。

源码:

public interface Lock {
    /**
     * @Description 获取锁
     */
    void lock();

    /**
     * @Description 若当前线程未被中断,则获取锁
     * @throws InterruptedException
     */
    void lockInterruptibly() throws InterruptedException;

    /**
     * @Description 仅在调用时,锁为空闲状态才获取锁
     * @return
     */
    boolean tryLock();
    
    /**
     * @Description 若锁在指定时间内是空闲状态,且当前线程未被中断才获取锁
     * @param time
     * @param unit
     * @return
     * @throws InterruptedException
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    
    /**
     * @Description 释放锁
     */
    void unlock();

    /**
     * @Description 获得 当前Lock对象的 condition 对象
     * @return
     */
    Condition newCondition();
}

Lock接口中主要的方法有:

  1. void lock(): 获取锁。如果锁被其他线程持有,则当前线程将被阻塞,直到可以获取锁。
  2. void lockInterruptibly() throws InterruptedException: 与lock()方法类似,但如果当前线程在等待锁的过程中被中断,则会抛出InterruptedException
  3. boolean tryLock(): 尝试非阻塞地获取锁。如果锁当前可用,则获取锁并返回true;否则返回false
  4. boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException: 尝试在给定的时间内获取锁。如果在指定的时间内锁变得可用,则获取锁并返回true;否则返回false。如果在等待过程中线程被中断,则会抛出InterruptedException
  5. void unlock(): 释放锁。如果当前线程不持有该锁,调用此方法将抛出IllegalMonitorStateException

使用Lock接口时,通常需要使用try-finally块来确保在使用完锁后释放锁,以避免发生死锁。例如:

	Lock lock = new ReentrantLock();  

	try {  

	    lock.lock();  

	    // 访问共享资源  

	} finally {  

	    lock.unlock();  

	}

Lock接口的实现类包括ReentrantLockReentrantReadWriteLock`等,这些实现类提供了不同的锁定策略以满足不同的并发需求

代码示例

package com.avgrado.demo.thread;

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

class Ticket {
    private int nums =30;
    Lock lock = new ReentrantLock();

    public  void saleticket(){
        lock.lock();
        try{
            if(nums>0){
                System.out.println(Thread.currentThread().getName()+":卖出:"+(nums--)+"剩余:"+nums);
            }
        }finally {
            lock.unlock();
        }

    }
}

public class ThreadDemo {

    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(()->{
           for (int i = 0; i <15 ; i++) {
               ticket.saleticket();
           }
       },"A").start();
        new Thread(()->{
            for (int i = 0; i <19 ; i++) {
                ticket.saleticket();
            }
        },"B").start();

        new Thread(()->{
            for (int i = 0; i <25 ; i++) {
                ticket.saleticket();
            }
        },"C").start();
    }
}

Lock 与的 Synchronized 区别

  1. 关联对象:synchronized 是 Java 内置的关键字,用于关联某个对象实例或类,实现对这个对象的锁定和解锁操作。而 Lock 是一个接口,需要显式地创建一个 Lock 对象来进行锁定和解锁操作。
  2. 可重入性:在 Java 中,synchronized 是可重入的,意味着如果一个线程已经获得了某个对象的锁,那么它可以再次获取该对象的锁而不会被阻塞。而 Lock 在默认情况下不是可重入的,但可以通过 ReentrantLock 类实现可重入锁。
  3. 锁的获取方式:synchronized 在获取锁时是隐式的,当线程进入 synchronized 保护的代码块时,会自动获取锁,并在退出代码块时释放锁。而 Lock 需要明确地调用 lock() 方法来获取锁,在使用完毕后需要调用 unlock() 方法来释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
  4. 条件变量:Lock 提供了 Condition 接口,可以通过 Condition 实现对线程的等待和唤醒操作,可以更灵活地控制线程。而在 synchronized 中,也可以使用 wait() 和 notify() 方法来实现类似的等待和唤醒操作,但是条件变量的使用更直观和灵活
  5. 性能:如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。
  6. 所属层面:synchronized关键字是属于JVM层面,底层是由monitorentermonitorexit组成,monitorenter通过monitor对象来完成,其wait和notify等方法也依赖于monitor对象,只有在同步块或方法中才能调用 wait/notify 等方法;Lock是具体类(java.util.concurrent.locks.Lock),属于API层面的锁