Java 多线程系列课程(三)

本文档包含以下内容:

  1. 多线程同步中的基本概念

  2. Volatile
  3. 乐观锁、悲观锁
  4. synchronized   
  5. ReentrantLock
  6. 读写锁
  7. 锁优化
  8. 死锁
  9. 练习题

一、多线程同步中的基本概念

  1. 为什么需要同步?

一块资源(共享资源)会有多个线程同时操作的的时候。并且我们没有进行任何的同步操作,就会发生冲突。因为我们不清楚每一个线程什么时候执行什么时候结束,也就无法控制程序最后运行的结果。

  1. 临界资源、临界区、临界区特点

临界资源:同一时刻只允许一个线程访问的资源

临界区 :访问临界资源的代码段

临界区特点:临界区(Critical Section)是一段供线程独占式访问的代码,也就是说若有一线程正在访问该代码段,其它线程想要访问,只能等待当前线程离开该代码段方可进入,这样保证了线程安全。

  1. 工作内存和主内存之间是如何交互的

关于主内存与工作内存之间具体交互的协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节Java内存模型中定义了八种操作来完成。

1.lock作用于主内存,把变量标识为线程独占状态。

2.unlock作用于主内存,解除独占状态。

3.read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。

4.load作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。

5.use作用工作内存,把工作内存当中的一个变量值传给执行引擎。

6.assign作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。

7.store作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。

8.write作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。

 

  1. 什么是内存一致性

Java内存模型

              

在多线程中,分为了工作内存,和主内存。

在所有线程工作的是操作共享资源的时候都是将共享资源从主内存拿到工作内存去操作。当使用完之后会放回主内存当中。那么在线程对私有的工作空间中的数据进行写操作,别的线程并没有读到最新的值,就会出现问题。

  1. 什么是指令重排序

编译器和处理器会通过多种方式比如重排序对代码进行优化,然而在重排序后可能会导致运行结果与预想的不同。

重排序的方式:

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:

- 编译器优化的重排序:

编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。【as-if-serial原则保证,as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。】

- 指令级并行的重排序:

现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。

- 内存系统重排序:

由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。

这种不一致性现象其实和我们之前线程内存模型引起的内存一致性问题类似。

            

  1. 什么是原子操作

happens-before原则  每次都会从内存中拿数据,

什么是原子操作,Java中的原子操作是什么?

原子操作是不可分割的操作,一个原子操作中间是不会被其他线程打断的,所以不需要同步一个原子操作。 ——只有非原子操作在多线程条件下才需要同步。那么同步的方式就是将一个非原子操作编程了原子操作。
   多个原子操作合并起来后就不是一个原子操作了,就需要同步了。i++不是一个原子操作,它包含读取-修改-写入操作,在多线程状态下是不安全的。 

原子性

原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。及时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。我们先来看看哪些是原子操作,哪些不是原子操作,有一个直观的印象:

int a = 10; //1
   a++; //2
   int b=a; //3
   a = a+1; //4

上面这四个语句中只有第1个语句是原子操作,将10赋值给线程工作内存的变量a,

而语句2(a++),实际上包含了三个操作:1. 读取变量a的值;2:对a进行加一的操作;3. 将计算后的值再赋值给变量a,而这三个操作无法构成原子操作。

对语句3,4的分析同理可得这两条语句不具备原子性。当然,java内存模型中定义了8中操作都是原子的,不可再分的。

 

如何实现原子性?

AtomicInteger   XXXX:

AtomicReference

加锁

 

  • Volatile

作用于变量。

(1)Volatile 解决了什么问题?

<1>保证了内存的一致性

<2>防止了指令重排序

<3>因为没有加锁操作所以Volatile对共享变量的同步不会

总结:也就是说Volatile 可以处理原子操作的线程同步问题。

  1. Java如何保证内存一致性:

首先加了关键字的变量保证了以下的规则:

read、load、use动作必须连续出现。

assign、store、write动作必须连续出现。

 

JVM会向处理器发送一条Lock前缀的指令

Lock指令用于解决缓存一致性。

解决缓存一致性方案有两种:

通过在总线加LOCK#锁的方式  总线锁定  -----》只能有一个线程才能获取该资源,其他处理器处于阻塞状态这样的效率非常低下。

通过缓存一致性协议

但是方案1存在一个问题,它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个 CPU能够运行,其他CPU都得阻塞,效率较为低下。

所以我们提出了比总线锁定更优化的方式——缓存一致性协议

第二种方案,缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据

什么是缓存异性协议呢?

