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 内部实现
image

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的时候,其实就是两个相移动

程序员开发者社区

微信公众号

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: Age of Ai 设计师: meimeiellie
应支付0元
点击重新获取
扫码支付

支付成功即可阅读