专栏首页程序员开发者社区synchronized 和 ReentrantLock 有什么区别?

synchronized 和 ReentrantLock 有什么区别?

synchronized 和 ReentrantLock 有什么区别?

synchronized 最慢,这话靠谱么?

Synchronized 是 Java 内建的同步机制,所以也有人称其为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞。Java 5 之前,synchronized 是仅有的同步手段,在代码中,Synchronized 可以用来修饰方法,代码块。

ReentrantLock , 通常翻译为可重入锁,是 Java 5 提供的锁实现,通过代码直接调用 lock() 方法获取代码书写也更加灵活,与此同时,ReentrantLock 提供了很多实用的方法,可以实现很多 synchronized 无法做到的细节控制,但是需要明确调用 unlock()方法释放。

什么是线程安全?

《Java并发编程实战》中定义,线程安全是一个多线程环境下正确性的概念。保证多线程环境下共享的可修改的状态的正确性。这里的状态其实可以看做程序中的数据。

换个角度,如果状态是不共享的, 不可修改的,也就不存在线程安全问题了。

如何保证线程安全

  • 封装: 通过封装,将内部对象隐藏保护起来。
  • 不可变:fina变量产生了某种程度的不可变( immutable)的效果,所以,可以用于保护只读数据,尤其是在并发编程中,因为明确地不能再赋值final变量,有利于减少额外的同步开肖,也可以省去一些防御性拷贝的必要

线程安全要保证几个基本特性

  • 原子性,相关操作不会中途被其他线程干扰,一般通过同步机制实现。
  • 可见性,一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主內存上,v. latile就是负责保证可见性的
  • 有序性, 保证线程内串性语义,避免指令重排。

一个非线程安全的例子

public class ThreadSafeSample {
    public int shareState;