MESI协议:是以缓存行(缓存的基本数据单位,在Intel的CPU上一般是64字节)的几个状态来命名的(全名是Modified、Exclusive、 Share or Invalid)。该协议要求在每个缓存行上维护两个状态位,使得每个数据单位可能处于M、E、S和I这四种状态之一,各种状态含义如下:

M:被修改的。处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。

    E:独占的。处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改,即与内存中一致。

    S:共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致。

    I:无效的。本CPU中的这份缓存已经无效。

一个处于M状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。

一个处于S状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。

一个处于E状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S。

当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取,并把自己状态变成S,如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果,如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。

Java提供了volatile关键字保证了内存的可见性,底层通过LOCK#或“缓存锁定”实现。

如果是一个volatile关键字修饰的变量,则会有第二行的汇编代码,这是一条含有lock前缀的代码。带  有lock前缀的代码则会通过LOCK#或通过“缓存锁定”实现线程间的可见性。

  1. 如何防止指令重排

          

在每个volatile写操作的前面插入一个StoreStore屏障。

在每个volatile写操作的后面插入一个StoreLoad屏障。

在每个volatile读操作的后面插入一个LoadLoad屏障。

在每个volatile读操作的后面插入一个LoadStore屏障。

 

四、乐观锁、悲观锁

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁

悲观锁机制存在以下问题:  

1. 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。

2. 一个线程持有锁会导致其它所有需要此锁的线程挂起。

3. 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

 

乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁但是在更新的时候会判断一下在此期间别人有没有去更新这个数据可以使用版本号等机制。乐观锁适用于多读的应用类型这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的

乐观锁的一种实现方式-CAS(Compare and Swap 比较并交换):

Java在JDK1.5之前都是靠 synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量。这就是一种独占锁,独占锁其实就是一种悲观锁,所以可以说 synchronized 是悲观锁。

乐观锁:

乐观锁( Optimistic Locking )在上文已经说过了,其实就是一种思想。相对悲观锁而言,乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

    上面提到的乐观锁的概念中其实已经阐述了它的具体实现细节:主要就是两个步骤:冲突检测和数据更新其实现方式有一种比较典型的就是 Compare and Swap ( CAS )

  CAS

    CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。   

CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“ 我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可这其实和乐观锁的冲突检查+数据更新的原理是一样的。

这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式

JAVACAS的支持:

JDK1.5 中新增 java.util.concurrent (J.U.C)就是建立在CAS之上的。相对于对于 synchronized 这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。

java.util.concurrent 中的 AtomicInteger 为例,看一下在不使用锁的情况下是如何保证线程安全的。主要理解 getAndIncrement 方法,该方法的作用相当于 ++i 操作。

 

五、synchronized   

  1. 如何获取和释放锁

受JVM控制,进大括号的时候获取锁。

