美文网首页
线程安全和锁机制(三)synchronized和Lock

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

作者: 勇敢地追 | 来源:发表于2021-02-22 22:37 被阅读0次

一、synchronized实现方法和原理

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

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

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

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

public class MainTest {
    
    public void method1() {
        synchronized(this) {
            
        }
    }
    
    public synchronized void method2() {
        
    }
    
    public synchronized static void method3() {
        
    }
}

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

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的定义

/**
 * @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{}块中进行)

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

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

三、ReentrantLock

先看一下构造函数

    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也是一个接口,在它里面只定义了两个方法:

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

相关文章

网友评论

      本文标题:线程安全和锁机制(三)synchronized和Lock

      本文链接:https://www.haomeiwen.com/subject/uwclfltx.html