Java 中的锁
讨论锁,首先要知道 锁 是为了解决啥问题产生的.
锁,是为了避免线程间共享资源出错而产生的.
在 Java 中,从 jdk1.0 开始,每个 Java 对象都有一个内部锁.
比如如果一个线程访问了某个 Java 对象中的数据,此时该线程获得该对象的锁,其余线程将无法访问该对象的数据.
这样说不严谨,实际上比如,一个线程访问了一个对象由 synchronized 修饰的方法,线程就获得该对象的锁,那么其它的线程想要访问由 synchronized 修饰的方法就因为获取不到锁而阻塞.但是,其他线程此时是可以访问非同步方法的.因为无需获取锁.
锁分为很多种.有的锁觉得资源不会发生变化,所以允许多个线程获取该资源.但是一个资源可能发生变化,那么就不能让多个线程获取某个资源了,因为这样可能产生一个线程修改了某资源的值,而另一个线程仍使用着某资源之前的值,从而产生问题.(或者另一个线程使用的值也被修改了,同样会出错.)
Java 的锁都是是对象锁.有的地方说当通过锁机制访问静态方法或数据时(方法声明为 synchronized),此时获取的是类锁.通常我们认为静态方法或属性是类的属性,而普通方法与属性,是对象的属性.参考 Java 类加载 ,我们知道,类在 JVM 中也是一个对象的形式存在的.所以所谓的"类锁"也是对象锁,只不过是类加载进 JVM 后的类对象的锁.
若一个方法用 synchronized 声明,则对象的锁保护整个方法.
对象的锁用来管理试图调用方法的线程.
Java 线程通信
上面提到 锁,是为了避免线程间共享资源出错而产生的.
线程共享资源的一种应用,就是 Java 的线程通信.即 共享内存模型 .
这里联系操作系统的 线程通信
而 Java 线程通信则是由 JMM(Java 内存模型) 类控制的.
Java 内存模型 (JMM)
JVM 尝试定义 JMM 来屏蔽各个硬件平台和操作系统的内存访问差异,以实现 Java 程序在各个平台下都能达到一致的内存访问效果.
JMM 规定所有的变量都存放在主存(物理内存),每个线程都有自己的工作内存(高速缓存).
线程对变量的操作必须在工作内存,不能直接对主存进行操作. --> 所以需要把工作内存的变量值同步至主存.
每个线程不能访问其他线程的工作内存.
上面在 Java 中的锁 中提到一个线程共享资源可能出错的场景,联系这里,可以知道,如果两个线程共享一个资源,那么一个线程修改了自己工作内存的资源,然后把该资源同步到主存.此时另一个线程使用的自己工作内存的资源还是之前的值,这里就可能出现问题.
实际上,会有一个不断触碰,判断工作内存的资源与主存是否一致的过程,当发现不一致,会把主存的值更新到工作内存.这样,也会出错.
Java 中的类与关键字
下面的内容有重复,但是为了从不同的方面对比,是必要的……
简单区分一下
synchronized,volatile --> 是线程同步的关键字
Lock --> 一般来说,是 Java 的锁实现
线程同步可能会使用到锁
而 java.lang.concurrent.atomic 包下的原子操作类 没有使用锁 而是使用 CAS 的方式来保证原子性.
synchronized volatile
简单对比
Java 使用的并发机制 依赖于 JVM 的实现和 CPU 的指令.
volatile 的实现是在 cpu 层面,使用了 lock 前缀的指令……
synchronized 的实现是在 JVM 层面,使用了 monitorenter,monitorexit……
具体怎么回事,看下面……
PS : 而 ReenTrantLock 是 JDK 实现的
synchronized
实现原理
在 JVM 规范中可以看到 synchronized 在 JVM 里的实现原理.
JVM 基于进入和退出 Monitor 对象来实现 方法同步 和 代码块同步.
但两者的实现细节不一样……
代码块同步 使用 monitorenter 和 monitorexit 指令实现……
方法同步 使用另一种方式,细节在 JVM 规范未说明.但是方法的同步也可以用上述俩指令实现.
synchronized 使用的锁存在 Java 对象头的 Mark Word 里……
早期版本中,synchronized 属于重量级锁,在操作系统层面的实现用到了 Mutex Lock,线程之间的切换需要从用户态转换到核心态,开销较大.
jdk6 做了优化.
Java 对象头和 Monitor 是实现 synchronized 的基础.
Java 对象头
对象在内存中的布局
- 对象头
- 实例数据
- 对齐填充
具体 看 JVM 类文件结构
简单讲,锁的各种状态存放在 Java 对象头 的 Mark Word 块里.
Monitor
一种同步机制?
JVM 中用 C++ 实现
jdk1.6 为 synchronized 做了优化,引入了 偏向锁,轻量级锁……
什么是 偏向锁,轻量级锁?
锁的升级
- 偏向锁
- 轻量级锁
- 重量级锁
[看下面锁的相关概念]
synchronized 的四种状态
- 无锁,偏向锁,轻量级锁,重量级锁
锁膨胀的方向 : 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
volatile
介绍
使用 volatile 在某些情况下比使用锁更方便.
与 JMM(Java 内存模型) 对比
与 Java 的锁机制联系
如果一个字段被声明为 volatile,Java 线程内存模型确保所有线程看到这个变量的值是一致的.
一个域(类的成员变量,类的静态成员变量)声明为 volatile :
保证了不同线程对这个变量进行操作的可见性.
若一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的.
禁止进行 指令重排序 .
- 能保证可见性 --> 读取的都是最新的值
- 不能保证原子性
- 一定程度保证有序性
原子性使用 java.lang.concurrent.atomic 包下的原子操作类.该包下的类没有使用锁,而是使用 CAS 的方式来保证原子性.
注意 : 原子操作类, CAS
不过啥是原子性,有序性……呢?
这是并发编程的三个概念 见下文 三个概念
实现
Java 代码编译为 Java 字节码,字节码被类加载器加载到 JVM,JVM 执行字节码,最终需要转化为汇编指令在 CPU 上执行.
volatile 修饰的变量进行 写操作 时,是通过 cpu 级别实现的.
查看汇编指令会发现多出 lock 前缀的指令……
实现了
- 把当前处理器缓存行的数据写回系统内存.
- 这个写回内存的操作会使其他 cpu 里的缓存无效.
lock 前缀指令实际上相当于一个内存屏障(也称 内存栅栏).提供三个功能
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
与操作系统联系
每个处理器通过 嗅探 在总线上传播的数据来检查自己缓存的值是否过期,当处理器发现自己缓存行对应的内存地址被修改,将会将当前处理器的缓存行设置成无效,当处理器对这个数据进行修改操作时,会重新从系统内存中把数据读到处理器缓存里.
原子操作
jdk1.5 开始 Java 增加了 atomic 包下的原子类
这些类使用 CAS 保证原子性
CAS 是什么?会出现什么问题?
处理器如何实现原子操作……??
- 总线锁定
- 缓存锁定
[回头再看看……]
jdk1.5 后, Java 可以在程序中使用 CAS,该操作由 sun.misc.Unsafe
类里的 compareAndSwapInt() 等方法包装提供.虚拟机在内部做了特殊处理,即时编译出来的结果就是一条平台相关的处理器 CAS 指令.
由于 Unsafe 类不是面向用户调用的类(而是面向 jdk 开发者),故只能通过反射或者其它的 Java api 来间接使用.
如 java.util.concurrent.atomic 下的原子类,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作.
1 | // java.util.concurrent.atomic.AtomicInteger |
unsafe.compareAndSwapInt()
是个 本地方法.
Synchronized 与 ReenTrantLock
Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。
synchronized
介绍
因为 jdk6 对 synchronized 有更新,特性应当与 jdk5 之前对比来看……
[待续……]
使用
- 修饰普通方法 --> 获取对象内置锁
- 修饰代码块 --> 获取对象内置锁
- 修饰静态方法 --> 获取类(类的字节码文件对象)锁
说明一下 :
static 修饰的成员,也就是静态成员,是类的属性.
普通的方法,成员变量,是类的具体实例,也就是对象的属性.
但是在 JVM 中,类和普通的实例,都是对象.
比如反射
1 | Class cls = Class.forName("Sutudent");// 获取一个类对象 |
类对象,即类的字节码文件对象.
而每个对象,都有一个内置锁.
事实上,获取的都是对象锁.但是为了区分,把实例的对象锁称为对象锁,把类对象的锁称为类锁.
实现
上面有提到过
在 JVM 规范中可以看到 synchronized 在 JVM 的实现原理.
JVM 基于 进入 和 退出 Monitor 对象来实现 方法 和 代码块 的同步,但两者实现细节不一样.
代码块同步使用 monitorenter 和 monitorexit 指令实现……
方法同步使用另一种方式,JVM 规范未说明.但是方法的同步也可以用上述俩指令实现.
…….
monitor
对象头保存信息……
类的字节码信息,格式……
在操作系统使用啥来实现的??
Mutex Lock……
…….
锁的状态
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。
特点
synchronized 基于 隐式获取锁,这是优势,也是面对其它方式的缺点.
隐式获取锁 的意思 : 无需显式获取锁,释放锁
PS : 这特么不是废话吗
也就是不需要的手动写代码获取,释放……
优点 : 便捷
缺点 : 扩展性低 --> 与 Lock 比较
- 方法,代码块执行完毕自动释放锁.
- 遇到异常,其持有的锁自动释放 --> JVM 操作.
内置锁的可重入性
与 Lock 的可重入锁 ReentrantLock 对比
支持一个线程对资源的重复加锁.
因为锁的持有者是线程,而非调用.
所以 : 一个线程对一个资源加锁了,下次还能继续进入这个锁.
相当于它能开锁,进入,再加锁. --> 这就是重复加锁(可重入)的意思.
Lock
介绍 A
Lock 是一个 接口,jdk1.5 之后引入.
与 Lock 相关的接口,类 都放在 java.util.locks
包下.
但是值得一提的是,jdk1.5 之后还与 Lock 一起引入了 ReadWriteLock 接口.
下面谈到的 ReetrantLock 是实现了 Lock 的类.
而 ReentrantReadWriteLock 是实现了 ReadWriteLock 接口的类.
很多资料把 Lock,ReetrantLock,ReentrantReadWriteLock 一起谈,搞得像后两者都是 Lock 的实现一样.虽然从概念上讲差不多,但是实际的实现并不是那么想当然.
使用
使用,肯定是使用接口的具体实现类.
- 可重入锁 ReetrantLock
- 读写锁 ReentrantReadWriteLock
可重入锁,读写锁……都是锁机制的一些概念.而 ReetrantLock 是通过 Lock 对可重入锁的 Java 实现,其他同理……
那锁有哪些概念呢??看下面 锁的相关概念
使用方式,比如
1 | Lock lock = new ReetrantLock(); |
介绍 B
我们回头来看 java.util.locks
包.
比较
jdk1.5 添加了 Lock 接口,提供了 Synchronized 相似的同步功能,只是使用需要显式获取,释放锁.
所以可以简单理解为 Lock 是为了解决 Synchronized 的效率问题引入的.
而 ReadWriteLock 是读写分离锁,啥是读写分离锁呢?(也称 读写锁)
读写分离锁 指 同一时刻允许多个线程进行读访问,但是如果有一个写访问线程,那么其它的读访问,写访问都被阻塞.
既然单独说 ReadWriteLock 是读写锁,那么 ReentrantLock 就不是读写锁啦.ReentrantLock 是一种 排他锁 .顾名思义,无论是读还是写,都只允许一个线程访问.
读写锁 可以有效的帮助减少锁的竞争,提升系统性能。
接口 Lock
1 | public interface Lock { |
####### 实现 ReentrantLock
1 | public class ReentrantLock implements Lock, java.io.Serializable { |
####### 接口 ReadWriteLock
1 | public interface ReadWriteLock { |
####### 实现 ReentrantReadWriteLock
1 | public class ReentrantReadWriteLock |
ReentrantLock
//具体谈谈……
看源码,实现提到了 AbstractQueuedSynchronizer,即 AQS……
单独了解一下吧.
ReentrantReadWriteLock
具体谈谈……
锁的类型
参考下面锁的相关概念.
Synchronized 是啥子锁
- 悲观锁
- 非公平锁
- 不可中断锁.
- 可重入锁 —. 是隐式的,获取了锁,直接再用就是了.
阻塞算法
ReentrantLock
- 独享锁
- 互斥锁
- 默认情况下是非公平锁,但是可以设置为公平锁.
- 可重入锁 --> 调用了 lock() 方法获取到锁的线程,可以再次调用 lock() 获取锁而不被阻塞.
- 不可中断锁.
- Lock 是可中断锁.使用 ockInterruptibly().
ReadWriteLock
- 读锁是共享锁,其写锁是独享锁。
- 读写锁
- 默认情况下是非公平锁,但是可以设置为公平锁.
java.util.concurrent.atomic 包下的原子类
- 乐观锁
- 建立在CAS之上的
- 非阻塞算法
锁的相关概念
从 锁的状态,锁的特性,锁的设计 有各式各样的锁……
乐观锁/悲观锁
从并发同步的角度来看,锁 在宏观上分为两种,乐观锁,悲观锁.
synchronized 是悲观锁。
在 JDK1.5 中新增 java.util.concurrent (J.U.C) 就是建立在 CAS 之上的.相对于 synchronized 这种阻塞算法,CAS 是非阻塞算法的一种常见实现.所以 J.U.C 在性能上有了很大的提升.
juc 是乐观锁
再看一下 atomic 类
悲观锁
假设最坏情况,每次都会修改数据,故没有线程访问就上锁.
“操作前先上锁.”
用于
传统关系型数据库,行锁,表锁,读锁,写锁……
Java, Synchronized,ReentrantLock
乐观锁
取数据,不上锁
更新数据,会检查有没有被别人更新.
用于
多读类型应用 --> 提高吞吐量
如 数据库 --> write_condition 机制
Java --> java.util.concurrent.atomic 包下的原子类 --> 用 CAS 实现
实现方式
版本号机制
就是加个版本号,每次修改操作,版本号相应变动.(如自增)
别的线程执行修改时,发现版本号不一致,就修改失败……
CAS 算法
CAS 算法可以单独提出来
即 compare and swap (比较与交换)
是一种 无锁算法 --> 不使用 锁 实现多线程之间的变量同步.
在没有线程被阻塞的情况下实现变量的同步 也称 --> 非阻塞同步(Non-blocking Synchronization)
涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
我认为 V 的值应该为 A,如果是,那么将 V 的值更新为 B,否则不修改并告诉 V 的值实际为多少.
1 | int compare_and_swap (int* reg, int oldval, int newval) |
一般情况是是一个 自旋操作 即不断地重试……
缺点
ABA 问题
如果一个值原来为 A 变化为 B 又变化为 A
看起来没变过 实际变了
解决办法 : 用版本号
循环时间长开销大
增加CPU的开销
只能保证一个共享变量的原子操作
为什么?
硬件实现的
因为它本身就只是一个锁住总线的原子交换操作
两个 CAS 操作之间并不能保证没有重入现象
只能保证一个共享变量的原子操作。当对多个共享变量操作时,CAS就无法保证操作的原子性,这时就可以用锁,或者把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
自旋锁
广泛应用的底层同步机制
- 许多情况下,共享资源的锁定状态持续时间较短,切换线程不值得
- 通过让线程执行忙循环等待锁的释放,不让出 CPU.
- 不过若锁被其它线程长时间占用,会带来许多性能上的开销.
- 因此自旋应当设置一个超时判断.
当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。
java.util.concurrent.atomic 包下的原子类就是自旋锁.CAS 就是自旋锁的一种是实现方式.
独享锁/共享锁
**独享锁:**是指该锁一次只能被一个线程所持有。
**共享锁:**是指该锁可被多个线程所持有。
Java ReentrantLock 是独享锁.
对于 Lock 的另一个实现类 ReadWriteLock,其读锁是共享锁,其写锁是独享锁.
读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的.
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享.
互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
- 互斥锁在Java中的具体实现就是
ReentrantLock
- 读写锁在Java中的具体实现就是
ReadWriteLock
读写锁将对一个资源的访问分成了2个锁,一个读锁,一个写锁.
同一时刻允许多个线程进行读访问.
但是如果有一个写访问线程,那么其它的读访问,写访问都被阻塞.
公平锁/非公平锁
公平锁
公平锁即尽量以请求锁的顺序来获取锁.
比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁.
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的.这样就可能导致某个或者一些线程永远获取不到锁.
在 Java 中,synchronized 就是非公平锁,它无法保证等待的线程获取锁的顺序.
而对于 ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁.
区分
- **公平锁:**是指多个线程按照申请锁的顺序来获取锁。
- **非公平锁:**是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
公平锁即尽量以请求锁的顺序来获取锁.
比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁,这种就是公平锁.
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的.这样就可能导致某个或者一些线程永远获取不到锁.
在 Java 中,synchronized 就是非公平锁,它无法保证等待的线程获取锁的顺序.
而对于 ReentrantLock 和 ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁.
可重入锁
具备可重入性就是可重入锁.
可重入性上面也提到了,即 支持一个线程对资源的重复加锁.
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
按上面提到的,synchronized 和 ReentrantLock 都是可重入锁.
不过 synchronized 是隐式的,获取了锁,直接再用就是了.
但 ReentrantLock 是 : 调用了 lock() 方法获取到锁的线程,可以再次调用 lock() 获取锁而不被阻塞.
读写锁
读写锁将对一个资源的访问分成了2个锁,一个读锁,一个写锁.
同一时刻允许多个线程进行读访问.
但是如果有一个写访问线程,那么其它的读访问,写访问都被阻塞.
可中断锁
顾名思义,就是可以中断的锁.
Java 中,synchronized 就是不可中断锁.
Lock 是可中断锁.使用 ockInterruptibly().
偏向锁/轻量级锁/重量级锁
指锁的状态
针对 Synchronized
分段锁
一种锁的设计.
把一块数据分成多个段,每个段用一把锁维护.
jdk 1.8 前 ConcurrentHashMap 所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问.
synchronized 升级后的三种锁
来自 : https://www.cnblogs.com/wade-luffy/p/5969418.html
锁的优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU | 追求响应时间,锁占用时间很短 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,锁占用时间较长 |
具体实现是通过操作 Java 对象头里的 Mark Word……
下面抄自上面链接,没有深入理解……
偏向锁
Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
轻量级锁
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁
重量级锁
重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。
并发编程的三个概念
原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性
即程序执行的顺序按照代码的先后顺序执行。
这个需要与 指令重排序 联系……
指令重排序
处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
Java 与 CAS
CAS 是乐观锁的一种实现机制
Compare And Swap的缩写,翻译过来就是比较并替换
Java 哪里用到了 CAS ?
atomic 系列类
Lock 系列
jdk1.6 以上的版本,synchronized 转变为重量级锁之前,也会采用 cas 机制.
JVM 对锁的优化
锁消除
JIT 编译时,对运行上下文进行扫描,去除不可能存在竞争的锁.
比如在没有竞争的情况下调用 StringBuffer 的 append(),JVM 会消除 append() 方法上的 synchronized 的影响.
锁粗化
通过扩大加锁范围,避免反复加锁解锁.
其他
- [ ] AQS
- [ ] happens-before
- [ ] 继续深入
- [ ] 源码
- [ ] 与操作系统联系