谈一谈synchronized和ReentrantLock

简介: 谈一谈synchronized和ReentrantLock

1 Synchronized

1.1 定义

      java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍可以访问该object中的非加锁代码块。

1.2 使用场景

网络异常,图片无法展示
|


1.2.1 方法锁

      方法声明时使用,放在范围操作符(public等)后,其返回类型声明(void等)之前。即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。

例如:

public synchronized void MethodA() {
   //方法体
}
复制代码


1.2.2 类加锁

      如果线程进入类锁,则线程在该类中所有操作不能进行,包括静态变量和静态方法,实际上,对于含有静态方法和静态变量的代码块的同步

例如:

public static void MethodA() {
    //加入对象锁
    synchronized (Student.class) {
        //第二次检查         
    }
}
复制代码


1.2.3 代码块加锁

      对某一代码块使用,synchronized后跟括号,括号里是变量,这样,一次只有一个线程进入该代码块.此时,线程获得的是成员锁.

例如:

public Object MethodA(Object obj){
    synchronized(obj){
        //方法体(一次只能有一个线程进入)
    }
}
复制代码
1.2.4 对象锁

      如果线程进入对象锁,则得到当前对象锁,那么别的线程在该类所有对象上的任何操作都不能进行.在对象级使用锁通常是一种比较粗糙的方法

例如:

public static void MethodA() {     
     //加入对象锁
     synchronized (this) {
         //代码块
     }
}
复制代码

1.3 作用

  • 原子性:确保线程互斥的访问同步代码;
  • 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
  • 有序性:有效解决重排序问题;

1.4 底层原理

/**
 * @author 17122
 */
public class Test01 {
    public static synchronized void sayHello(){
        System.out.println("Hello");
    }
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
    public static void main(String[] args) {
        sayHello();
    }
}
复制代码

进行编译和反编译:

## javac Test01.java
## javap -c Test01.class
## 结果
Compiled from "Test01.java"
public class test0706.Test01 {
  public test0706.Test01();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public static synchronized void sayHello();
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String Hello
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
  public void method();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter  # 监视器锁
       4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #5                  // String Method 1 start
       9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: aload_1
      13: monitorexit   #监视器锁释放锁
      14: goto          22
      17: astore_2
      18: aload_1
      19: monitorexit
      20: aload_2
      21: athrow
      22: return
    Exception table:
       from    to  target type
           4    14    17   any
          17    20    17   any
  public static void main(java.lang.String[]);
    Code:
       0: invokestatic  #6                  // Method sayHello:()V
       3: return
}
复制代码


synchronized关键字的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出llegalMonitorStateException的异常的原因。

2 ReentrantLock

2.1 定义

      可重入互斥锁与使用synchronized方法和语句访问的隐式监视器锁具有相同的基本行为和语义 ,但具有扩展功能。一个ReentrantLock是拥有该线程最后成功锁定,但尚未解锁的。lock当锁不为另一个线程所有时,调用线程 将返回,成功获取锁。如果当前线程已经拥有锁,该方法将立即返回。

2.2 使用场景

2.2.1 一般使用(悲观锁)
Lock lock = new ReentrantLock();
public void MethodA()){
    lock.lock(); // 细节问题:lock.lock(); lock.unlock(); // lock 锁必须配对,否则就会死在里面
    try {
         //代码块
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        lock.unlock();    
    }
}
复制代码
2.2.2 公平锁

非常公平, 不能够插队,必须先来后到

网络异常,图片无法展示
|


/**
 * 声明公平锁
 */
ReentrantLock reentrantLock = new ReentrantLock(true);
复制代码
2.2.3 非公平锁

非常不公平,可以插队 (看源码可知,默认是不公平的锁)

网络异常,图片无法展示
|


2.2.4 响应中断
public class Test01 {
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        //该线程先获取锁1,再获取锁2
        Thread thread1 = new Thread(new ThreadDemo(lock1, lock2));
        //该线程先获取锁2,再获取锁1
        Thread thread2 = new Thread(new ThreadDemo(lock2, lock1));
        thread1.start();
        thread1.start();
        //第二个线程中断
        thread2.interrupt();
    }
    static class ThreadDemo implements Runnable {
        Lock firstLock;
        Lock secondLock;
        public ThreadDemo(Lock firstLock, Lock secondLock) {
            this.firstLock = firstLock;
            this.secondLock = secondLock;
        }
        @Override
        public void run() {
            try {
                firstLock.lockInterruptibly();
                sleep(10);//更好的触发死锁
                secondLock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                firstLock.unlock();
                secondLock.unlock();
                System.out.println(Thread.currentThread().getName() + "正常结束!");
            }
        }
    }
}
复制代码

除非当前线程被中断,否则获取锁 。

如果其他线程没有持有锁,则获取该锁并立即返回,将锁持有计数设置为 1。

如果当前线程已经持有这个锁,那么持有计数就会增加一并且该方法立即返回。

如果锁被另一个线程持有,那么当前线程将被禁用以进行线程调度并处于休眠状态,直到发生以下两种情况之一:

  • 锁被当前线程获取;或者
  • 一些其他线程中断当前线程。

如果当前线程获取了锁,则锁保持计数设置为 1。

如果当前线程:

  • 在进入此方法时设置其中断状态;或者
  • 在获取锁时被中断

然后InterruptedException被抛出并清除当前线程的中断状态。

2.2.5 限时等待

如果在给定的等待时间内没有被另一个线程持有并且当前线程没有被中断,则获取锁 。

如果锁未被另一个线程持有,则获取该锁并立即返回值true,将锁持有计数设置为 1。如果此锁已设置为使用公平排序策略,则如果任何其他线程正在等待该锁,则不会获取可用锁。这与tryLock()方法相反。如果你想要一个tryLock允许插入公平锁的定时,那么将定时和非定时形式组合在一起:

