之前说过hashMap,我们知道hashMap是一种非线程安全的集合,主要原因是它在多线程的情况下,插入、删除、扩容的时候容易导致数据丢失或者链表环

那我们也知道ConcurrentHashMap、hashTable是线程安全的,我们看hashTable的源码时,会发现源码中很多方法都是加了synchronized 关键字的 那我们今天就来分析下synchronized 关键字的使用场景及如何起作用。

使用场景:

synchronized关键字最主要有以下3种应用方式

1.修饰普通方法,作用于当前实例加锁,进入同步方法前要获得当前实例的锁

public class SynchronizedTest implements Runnable{

    static int i =0;

    public synchronized void increase(){
System.out.println(i++);
System.out.println("进入增加方法:"+Thread.currentThread().getName());
} @Override
public void run() {
     System.out.println(Thread.currentThread().getName()+"在执行");
for(int j=0;j<10;j++){
System.out.println("即将进入增加方法:"+Thread.currentThread().getName());
increase();
}
} public static void main(String[] args) {
SynchronizedTest test1 = new SynchronizedTest();
Thread thread1 = new Thread(test1);
Thread thread2 = new Thread(test1);
Thread thread3 = new Thread(test1);
Thread thread4 = new Thread(test1); thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
  //结果为线程名称乱序
}

也就是说如果两个线程,线程A  和线程B 同时执行 对于同一个对象而言,能够同时进入非 synchronized声明的方法,但是如果这个方法中调用了 synchronized声明的方法,则将会进行线程抢占,同一时间只有一个线程能够拿到进入这个方法的锁。

这里注意:它的锁的范围是这个对象中的这个方法。

2.修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

public class SynchronizedTest implements Runnable{

    static int i =0;

    public static synchronized void increase(){
System.out.println(i++);
System.out.println("进入增加方法:"+Thread.currentThread().getName());
} @Override
public void run() {
System.out.println(Thread.currentThread().getName()+"在执行");
for(int j=0;j<10;j++){
System.out.println("即将进入增加方法:"+Thread.currentThread().getName());
increase();
}
} public static void main(String[] args) {
SynchronizedTest test1 = new SynchronizedTest();
Thread thread1 = new Thread(test1);
Thread thread2 = new Thread(test1);
Thread thread3 = new Thread(test1);
Thread thread4 = new Thread(test1); thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}

注意:该锁对象是Class实例,因为静态方法存在于永久代,因此静态方法锁相当于该类的一个全局锁;

3.修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁

public class SynchronizedTest implements Runnable{

    static SynchronizedTest synchronizedTest = new SynchronizedTest();

    static int i =0;

    @Override
public void run() {
System.out.println(Thread.currentThread().getName()+"准备执行");
for(int j=0;j<10;j++){
System.out.println("即将进入增加方法:"+Thread.currentThread().getName());
try {
Thread.sleep(1000L);
} catch (Exception e) { }
synchronized (synchronizedTest){
System.out.println(Thread.currentThread().getName()+"执行中");
i++;
}
}
} public static void main(String[] args) {
Thread thread1 = new Thread(synchronizedTest);
Thread thread2 = new Thread(synchronizedTest);
Thread thread3 = new Thread(synchronizedTest);
Thread thread4 = new Thread(synchronizedTest);
Thread thread5 = new Thread(synchronizedTest);
Thread thread6 = new Thread(synchronizedTest); thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
thread6.start();
} }

这种就是对进入synchronize代码块的对象加锁,也是对象锁。

我们再来看下把这些代码反编译之后出现的指令:

我们可以看到明显的monitorenter和monitorexit命令,还有一些是在方法维度有ACC_SYNCHRONIZED

其实这个关键字是同monitorenter和monitorexit命令是一样的,当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个锁。

我们先说下这两个命令的工作方式:

    1. monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

      1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
      2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
      3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
    2. monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

      monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁

我们先说下monitor,我们认为每个java对象都有一种天生的锁属性,那么这个锁属性是体现在哪里的呢?那我们就来看下在JVM中对象是怎么样的?

(来自于:https://www.jianshu.com/p/e62fa839aa41)

如图:

  java的对象可以分为三个部分:对象头、实例数据、对齐填充

  实例数据 中存储的就是:存放类的属性数据信息,包括父类的属性信息

  对齐信息:由于HotSpot规定对象的大小必须是8的整数倍,而对象头刚好是8的整数倍,如果对象实例数据这部分不是的话,就需要占位符对齐填充。

  最后说下最重要的对象头:对象头包含如下信息

    Mark Word(标记字段):这里包含了很多重要的信息,比如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等 我们把这里分为两块来说,下一次会主要讲这里因为锁状态的变化这里的改变。

    Klass Pointe(类型指针):对象指向它的类元数据的指针,JVM通过这个指针确定该对象是哪个类的实例。

    Array Length(数组长度):只有在对象是个数据的前提下才有这个信息,否则没有。

    • 普通对象包含:Mark Word、Klass Pointer
    • 数组对象包含:Mark Word、Klass Pointer、Array Length

Mark Word 这个部分在32位下是32bit,在64位下是64bit 但是不论其他结构如何,最后两位一定标识的是锁的状态,我们先观察最后这两位:

32位JVM

存储内容(30bit) 锁状态(2bit)
identify_hashcode:25 | age:4 | biased_lock:1 (01)无锁
threadId:23 | age:4 | epoch:2 | biased_lock:1 (01)偏向锁
ptr_to_lock_record:30 (00)轻量级锁
ptr_to_heavyweight_monitor:30 (10)重量级锁
gc_info:30 (11)GC标记

64位JVM

存储内容(62bit) 锁状态(2bit)
unused:25 | identify_hashcode:25 | unused:1 | age:4 | biased_lock:1 (01)无锁
threadId:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 (01)偏向锁
ptr_to_lock_record:62 (00)轻量级锁
ptr_to_heavyweight_monitor:62 (10)重量级锁
gc_info:62 (11)GC标记


当MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。

在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):

ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

