深入显出一篇能懂Java锁机制,Synchronized和ReentrantLock

Posted 陆海潘江小C

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入显出一篇能懂Java锁机制,Synchronized和ReentrantLock相关的知识,希望对你有一定的参考价值。

目录

一、synchronized关键字特性

二、synchronized应用:双重校验锁实现单例模式

三、显式锁:Lock和ReentrantLock

1、Lock接口

2、ReentrantLock

四、锁的公平性

五、synchronized和ReentrantLock的区别 


本篇内容共 2303 字,5234字符,阅读需要 6分钟。 

在Java中,多线程并发的应用场景下,同步性和安全性在设计中尤为重要,这也是面试当中时常提及的问题。在项目中,如果有使用多线程的经历,通常会应用到Java中的锁来保证线程安全和数据的一致性。

到目前,Java中的锁机制可以说很全面且强大了。同步、共享、互斥等功能的实现,我们有了许多选择,包括实现共享对象访问的机制:synchronized和volatile,还有提供高级特性的ReentrantLock机制

这篇文章就简单记录一下synchronized 和 ReentrantLock 的原理应用和区别,在以后的使用选择中有着更加明确的依据。

一、synchronized关键字特性

sychronized关键字保证多个线程之间访问资源的同步性,被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

synchronized关键字是原生语法层面的互斥,在JVM层面实现,是一种重量级锁,因为在互斥状态下,没有获取到锁的线程会被挂起阻塞,而挂起线程和恢复线程的操作都需要系统转入内核态中完成。

在JDK1.6官方对synchronized在JVM层面进行大量的优化,引入了自旋锁、偏向锁、适应性自旋锁、锁消除、锁粗化、轻量级锁等技术来减少锁操作的开销。

另外,上面提到的使用synchronized修饰方法和代码块,主要有三种情况:

  1. 修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。
  2. 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。

二、synchronized应用:双重校验锁实现单例模式

以下实现的单例模式是线程安全的,使用synchronized关键字将类对象加锁。

/**
 * 双重校验锁实现单例模式(线程安全)
 * @author Charzous
 * @date 2021-07-03 下午 06:56
 *
 */
public class singleton {
    private static volatile singleton singletonInstance;

    private singleton(){

    }

    public static singleton getSingletonInstance(){
//        先判断对象是否已经实例过,没有实例化过才进入加锁代码块
        if (singletonInstance==null){
//            类对象加锁
            synchronized (singleton.class){
                if (singletonInstance==null){
                    singletonInstance=new singleton();
                }
            }
        }
        return singletonInstance;
    }
}

其中,volatile关键字是必要的,因为 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2

在多线程的情况下,volatile关键字保证了可见性,禁止 JVM 的指令重排,保证在多线程环境下也能正常运行 。

三、显式锁:Lock和ReentrantLock

1、Lock接口

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

 在JDK1.5,开始提供Lock接口,从上面的源码可以发现,与synchronized这种内部加锁机制不同,Lock提供了无条件、定时的、可轮询、可中断的锁操作加锁和解锁的操作都是显式进行的。

2、ReentrantLock

ReentrantLock是JDK1.5提供的互斥锁API。它实现了Lock接口,提供了跟synchronized相同的互斥和内存可见。

ReentrantLock支持Lock接口定义的所有获取锁的模式,因此对于处理不可用的锁提供了更多的灵活性。ReentrantLock锁机制的本质跟内部锁十分相似,它的优势就体现在:

  • ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态
  • 简化了代码,更好地与异常处理机制相结合,某些情况下,提供了更好的性能。

当然,使用ReentrantLock的时候,需要知道它的特性以及带来的问题。

上面说到,Lock的加锁和解锁都是显式操作,因此这种锁比内部锁更加复杂,锁必须在finally块中释放。另外,如果在加锁的代码块之外抛出了异常,锁将永远不会被释放。这些问题都是需要警惕的,出现错误很难找出程序的发生点。

class X {
     ReentrantLock lock = new ReentrantLock();
     // ...
     public void m() {
     assert !lock.isHeldByCurrentThread();
     lock.lock();
         try {
             // ... method body
         } finally {
             lock.unlock();
         }
     }
}

四、锁的公平性

ReentrantLock的构造函数中提供了两种锁的选择:非公平锁(默认)和公平锁

部分源码如下:

    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

    /**
     * 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实现的HashMap,即ConcurrentHashMap,表现出的性能不错,而公平锁表现最差。

这是因为:

在竞争激烈的情况下,抢占机制可以使得线程获取资源的效率更高。非公平锁的合适使用场景持有锁的时间段,或者请求锁的平均时间间隔比较短

假设线程A持有一个锁,线程B请求该锁,此时线程B会被挂起,当A释放锁之后B才开始。同时,存在线程C请求该锁,C有机会得到这个锁,而且可能在B唤醒前C已经释放该锁。因此,这种情况下,C更早获取锁,B也在唤醒时得到锁,这是吞吐量得到了提高。

 相反,对于持有锁时间长,请求锁平均时间间隔长的,公平锁就更有优势。

五、synchronized和ReentrantLock的区别

学习到这里,我们已经初步了解synchronized关键字和ReentrantLock的原理和一些应用场景,在分析过程中也可以发现他们相似和相异之处,这里简单总结一下:

  1. 两者都是可重入锁。
  2. synchronized是关键字,属于JVM层面,而ReenTrantLock是锁API。
  3. ReenTrantLock 比 synchronized 增加了一些高级功能,可见上文。
  4. ReentrantLock可实现选择性锁机制。

学习Java开发,多线程等知识,最近我也在跟着这一套 《Java 工程师学习成长知识图谱》进行体系的学习,是CSDN官方推出的,质量很不错!

其中包含了Java专业体系结构完整详细,推荐给大家学习使用,有兴趣可以扫码查看,最近我也在学习当中,当然,我的文章会记录学习,欢迎大家阅读,比如我的专栏《Java宝藏》、《Socket网络编程》。

展开就是这样的,尺寸870mm*560mm排版好看,内容很充实。推荐给有需要的伙伴,一起来学习Java!


如果觉得不错欢迎“一键三连”哦,点赞收藏关注,评论提问建议,欢迎交流学习!一起加油进步,我们下篇见! 

本篇内容首发我的CSDN博客:https://csdn-czh.blog.csdn.net/article/details/118438799

以上是关于深入显出一篇能懂Java锁机制,Synchronized和ReentrantLock的主要内容,如果未能解决你的问题,请参考以下文章

深入研究 Java Synchronize 和 Lock 的区别与用法

[转] 深入研究 Java Synchronize 和 Lock 的区别与用法

WebRTC 传输安全机制:深入显出 SRTP 协议

深入研究 Java Synchronize 和 Lock 的区别与用法

深入浅出Java并发包—锁机制

带你解锁蓝牙skill