一, Synchronized 的概念
是利用锁的机制来实现同步的.
锁机制有如下两种特性:
互斥性: 即在同一时间只允许一个线程持有某个对象锁, 通过这种特性来实现多线程中的协调机制, 这样在同一时间只有一个线程对需同步的代码块 (复合操作) 进行访问. 互斥性我们也往往称为操作的原子性.
可见性: 必须确保在锁被释放之前, 对共享变量所做的修改, 对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值), 否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致.
二, Synchronized 的使用
修饰静态方法
- // 修饰静态方法
- public static synchronized void print() {
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName()+"runing");
- }
修饰非静态方法
- // 修饰非静态方法
- public synchronized void print1() {
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName()+"runing");
- }
修饰代码块
- // 代码块的使用
- public void prnit2() {
- synchronized (this) {
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName()+"runing");
- }
- }
- // 代码块的使用
- public void prnit3() {
- synchronized (SynchroTest.class) {
- try {
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName()+"runing");
- }
- }
疑问: 修饰代码块的时候, Synchronized 参数对象起到了什么作用?
答: 当 JVM 是用 ClassLoader 加载字节码的时候, 会在方法区创建一个对象, 同时也会在堆去创建一个 Class(注意是大写的). 使用 Synchronized.xxx.class 就是说明所有 Class 对应的对象都使用共同一个锁(比较抽象). 这个 Class 一定是一个 final 的类. 详情请看原理分析 JVM 指令分析的 Monitorenter,Monitorexit.JVM 会将对象与 Monitor 相关联.
java 中每一个对象都会有一个 monitor 对象(监视器). 它的作用就是给对象加锁用的.
某一线程占有这个对象的时候, 先看 monitor 的计数器是不是为 0, 如果是 0 说明还没有线程占有, 此时会将 monitor 的计数器 + 1. 如果不为 0, 表示有其他线程占有这个对象, 需要等待. 当占有这个对象的线程释放这个对象的时候, 那么此时会将这个对象的 monitor-1(并不是 monitor=0, 而是 - 1). 类似于 CPU 中的 cache line 作用.
三, Synchronized 的原理分析
线程堆栈分析(排他性)
- public static void main(String[] args) throws InterruptedException {
- final SynchroTest tSynchroTest1 = new SynchroTest();
- for (int i = 10; i <15; i++) {
- new Thread(tSynchroTest1::print1).start();
- }
- }
- // 修饰非静态方法
- public synchronized void print1() {
- try {
- TimeUnit.MINUTES.sleep(2);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName() + "runing");
- }
根据以上代码进行分析.
jconsole 分析, 可以看出当前拥有对象锁的 Thread-3 因为线程睡眠, 所以状态为 TIMED_WAITING(等待状态). 看 Thread-2 可以看出总阻止数为 1, 也就是 BLOCKED 状态(上锁状态).
jstack pid 分析也可以看到 dos 命令窗口中的当前用对象线程是 TIME_WAITING, 阻塞线程是 BLOCKED
JVM 的指令分析
javap(反编译)使用 cmd, 找到 calss 文件路径 D:\workspace1\GuaHao\bin\BlockQueueTest>javap -v SynchroTest. 代码块的一个加锁方式, 是使用 Monitorenter,Monitorexit 配合使用
细心的朋友上图会看到 51 有一个 monitorexit 和 56 还有一个 monitorexit? 为什么会有两个互斥出口
答: 51 行是正常出口, 56 是一个异常出口. 当程序发生运行时异常的时候会从 56monitorexit 跳出.
2. 方法加锁方式, 是使用, 配合使用
对象与 monitor: 一个实例对象包含了对象头, 实例变量, 填充数据.
对象头: 加锁的一个基础.
实例变量: 私有变量, 属性变量信息
填充数据: 对象的启示地址, 8 字节表示.
对象头
JDK6 之前 Synchronized 是重量锁, 比较暴力.
JDK6 之后 添加了
偏向锁: 再对象第一次被某一线程占有的时候, 是否偏向锁置 1, 锁表 01, 写入线程号; 当其他的线程访问的时候, 就会竞争, 结果分别是失败和成功. 很多次被第一线程占有它的线程获得次数比较多此时就是偏向锁. 相当于找对象(女朋友), 当这个对象与第一个男人分手后, 找到第二个男人之后, 可能还会去找前男友, 这就是属于偏向锁. 竞争不激烈的时候使用. 偏向锁与无所状态时间很接近. CAS 算法 campare and set(留个小作业, 什么叫 CAS 算法?)
轻量级锁: 线程有交替适用, 互斥性不是很强, 使用 CAS 算法不在使用, 锁标志 00.
重量级锁: 强互斥, 锁标记 10, 等待时间长.
他们的区别是时间上的区别.
用户线程和核心线程相互转换(非常耗时)
由偏向锁转轻量级锁在转重量级锁的时候存在不可逆行为, 只能低转高, 不可反之.
自旋锁: 再转换过程中还存在一个叫做自旋锁的情况, 再轻量级装重量级的时候, 并不是马上从用户线程转成核心线程, 再互斥的情况下会执行几 次空循环, 如果空循环完成后再没有拿到占有权, 则才会进行转换.
消除锁: JIT(编译)的时候如果定义一个 int i=0, 并对这个定义进行加锁, 那么由于上篇文章说道了, 定义 int i=0, 是存在原子性的, 所以这个这 个锁会在编译期间自动消除, 防止 JVM 产生不必要的资源浪费
CAS 算法: 引用 GA 特大的 Java VAS 原理深度分析文章 https://blog.csdn.net/tanga842428/article/details/52742698
欢迎大家来提问, 如果有不理解的可以再评论区提出, 我会定时解惑.
同舟共济, 新海同航
来源: http://www.bubuko.com/infodetail-2992080.html