Java--锁

       在计算机科学中,互斥(来自互斥)是一种同步机制,用于在多线程执行环境中强制限制对资源的访问。 锁旨在实施互斥并发控制策略。锁机制的引入就是为了解决多线程环境下结果不可预知的情况。

分类

  • 建议锁:常见的锁大多都是建议锁,它是指线程在访问数据之前会通过获取锁来协助工作,此时也可以不通过获取锁来读取相应数据,一样不会出现异常,只是结果可能无法预期。
  • 强制锁:还有一些系统会使用强制锁,在这些系统中如果对锁定的资源没有进行授权而直接访问,会直接抛出异常,导致读取终端,并强制抛出异常。

       可以说锁的存在就是为了应对多线程环境下临界资源的并发访问可能出现的问题。这些问题其实还是和计算机或者是程序所处的环境有直接关系,这里就不得不提一个概念--内存模型

Java内存模型(JMM)

       Java程序本身也存在它的内存模型,这个是跟jvm息息相关的,在了解它之前,先得了解以下计算机本身的物理内存模型,然后通过它与JMM做类比,大致就能明白了。

计算机内存模型

       我们知道,在计算机中,比较核心的是:CPU内存这两个部分,至于其他的显卡,主板之类主要是能提升整体的使用性能,但是计算机的核心还是计算,计算就离不开CPU,同时计算就会产生数据,有了数据就离不开存储,所以CPU+内存处于核心地位。

       在早期计算机刚刚出现的时候,其实CPU和内存的处理速度基本上没有太大差距,所以CPU可以和内存直接互连,内存的存取效率也完全跟得上CPU的处理效率,但是随着计算机的发展,CPU的速度越来越快,尤其还出现了著名的摩尔定律,但是与此同时内存的发展远远跟不上CPU的发展,导致它们之间出现了难以逾越的鸿沟,此时就会面临CPU的性能受限于内存的性能,不能完全发挥CPU高速运转的特性,所以出现了一级缓存(L1)。

       一级缓存的作用就是缓解内存与CPU之间的性能差距,通常都是一级缓存内部存储一定量的内存空间中的数据(一般都是高频使用的数据),这样CPU读取或者写出这些被缓存的数据时,可以无需与内存打交道,直接通过一级缓存就行,一级缓存都内置在CPU内部并与CPU同速运行,可以有效的提高CPU的运行效率。一级缓存越大,CPU的运行效率越高,但受到CPU内部结构的限制,一级缓存的容量都很小。

       然后因为CPU发展极其迅速,到了现在也会有二级缓存(L2)和三级缓存(L3)这种,它们之间:L1存储的所有数据只是L2的一部分,同理L2存储的所有数据也只是L3的一部分。

       看到过一个比喻,可能不太恰当,但是也是比较形象的:说缓存的层级就像一个公司一样,创业初期,老板(CPU)与员工(内存)可能没有太大的差距,但是随着公司规模的扩大,老板的成长速度远远超过了员工的成长速度,老板就无法对每个员工面面俱到,所以就出现了管理层,老板无需对每个员工都直接对接发布任务,直接对接管理层(L1),由管理层下发各个命令。同时随着公司进一步扩大,可能会出现多级管理层。

       现代的计算机CPU可能不止一个,大多数的计算机都是多核,这就类似于公司里的合伙人一样,公司规模较大之后,每个合伙人(CPU)都有自己的嫡系手下(各自的缓存层),所以对应的是多核计算机会有多个L1、L2缓存,分属于不同的CPU核心,但是L3缓存是共享的,就像合伙人公司底层员工一样,所有合伙人共享整个员工资源。

Java内存模型

       类比计算机的内存模型,Java也有一套自己的内存模型,它没有计算机那么多缓存层级,Java的内存模型只有类似于“一级缓存”的模型。在Java中,一般关于对象的数据(如:实例域、静态域和数组元素等)存在于堆内存中,这块区域是Java内部所有线程共享的,这里称之为公共空间;每个线程在运行阶段还会有各个线程自己的私有空间(类比于一级缓存),其他线程无法触及到该空间区域,这里称之为线程的工作空间。

