131 阅读7分钟

java中的锁机制主要分为Lock和Synchronized两大体系。

锁的使用

在Lock接口出现之前,java用synchronized实现锁。java5之后,增加了Lock接口和实现类,提供了相同功能,但需要手动加锁释放锁,并拥有了可中断获取锁,超时获取锁等特性。

  • 可中断获取锁
  • 可非阻塞获取锁
  • 可限定获取锁的超时时间

Mark Word

image.png 什么都不加,Mark Word就是Normal. 加锁后数据就会变化,从上到下依次为偏向锁,轻量级锁,重量级锁. 重量级锁Mark word存的是Monitor指针,占用30bit.

Monitor

Monitor译作监视器或管程. Monitor是JVM提供的,每个Java对象都可以关联一个Monitor对象,在给一个对象加上synchronized后,这个对象头的Mark word中就会设置一个指向Monitor对象的指针.

Monitor的结构

image.png

image.png Thread2执行到了临界区代码,获取了对象obj的锁, 那么Monitor的owner就会是Thread2

image.png

这时候Thread1也执行到了临界区代码,它尝试获取锁,但是Monitor已经有了所有者,它获取不到. 这时候,它会和Monitor的EntryList关联,EntryList是阻塞队列,Thread1和EntryList关联,就相当于进入了阻塞队列,变为BLOCKED状态.

image.png

这时候Thread3也执行到了临界区代码,一样进入EntryList,变为BLOCKED状态.

image.png

当Thread2执行完后,Monitor的owner就会空出来.唤醒阻塞队列中的线程,,唤醒规则看实现方式.

image.png

synchronized

锁是由操作系统实现的,挂起和唤醒线程,需要操作系统帮忙完成。操作系统实现线程切换需要从用户态转到内核态。状态的转换要花费较长的时间。 使用方式:

  1. 方法
  2. 静态方法
  3. 代码块,可指定加锁对象。

保证可见性、原子性、一致性

实现原理:

被synchronized修饰的程序,在编译后的字节码指令中会多出 monitorenter和monitorexit指令。 monitorenter指向同步代码块的开始,monitorexit指向结束。

  • 当jvm执行到monitorenter指令时,线程试图获取锁,也就是获取monitor。 monitor由锁对象的对象头的Mark word中存储。 执行monitorenter指令后,如果monitor的进入数为0(这个锁未被占用),则该线程进入monitor,获取成功,并将进入数设置为1,该线程车成为monitor的所有者。

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

  • 在执行 monitorexit 指令后,将锁计数器-1,如果为0了,锁被释放。

  • 如果获取对象锁失败,那当 前线程就要阻塞等待,直到锁被另外一个线程释放为止。

  • synchronized加在同一个对象上,才能线程安全。

  • 一个线程已经拥有了对象的锁,再次申请还能获取该对象的锁,是可重入的。这样能避免自己锁死自己

  • synchronized是非公平锁: 非公平指的是并不是按照线程申请锁的先后顺寻分配锁,而是在锁被释放后,其他线程都能争抢锁。

  • 悲观锁 :

ReenTrantLock

synchronized是基于JVM实现的。

ReenTrantLock是API层面,基于Lock接口的实现类。 使用lock()和unlock()等方法完成。

Lock由AQS实现, AQS阻塞和唤醒线程靠的是LockSupport类的park和unpark方法。

ReenTrantLock可以指定是公平的还是非公平的锁。 synchronized是非公平锁。 公平锁是先等待的线程先获取锁。

如何避免死锁:

  1. 响应中断 等待过程中,线程可以根据需要取消对锁的请求。

  2. 可轮询锁: 通过trylock()获取锁,如果有可用锁,则获取该锁并返回true。 如无可用锁,则立即返回false。

  3. 定时锁 通过boolean tryLock(long time,TimeUnit unit) throws InterruptedException获取定时锁。如果在给定的时 间内获取到了可用锁,且当前线程未被中断,则获取该锁并返回 true。如果在给定的时间内获取不到可用锁,将禁用当前线程

ReentrantLock支持公平锁和非公平锁两种方式。

锁升级

java1.6中,为了减少获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。

锁升级的过程体现在Mark word的标志位发生变化。 img

由于java线程的操作最终还是靠操作系统的线程,所以线程的阻塞和唤起都需要操作系统执行,需要从用户态切换到内核态,十分耗费性能。 所以对锁进行了优化,根据竞争程度的不同加合适的锁。

无锁

偏向锁

只有一个线程竞争,那这个锁就偏向于这个线程。 当一个线程访问同步代码块时,通过CAS操作,在锁对象头的Mark Word中记录占用锁的线程id,一表示这个锁对象偏向于这个线程。如果成功,Mark word就会存储当前线程id,执行同步代码块。

如果一个线程在获取锁时,发现已经有其他线程获取了锁,说明存在对所的竞争,就需要升级成轻量级锁。

升级锁要等到全局安全点,即没有线程执行时。

轻量级锁

轻量级锁对使用者是透明的,仍然使用synchronizd. 还是用CAS操作Mark word,来试图获取锁,如果重试成功,就是用轻量级锁,否则进一步升级成重量级锁。

假设有两个方法,用同一个对象加锁:

image.png

  • 线程的栈帧中会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
  • 锁记录中的Object reference指向锁对象,并尝试用cas替换Object的Mark Word.将maek word的值存入锁记录.

image.png

  • 如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁.

重量级锁

大量竞争时,升级为重量级锁

volatile

  • 保证变量可见性,一个变量在多个线程间可见。
  • 禁止 JVM 的指令重排。

线程把主存中的变量读到自己的工作内存,修改后,再写回主存, 但是这样会导致主存中的值已经变了,线程还在使用自己工作内存的数据,这就会导致数据不一致。要解决这个问题,就要把变量声明为volatile 。 volatile指示jvm,这个变量是不稳定的,每次使用都需要先到主存中读取。

synchronized 关键字和 volatile 关键字的区别

  • volatile是一种更轻量的同步机制,使用volatitle不会有上下文切换或线程调度,所以性能比synchronized好。但是volatile只能作用于变量。
  • volatile能保证数据的可见性,但不能保证数据的原子性。

内存屏障 对volatile变量的写指令后会加入写屏障 对volatile变量的读指令后会加入读屏障

如何保障可见性

写屏障 sfence 保证在该屏障之前的,对共享变量的改动,都同步到主存中.

读屏障 lfence 保证该屏障之后,对共享变量的读取,加载的是主存中的最新数据.

各种锁

锁是为了保证数据一致性。 在多线程编程中,为了保证数据一致性,通常需要在使用该对象或者方法之前加锁。

乐观锁

是一种思想,并不会上锁。是在更新数据时,先读出当前版本号,和上一次的版本号比较,如果版本号一致,说明期间没有被其他线程修改过,则更新。如果版本号不一致,则继续读取,比较。 乐观锁通过CAS实现的。 实现方式:首先检查某块内存的值是否跟之前读取时的值相同,如果不一样,说明内存的值被其他线程修改了,就不进行操作,如果一样,就把新值设置给内存。

优点:提高了性能 缺点:只能保证一个共享变量的原子操作 长时间CAS如果不成功,cpu开销大

悲观锁

读取数据时就加锁。 基于AQS实现。

自旋锁

CAS就是自旋。 自旋就是在进行CAS操作。 线程或取不到锁,并不挂起,而是等一等。避免用户态和内核态的切换。 线程自旋占用CPU,如果等待时间太长,会浪费CPU资源 ,所以需要设置一个最大等待时间。