java中的锁机制主要分为Lock和Synchronized两大体系。
锁的使用
在Lock接口出现之前,java用synchronized实现锁。java5之后,增加了Lock接口和实现类,提供了相同功能,但需要手动加锁释放锁,并拥有了可中断获取锁,超时获取锁等特性。
- 可中断获取锁
- 可非阻塞获取锁
- 可限定获取锁的超时时间
Mark Word
什么都不加,Mark Word就是Normal. 加锁后数据就会变化,从上到下依次为偏向锁,轻量级锁,重量级锁.
重量级锁Mark word存的是Monitor指针,占用30bit.
Monitor
Monitor译作监视器或管程. Monitor是JVM提供的,每个Java对象都可以关联一个Monitor对象,在给一个对象加上synchronized后,这个对象头的Mark word中就会设置一个指向Monitor对象的指针.
Monitor的结构
Thread2执行到了临界区代码,获取了对象obj的锁, 那么Monitor的owner就会是Thread2
这时候Thread1也执行到了临界区代码,它尝试获取锁,但是Monitor已经有了所有者,它获取不到. 这时候,它会和Monitor的EntryList关联,EntryList是阻塞队列,Thread1和EntryList关联,就相当于进入了阻塞队列,变为BLOCKED状态.
这时候Thread3也执行到了临界区代码,一样进入EntryList,变为BLOCKED状态.
当Thread2执行完后,Monitor的owner就会空出来.唤醒阻塞队列中的线程,,唤醒规则看实现方式.
synchronized
锁是由操作系统实现的,挂起和唤醒线程,需要操作系统帮忙完成。操作系统实现线程切换需要从用户态转到内核态。状态的转换要花费较长的时间。 使用方式:
- 方法
- 静态方法
- 代码块,可指定加锁对象。
保证可见性、原子性、一致性
实现原理:
被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是非公平锁。 公平锁是先等待的线程先获取锁。
如何避免死锁:
-
响应中断 等待过程中,线程可以根据需要取消对锁的请求。
-
可轮询锁: 通过trylock()获取锁,如果有可用锁,则获取该锁并返回true。 如无可用锁,则立即返回false。
-
定时锁 通过boolean tryLock(long time,TimeUnit unit) throws InterruptedException获取定时锁。如果在给定的时 间内获取到了可用锁,且当前线程未被中断,则获取该锁并返回 true。如果在给定的时间内获取不到可用锁,将禁用当前线程
ReentrantLock支持公平锁和非公平锁两种方式。
锁升级
java1.6中,为了减少获取锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。
锁升级的过程体现在Mark word的标志位发生变化。
由于java线程的操作最终还是靠操作系统的线程,所以线程的阻塞和唤起都需要操作系统执行,需要从用户态切换到内核态,十分耗费性能。 所以对锁进行了优化,根据竞争程度的不同加合适的锁。
无锁
偏向锁
只有一个线程竞争,那这个锁就偏向于这个线程。 当一个线程访问同步代码块时,通过CAS操作,在锁对象头的Mark Word中记录占用锁的线程id,一表示这个锁对象偏向于这个线程。如果成功,Mark word就会存储当前线程id,执行同步代码块。
如果一个线程在获取锁时,发现已经有其他线程获取了锁,说明存在对所的竞争,就需要升级成轻量级锁。
升级锁要等到全局安全点,即没有线程执行时。
轻量级锁
轻量级锁对使用者是透明的,仍然使用synchronizd. 还是用CAS操作Mark word,来试图获取锁,如果重试成功,就是用轻量级锁,否则进一步升级成重量级锁。
假设有两个方法,用同一个对象加锁:
- 线程的栈帧中会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
- 锁记录中的Object reference指向锁对象,并尝试用cas替换Object的Mark Word.将maek word的值存入锁记录.
- 如果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资源 ,所以需要设置一个最大等待时间。