Synchronized锁机制和ReentrantLock
Synchronized
Java中的每个对象都可以作为锁。
- 普通同步方法,锁是当前实例对象。
- 静态同步方法,锁是当前类的class对象。
- 同步代码块,锁是括号中的对象。
锁的内部机制
一般锁有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的内部
- 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存放拥有该锁的线程的唯一标识,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码。
锁的状态:
- 偏向锁
下述代码中,当线程访问同步方法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是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。
public class SynchronizedTest {
private static Object lock = new Object();
public static void main(String[] args) {
method1();
method2();
}
synchronized static void method1() {}
synchronized static void method2() {}
}
轻量级锁
利用了CPU原语Compare-And-Swap。
线程可以通过两种方式锁住一个对象:
- 通过膨胀一个处于无锁状态(状态位001)的对象获得该对象的锁;
- 对象处于膨胀状态(状态位00),但LockWord指向的monitor的Owner字段为NULL,则可以直接通过CAS原子指令尝试将Owner设置为自己的标识来获得锁。
获取锁(monitorenter)的大概过程:
- 对象处于无锁状态时(LockWord的值为hashCode等,状态位为001),线程首先从monitor列表中取得一个空闲的monitor,初始化Nest和Owner值为1和线程标识,一旦monitor准备好,通过CAS替换monitor起始地址到LockWord进行膨胀。如果存在其它线程竞争锁的情况而导致CAS失败,则回到monitorenter重新开始获取锁的过程即可。
- 对象已经膨胀,monitor中的Owner指向当前线程,这是重入锁的情况(reentrant),将Nest加1,不需要CAS操作,效率高。
- 对象已经膨胀,monitor中的Owner为NULL,此时多个线程通过CAS指令试图将Owner设置为自己的标识获得锁,竞争失败的线程则进入第4种情况。
- 对象已经膨胀,同时Owner指向别的线程,在调用操作系统的重量级的互斥锁之前自旋一定的次数,当达到一定的次数如果仍然没有获得锁,则开始准备进入阻塞状态,将rfThis值原子加1,由于在加1的过程中可能被其它线程破坏对象和monitor之间的联系,所以在加1后需要再进行一次比较确保lock word的值没有被改变,当发现被改变后则要重新进行monitorenter过程。同时再一次观察Owner是否为NULL,如果是则调用CAS参与竞争锁,锁竞争失败则进入到阻塞状态。
释放锁(monitorexit)的大概过程:
- 检查该对象是否处于膨胀状态并且该线程是这个锁的拥有者,如果发现不对则抛出异常。
- 检查Nest字段是否大于1,如果大于1则简单的将Nest减1并继续拥有锁,如果等于1,则进入到步骤3。
- 检查rfThis是否大于0,设置Owner为NULL然后唤醒一个正在阻塞或等待的线程再一次试图获取锁,如果等于0则进入到步骤4。
- 缩小(deflate)一个对象,通过将对象的LockWord置换回原来的HashCode等值来解除和monitor之间的关联来释放锁,同时将monitor放回到线程私有的可用monitor列表。
重量级锁
当锁处于这个状态下,其他线程试图获取锁都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程。
线程的内存可见性
- 线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
- 线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
ReentrantLock
在Java多线程中,可以使用Synchronized实现线程之间的同步互斥,但在JDK1.5中加入了ReentrantLock类也能达到同样的效果,并且在扩展功能上更加强大,比如:嗅探锁定、多路分支通知等。而且使用更加灵活。
先来看一下ReentrantLock的源码:
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer { /**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock(); /**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
} protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
} }
//默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//fair为false时,采用公平锁策略
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() {
sync.lock();
}
public void unlock() { sync.release(1);}
public Condition newCondition() {
return sync.newCondition();
}
...
}
调用ReentrantLock对象的lock()方法获取锁,unlock方法释放锁。
调用lock.lock()方法的线程就有了“对象监视器”,其他线程只有等待被释放时再争抢,效果和Synchronized关键字一样,线程之间的执行顺序是随机的。
代码见:https://github.com/JJfan0327/Reentrant
关键字Synchronized和wait notifyAll notify共同组成了通信机制,Reentrantlock同样可以,但需要借助Condition对象。Condition类具有实现多路通知功能,也就是一个lock里面有多个Condition(对象监视器)实例,线程对象可以注册在指定的Condition中,从而有选择性的通知,调度线程更加灵活。
而Synchronized相当于整个locked对象中有一个Condition对象,所有线程注册在它一个对象上。
Object类中的wait方法===>>Condition类中的await方法
wait(long)==>>await(long)
notify()==>>signal()
notifyAll()==>>signalAll()
多个Condition对象通知指定的线程对象:代码https://github.com/JJfan0327/Condition
Synchronized锁机制和ReentrantLock的更多相关文章
- Java多线程5:Synchronized锁机制
一.前言 在多线程中,有时会出现多个线程对同一个对象的变量进行并发访问的情形,如果不做正确的同步处理,那么产生的后果就是“脏读”,也就是获取到的数据其实是被修改过的. 二.引入Synchronized ...
- synchronized锁机制的实现原理
Synchronized 锁机制的实现原理 Synchronized是Java种用于进行同步的关键字,synchronized的底层使用的是锁机制实现的同步.在Java中的每一个对象都可以作为锁. J ...
- Java多线程4:synchronized锁机制
脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过 ...
- java 多线程8 : synchronized锁机制 之 方法锁
脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数 ...
- 线程 synchronized锁机制
脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过 ...
- Java多线程学习——synchronized锁机制
Java在多线程中使用同步锁机制时,一定要注意锁对对象,下面的例子就是没锁对对象(每个线程使用一个被锁住的对象时,得先看该对象的被锁住部分是否有人在使用) 例子:两个人操作同一个银行账户,丈夫在ATM ...
- synchronized锁机制(六)
前言 1.理解同步关键词synchronized 2.同步方法与同步代码块的区别 3.理解锁的对象this 脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量进行并发访问的 ...
- synchronized锁机制 之 代码块锁(转)
synchronized同步代码块 用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个较长时间的任务,那么B线程必须等待比较长的时间.这种情况下可以尝试使用 ...
- java 多线程9 : synchronized锁机制 之 代码块锁
synchronized同步代码块 用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个较长时间的任务,那么B线程必须等待比较长的时间.这种情况下可以尝试使用 ...
随机推荐
- Slog64_项目上线之ArthurSlog个人网站上线3
ArthurSlog SLog-64 Year·1 Guangzhou·China September 9th 2018 ArthurSlog Page GitHub NPM Package Page ...
- vue 组件中添加样式不生效
如何产生 在开发项目中遇到在组件中添加样式不生效的情况.具体场景如下 //// vue 组件 <template> <div class="box" data-v ...
- 说一说Vue(2.0)组件的通信方式
Vue(2.0)是组件化的开发模式,在不借助vuex框架的前提下,组件之间如何通信呢?接下来我在开发中总结了几种情况.1.父组件给子组件传值(props): 父组件给子组件传值的方式主要是用函数pro ...
- Web架构之Nginx基础配置
目录 1.Nginx 虚拟主机 1.1.基于域名的虚拟主机 1.2.基于端口的虚拟主机 1.3.基于IP的虚拟主机 2.Nginx include 3.Nginx 日志配置 3.1.访问日志 3.2. ...
- scrapy-redis分布式爬取知乎问答,使用docker布置多台机器。
先上结果: 问题: 答案: 可以看到现在答案文档有十万多,十万个为什么~hh 正文开始: 分布式爬虫应该是在多台服务器(A B C服务器)布置爬虫环境,让它们重复交叉爬取,这样的话需要用到状态管理器. ...
- 【图文+视频新手也友好】Java一维数组详细讲解(内含练习题答案+详解彩蛋喔~)
目录 视频讲解: 一.数组的概述 二.一维数组的使用 三.Arrays工具类中的sort方法(sort方法用的多,我们具体讲一下) 四.数组中的常见异常 五.一维数组练习题 六.彩蛋(本期视频使用的P ...
- qtp10安装步骤(比较完整)
在安装qtp之前,要提前安装软件(不知道是干嘛的,反正安装就完了),右键--管理员运行完成安装,如下图: 解压qtp镜像: 双击setup: 第一步,点击第一排 第二步, 注:安装QTP系统提示“ 计 ...
- C++ 按行读取文件并打印
#include<iostream> #include<fstream> #include<string> #include <vector> #inc ...
- Netty之缓冲区ByteBuf解读(二)
上篇介绍了 ByteBuf 的简单读写操作以及读写指针的基本介绍,本文继续对 ByteBuf 的基本操作进行解读. 读写指针回滚 这里的 demo 例子还是使用上节使用的. ByteBuf buf = ...
- Codeforces Round #200 (Div. 2)E
Read Time 题意:有一个数组,很多指针指在这个数组上,每次每个指针可以向左或向右移动一个位置.给出一些需要访问的位置,问访问用的最少时间. 一个指针只可能转一次方向.二分答案. #includ ...