Java内存模型.PNG

        假设现在有A和B之间有个变量x要实现通讯,线程A从公共空间读取x的值,改变x的值后,将x重新刷回到主内存去;然后线程B从主内存区域读取 ,将x拷贝一份到B的工作空间,而对于x的刷入公共空间以及将数据拷贝到工作空间这个过程就是JMM(Java内存模型)在控制。

       JDK1.5之后Java的内存模型其实经过了一次重大的升级(SR -133内存模型),在此之前的内存模型不再深入了解,只要知道它存在很多问题,甚至在一些条件下final都可以被修改,所以说老的内存模型存在很大的漏洞。

导致并发问题的原因

       因为多线程并发操作,结合前面的JMM,我们可以发现,如果线程A在处理x变量的时候,如果需要对x进行++操作,同时线程B也在进行相同的操作,可能会出现的问题是:初始时x都是0,所以A的工作空间和B的工作空间x都是0,都进行了++操作,最后线程A得到x = 1,线程B也得到x = 1,这显然不符合理想结果(理想结果是x = 2)。这就带来了并发问题。
一般并发问题都离不开三个概念:重排序、可见性和原子性。

重排序

       在实际的编码过程中,开发人员写出来的程序代码,它的执行顺序和具体编译后执行的顺序可能不太一样,这个主要是编译器以及计算机内部有一个优化执行效果的过程,用于提高程序执行性能,它有一个原则:如果两个步骤之间不存在依赖的关系,同时在不改变单线程程序语义的情况下,允许重新安排指令的执行顺序

一般有三种重排序:

  • 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

       上面的后两种重排序属于处理器级别的重排序。同时重排序也是基于一定的规则的,SR-133模型主要就是为了阐述基于内存的操作和它们之间的可见性原则,即:如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
  • 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。

       注意:这里的happens-before仅仅只是要求前一个操作(执行的结果)对后一个操作可见,并不要求前一个操作必须要在后一个操作之前执行,这里我的理解是:如果存在step1和step2两个步骤的操作,但是两个操作互不影响,虽然step1 happens before step2,但是实际中如果发生重排序,可能会出现step2 先于step1执行,因为这里的执行结果与happens-before的结果一致,JMM就会认为它是合法的。

可见性

所谓可见性:就是指在多线程环境中,某个数据被其中一条线程修改了,能够立即被其他线程感知到。

       volatile关键字主要用于修饰变量,在Java中的作用就相当于强制给代码加了一个内存屏障指令,被它修饰的变量,在线程执行时,如果读取该变量,会强制到主内存中去读取最新的值到工作空间中,而不是先到工作空间中区读取线程缓存的值;对应的,如果对变量进行写操作,会强制将更新后的变量刷新回主内存中,保证线程间的可见性。

       在JMM的控制下,用volatile修饰的变量在一定程度上是不会进行重排序的,这也从微观上对volatile字段进行了一致性的加强。但是volatile仅仅只是加了一个内存屏障指令,也就是说对于数据在线程之间的可见性是可以保障的,比如线程t1对变量x的修改,其他线程在读取x变量的值时,可以获取到最新变化的值;可是它却无法保障原子性,即所谓的:要么全执行,要么全不执行。

        举例:x++操作,在指令层面大致可以分为三步,读取x值,对读取到的值加1,将加1后的值写回x,现两个线程同时对初始值为0x执行加加操作,各5000次之加加后,得到的结果一般会小于10000。归根结底就是:只要是指令层面无法一条指令能够完成的,如果没有特殊手段,都会存在原子性问题。

       一般volatile主要用于相互之间不存在依赖关系的语句中,上面x++的例子之所以有问题,就是因为对x++的操作是与上一步操作的结果相依赖的。所以在并发环境下一般volatile用于修饰boolean变量,它的值只能是truefalse,利用它的变化,来实现一些其他操作,因为它不会存在相互依赖的关系。

原子性

       原子性说的就是某一段执行逻辑,具有不可拆分的特性,要么全部执行,要么全部不执行,不存在执行到一半放弃CPU权限的情况。就像日常生活中银行交易系统一样,取钱和扣款必须是一个原子操作,不能存在只取钱不扣款的情况,或者是只扣款不取钱的情况。

       Java中可以用synchronized保证原子性和可见性,相比于volatilesynchronized的开销就非常大了,但是它可以保证原子性和可见性。当线程对锁释放的时候,其内存语义就是把该线程对应的本地内存中的共享变量刷新到主内存中。当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量。

public class SynchronizedTest {

    public synchronized void method1() {
        System.out.println("synchronized method");
    }

