Lock锁操作机制

本文深入探讨了Java中的Lock锁机制,包括Lock接口、ReentrantLock(可重入锁)、synchronized与ReentrantLock的区别,以及newCondition和ReadWriteLock(读写锁)的应用。通过示例和对比分析,展示了Lock锁在多线程同步中的高级用法,如可中断等待、公平锁和非公平锁,以及条件对象的等待/通知模式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是Lock锁

Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对 象。Lock 提供了比 synchronized 更多的功能。

Lock 与的 Synchronized 区别

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

1. lock接口:

在这里插入图片描述

Look接口常用方法

public interface Lock {
    void lock();//获得锁
    //获取锁定,除非当前线程是interrupted
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();//只有在调用时才可以获取锁
    //如果在给定的时间内是空闲的,并且当前的线程尚未得到释放,即可获取锁
    boolean tryLock(long time, TimeUnit unit)throws InterruptedException;
    void unlock();//释放锁
    Condition newCondition();//返回一个新的Condition绑定到Lock实例
}
  • lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。 采用 Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用 Lock 必须在 try{}catch{}块中进行,并且将释放锁的操作放在finally 块中进行,以保证锁一定被被释放,防止死锁的发生。

  • unlock()方法用于释放锁。

示例

  ock.lock();
  try{
  //处理任务
  }catch(Exception ex){
  }finally{
  lock.unlock(); //释放锁
  }

2. ReentrantLock(可重入锁)

ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更多的方法。

在这里插入图片描述

public class ReentrantLock {
    private ArrayList<Integer> arrayList = new ArrayList<Integer>();

    public static void main(String[] args) {
        final ReentrantLock test = new ReentrantLock();
        new Thread(() -> test.insert(Thread.currentThread())).start();
        new Thread(() -> test.insert(Thread.currentThread())).start();
    }

    public void insert(Thread thread) {
        java.util.concurrent.locks.ReentrantLock lock =  new java.util.concurrent.locks.ReentrantLock(); //注意这个地方
        lock.lock();
        try {
            System.out.println(thread.getName() + "得到了锁");
            for (int i = 0; i < 5; i++) {
                arrayList.add(i);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(thread.getName() + "释放了锁");
            lock.unlock();
        }
    }
}

ReentrantLock()锁既可以是公平锁也可以是非公平锁

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

ReentrantLock()默认情况下是非公平锁状态,当传参为true时表示公平锁

3 synchronized 和 ReentrantLock 的区别

  1. synchronized依赖于JVM,官方在JDK1.6之后对其进行了优化,但是这些优化都在在底层实现的,没有直接暴漏出来;而ReentrantLock依赖与API,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的
  2. 相比于synchronized , ReentrantLock有以下几个优点:
    1. 等待可中断ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
    2. 可实现公平锁ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
    3. 两者都是可重入锁

“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。

在这里插入图片描述
当一个线程获得当前实例的锁lock,并且进入了方法A,该线程在方法A没有释放该锁的时候,是否可以再次进入使用该锁的方法B?

  • 不可重入锁:在方法A释放锁之前,不可以再次进入方法B

  • 可重入锁:在方法A释放该锁之前,可以再次进入方法B

重入进一步提升了加锁行为的封装性,因而简化了面向对象并发代码的开发。在以下程序中,子类改写了父类的 synchronized 方法,然后调用父类中的方法,此时如果内置锁不是可重入的,那么这段代码将产生死锁。由于 Widget 和 LoggingWidget 中 doSomething 方法都是 synchronized 方法,因此每个每个 doSomething 方法在执行前都会获取 Widget 上的锁。然而如果内置锁不是可重入的,那么调用 super.doSomething( )时无法获得 Widget 上的锁,因为这个锁已经被持有,从而线程将永远停顿下去,等待一个永远也无法获得的锁。重入则避免了这种死锁情况的发生。

public class Widget{
public synchronized void doSomething(){

}
}

public class LoggingWidget extends Widget{
public synchronized void doSomething(){
super.doSomething();
}
}

synchronized是隐式可重入锁,Lock是显示可重入锁

 public class SyncLockDemo {

    public static void main(String[] args) {
        Object o = new Object();
        new Thread(()->{
            synchronized(o) {
                System.out.println(Thread.currentThread().getName()+" 外层");

                synchronized (o) {
                    System.out.println(Thread.currentThread().getName()+" 中层");

                    synchronized (o) {
                        System.out.println(Thread.currentThread().getName()+" 内层");
                    }
                }
            }

        },"t1").start();
    }

}

输出结果:

t1 外层
t1 中层
t1 内层

进程已结束,退出代码为 0

在类中创建一个add方法

    public synchronized void add() {
        add();
    }

在main方法中调用该方法

new SyncLockDemo().add();

结果出现错误:

Exception in thread "main" java.lang.StackOverflowError
	at com.SyncLockDemo.add(SyncLockDemo.java:12)
	at com.SyncLockDemo.add(SyncLockDemo.java:12)

原因

synchronized是一个可重入锁,用的是同一把锁的情况下,该方法不断调用add方法,导致方法栈溢出,

public static void main(String[] args) {
        //Lock演示可重入锁
        ReentrantLock lock = new ReentrantLock();//非公平锁
        //创建线程
        new Thread(()->{
            try {
                //上锁
                lock.lock();
                System.out.println(Thread.currentThread().getName()+" 外层");

                try {
                    //上锁
                    lock.lock();
                    System.out.println(Thread.currentThread().getName()+" 内层");
                }finally {
                    //释放锁
                    lock.unlock();
                }
            }finally {
                //释放做
                lock.unlock();
            }
        },"t1").start();
    }
 }