  1. 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1
  2. 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒
  3. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁)

同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),

通过上面描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

synchronized底层浅析(一)的更多相关文章

  1. synchronized底层浅析(二)

    一张图了解锁升级流程:

  2. synchronized底层实现学习

    上文我们总结了 synchronized 关键字的基本用法以及作用,并未涉及 synchronized 底层是如何实现的,所谓刨根问底,本文我们就开始 synchronized 原理的探索之旅吧(*& ...

  3. Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  4. 并发-Synchronized底层优化(偏向锁、轻量级锁)

    Synchronized底层优化(偏向锁.轻量级锁) 参考: http://www.cnblogs.com/paddix/p/5405678.html 一.重量级锁 上篇文章中向大家介绍了Synchr ...

  5. 面试官都叫好的Synchronized底层实现,这工资开多少一个月?

    本文为死磕Synchronized底层实现第三篇文章,内容为重量级锁实现. 本系列文章将对HotSpot的synchronized锁实现进行全面分析,内容包括偏向锁.轻量级锁.重量级锁的加锁.解锁.锁 ...

  6. 一文让你读懂Synchronized底层实现,秒杀面试官

    本文为死磕Synchronized底层实现第三篇文章,内容为轻量级锁实现. 轻量级锁并不复杂,其中很多内容在偏向锁一文中已提及过,与本文内容会有部分重叠. 另外轻量级锁的背景和基本流程在概论中已有讲解 ...

  7. 说一下 synchronized 底层实现原理?(未完成)

    说一下 synchronized 底层实现原理?(未完成)

  8. Java多线程和并发(八),synchronized底层原理

    目录 1.对象头(Mark Word) 2.对象自带的锁(Monitor) 3.自旋锁和自适应自旋锁 4.偏向锁 5.轻量级锁 6.偏向锁,轻量级锁,重量级锁联系 八.synchronized底层原理 ...

  9. 死磕synchronized底层实现

    点赞再看,养成习惯,微信搜索[三太子敖丙]第一时间阅读. 本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点.资料以及我的系列文章. 前言 ...

随机推荐

  1. 大一C语言学习笔记(5)---函数篇-定义函数需要了解注意的地方;定义函数的易错点;详细说明函数的每个组合部分的功能及注意事项

    博主学习C语言是通过B站上的<郝斌C语言自学教程>,对于C语言初学者来说,我认为郝斌真的是在全网C语言学习课程中讲的最全面,到位的一个,这个不是真不是博主我吹他哈,大家可以去B站去看看,C ...

  2. Linux服务——二、配置NFS及autofs自动挂载服务

    一.NFS服务配置步骤 NFS的作用:能够使两台虚拟机之间实现文件共享.数据同步 准备:主机名.网络.yum源 Server端: 1.安装nfs-util和rpcbind:(图形化自带) [root@ ...

  3. selenium基本使用,及cannot find chrome binary解决方案

    什么是selenium? Selenium是一个用于Web应用程序测试的工具. Selenium 测试直接运行在浏览器中,就像真正的用户在操作一样. 支持通过各种driver(FirfoxDriver ...

  4. python实现圆检测

    目录: (一)霍夫圆检测原理 (二)代码实现 (一)霍夫圆检测原理 (二)代码实现 1 #霍夫圆检测 2 import cv2 as cv 3 import numpy as np 4 5 def d ...

  5. 如何看待 SAE 在2014 年 3 月 24 日发生的的大面积宕机事故?

    3 月 24 日晚间大约 23 点左右,新浪云 SAE 一处核心机柜掉电,导致 SAE 平台下大量应用无法正常访问,并在 10 小时后才陆续修复.这次事故暴露 SAE 的哪些缺陷?SAE 运维人员又是 ...

  6. Javascript复制内容到剪贴板,解决navigator.clipboard Cannot read property 'writeText' of undefined

    起因 最近帮同事实现了一个小功能--复制文本到剪贴板,主要参考了前端大神阮一峰的博客,根据 navigator.clipboard 返回的 Clipboard 对象的方法 writeText() 写文 ...

  7. [cf461E]Appleman and a Game

    考虑我的每一次添加操作,要满足:1.该串是t的子串:2.该串不能与下一次的串开头字母构成t的子串.那么,设f[i][j][k]表示拼i次,第i次填入的开头字母是j,第i+1填入的开头字母是k的最短长度 ...

  8. 史上最俗的MODBUS介绍

    如今网购正深深地改变着人们的生活,以前买东西要逛商场,先找楼层导购,再逛到相应柜台,接着愉快购物,选好东西后经过一番讨价还价,最后付钱拿货走人,这些都是稀松平常的场景.可是,如果没有实际看见东西,只在 ...

  9. Go语言核心36讲(Go语言实战与应用十三)--学习笔记

    35 | 并发安全字典sync.Map (下) 我们在上一篇文章中谈到了,由于并发安全字典提供的方法涉及的键和值的类型都是interface{},所以我们在调用这些方法的时候,往往还需要对键和值的实际 ...

  10. 洛谷 P6788 - 「EZEC-3」四月樱花(整除分块)

    题面传送门 题意: 求 \[\prod\limits_{x=1}^n\prod\limits_{y|x}\frac{y^{d(y)}}{\prod\limits_{z|y}z+1} \pmod{p} ...