if (lock.tryLock() ||lock.tryLock(timeout, unit)) {
   ...
 }
复制代码

如果当前线程已持有此锁,则持有计数将增加 1 并且该方法返回true

如果锁被另一个线程持有,那么当前线程将被禁用以进行线程调度并处于休眠状态,直到发生以下三种情况之一:

  • 锁被当前线程获取;或者
  • 其他一些线程中断当前线程;或者
  • 经过指定的等待时间

如果获取了锁,true则返回该值并将锁保持计数设置为 1。

如果当前线程:

  • 在进入此方法时设置其中断状态;或者
  • 在获取锁时被中断,然后InterruptedException被抛出并清除当前线程的中断状态。

如果指定的等待时间过去,则false 返回该值。如果时间小于或等于零,则该方法根本不会等待。

在此实现中,由于此方法是显式中断点,因此优先响应中断而不是正常或可重入获取锁,并优先报告等待时间的过去。

2.3 作用

  • ReentrantLock是可重入的独占锁
  • 比起synchronized功能更加丰富
  • 支持公平锁实现
  • 支持中断响应以及限时等待等等
  • 可以配合一个或多个Condition条件方便的实现等待通知机制。

2.4 原理

  • ReentrantLock是基于AbstractQueuedSynchronizer(AQS)的,AQS是Java并发包中众多同步组件的构建基础,它通过一个int类型的状态变量state和一个FIFO队列来完成共享资源的获取,线程的排队等待等。
  • AQS是个底层框架,采用模板方法模式,它定义了通用的较为复杂的逻辑骨架,比如线程的排队,阻塞,唤醒等,将这些复杂但实质通用的部分抽取出来,这些都是需要构建同步组件的使用者无需关心的,使用者仅需重写一些简单的指定的方法即可(其实就是对于共享变量state的一些简单的获取释放的操作)。其内部定义了三个重要的静态内部类,Sync,NonFairSync,FairSync。
  • Sync作为ReentrantLock中公用的同步组件,继承了AQS(要利用AQS复杂的顶层逻辑嘛,线程排队,阻塞,唤醒等等);NonFairSync和FairSync则都继承Sync,调用Sync的公用逻辑,然后再在各自内部完成自己特定的逻辑(公平或非公平)。

3 相同点

  • 语义基本相同
  • 都可以实现线程安全

4 不同点

  • ReentrantLock是Lock的实现类,而synchronized是Java中的一个关键字
  • Lock必须手动获取与释放锁,而synchronized不需要手动释放和开启锁
  • Lock只适用于代码块锁,而synchronized可用于修饰方法、代码块等


相关文章
|
6月前
|
Java
【面试问题】Synchronized 和 ReentrantLock 区别?
【1月更文挑战第27天】【面试问题】Synchronized 和 ReentrantLock 区别?
|
5月前
|
安全 Java 开发者
Java并发编程:深入理解synchronized和ReentrantLock
在Java并发编程中,正确使用同步机制是确保线程安全的关键。本文将深入探讨Java内置的两种同步机制——synchronized关键字和ReentrantLock类。我们将通过权威数据、经典理论和实际案例,对比分析它们的性能、用法和适用场景,帮助开发者做出明智的选择。
40 0
|
2月前
|
缓存 Java 编译器
JAVA并发编程synchronized全能王的原理
本文详细介绍了Java并发编程中的三大特性:原子性、可见性和有序性,并探讨了多线程环境下可能出现的安全问题。文章通过示例解释了指令重排、可见性及原子性问题,并介绍了`synchronized`如何全面解决这些问题。最后,通过一个多窗口售票示例展示了`synchronized`的具体应用。
|
3月前
|
安全 Java
Java并发编程实战:使用synchronized和ReentrantLock实现线程安全
【8月更文挑战第31天】在Java并发编程中,保证线程安全是至关重要的。本文将通过对比synchronized和ReentrantLock两种锁机制,深入探讨它们在实现线程安全方面的优缺点,并通过代码示例展示如何使用这两种锁来保护共享资源。
|
5月前
|
Java
Java并发编程:深入理解synchronized与ReentrantLock
【6月更文挑战第22天】本文将深入探讨Java并发编程中两个重要的同步机制:synchronized关键字和ReentrantLock类。我们将通过实例分析它们之间的差异,以及在实际应用中如何根据场景选择恰当的同步工具。
|
4月前
|
安全 Java
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
Java多线程中的锁机制:深入解析synchronized与ReentrantLock
87 0
|
4月前
|
安全 Java 开发者
Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
Java多线程:synchronized关键字和ReentrantLock的区别,为什么我们可能需要使用ReentrantLock而不是synchronized?
59 0
|
5月前
|
存储 Java
Java并发编程 Synchronized原理
Java并发编程 Synchronized原理
24 0
|
6月前
|
Java
从源码入手详解ReentrantLock,一个比synchronized更强大的可重入锁
【5月更文挑战第6天】从源码入手详解ReentrantLock,一个比synchronized更强大的可重入锁
36 1
|
6月前
|
安全 Java 程序员
Java并发编程:理解并应用ReentrantLock
【4月更文挑战第30天】 在多线程的世界中,高效且安全地管理共享资源是至关重要的。本文深入探讨了Java中的一种强大同步工具——ReentrantLock。我们将从其设计原理出发,通过实例演示其在解决并发问题中的实际应用,以及如何比传统的synchronized关键字提供更灵活的锁定机制。文章还将讨论在使用ReentrantLock时可能遇到的一些挑战和最佳实践,帮助开发者避免常见陷阱,提高程序性能和稳定性。