    public void noSafeAction() {
        while (shareState < 10000) {
            int former = shareState++;
            int latter = shareState;
            if (former != latter - 1) {
                System.out.println("Observerd data race, former is " + former + ", latter is " + latter);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadSafeSample sample = new ThreadSafeSample();
        Thread threadA = new Thread() {
            @Override
            public void run() {
                sample.noSafeAction();
            }
        };

        Thread threadB = new Thread() {
            public void run() {
                sample.noSafeAction();
            };
        };

        threadA.start();
        threadB.start();
        threadA.join();
        threadB.join();

    }

}

运行结果:

Observerd data race, former is 8832, latter is 8836

可以看到 保证 shareState 这个字段是线程不安全的,两个线程同时操作,会导致与预期结果不符合。如果想要线程安全,可以做如下修改:

 synchronized(this){
    int former = shareState++;
    int latter = shareState;
    if (former != latter - 1) {
        System.out.println("Observerd data race, former is " + former + ", latter is " + latter);
    }
}

synchronized ,ReentrantLock 底层实现。

synchronized

如果用 Javap反编译,可以看到类似片段,利用 monitorenter/monitorexit通对实现了同步的语义。测试代码:

public class SynchronizedDemo {
    
    public  static synchronized void doSth(){
        System.out.println("Hello World");
    }
    
    
    public static void doSth1(){
         synchronized(SynchronizedDemo.class){
             System.out.println("Hello World 1");
         }
    }
    
	  /**
     * @param args
     */
    public static void main(String[] args) {
        SynchronizedDemo.doSth1();
    }
    
}

javac javap 可以看到 synchronized 内部实现

ReentrantLock

  • 可重入性 它是表示当一个线程试图获取一个它已经获取的锁时,这个获取动作就自动成功,这是对锁获取粒度的一个概念,也就是锁的持有是以线程为单位而不是基于调用次数。Java锁实现强调再入性是为了和 pthread的行为进行区分。
  • 公平性 再入锁可以设置公平性( fairness),我们可在创建再入锁时选择是否是公平的。
ReentrantLock fairLock= new ReentrantLock(true)

这里所谓的公平性是指在竞争场景中,当公平性为真时,会倾向于将锁赋予等待时间最久的线程。公平性是减少线程¨饥饿”(个别线程长期等待锁,但始终无法获取)情况发生的一个办法,避免线程饿死。使用示例:为保证锁释放,每一个lock动作,我建议都立即对应一个try- catch- finally,典型的代码结构如下,这是个良好的习惯。

Reentrantlock fairlock= new Reentrantlock(true); //这里是演示创健公平锁,一般情况不需要。
try{
  // do something
}finally{
  fairLock.unlock();
}

Reentrantlock 与 synchronized区别

  • 带超时的获取锁尝试
  • 可以判断是否有线程,或者某个特定线程,在排队等待获取锁。
  • 可以响应中断请求

理解锁膨胀、降级;理解偏斜锁、自旋锁、轻量级锁、重量级锁等概念。

  • https://mp.weixin.qq.com/s?__biz=MzU4NDEwMzU3Mg==&mid=2247484401&idx=1&sn=8da04d5348487d403bb109f737afcb76&chksm=fd9fa00acae8291cfb42e875d51bd37f7ad34f93256839998c00d0e05a9a978cd616235da5eb&token=22571043&lang=zh_CN#rd

java.util.concurrent.Condition。

这里我特别想强调条件变量(java.util.concurrent.Condition),如果说 Reentrantlock是 synchronized的替代选择, Condition则是将wait、 notify、 notify等操作转化为相应的对象,将复杂而晦涩的同步操作转变为直观可控的对象行为。

Condition 的一个应用场景是标准类库中的 ArrayBlockingQueue。 ArrayBlockingQueue 构造函数

/** Main lock guarding all access */
final ReentrantLock lock;

/** Condition for waiting takes */
private final Condition notEmpty;

/** Condition for waiting puts */
private final Condition notFull;
public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

两个Condition 都是从一个可重入锁中创建出来的。

 public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

当队列为空时,试图take的线程的正确行为应该是等待入队发生,而不是直接返回,这是 Blockingqueue的语义,使用条件notempty就可以优雅地实现这一逻辑。那么,怎么保证入队触发后续take操作呢?请看 enqueue实现

private void enqueue(E x) {
      // assert lock.getHoldCount() == 1;
      // assert items[putIndex] == null;
      final Object[] items = this.items;
      items[putIndex] = x;
      if (++putIndex == items.length)
          putIndex = 0;
      count++;
      notEmpty.signal();
  }

通过 signal/await的组合,完成了条件判断和通知等待线程,非常顺畅就完成了状态流转。注意, signa和 await成对调用非常重要,不然假设只有 await动作,线程会一直等待直到被打断(interrupt)。

总结

  1. 用法比较

Lock使用起来比较灵活,但是必须有释放锁的配合动作Lock必须手动获取与释放锁,而 synchronized不需要手动释放和开启锁Lock只适用于代码块锁,而 synchronized可用于修饰方法、代码块等

  1. 特性比较

ReentrantLock的优势体现在具备尝试非阻塞地获取锁的特性:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁迮被中断地获取锁的特性:与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛岀,同时锁会被释放超时获取锁的特性:在指定的时间范围内获取锁;如果截止时间到了仍然 无法获取锁,则返回.

  1. 注意事项

在使用 Reentrantlock类的时,一定要注意三点在 finally 中释放锁,目的是保证在获取锁之后,最终能够被释放不要将获取锁的过程写在try块內,因为如果在获取锁时发生了异常,异常拋岀的同时,也会导致锁无故被释放。 Reentrantlock提供了—个 newCondition的方法,以便用户在同一锁的情况下可以根据不同的情况执行等待或唤醒的动作。

AQS和 Condition各自维护了不同的队列,在使用lock和 condition的时候,其实就是两个相移动。

本文分享自微信公众号 - 程序员开发者社区(gh_016ffe40d550),作者:猿星人

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-05-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 线程池原理

    一般开发者是利用 Executors 提供的统一线程创建方法,取创建不同配置的线程池,主要区别在于不同的 ExecutorService类型或者不同的初始参数。

    王小明_HIT
  • 一个线程调用两次 start()方法会出现什么情况?

    一个线程两次调用 start()方法会出现什么情况?谈谈线程的生命周期和状态转移。在第二次调用 start() 方法的时候,线程可能处于终止或者其他(非NEW)...

    王小明_HIT
  • 并发的本质是什么?

    进程是分配资源的基本单位,线程是调度的基本单位。每个线程有一组寄存器,堆栈,一个程序计数器。

    王小明_HIT
  • JAVA并发修炼手册 | 并发的概念

    它是互联网分布式系统架构设计中必须考虑的因素之一,通常是指,保证系统能够同时并行化处理海量请求

    battcn
  • Java并发-从JDK源码角度看什么时候使用CAS机制

     如果我问你在Java语言环境下何时使用CAS机制,你可能会说:出现线程不安全可能性的时候就是我们应当使用CAS机制的时候。但是这个说话虽然是正确的,但是太笼统...

    Fisherman渔夫
  • 想搞懂JAVA高并发,怎么能不懂这些概念?

    它是互联网分布式系统架构设计中必须考虑的因素之一,通常是指,保证系统能够同时并行化处理海量请求

    程序员内点事
  • synchronized和lock的使用分析(优缺点对比详解)

    synchromized缺陷 synchronized是java中的一个关键字,也就是说是java语言的内置的特性。

    yaphetsfang
  • 线程安全相关问题总结

    当多个线程访问某个类,不管运行时环境采用何种调度方式或者这些线程如何交替执行,并且在主调代码中不需 要任何额外的同步或协同,这个类都能表现出正确的行为,那么就...

    Dream城堡
  • Java 中的锁原理、锁优化、CAS、AQS 详解!

    结论:如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

    Java团长
  • 多线程编程必备技术—— volatile,synchronized,lock

    volatile: volatile是一个类型修饰符(type specifier)。它是被设计用来修饰被不同线程访问和修改的变量。确保本条指令不会...

    Java深度编程

扫码关注云+社区

领取腾讯云代金券