前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >线程安全和锁机制(三)synchronized和Lock

线程安全和锁机制(三)synchronized和Lock

作者头像
提莫队长
发布2021-03-03 14:31:32
4070
发布2021-03-03 14:31:32
举报
文章被收录于专栏:刘晓杰

一、synchronized实现方法和原理

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础。从语法上讲,Synchronized总共有三种用法:

  • 普通同步方法,锁是当前实例对象
  • 静态同步方法,锁是当前类的class对象
  • 同步方法块,锁是括号里面的对象

我们先写如下方法对应上面三种用法

代码语言:javascript
复制
public class MainTest {
    
    public void method1() {
        synchronized(this) {
            
        }
    }
    
    public synchronized void method2() {
        
    }
    
    public synchronized static void method3() {
        
    }
}

然后用 javap -verbose MainTest.class 查看代码

代码语言:javascript
复制
Classfile /D:/eclipse-jee-luna-SR2-win32-x86_64/ws/JavaTest/src/test/MainTest.class
  Last modified 2021-2-21; size 454 bytes
  MD5 checksum 56b092821f8394976e88544cf994100e
  Compiled from "MainTest.java"
public class test.MainTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#17         // java/lang/Object."<init>":()V
   #2 = Class              #18            // test/MainTest
   #3 = Class              #19            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               method1
   #9 = Utf8               StackMapTable
  #10 = Class              #18            // test/MainTest
  #11 = Class              #19            // java/lang/Object
  #12 = Class              #20            // java/lang/Throwable
  #13 = Utf8               method2
  #14 = Utf8               method3
  #15 = Utf8               SourceFile
  #16 = Utf8               MainTest.java
  #17 = NameAndType        #4:#5          // "<init>":()V
  #18 = Utf8               test/MainTest
  #19 = Utf8               java/lang/Object
  #20 = Utf8               java/lang/Throwable
{
  public test.MainTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public void method1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_1
         5: monitorexit
         6: goto          14
         9: astore_2
        10: aload_1
        11: monitorexit
        12: aload_2
        13: athrow
        14: return
      Exception table:
         from    to  target type
             4     6     9   any
             9    12     9   any
      LineNumberTable:
        line 6: 0
        line 8: 4
        line 9: 14
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 9
          locals = [ class test/MainTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public synchronized void method2();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 13: 0

  public static synchronized void method3();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 17: 0
}
SourceFile: "MainTest.java"
method1 的反编译代码

注意看 monitorenter 和 monitorexit。具体解释如下 每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  • 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  • 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  • 3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

执行monitorexit的线程必须是objectref所对应的monitor的所有者。 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

method2 ,method3 的反编译代码

从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。

现在我们应该知道,Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。JDK中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。(这一部分简称锁优化,文末有链接)

二、synchronized缺陷和Lock

如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  • 1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  • 2)线程执行发生异常,此时JVM会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。 因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。 另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。 总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

  • 1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
  • 2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

接下来看一下Lock的定义

代码语言:javascript
复制
/**
 * @see ReentrantLock
 * @see Condition
 * @see ReadWriteLock
 */
public interface Lock {

    /**
     * 用来获取锁。如果锁已被其他线程获取,则进行等待。
     */
    void lock();

    /**
     * lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。
     * 也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
     */
    void lockInterruptibly() throws InterruptedException;

    /**
     * 用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,
     * 这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
     */
    boolean tryLock();

    /**
     * 通 tryLock。加入了获取锁的超时时间
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    /**
     * 用来释放锁的
     */
    void unlock();

    /**
     * https://www.cnblogs.com/gemine/p/9039012.html    《Java并发之Condition》 这里面有介绍
     */
    Condition newCondition();
}

基本使用方法如下:(一般来说,使用Lock必须在try{}catch{}块中进行)

代码语言:javascript
复制
        Lock lock = new ReentrantLock();
        if(lock.tryLock()) {
             try{
                 //处理任务
             }catch(Exception ex){
                 
             }finally{
                 lock.unlock();   //释放锁
             } 
        }else {
            //如果不能获取锁,则直接做其他事情
        }

另外注意一下 Lock 的文档,在定义的最上面还提到了ReentrantLock和ReadWriteLock。接下来就讲这些

三、ReentrantLock

先看一下构造函数

代码语言:javascript
复制
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

默认非公平锁。但也可以指定公平锁。

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。 非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

其实查阅整个源代码,会发现所有的方法都会去调用Sync里面的方法。而Sync继承自AbstractQueuedSynchronizer(AQS) (相关AQS的代码在文末的《Java并发之AQS详解》有介绍。这里不贴出来了。主要是还是晦涩难懂,本人没有看明白,只看懂了两个点)

  • AQS内部有一个FIFO线程等待队列,多线程争用资源被阻塞时会进入此队列
  • 线程在等待过程中是通过死循环+CAS来判断状态的

四、ReadWriteLock 和 ReentrantReadWriteLock

ReadWriteLock也是一个接口,在它里面只定义了两个方法:

代码语言:javascript
复制
public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。

相关解析在文末的《Java并发编程--ReentrantReadWriteLock》。说实在的,依然没怎么看懂。。。。。。

五、Lock和synchronized的选择

总结来说,Lock和synchronized有以下几点不同:

  • 1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
  • 2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  • 3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  • 4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  • 5)Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

六、自定义锁

自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,也就是如下四个函数

  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleaseShared

(前面两个是排他锁(写锁),后面两个是共享锁(读锁)) 至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

七、参考资料

Java并发编程:Synchronized及其实现原理

JVM内部细节之一:synchronized关键字及实现细节(轻量级锁Lightweight Locking)

Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)

Java并发编程:Lock

Lock原理分析

Lock锁底层原理

Java并发编程:Lock

Java并发之AQS详解

ReentrantLock原理

Java并发编程--ReentrantReadWriteLock

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、synchronized实现方法和原理
  • 二、synchronized缺陷和Lock
  • 三、ReentrantLock
  • 四、ReadWriteLock 和 ReentrantReadWriteLock
  • 五、Lock和synchronized的选择
  • 六、自定义锁
  • 七、参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档