输出结果:

t1 外层
t1 内层

特殊情况,注释掉try-finally中的try-finally中的释放锁

输出结果正常;

原因:Lock是可重入锁,自己操作自己可以正常运行,

在main中创建一个新线程

        //创建新线程
        new Thread(()->{
            lock.lock();
            System.out.println("aaaa");
            lock.unlock();
        },"aa").start();

结果不能正常输出,因为Lock锁没有 释放,新线程无法获取锁。必须等到lock锁释放,程序才能往下走。

4.newCondition

Condition因素出Object监视器方法( waitnotifynotifyAll )成不同的对象,以得到具有多个等待集的每个对象,通过将它们与使用任意的组合的效果Lock实现。 Lock替换synchronized方法和语句的使用, Condition取代了对象监视器方法的使用。


一个Condition实例本质上绑定到一个锁。 要获得特定Condition实例的Condition实例,请使用其newCondition()方法。

在这里插入图片描述

关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock 锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式。

用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知, Condition 比较常用的两个方法:

• await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重新获得锁并继续执行。

• signal()用于唤醒一个等待的线程。

注意:在调用 Condition 的 await()/signal()方法前,也需要线程持有相关的 Lock 锁,调用 await()后线程会释放这个锁,在 singal()调用后会从当前Condition 对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。

虚假唤醒
class BoundedBuffer {
    final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length) //注意这里用的是while
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
      ++count;
       notEmpty.signal();
    } finally {
       lock.unlock();
    }
  }
​

当前线程调用wait的时候必须先获取到锁,而执行了wait方法之后当前线程回释放该锁并且进入进入waiting状态,直到其他线程调用nodify方法或者notifyAll方法。

我们知道Condition中的signal方法和Object类里面的notify方法效果类似,并且调用方法之前需要先获取到锁,Condition中的await方法和Object类里面的wait方法效果类似,也是调用之前需要获取到锁,而调用之后会释放锁。

注意重点调用await方法或者wait方法之后会释放掉锁,有了这个前提再看上面的代码,只看重点的while循环这一步,

...
while (count == items.length) //注意这里用的是while
         notFull.await();
...

当count == items.length条件满足的时候,走到await方法,当前线程挂起,释放掉锁,这时候count变量由于没有被锁住,所以可能会有其他线程将count值修改,导致条件判断产生变化。当另一个线程调用signal唤醒此线程的时候,if和while的区别就体现出来了,如果是if的话线程被唤醒就会直接从await方法往下执行,但是这时候count可能被修改,count == items.length条件可能又满足了,这种情况应该是让线程继续挂起,但是用if会唤醒线程,产生了所谓的虚假唤醒,而如果用while的话,线程被唤醒之后还会在执行一次条件判断,确保条件count == items.length不满足的时候才唤醒线程。

5.ReadWriteLock(读写锁)

ReadWriteLock 也是一个接口,在它里面只定义了两个方法:

  1. Lock readLock(); //读锁
  2. Lock writeLock(); //写锁
5.1使用synchronized锁创建多线程同时进行读操作。
public class Test {

    public static void main(String[] args) {
        final Test test = new Test();
        new Thread(() -> test.get(Thread.currentThread())).start();
        new Thread(() -> test.get(Thread.currentThread())).start();
    }

    public synchronized void get(Thread thread) {
        long start = System.currentTimeMillis();
        while (System.currentTimeMillis() - start <= 1) {
            System.out.println(thread.getName() + "正在进行读操作");
        }
        System.out.println(thread.getName() + "读操作完毕");
    }
}

结果:

...
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-1读操作完毕

进程已结束,退出代码为 0

有结果可知:使用synchronized锁进行多线程读操作为非公平锁

5.2使用ReadReadWriteLock锁进行多线程同时进行读操作

public class Test {

    private ReentrantReadWriteLock rwl = new
            ReentrantReadWriteLock();

    public static void main(String[] args) {
        final Test test = new Test();

          new Thread(() -> test.get(Thread.currentThread())).start();
          new Thread(() -> test.get(Thread.currentThread())).start();

    }

    public void get(Thread thread) {
        rwl.readLock().lock();//加锁
        try {
            long start = System.currentTimeMillis();

            while (System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName() + "正在进行读操作");
            }
            System.out.println(thread.getName() + "读操作完毕");
        } finally {
            rwl.readLock().unlock();//释放锁
        }
    }
}

结果:

...
Thread-1正在进行读操作
Thread-1正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-0正在进行读操作
Thread-1正在进行读操作
Thread-1读操作完毕
Thread-0读操作完毕

进程已结束,退出代码为 0

说明 两个进程在同时进行读操作。这样就大大提升了读操作的效率。使用读写锁之前需要先加锁

注意:

• 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。

• 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值