    public void method2() {
        synchronized (this) {
            System.out.println("synchronized code block");
        }
    }

}

现在我们通过javap命令反编译上面的代码:

G:\study_workspace> javap -v -c SynchronizedTest.class
......//省略前面部分
public synchronized void method1();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED 
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String synchronized method
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 4: 0
        line 5: 8

  public void method2();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         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 synchronized code block
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
//......省略后面分

       反编译SynchronizedTest类,我们可以看到Java编译器为我们生成的字节码。在对于method1和method2的处理上稍有不同。也就是说。JVM对于同步方法和同步代码块的处理方式不同。对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。 对于同步代码块。JVM采用monitorenter、monitorexit两个指令来实现同步。

       这里的monitorenter和monitorexit,在大学操作系统上其实是有介绍的,在操作系统层面,它其实叫管程(Monitors),也称为监视器,是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计。 管程提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。

       我们可以把监视器理解为包含一个特殊的房间的建筑物,这个特殊房间同一时刻只能有一个客人(线程)。如果一个顾客想要进入这个特殊的房间,他首先需要在走廊(Entry Set)排队等待。调度器将基于某个标准(比如 FIFO)来选择排队的客户进入房间。如果,因为某些原因,该客户客户暂时因为其他事情无法脱身(线程被挂起),那么他将被送到另外一间专门用来等待的房间(Wait Set),这个房间的可以在稍后再次进入那间特殊的房间。如上面所说,这个建筑屋中一共有三个场所。


image.png

有序性

       有序性即程序执行的顺序按照代码的先后顺序执行。除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save 有可能被优化成load->save->add 。这就是可能存在有序性问题。这里需要注意的是,synchronized是无法禁止指令重排和处理器优化的。也就是说,synchronized无法避免上述提到的问题。

as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

       as-if-serial语义保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的。当然,实际上还是有重排的,只不过我们无须关心这种重排的干扰。

       由于synchronized修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性。

锁优化

       synchronized其实是借助Monitor实现的,Monitor是基于C++实现的,在加锁时会调用objectMonitorenter()方法,解锁的时候会调用exit方法。事实上,只有在JDK1.6之前,synchronized的实现才会直接调用ObjectMonitor的enter()exit(),这种锁被称之为重量级锁。

       在JDK1.6中出现对锁进行了很多的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化(自旋锁在1.4就有,只不过默认的是关闭的,jdk1.6是默认开启的),这些操作都是为了在线程之间更高效的共享数据 ,解决竞争问题。

       JDK 1.5 到 JDK 1.6的一个重要改进就是高效并发,作为一个Java开发,你只需要知道你想在加锁的时候使用synchronized就可以了,具体的锁的优化是虚拟机根据竞争情况自行决定的。也就是说,在JDK 1.5 以后,锁优化的这些概念,都被封装在synchronized中了,对于我们编码来说是无感知的。

        提到锁优化,就不得不提一个银行办业务的例子:去银行办业务,你到了银行之后,要先取一个号,然后你坐在休息区等待叫号,过段时间,广播叫到你的号码之后,会告诉你去哪个柜台办理业务,这时,你拿着你手里的号码,去到对应的柜台,找相应的柜员开始办理业务。当你办理业务的时候,这个柜台和柜台后面的柜员只能为你自己服务。当你办完业务离开之后,广播再喊其他的顾客前来办理业务。

       在上面这个案例中:每个顾客是一个线程;柜台前面的那把椅子,就是锁;柜台后面的柜员,就是共享资源;无法直接办理业务,要取号等待的过程叫做阻塞;当你听到叫你的号码的时候,你起身去办业务,这就是唤醒;当你坐在椅子上开始办理业务的时候,你就获得锁;当你办完业务离开的时候,你就释放锁。

