Java中锁的学习(一)

文章目录:
1、什么是锁
2、隐式锁和显示锁
3、悲观锁和乐观锁
4、公平锁和非公平锁
5、可重入锁和非可重入锁

什么是锁

Java中的锁是一种多线程编程中的同步机制,用于**控制线程对共享资源的访问**,防止并发访问时的数据竞争和死锁问题。通过使用锁机制,可以实现数据的同步访问,确保多个线程安全地访问共享资源,从而提高程序的并发性能。

隐式锁和显示锁

显式锁和隐式锁是根据锁的获取和释放方式来进行区分的。

隐式锁

隐式锁(Implicit Lock,又称为内置锁或自动锁)是通过Java中的synchronized关键字来实现的,它在代码块或方法上加上synchronized关键字,从而隐式地获取和释放锁,不需要显式地调用锁的获取和释放方法。

隐式锁的实现主要有两种:

  • 互斥锁(Mutex):基于操作系统的互斥量(Mutex)实现,通常由操作系统提供的底层机制来保证同一时刻只有一个线程可以持有锁。
  • 内部锁(Intrinsic Lock):也称为监视器锁(Monitor Lock),是Java对象的内置锁,每个Java对象都有一个关联的监视器锁。通过synchronized关键字来获取和释放对象的监视器锁。
  • 优点:

  • 使用简便:隐式锁通常是由编程语言、运行时库或者虚拟机自动管理的,不需要程序员手动调用锁的获取和释放方法,使用较为简便。
  • 自动释放:隐式锁通常会在不需要的时候自动释放,从而减少了由于忘记释放锁而导致的死锁等问题的风险。
  • 缺点:

  • 锁定粒度较大:隐式锁通常是在方法或者代码块级别上加锁的,锁定粒度较大,可能会导致性能下降或者并发度降低。
  • 功能较少:隐式锁通常提供的功能较为有限,可能不足以满足复杂的并发场景的需求。,
  • 显示锁

    显式锁(Explicit Lock,又称手动锁)是通过Java中的Lock接口及其实现类来实现的,它提供了显式地获取锁和释放锁的方法,例如lock()和unlock()方法,需要在代码中明确地调用这些方法来获取和释放锁。

    常见的显式锁实现包括:

  • ReentrantLock:可重入锁,支持公平锁和非公平锁,并提供了丰富的特性如可中断、超时、条件等。
  • ReentrantReadWriteLock:可重入读写锁,支持多线程对共享资源的读操作,以及独占写操作。 StampedLock:支持乐观读、悲观读和写操作,并提供了乐观读的优化。
  • 悲观锁和乐观锁

    乐观锁和悲观锁是以对共享资源的访问方式来区分的。

    悲观锁

    悲观锁在并发环境中认为数据随时会被其他线程修改,因此每次在访问数据时都会加锁,直到操作完成后才释放锁。悲观锁适用于写操作多、竞争激烈的场景,比如多个线程同时对同一数据进行修改或删除操作的情况。悲观锁可以保证数据的一致性,避免脏读、幻读等问题的发生。悲观锁适用于读少写多的场景。

    Java中常用的悲观锁是synchronized关键字和ReentrantLock类。
    悲观锁存的问题:

    1、效率低:悲观锁需要获取锁才能进行操作,当有多个线程需要访问同一份数据时,每个线程都需要先获取锁,然后再进行操作,如果锁竞争激烈,就会导致线程等待锁的释放,浪费了大量的时间。
    2、容易引起死锁:悲观锁在获取锁的过程中,如果获取不到就会一直等待,如果不同的线程都在等待对方释放锁,就会导致死锁的情况出现。
    3、可能会引起线程阻塞:当某个线程获取到锁时,其他线程需要等待,如果等待的时间过长,就会导致线程阻塞,影响应用的性能

    乐观锁

    乐观锁在并发环境中认为数据一般情况下不会被其他线程修改,因此在访问数据时不加锁,而是在更新数据时进行检查。如果检查到数据被其他线程修改,则放弃当前操作,重新尝试更新。乐观锁适用于读操作多、写操作少的场景,比如多个线程同时对同一数据进行读取操作的情况。乐观锁可以减少锁的竞争,提高系统的并发性能。

    Java中常用的乐观锁是基于CAS(Compare and Swap,比较和交换)算法实现的。

    CAS操作包括三个操作数:内存地址V、旧的预期值A和新的值B。CAS操作首先读取内存地址V中的值,如果该值等于旧的预期值A,那么将内存地址V中的值更新为新的值B;否则,不进行任何操作。在更新过程中,如果有其他线程同时对该共享资源进行了修改,那么CAS操作会失败,此时需要重试更新操作。

    乐观锁存在的问题

    CAS虽然很⾼效的解决原⼦操作,但是CAS仍然存在三⼤问题:ABA问题,自旋时间过长和只能保证单个变量的原子性。

    1.ABA问题:CAS算法在比较和替换时只考虑了值是否相等,而没有考虑到值的版本信息。如果一个值在操作过程中被修改了两次,从原值变成新值再变回原值,此时CAS会认为值没有发生变化,从而出现操作的错误。为了解决ABA问题,可以在共享资源中增加版本号,每次修改操作都将版本号加1,从而保证每次更新操作的唯一性。在更新数据时先读取当前版本号,如果与自己持有的版本号相同,则可以更新数据,否则更新失败。版本号算法可以避免ABA问题,但需要维护版本号,增加了代码复杂度和内存开销。

    2.自旋时间过长:由于CAS算法在失败时会一直自旋,等待共享变量可用,如果共享变量一直不可用,就会出现自旋时间过长的问题,浪费CPU资源。

    3.只能保证单个变量的原子性:CAS算法只能保证单个变量的原子性,如果需要多个变量的原子操作,就需要使用锁等其他方式进行保护。

    公平锁和非公平锁

    按照是否按照请求的顺序来分配锁,锁分为公平锁和非公平锁。

    公平锁

    公平锁(Fair Lock)是指当多个线程竞争锁时,先到先得,后到后等,按照请求的先后顺序来获取锁。这样可以避免某些线程长时间等待锁,从而提高系统的公平性。但是,公平锁也有一些缺点,比如性能开销较大,因为需要维护一个有序的等待队列,并且可能导致更多的上下文切换。

    优点:

    1.公平性较好,避免了线程饥饿现象,每个线程都有公平的机会获取锁。

    2.具有较高的线程公平性,适用于对公平性要求较高的场景。

    缺点:

    1.公平锁的实现较为复杂,可能会导致性能较低,因为需要频繁地切换线程和维护等待队列。

    2.在高并发场景下,可能会导致大量的线程切换和等待,影响性能。

    3.可能引起死锁:如果某个线程获取锁失败而进入等待状态,而锁的持有者又在等待该线程的资源,就会出现死锁的情况。

    非公平锁

    非公平锁(Unfair Lock)是指当多个线程竞争锁时,不一定按照请求的顺序来分配锁,而是由操作系统决定哪个线程先获取锁。这样可以减少一些开销,提高系统的吞吐量。

    优点:

    1.实现简单:非公平锁的实现较为简单,通常性能较高,因为无需维护等待队列和频繁地切换线程。

    2.性能高:由于非公平锁不考虑线程的等待顺序,所以在高并发环境下可以更快地获取锁,从而提高系统的处理能力。

    缺点:

    1.不公平:非公平锁会导致一些线程长期无法获取到锁,而其他线程会一直占用锁资源,这种情况会导致线程的饥饿现象,不公平性较高。
    2.可能导致线程饥饿:如果一些线程一直占用锁资源,而其他线程无法获取锁,则后者可能永远无法执行,从而导致线程饥饿现象。
    3.不利于资源调度:非公平锁不考虑线程的等待时间,也就是说,一个刚刚进入等待队列的线程可能会比一个已经等待很久的线程先获得锁,这种情况不利于资源的合理调度,容易导致一些线程长时间处于等待状态。

    Java中有多种实现方式可以实现公平锁和非公平锁。其中最常用的是ReentrantLock类,它是一个可重入的互斥锁,可以通过构造函数传入一个boolean类型的值来指定是否为公平锁。例如:

    ReentrantLock fairLock = new ReentrantLock(true); //创建一个公平锁
    ReentrantLock unfairLock = new ReentrantLock(false); //创建一个非公平锁
    

    除了ReentrantLock之外,还有其他一些类可以实现公平锁和非公平锁,比如synchronized关键字是非公平锁、 Semaphore(信号量)、ReadWriteLock(读写锁)、StampedLock(乐观锁)等。

    下面是一段ReentrantLock公平锁的简单实现

    import java.util.concurrent.locks.ReentrantLock;
    
    public class FairLockExample {
        private static final ReentrantLock lock = new ReentrantLock(true); // true 表示使用公平锁
    
        public static void main(String[] args) {
            new Thread(() -> {
                while (true) {
                    lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + " get the lock.");
                    } finally {
                        lock.unlock();
                    }
                }
            }, "Thread-A").start();
    
            new Thread(() -> {
                while (true) {
                    lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + " get the lock.");
                    } finally {
                        lock.unlock();
                    }
                }
            }, "Thread-B").start();
        }
    }
    
    

    运行结果截图
    在这里插入图片描述

    从运行结果可以看到Thread-A和Thread-B是交替运行获取锁的,如果想使用非公平锁只需要new ReentrantLock(false),想尝试的可以自己试一下。

    选择公平锁还是非公平锁应该根据具体的业务需求和性能要求来进行权衡。如果对公平性要求较高,并且能容忍性能的一定降低,可以选择公平锁;如果对性能要求较高,并且能容忍一定程度的线程不公平现象。

    可重入锁和非可重入锁

    根据是否支持同一线程对同一锁的重复获取进行分类,分为可重入锁和非可重入锁。

    可重入锁

    可重入锁(Reentrant Lock)允许同一线程多次获取同一锁,并且不会造成死锁。可重入锁实现了锁的递归性,同一线程可以重复获取锁而不会被阻塞,因为锁会记录持有锁的线程和锁的重入次数。在线程持有锁的情况下,再次请求该锁时,如果锁是可重入的,线程会成功获取锁而不会被阻塞。

    常见的可重入锁有:

    1.java.util.concurrent.ReentrantLock 类:这是 Java 并发包中提供的一个可重入锁实现,提供了丰富的功能,如支持公平和非公平锁、可设置超时时间、支持多个条件等。

    2.java.util.concurrent.ReentrantReadWriteLock 类:这是 Java 并发包中提供的一个可重入读写锁实现,允许多个线程同时读取共享资源,但在写操作时需要互斥。

    3.Java 的内置锁 synchronized关键字是可重入的

    import java.util.concurrent.locks.ReentrantLock;
    
    public class ReentrantLockExample {
        private ReentrantLock lock = new ReentrantLock(); // 创建一个可重入锁
    
        public void doSomething() {
            lock.lock(); // 加锁
            try {
                // 这里是需要加锁的代码
                System.out.println("线程 " + Thread.currentThread().getName() + " 获取到锁");
                doSomethingElse(); // 可以再次调用自己的方法,多次获取锁,不会导致死锁
            } finally {
                lock.unlock(); // 解锁
                System.out.println("线程 " + Thread.currentThread().getName() + " 释放锁");
            }
        }
    
        public void doSomethingElse() {
            lock.lock(); // 再次加锁,同一个线程可以多次获取同一个锁
            try {
                // 这里是需要加锁的代码
                System.out.println("线程 " + Thread.currentThread().getName() + " 获取到锁(再次获取)");
            } finally {
                lock.unlock(); // 解锁
                System.out.println("线程 " + Thread.currentThread().getName() + " 释放锁(再次获取)");
            }
        }
    
        public static void main(String[] args) {
            ReentrantLockExample example = new ReentrantLockExample();
            for (int i = 1; i <= 3; i++) {
                new Thread(() -> {
                    example.doSomething();
                }, "Thread " + i).start();
            }
        }
    }
    
    

    结果:

    线程 Thread 1 获取到锁
    线程 Thread 1 获取到锁(再次获取)
    线程 Thread 1 释放锁(再次获取)
    线程 Thread 1 释放锁
    线程 Thread 2 获取到锁
    线程 Thread 2 获取到锁(再次获取)
    线程 Thread 2 释放锁(再次获取)
    线程 Thread 2 释放锁
    线程 Thread 3 获取到锁
    线程 Thread 3 获取到锁(再次获取)
    线程 Thread 3 释放锁(再次获取)
    线程 Thread 3 释放锁
    

    在上面的示例中,我们使用了ReentrantLock来创建一个可重入锁,并通过lock()方法加锁,通过unlock()方法解锁。在doSomething()方法中,我们可以多次获取同一个锁,而不会导致死锁。这就是可重入锁的特性,使得同一个线程在持有锁的情况下可以继续获取锁,从而避免了死锁的可能性。

    优点

    1.支持线程的重复获取:同一个线程可以多次获取同一个锁,避免了死锁的可能性。

    2.支持公平和非公平锁:可以根据需求选择公平或非公平锁,灵活性高。

    3.提供了丰富的功能:如可设置超时时间、支持多个条件等,使得锁的使用更加灵活。

    缺点

    1.复杂性高:相比于内置锁 synchronized 关键字,可重入锁的使用复杂度较高,需要手动加锁和解锁,容易出错。

    2.性能相对较低:相较于内置锁 synchronized 关键字,可重入锁的性能可能较低,因为它提供了更多的功能和灵活性。

    非可重入锁
    非可重入锁(Non-reentrant Lock)不允许同一线程多次获取同一锁,否则会造成死锁。非可重入锁实现了简单的互斥,但不支持同一线程对同一锁的重复获取。

    缺点

    1.不支持线程的重复获取:同一个线程无法多次获取同一个锁,容易导致死锁。

    2.功能较为简单:非可重入锁通常只提供了基本的加锁和解锁功能,缺乏灵活性和丰富的功能。

    是的,你没有看错非可重入锁没有优点。非可重入锁在 Java 中没有现成的实现。

    未完待续。。。

    下一篇我们讲讲什么是独占锁、共享锁、分段锁、自旋锁、轻量级锁、重量级锁等

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值