Synchronized锁机制和ReentrantLock

Synchronized

Java中的每个对象都可以作为锁。

  1. 普通同步方法,锁是当前实例对象。
  2. 静态同步方法,锁是当前类的class对象。
  3. 同步代码块,锁是括号中的对象。

锁的内部机制

一般锁有4种状态:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。

在进一步深入之前,我们先认识下两个概念:对象头和monitor。

什么是对象头?

在hotspot虚拟机中,对象在内存的分布分为3个部分:对象头,实例数据,和对齐填充。
mark word被分成两部分,lock word和标志位。
Klass ptr指向Class字节码在虚拟机内部的对象表示的地址。
Fields表示连续的对象实例字段。

mark word 被设计为非固定的数据结构,以便在极小的空间内存储更多的信息。比如:在32位的hotspot虚拟机中:

如果对象处于未被锁定的情况下。mark word 的32bit空间中有25bit存储对象的哈希码、4bit存储对象的分代年龄、2bit存储锁的标记位、1bit固定为0。而在其他的状态下(轻量级锁、重量级锁、GC标记、可偏向)下对象的存储结构为

什么是monitor(锁)?

monitor是线程私有的数据结构,每一个线程都有一个可用monitor列表(栈?),同时还有一个全局的可用列表,先来看monitor的内部

  • Owner:初始时为NULL表示当前没有任何线程拥有该monitor,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
  • EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor失败的线程。
  • RcThis:表示blocked或waiting在该monitor上的所有线程的个数。
  • Nest:用来实现重入锁的计数。
  • HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
  • Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值:0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。

monitor的作用:在 java 虚拟机中,线程一旦进入到被synchronized修饰的方法或代码块时,指定的锁对象通过某些操作将对象头中的LockWord指向monitor的起始地址与之关联,同时monitor中的Owner存放拥有该锁的线程的唯一标识,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码。

锁的状态:

  1. 偏向锁

下述代码中,当线程访问同步方法method1时,会在对象头(SynchronizedTest.class对象的对象头)和栈帧的锁记录中存储锁偏向的线程ID,下次该线程在进入method2,只需要判断对象头存储的线程ID是否为当前线程,而不需要进行CAS操作进行加锁和解锁(因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟)。

Compare And Swap:CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”。它们虽然看似复杂,但却是 Java 5 并发机制优于原有锁机制的根本。(这段描述引自《Java并发编程实践》)。简单的来说,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。

1 public class SynchronizedTest {
2     private static Object lock = new Object();
3     public static void main(String[] args) {
4         method1();
5         method2();
6     }
7     synchronized static void method1() {}
8     synchronized static void method2() {}
9 }

轻量级锁

利用了CPU原语Compare-And-Swap。

线程可以通过两种方式锁住一个对象:

  1. 通过膨胀一个处于无锁状态(状态位001)的对象获得该对象的锁;
  2. 对象处于膨胀状态(状态位00),但LockWord指向的monitor的Owner字段为NULL,则可以直接通过CAS原子指令尝试将Owner设置为自己的标识来获得锁。

获取锁(monitorenter)的大概过程:

  1. 对象处于无锁状态时(LockWord的值为hashCode等,状态位为001),线程首先从monitor列表中取得一个空闲的monitor,初始化Nest和Owner值为1和线程标识,一旦monitor准备好,通过CAS替换monitor起始地址到LockWord进行膨胀。如果存在其它线程竞争锁的情况而导致CAS失败,则回到monitorenter重新开始获取锁的过程即可。
  2. 对象已经膨胀,monitor中的Owner指向当前线程,这是重入锁的情况(reentrant),将Nest加1,不需要CAS操作,效率高。
  3. 对象已经膨胀,monitor中的Owner为NULL,此时多个线程通过CAS指令试图将Owner设置为自己的标识获得锁,竞争失败的线程则进入第4种情况。
  4. 对象已经膨胀,同时Owner指向别的线程,在调用操作系统的重量级的互斥锁之前自旋一定的次数,当达到一定的次数如果仍然没有获得锁,则开始准备进入阻塞状态,将rfThis值原子加1,由于在加1的过程中可能被其它线程破坏对象和monitor之间的联系,所以在加1后需要再进行一次比较确保lock word的值没有被改变,当发现被改变后则要重新进行monitorenter过程。同时再一次观察Owner是否为NULL,如果是则调用CAS参与竞争锁,锁竞争失败则进入到阻塞状态。

释放锁(monitorexit)的大概过程:

  1. 检查该对象是否处于膨胀状态并且该线程是这个锁的拥有者,如果发现不对则抛出异常。
  2. 检查Nest字段是否大于1,如果大于1则简单的将Nest减1并继续拥有锁,如果等于1,则进入到步骤3。
  3. 检查rfThis是否大于0,设置Owner为NULL然后唤醒一个正在阻塞或等待的线程再一次试图获取锁,如果等于0则进入到步骤4。
  4. 缩小(deflate)一个对象,通过将对象的LockWord置换回原来的HashCode等值来解除和monitor之间的关联来释放锁,同时将monitor放回到线程私有的可用monitor列表。

重量级锁

当锁处于这个状态下,其他线程试图获取锁都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程。

线程的内存可见性

  1. 线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
  2. 线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

 ReentrantLock