自旋锁

       synchronized的实现方式中使用Monitor进行加锁,这是一种互斥锁,为了表示他对性能的影响我们称之为重量级锁。这种互斥锁在互斥同步上对性能的影响很大,Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到内核态,因此状态转换需要花费很多的处理器时间。

       就像去银行办业务的例子,当你来到银行,发现柜台前面都有人的时候,你需要取一个号,然后再去等待区等待,一直等待被叫号。这个过程是比较浪费时间的,那么有没有什么办法改进呢?

       有一种比较好的设计,那就是银行提供自动取款机,当你去银行取款的时候,你不需要取号,不需要去休息区等待叫号,你只需要找到一台取款机,排在其他人后面等待取款就行了。

       在程序中,Java虚拟机的开发工程师们在分析过大量数据后发现:共享数据的锁定状态一般只会持续很短的一段时间,为了这段时间去挂起和恢复线程其实并不值得。

       如果物理机上有多个处理器,可以让多个线程同时执行的话。我们就可以让后面来的线程“稍微等一下”,但是并不放弃处理器的执行时间,看看持有锁的线程会不会很快释放锁。这个“稍微等一下”的过程就是自旋。即:对于线程而言,无需进入挂起状态,只要一直保持可运行状态即可,这样一旦前一个线程释放资源,后一个线程可以立即补上,无需阻塞或唤醒线程。

       自旋锁阻塞锁最大的区别就是,到底要不要放弃处理器的执行时间。对于阻塞锁自旋锁来说,都是要等待获得共享资源。但是阻塞锁是放弃了CPU时间,进入了等待区,等待被唤醒。而自旋锁是一直“自旋”在那里,时刻的检查共享资源是否可以被访问。

       由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

消除锁

       你去银行取钱,所有情况下都需要取号,并且等待吗?其实是不用的,当银行办理业务的人不多的时候,可能根本不需要取号,直接走到柜台前面办理业务就好了。能这么做的前提是,没有人和你抢着办业务。

       这种例子,在锁优化中被称作“锁消除”,是JIT编译器对内部锁的具体实现所做的一种优化。在动态编译同步块的时候,JIT编译器可以借助一种被称为逃逸分析(Escape Analysis)的技术来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。

public void f() {
    Object lock = new Object();
    synchronized(lock) {
        System.out.println(lock);
    }
}

       代码中对lock这个对象进行加锁,但是lock对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。优化成:

public void f() {
    Object lock = new Object();
    System.out.println(lock);
}

       由于这段优化是在JIT阶段,不是在前端编译阶段,所以常规的反编译方式是无法看到优化后的代码的,但是,如果感兴趣,还是可以看的,只是会复杂一点,首先你要自己build一个fasttest版本的jdk,然后在使用java命令对.class文件进行执行的时候加上-XX:+PrintEliminateLocks参数。而且jdk的模式还必须是server模式。

       总之,在使用synchronized的时候,如果JIT经过逃逸分析之后发现并无线程安全问题的话,就会做锁消除.

锁粗化

       很多人都知道,在代码中,需要加锁的时候,我们提倡尽量减小锁的粒度,这样可以避免不必要的阻塞。

       还是我们去银行柜台办业务,最高效的方式是你坐在柜台前面的时候,只办和银行相关的事情。如果这个时候,你拿出手机,接打几个电话,问朋友要往哪个账户里面打钱,这就很浪费时间了。最好的做法肯定是提前准备好相关资料,在办理业务时直接办理就好了。

       加锁也一样,把无关的准备工作放到锁外面,锁内部只处理和并发相关的内容。这样有助于提高效率。但是当你去银行办业务,你为了减少每次办理业务的时间,你把要办的五个业务分成五次去办理,这反而适得其反了。因为这平白的增加了很多你重新取号、排队、被唤醒的时间。

       如果在一段代码中连续的对同一个对象反复加锁解锁,其实是相对耗费资源的,这种情况可以适当放宽加锁的范围,减少性能消耗。

