Java中常用锁的实现方式

ReentrantLock 介绍

介绍:

ReentrantLock  是一个可重入且独占式,是基于 Lock 实现的可重入锁,所有的 Lock 都是基于 AQS 实现的,AQS 和 Condition 各自维护不同的对象,在使用 Lock 和 Condition 时,其实就是两个队列的互相移动。它所提供的共享锁、互斥锁都是基于对 state 的操作。而它的可重入是因为实现了同步器 Sync,在 Sync 的两个实现类中,包括了公平锁和非公平锁,它的功能类似于 synchronized 是一种互斥锁,可以保证线程安全。

ReentrantLock 和 synchronized 的区别

相同点:

  1. 它们都是加锁方式同步
  2. 都是重入锁
  3. 阻塞式的同步;也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善);

不同点:

比较点 synchronized ReentrantLock(实现了 Lock 接口
构成 它是 Java 语言的关键字,是原生语法层面的互斥,需要 jvm 实现 它是 JDK 1.5 之后提供的 API 层面的互斥锁类
实现 通过 JVM获取锁/释放锁 API 层面获取释放锁,释放锁需要手动
代码编写 采用 synchronized 不需要手动释放锁,当 synchronized 方法或者 synchronized 代码块执行之后,系统会自动让线程释放随所的占用,更安全 ReentrantLock 则必须要手动释放锁,如果没有主动释放锁,就有可能出现死锁,需要 lock() 和 unlock() 方法配置 try-finally 语句块完成
灵活性 锁的范围是整个方法或 synchronized 代码块部分 使用 Lock 接口的方法调用,可以跨方法,灵活性更强大
中断 不可中断,除非抛出异常(释放锁的方式):
1、代码执行完,正常释放锁
2、抛出异常,由 JVM 退出等待
可中断,持有锁的线程长期不释放的时候,正在等待的线程以选择放弃等待(方法):
1、设置超时方式 tryLock(long timeout, TimeUnit unit) 时间过了就放弃等待
2、lockInterruptibly() 放代码块中,调用 interrupt()  方法可中断
公平 非公平锁,不考虑排队问题直接尝试获取锁 公平锁和非公平锁两者都可以,默认非公平锁,检查是否有排队等待的线程,先来者先得锁,构造器可以传入 boolean 值,true 为公平锁,false 为非公平锁
条件 通过多次 new Condition 可以获取多个 Condition 对象,可以简单的实现比较复杂的线程同步功能
高级 通过提供方法来监听当前锁的信息,如:
getHoldCount()
getQueueLength()
isFair()
isHeldByCurrentThread()
isLocked()
便利 方便清洁,由编译器去保证的加锁和释放 需要手动来声明加锁和释放锁
场景 资源竞争不是很激烈的情况下,偶尔会有同步的情形下, synchronized 是很合适的。原因在于,编译程序通常会尽可能的进行优化 synchronized,另外可读性非常好 提供了多样化的同步,比如有时间限制的同步,可以被 Interrupt 的同步等
性能 并发优先,性能好,可读性好 高并发优先

synchronized 加锁实现

说明:synchronized 关键字是Java中最基本的加锁方式,用于实现线程的同步。它可以修饰方法或代码块,并且在对象级别或类级别上加锁。当线程进入 synchronized 修饰的代码块或方法时,会尝试获取锁,如果锁已被其他线程持有,则该线程会被阻塞,直到获取到锁为止。

代码实现

public synchronized void synchronizedMethod() {
    // 同步的方法体
}

public void someMethod() {
    synchronized (lockObject) {
        // 同步的代码块
    }
}

ReentrantLock 初始化

Lock lock = new ReentrantLock(true);  // true:公平锁
// Lock lock = new ReentrantLock();  // 默认 false 非公平锁
// 加锁 获取不到锁一直等待直到获取锁
lock.lock();
try {
    // 相关代码
finally {
    // 释放锁 如果不释放其他线程就获取不到锁
    lock.unlock();
}

Lock 中的方法说明

/**
* 获取锁。
* 如果锁不可用,则当前线程将因线程调度而被禁用,并处于休眠状态,直到获得锁为止。
*/

void lock();

/**
当前线程被中断,获取锁
获取锁(如果可用)并立即返回。
如果锁不可用,则当前线程将因线程调度而被禁用,并处于休眠状态,直到发生以下两种情况之一:
1. 锁被当前线程获取;或者
(获取锁正常返回)
2.其他线程中断当前线程,并支持当前线程在获取锁时中断.
如果当前线程:
在进入此方法时设置其中断状态;或获取锁时中断,支持锁获取中断,
然后抛出InterruptedException并清除当前线程的中断状态。 
(意思睡眠时其他线程中断了当前线程获取锁直接清除当前线程睡眠状态)
*/

void lockInterruptibly() throws InterruptedException;

/**
只有在调用时它是空闲的时才获取锁。 (意思锁可能拿不到 lock是一定能拿得到)
获取锁(如果可用),并立即返回值true。如果此方法不可用,则该方法将立即返回false。
此方法的典型用法是:
Lock lock = ...;
if (lock.tryLock()) {
    try {
        // manipulate protected state
    } finally {
     lock.unlock();
     }
 } else {
    // perform alternative actions
 }
这种用法确保锁在被获取时被解锁,而在未获得锁时不尝试解锁。
*/

boolean tryLock();

/**
tryLock重载方法
如果锁在给定的等待时间内空闲并且当前线程没有中断,则获取该锁。
如果指定的等待时间为false,则返回的值为false。如果时间小于或等于零,则该方法根本不会等待。
time–等待锁定的最长时间
unit–时间参数的时间单位
*/

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

/**
释放锁。
注意:
锁实现通常会对线程释放锁施加限制(通常只有锁的持有者才能释放锁),如果违反了限制,
则可能会抛出(未检查的)异常。任何限制和异常类型都必须由该锁实现记录。
*/

void unlock();

/**
返回绑定到此Lock实例的新条件实例。

在等待条件之前,锁必须由当前线程持有。打电话给Condition.await() 将在等待之前自动释放锁,
并在等待返回之前重新获取锁。

注意事项

条件实例的确切操作取决于锁实现,并且必须由该实现记录。
Condition  实现 wait notify 的功能 并且功能更强大
*/

Condition newCondition();

应用场景

解决并发安全问题

解决并发安全问题

@Slf4j
public class Test {
    private static int sum = 0;
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(() -> {
                //加锁
                lock.lock();
                try {
                    for (int j = 0; j < 10000; j++) {
                        sum++;
                    }
                } finally {
                    // 解锁
                    lock.unlock();
                }
            });
            thread.start();
        }
        Thread.sleep(2000);
        System.out.println("结果:" + sum);
    }
}