先来看一下ReentrantLock的源码:

 1 public class ReentrantLock implements Lock, java.io.Serializable {
 2     private final Sync sync;
 3     abstract static class Sync extends AbstractQueuedSynchronizer {
 4
 5         /**
 6          * Performs {@link Lock#lock}. The main reason for subclassing
 7          * is to allow fast path for nonfair version.
 8          */
 9         abstract void lock();
10
11         /**
12          * Performs non-fair tryLock.  tryAcquire is implemented in
13          * subclasses, but both need nonfair try for trylock method.
14          */
15         final boolean nonfairTryAcquire(int acquires) {
16             final Thread current = Thread.currentThread();
17             int c = getState();
18             if (c == 0) {
19                 if (compareAndSetState(0, acquires)) {
20                     setExclusiveOwnerThread(current);
21                     return true;
22                 }
23             }
24             else if (current == getExclusiveOwnerThread()) {
25                 int nextc = c + acquires;
26                 if (nextc < 0) // overflow
27                     throw new Error("Maximum lock count exceeded");
28                 setState(nextc);
29                 return true;
30             }
31             return false;
32         }
33
34         protected final boolean tryRelease(int releases) {
35             int c = getState() - releases;
36             if (Thread.currentThread() != getExclusiveOwnerThread())
37                 throw new IllegalMonitorStateException();
38             boolean free = false;
39             if (c == 0) {
40                 free = true;
41                 setExclusiveOwnerThread(null);
42             }
43             setState(c);
44             return free;
45         }
46
47     }
48     //默认非公平锁
49     public ReentrantLock() {
50         sync = new NonfairSync();
51     }
52     //fair为false时,采用公平锁策略
53     public ReentrantLock(boolean fair) {
54         sync = fair ? new FairSync() : new NonfairSync();
55     }
56     public void lock() {
57         sync.lock();
58     }
59     public void unlock() {    sync.release(1);}
60     public Condition newCondition() {
61         return sync.newCondition();
62     }
63     ...
64 }
时间: 2024-11-05 19:02:20

Synchronized锁机制和ReentrantLock的相关文章

Java多线程4:synchronized锁机制

脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过的. 多线程线程安全问题示例 看一段代码: public class ThreadDomain13 { private int num = 0; public void addNum(String userName) { try { if ("a".equals(userName)) {

synchronized锁机制

http://www.cnblogs.com/xrq730/p/4851350.html 脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过的. 多线程线程安全问题示例 看一段代码: public class ThreadDomain13 { private int num = 0; public void addNum(String userName) {

java 多线程8 : synchronized锁机制 之 方法锁

脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过的.注意这里 局部变量是不存在脏读的情况 多线程线程实例变量非线程安全 看一段代码: public class ThreadDomain13 { private int num = 0; public void addNum(String userName) { try { if ("

java 多线程9 : synchronized锁机制 之 代码块锁

synchronized同步代码块 用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个较长时间的任务,那么B线程必须等待比较长的时间.这种情况下可以尝试使用synchronized同步语句块来解决问题.看一下例子: 下面例子是优化后的例子 使用代码块锁,原先例子是方法锁,就是同步 必须要执行2个for  public class ThreadDomain18 { public void doLongTimeTask() throws Exception

synchronized锁机制 之 代码块锁(转)

synchronized同步代码块 用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个较长时间的任务,那么B线程必须等待比较长的时间.这种情况下可以尝试使用synchronized同步语句块来解决问题.看一下例子: 下面例子是优化后的例子使用代码块锁,原先例子是方法锁,就是同步必须要执行2个for. public class ThreadDomain18 { public void doLongTimeTask() throws Exception {

JAVA synchronized关键字锁机制(中)

synchronized 锁机制简单的用法,高效的执行效率使成为解决线程安全的首选. 下面总结其特性以及使用技巧,加深对其理解. 特性: 1. Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码.       2. 当一个线程同时访问object的一个synchronized(this)同步代码块时,其它线程仍然可以访问非修饰的方法或代码块.       3. 当多个线程同时访问object的synchronized(this)同步代码

【转载】Java中的锁机制 synchronized &amp; Lock

参考文章: http://blog.csdn.net/chen77716/article/details/6618779 目前在Java中存在两种锁机制:synchronized和Lock,Lock接口及其实现类是JDK5增加的内容,其作者是大名鼎鼎的并发专家Doug Lea.本文并不比较synchronized与Lock孰优孰劣,只是介绍二者的实现原理. 数据同步需要依赖锁,那锁的同步又依赖谁?synchronized给出的答案是在软件层面依赖JVM,而Lock给出的方案是在硬件层面依赖特殊的

[转载]深入JVM锁机制-synchronized

转自:http://blog.csdn.net/chen77716/article/details/6618779,并加上少量自己的理解 目前在Java中存在两种锁机制:synchronized和Lock,Lock接口及其实现类是JDK5增加的内容,其作者是大名鼎鼎的并发专家Doug Lea.本文并不比较synchronized与Lock孰优孰劣,只是介绍二者的实现原理. 数据同步需要依赖锁,那锁的同步又依赖谁?synchronized给出的答案是在软件层面依赖JVM,而Lock给出的方案是在硬

java的多线程安全,ReentrantLock与synchronized锁

前言 多线程总的来说是一个很大的模块,所以虽然之前就想写但一直感觉有地方没有理解透,在经过了一段时间学习后,终于有点感觉了,在此写下随笔. 多线程安全问题##: 首先和大家讨论一下多线程为什么会不安全,大家先看下面的程序. /** - @author lw */ public class Test extends Thread{ public void run() { for(int i=1;i<=10;i++) { System.out.println(i); } } public stati