for(int i=0;i<100000;i++){  
    synchronized(this){  
        do();  
} 
//会被粗化成:
synchronized(this){  
    for(int i=0;i<100000;i++){  
        do();  
}  

       这其实和我们要求的减小锁粒度并不冲突。减小锁粒度强调的是不要在银行柜台前做准备工作以及和办理业务无关的事情。而锁粗化建议的是,同一个人,要办理多个业务的时候,可以在同一个窗口一次性办完,而不是多次取号多次办理。

Lock

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

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

       这样导致的一个直接问题就是:如果前一个已经获取到锁的线程,正在进行IO操作或者sleep等等其他比较耗时的操作,那么在这个线程没有释放锁资源之前,后续的线程是无法执行的,只能无限制等待。但是在实际应用中并发并不是想象的那么100%发生,例如:在多个线程对文件进行操作时,读写操作属于冲突操作,写和写操作也是冲突操作,但是读和读操作是不应该发生冲突的,此时如果按照synchronized写法,就会极大地降低程序执行的效率。

       Lock的存在就解决了在某些情况下(例如:读读操作)线程之间避免发生冲突。同时Lock有一个特性,可以知道线程又没有成功获取到锁,这个是synchronized所不具备的。

       Locksynchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

  • unlock()方法:它是用的最多的方法,用来获取锁和释放锁,如果已经被其他线程获取,则等待。Lock是需要手动释放锁资源的,所以一般它都会用在try-catch语句中,释放锁资源的操作一般都放在finally语句块中。

  • tryLock()方法:这个方法返回一个布尔值,看方法名就能明确,它是尝试获取锁,如果锁已经被其他线程获取,此时会返回false,即尝试获取锁失败,它会立即给出返回值,不会一直等待获取锁。

*tryLock(long time, TimeUnit unit):这个同样是尝试获取锁操作,但是它会等待一定的时间,在等待时间内如果获取到锁,返回true,否则超过指定的时间,仍然拿不到锁就返回false

*lockInterruptibly():这个方法同样是一个获取线程锁的方法,只是比较特殊,如果线程正在等待获取锁,在等待期间这个线程是可以被打断的,即:中断线程的等待状态。假设有A和B两个线程同时使用lockInterruptibly()方法获取锁,此时加入A获取到了锁,B就会在等待状态,此时对线程B可以使用interrupt()方法来终端线程B的等待状态。如果线程获取了锁之后是不能被中断的,interrupt方法只能中断阻塞过程中的线程,而不能中断正在运行过程中的线程。

  • newCondition():它返回一个Condition实例,调用这个方法有个前提条件,当前线程已经获取到锁。Condition中存在一个await方法,调用它会以原子的方式释放锁,在等待返回之前会重新获取锁。

继承和实现

  • ReadWriteLock:也是一个接口,只定义了两个方法readLockwriteLock,返回Lock实例,把文件的读写操作分开,分成两个锁分配给线程,因此可以有多个线程同时进行读操作。

  • ReentrantReadWriteLock:如果有一个线程已经占用了读锁,此时若有其他线程需要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,此时若有其他线程需要申请读锁或者写锁,申请的线程会一致等待释放写锁。

       Java中synchronized是非公平锁,它无法保证等待线程获取锁的顺序,ReentrantLockReentrantReadWriteLock默认也是非公平锁,但是可以进行设置(FairSyncNofairSync)。在new的时候传入布尔值truefalsetrue表示公平锁,反之则是非公平锁,默认非公平锁。

对比Lock和synchronized

  • Lock是一个接口,而synchronized是Java内置的
  • synchronized在发生异常时,会自动释放线程占有的锁,不会造成死锁;Lock如果发生异常,若没有主动释放锁资源,很可能会造成死锁现象
  • Lock可以让等待锁的线程响应中断,synchronized不行
  • Lock可以得知又没有获取到锁,synchronized不行
  • Lock可以提高多个线程读操作的效率

重入锁

public synchronized void m1() {
    m2();
}
public synchronized void m2() {
    //do sth
}

       这时候如果没有锁的可重入特性,就会产生死锁,因为此时Thread进入m1之后,获取到了锁,m2方法同样需要获取锁,如果没有可重入特性,此时就会等待m1方法内释放锁,但是m1又必须等待m2执行完才能释放锁,造成死锁。

       锁重入可以类比井口打水的例子:只有一口井,井边安排一个负责人维护秩序,村民打水,先来的可以先打水,后到的在后面排队,如果是队伍首部人的家属,可以不用排队,直接打水。这个其实就是公平锁的原理。而非公平锁则是:如果后面来的村民看到排头正在打水,则正常排队,如果排头的人刚刚打完水,还未完成交接工作,后来的人可以直接尝试获取打水权限,抢到了就直接打水,这就是非公平锁模型,新来的不一定会乖乖排在队伍后面,所以会存在有些人会等待很久很久。

       上面的例子中,可以发现只要你是队伍首部人的家属,可以跳过排队,直接打水,这点类似于上面代码示例中m1执行了之后,在m1方法内如果执行m2需要获取锁,可以直接进入,无需等待锁释放的过程。

       可重入其实就是线程有一个state计数器,当线程获取锁之后,state会加1,释放锁资源的时候state会减1,因此当已经获取锁的线程再次申请获取锁时,若原来的state1,此时state就变成了2。只有当state值为0的时候其他线程才能去竞争锁资源。

你可能感兴趣的:(Java--锁)