二、Lock接口简介

前言

这个只是很简单的介绍,但是可以有一个大概的了解了!等后续通过《Java并发编程艺术》这个书的学习,再加深。

2.1 synchronizd

2.1.1 synchronizd关键字基础

synchronized 是 Java 中的关键字,Lock是一种同步锁(本质上是一种监视器monitor)。它修饰的对象有以下几种:

  1. 修饰方法

被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
tips:
虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,因此,synchronized 关键字不能被继承。如果在父类中的某个方法使用了 synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了

  1. 修饰类

其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象 。

  1. 修饰代码库

修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;

  1. 修饰静态方法

其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

2.1.2 多线程编程步骤-1

  1. 创建资源类,在资源类创建属性和操作方法。
  2. 创建多个线程,调用资源类的操作方法。

2.1.3 synchronizd案例(售票)

/**
 * @author LWJ
 * @date 2023/6/17
 */

class Tickets{
    private int numbers = 30;

    public synchronized void sale(){
        if(numbers > 0){
            System.out.println(Thread.currentThread().getName() 
                               + "售出1张票,剩余:" + --numbers + "张票" );
        }
    }
}

public class SaleTickets {

    public static void main(String[] args) {
        Tickets tickets = new Tickets();
        //线程A
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {  //假设一个比30大的数就行,举例而已
                    tickets.sale();
                }
            }
        },"thread-A").start();
        //线程B
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {  //假设一个比30大的数就行,举例而已
                    tickets.sale();
                }
            }
        },"thread-B").start();
        //线程C
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {  //假设一个比30大的数就行,举例而已
                    tickets.sale();
                }
            }
        },"thread-C").start();
    }
}

这里可能会有疑惑,“三个线程的票难道不都是独立的吗?”
当然不是,Runnable接口和Thread都可以正确的实现资源的共享。
对于Thread和Runnable资源共享问题,参考:
Thread和Runnable关于共享资源的对比_runnable资源共享_Hadoop_Liang的博客-CSDN博客

2.1.4 分析售票案例存在的问题

如果一个代码块被 synchronized 修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1. 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  2. 线程执行发生异常,此时 JVM 会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待 IO 或者其他原因(比如调用 sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过 Lock 就可以办到。

2.2 Lock

二、Lock接口简介_第1张图片
image.png
Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。 Lock 提供了比 synchronized 更多的功能。

2.2.1 Lock接口

public interface Lock {
    /*
	获取锁
	如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。

	实现注意事项:
    	Lock 实现可能能够检测到锁的错误使用,比如会导致死锁的调用,在那种环境下还可能
    	抛出一个 (unchecked) 异常。Lock实现必须对环境和异常类型进行记录。

        lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
    	采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。

	因此一般来说,使用 Lock 必须在 try{}catch{}块中进行,并且将释放锁的操作放在finally 块中
	进行,以保证锁一定被被释放,防止死锁的发生。

	通常使用 Lock来进行同步的话,是以下面这种形式去使用的:
	Lock lock = ...;
    lock.lock();
    try{
    	//处理任务
    }catch(Exception ex){
    }finally{
    	lock.unlock()
    }
    */
	void lock();	//获取锁

    /*
	如果当前线程未被中断,则获取锁。
    如果锁可用,则获取锁,并立即返回。
    
    如果锁不可用,出于线程调度目的,将禁用当前线程,并且在发生以下两种情况之一以前,该线程将一直
	处于休眠状态:
        1.锁由当前线程获得;
        2.其他某个线程中断当前线程,并且支持对锁获取的中断。
    
	如果当前线程:
        1.在进入此方法时已经设置了该线程的中断状态;
        2.在获取锁时被中断,并且支持对锁获取的中断,
        则将抛出 InterruptedException,并清除当前线程的已中断状态。
    
	实现注意事项:
        1.在某些实现中可能无法中断锁获取,即使可能,该操作的开销也很大。
    	程序员应该知道可能会发生这种情况。在这种情况下,该实现应该对此进行记录。
        2.相对于普通方法返回而言,实现可能更喜欢响应某个中断。
        3.Lock实现可能可以检测锁的错误用法,例如,某个调用可能导致死锁,
    	在特定的环境中可能抛出(未经检查的)异常。该 Lock 实现必须对环境和异常类型进行记录。
    
    抛出:
        InterruptedException - 如果在获取锁时,当前线程被中断(并且支持对锁获取的中断)。
	*/
	void lockInterruptibly() throws InterruptedException;//如果当前线程未被中断,则获取锁。
	boolean tryLock(); //获取锁
	boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//获取锁,有时间限制
	void unlock();	//释放锁
    /*
	返回绑定到此 Lock 实例的新 Condition 实例。

    在等待条件前,锁必须由当前线程保持。调用 Condition.await() 将在等待前以原子方式释放锁,
	并在等待返回前重新获取锁。
    
    实现注意事项:
    	Condition 实例的具体操作依赖于 Lock 实现,并且该实现必须对此加以记录。
    
    返回:
    	用于此 Lock 实例的新 Condition 实例
    抛出:
    	UnsupportedOperationException - 如果此 Lock 实现不支持条件
	*/
	Condition newCondition();
}

2.2.2 Lock 与的 Synchronized 区别

  • Lock不是 Java 语言内置的,synchronized是Java 语言的关键字,因此是内置特性。Lock 是一个类,通过这个类可以实现同步访问;
  • Locksynchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized 方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

2.2.3 用Lock实现售票案例

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author LWJ
 * @date 2023/6/17
 */

//创建资源,和资源的属性与操作
class LTickets {
    private int numbers = 30;
    ReentrantLock lock = new ReentrantLock();

    public synchronized void sale() {
        lock.lock();
        try {
            if (numbers > 0) {
                System.out.println(Thread.currentThread().getName() + "售出1张票,剩余:" + --numbers + "张票");
            }
        } finally {
            lock.unlock();
        }
    }
}

public class LSaleTickets {

    //创建线程操作资源
    public static void main(String[] args) {
        Tickets tickets = new Tickets();
        //线程A
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {  //假设一个比30大的数就行,举例而已
                tickets.sale();
            }
        }, "thread-A").start();
        //线程B
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {  //假设一个比30大的数就行,举例而已
                tickets.sale();
            }
        }, "thread-B").start();
        //线程C
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {  //假设一个比30大的数就行,举例而已
                tickets.sale();
            }
        }, "thread-C").start();
    }
}