不加锁结果:

Java中常用锁的实现方式

解锁结果:

Java中常用锁的实现方式

可重入

  • 可重入锁是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此 有权利再次获取这把锁
  • 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
@Slf4j
public class Test {
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        // 如果有竞争就进入`阻塞队列`, 一直等待着,不能被打断
        // 主线程main获得锁
        lock.lock();
        try {
            log.info("main");
            method1();
        } finally {
            lock.unlock();
        }
    }


    public static void method1() {
        lock.lock();
        try {
            log.info("方法1");
            method2();
        } finally {
            lock.unlock();
        }
    }

    public static void method2() {
        lock.lock();
        try {
            log.info("方法2");
            method3();
        } finally {
            lock.unlock();
        }
    }

    public static void method3() {
        lock.lock();
        try {
            log.info("方法3");
        } finally {
            lock.unlock();
        }
    }
}

运行结果:

Java中常用锁的实现方式

可中断

synchronized 和 reentrantlock.lock() 的锁, 是不可被打断的; 也就是说别的线程已经获得了锁, 线程就需要一直等待下去,不能中断,直到获得到锁才运行。

通过 reentrantlock.lockInterruptibly(); 可以通过调用阻塞线程的 t1.interrupt(); 方法打断。

@Slf4j
public class Test {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            log.info("t1线程启动");
            try {
                log.info("尝试获得锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.info("t1线程没有获得锁,被中断...");
                return;
            }
            try {
                log.info("t1线程获得了锁");
            } finally {
                lock.unlock();
            }
        }, "t1");

        // t1启动前 主线程先获得了锁
        lock.lock();
        try {
            log.info("t1获取线程前,main先获取锁");
            t1.start();
            // 先让线程t1执行
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t1.interrupt();
            log.info("线程t1执行中断");
        } finally {
            lock.unlock();
        }
    }
}

运行结果:

Java中常用锁的实现方式

可以看到 t1 一直都没有获取到锁

公平锁

多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。优点:所有的线程都能得到资源,不会饿死在队列中。缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

@Slf4j
public class Test {
    private static final ReentrantLock lock = new ReentrantLock(true);

    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            new Thread(new TestThread(i)).start();
        }

    }

    static class TestThread implements Runnable {
        Integer id;

        public TestThread(Integer id) {
            this.id = id;
        }

        @Override
        public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 2; i++) {
                lock.lock();
                System.out.println("获得锁的线程:" + id);
                lock.unlock();
            }
        }
    }
}

运行结果:哪个线程先抢到锁就先获取

Java中常用锁的实现方式

非公平锁

多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

public class Test {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            new Thread(new TestThread(i)).start();
        }

    }

    static class TestThread implements Runnable {
        Integer id;

        public TestThread(Integer id) {
            this.id = id;
        }

        @Override
        public void run() {
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 2; i++) {
                lock.lock();
                System.out.println("获得锁的线程:" + id);
                lock.unlock();
            }
        }
    }
}

运行结果:每个线程获取锁两次后,下一线程在获取锁

Java中常用锁的实现方式

大部分情况下我们使用非公平锁,因为其性能比公平锁好很多。但是公平锁能够避免线程饥饿,某些情况下也很有用。

Lock锁中 lock() 与tryLock()的区别

在ReentrantLock 中

Lock()方法:lock()方法是一个无条件的锁,与synchronize意思差不多,直接去获取锁。成功了就ok了,失败了就进入阻塞等待了。不同的是lock锁是可重入锁。另一个方法 tryLock()方法:当获取锁时,只有当该锁资源没有被其他线程持有时才可以获取到,成功获取到锁资源之后,会立即返回true;当获取锁时,如果有其他的线程正在持有该锁,无可用锁资源,则会立即返回false!这时候当前线程不用阻塞等待,可以先去做其他事情;如果为这个方法加上timeout参数,则会在等待timeout的时间之后,才会返回false,或者在获取到锁之后返回true。

原文始发于微信公众号(师小师):Java中常用锁的实现方式

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/226328.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!