这里释放锁只会有两种情况:

  (1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

(2)线程执行发生异常,此时JVM会让线程自动释放锁。

  1. synchronized 使用

(1)修饰一个方法 - 对象锁

(2)修饰代码块 - 对象锁

(3)修饰一个类 - 类锁

(4)修饰静态方法 - 类锁

(5)当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁:

但是实际上synchronized 都是对对象加锁。

  1. synchronized 原理

修饰代码块时

monitorenter

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

这段话的大概意思为:

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

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1

3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit: 

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

这段话的大概意思为:

执行monitorexit的线程必须是objectref所对应的monitor的所有者。

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

  通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

修饰方法时

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

实现原理:

通常将synchronized 称为重量级锁是因为Synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,有些情况下它并不那么重了,Java SE1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。

我们首先来看一下何为偏向锁。

偏向锁,比较受限制,因为它限定了程序中个只有一个线程。

在此线程之后的执行过程中,如果再次进入或者退出同一段同步块代码,并不再需要去进行加锁或者解锁操作,而是会做以下的步骤:

(1)Load-and-test,也就是简单判断一下当前线程id是否与Markword当中的线程id是否一致.

(2)如果一致,则说明此线程已经成功获得了锁,继续执行下面的代码.

(3)如果不一致,则要检查一下对象是否还是可偏向,即“是否偏向锁”标志位的值。

(3)如果还未偏向,则利用CAS操作来竞争锁,也即是第一次获取锁时的操作。

(4)如果此对象已经偏向了,并且不是偏向自己,则说明存在了竞争。此时可能就要根据另外线程的情况,可能是重新偏向,也有可能是做偏向撤销,但大部分情况下就是升级成轻量级锁了。

那么轻量级锁获取锁的过程是?

轻量级锁获取锁的过程:

(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图: 

(2)拷贝对象头中的Mark Word复制到锁记录中;

(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。

(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。

(5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁。

             

 

重量级锁

轻量级锁膨胀之后,就升级为重量级锁了。重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。当轻量级所经过锁撤销等步骤升级为重量级锁之后,它的Markword部分数据大体如下

bit fields

锁标志位

指向Mutex的指针

10

 

 

 

为什么说重量级锁开销大呢

主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
这就是说为什么重量级线程开销很大的。

通过上面的分析,我们知道了为什么synchronized关键字为何又深得人心,也知道了锁的演变过程。
也就是说,synchronized关键字并非一开始就该对象加上重量级锁,也是从偏向锁,轻量级锁,再到重量级锁的过程。
    这个过程也告诉我们,假如我们一开始就知道某个同步代码块的竞争很激烈、很慢的话,那么我们一开始就应该使用重量级锁了,从而省掉一些锁转换的开销。

自旋锁的一些问题

如果同步代码块执行的很慢,需要消耗大量的时间,那么这个时侯,其他线程在原地等待空消耗cpu,这会让人很难受。

本来一个线程把锁释放之后,当前线程是能够获得锁的,但是假如这个时候有好几个线程都在竞争这个锁的话,那么有可能当前线程会获取不到锁,还得原地等待继续空循环消耗cup,甚至有可能一直获取不到锁。

基于这个问题,我们必须给线程空循环设置一个次数,当线程超过了这个次数,我们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁。
默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin来进行更改。

Java对象头、monitor

Java对象头和monitor是实现synchronized的基础!下面就这两个概念来做详细介绍。

Java对象头

synchronized用的锁是存在Java对象头里的,那么什么是Java对象头呢?Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键,所以下面将重点阐述

元数据是一种二进制信息,用以对存储在公共语言运行库可移植可执行文件 (PE) 文件或存储在内存中的程序进行描述。将您的代码编译为 PE 文件时,便会将元数据插入到该文件的一部分中,而将代码转换为 Microsoft 中间语言 (MSIL) 并将其插入到该文件的另一部分中。在模块或程序集中定义和引用的每个类型和成员都将在元数据中进行说明。当执行代码时,运行库将元数据加载到内存中,并引用它来发现有关代码的、成员、继承等信息。

Mark Word

Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit
      Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。下图是Java对象头的存储结构(32位虚拟机):

 

对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,变化状态如下(32位虚拟机):

 

Monitor

什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。
    与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
    Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。其结构如下:
                          
Owner初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。
RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。
Nest:用来实现重入锁的计数。
HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

  • ReentrantLock实现原理:

Lock接口

public interface Lock {

                            void lock();

                            void lockInterruptibly() throws InterruptedException;

                            boolean tryLock();

                            boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

                            void unlock();

                       Condition newCondition();

}

 

lock():

方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

tryLock():

方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

tryLock(long time, TimeUnit unit):

方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

lockInterruptibly():

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

实现原理:

ReentrantLock实现的前提就是AbstractQueuedSynchronizer,简称AQS,是java.util.concurrent的核心。AQS是基于FIFO队列的实现AQS 中的队列是由 Node节点组成的双向链表实现的。所有的操作都是在这个AQS队列当中。如果一个线程获取锁成功那就成功了,如果失败了就将其放入等待队列当中。

 lock加锁的过程,以非公平性锁为例:
      1、当前线程通过CAS操作来抢占锁,抢占成功则修改锁状态为1,将线程信息记录到锁当中,返回。
      2、否则抢占不成功
        2.1、获取当前锁的状态 getState
        2.2、当前锁状态为0,表示锁空闲,没有线程获取则当前线程通过CAS                                   操作直接获取锁,成功则将锁状态,线程信息记录,返回
         2.3、当当前线程和获取锁的线程相同时:对锁状态+1操作,判断锁                            是否到达上限,到达则抛出异常,否则更新锁状态值,返回    
    unlock释放锁的过程:
      1、获取新的锁状态值(获取原来锁的状态值-1)
      2、判断当前释放锁线程和锁中线程信息是否一致,不一致则抛出异常
      3、当线程信息一致时
         3.1、判断锁状态是否是0,即锁不在被占用,将锁中当前线程信息清除掉
         3.2、当锁状态不为空闲状态,将最新锁状态值更细一下
         
            那有的同学就会问了那没有获取到锁的线程怎么办呢?(会将他挂起)

        
          condition也是一种通信机制,和wait、notify、notifyAll作用类似,但其操作更加丰富
  七、Happen-before原则    

多线程 的操作是十分复杂和繁琐的,我们通过volatile和锁机制使得对多线程的操作可控。但是如果所有的操作都需要我们认为的控制那么多线程的操作将变得十分啰嗦。

所以我们通过一种规则来判断多线程下数据是否存在竞争,并且解决可能存在的冲突问题。这个规则被我们称为Happen-before原则。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值