2.2.4 多线程编程步骤

  1. 创建资源类,在资源类创建属性和操作方法。
  2. 在资源类操作方法
    • 判断
    • 干活
    • 通知
  3. 创建多个线程,调用资源类的操作方法。

2.3 总结

Locksynchronized 有以下几点不同

  1. Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。
  2. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
    3. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用synchronized 时,等待的线程会一直等待下去,不能够响应中断;
  3. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  4. Lock 可以提高多个线程进行读操作的效率。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于synchronized

Lock为什么比synchronized效率高?

Lock(锁)与 synchronized(同步)是 Java 中用于实现多线程同步的两种机制,它们在实现线程互斥和协作的方式上有一些不同,从而导致 Lock 相对于 synchronized 具有更高的效率。

  1. 粒度更细:Lock 提供了更细粒度的锁控制。在 synchronized 中,锁的粒度是以整个方法或代码块为单位的,而 Lock 允许开发者更细粒度地控制锁的范围,可以仅对关键部分进行加锁。这样可以减少线程之间的竞争,提高并发性能。

  2. 可中断性:Lock 提供了可中断的获取锁的方式。在某些场景下,如果一个线程在等待锁的过程中可以被中断,那么它可以根据实际情况来决定是否继续等待,而 synchronized 则无法实现这种中断操作。可中断的锁对于处理复杂的线程交互和超时控制非常有用。

  3. 公平性:Lock 可以提供更好的公平性。在 synchronized 中,无法控制线程获得锁的顺序,而 Lock 可以通过公平锁和非公平锁的选择来决定线程获取锁的顺序。公平锁会按照线程请求锁的顺序进行处理,避免了饥饿现象。

  4. 高度可定制性:Lock 提供了更高度的可定制性和灵活性。它支持多种锁的实现方式,比如 ReentrantLock、StampedLock 等,并且可以通过参数来设置锁的特性,如超时等待、尝试获取锁等。这使得开发者能够根据具体需求进行优化和调整,从而提高效率。

尽管 Lock 相对于 synchronized 具有更高的效率,但它的使用也更加复杂和容易出错。在使用 Lock 时需要手动进行锁的获取和释放,而 synchronized 则由 JVM 自动管理锁的获取和释放。因此,在编写多线程代码时,如果不需要 Lock 特有的功能,使用 synchronized 是更简单和安全的选择。只有在需要更高级的线程同步机制时,才需要使用 Lock

你可能感兴趣的:(JUC学习,java,开发语言)