Synchronized是什么?
- synchronized 关键字可以在方法、对象、类上加锁,当被他锁定了后有且只有一个线程能够执行访问临界区的数据,而其他的线程必须要等待获取到锁的线程释放锁之后才能去访问临界区的数据,以保证临界区的数据不会因为多个线程的读写操作造成数据问题。
Synchronized的三种方式
- 普通同步方法:锁的是当前的实例对象,进入同步代码前都需要获取当前的实例对象
- 静态同步方法:锁的是当前的class类,进入同步代码前都需要获取当前类对象
- 同步代码块:锁的当前代码块设置的对象,给自定义的对象加锁,所有需要进入同步代码的线程都要获取该自定义对象。
对象在内存中的结构
- 我们在理解Synchronized之前先说明一下关于对象在虚拟中的内存结构:
- 我们看到对象在内存中被拆分为对象头、实例数据和对齐填充三个部分,我们就想看看其分别是什么的
- 对象头:
- markWord(标记字段):用于存储对象自身的运行时数据,包括hashcode、gc分代年龄 、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;因为markWord需要在极小的空间内存储尽量多的信息,所以它会根据对象的状态复用自己的存储空间,下面就是各状态下的存储结构。
- class对象指针:指向该对象的字节码对象,也就是元对象的引用地址,以此来判断该对象是那个类的实例对象。
- 当对象为数组的时候,对象头会增加4字节的存储空间,用来存储数组的length,因为jvm需要获取对象的大小时都可以通过元数据来确定java对象的大小,但时无法从数组的元数据来确认数组的大小,所以需要一块区域单独记录数据的大小。
- markWord(标记字段):用于存储对象自身的运行时数据,包括hashcode、gc分代年龄 、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;因为markWord需要在极小的空间内存储尽量多的信息,所以它会根据对象的状态复用自己的存储空间,下面就是各状态下的存储结构。
- 实例数据:里面包括了对象的成员变量,其大小是根据成员变量的大小来决定。
- 对齐填充:该部分的出现是因为Hotspot虚拟机要求存储的对象的起始地址必须为8字节的整数倍,换句话说就是要求整个对象所占用的字节数能被8整除,当不能整除时,就需要补充字节数以达到被8整除的目的。
- 对象头:
Synchronized的原理
- 锁优化:上面我们查看了关于对象在内存中的结构,并且在对象头的markword中包含了对轻量级锁、重量级锁和偏向锁的标志位信息。而轻量级锁和偏向锁则是jdk1.6时为了优化Synchronized而增加的。
- 偏向锁:关于偏向锁我们看了上面markword的结构后知道了当为偏向锁时会保留一个线程的ID,因为偏向锁的实现需要通过线程ID作为实现,当锁对象第一次被对象访问时会保留该对象线程ID,之后该线程再次访问该对象的时候就不用再进行加锁和解锁了,而是通过查看对象头中的markword中是否存储这个当前线程的ID即可。
- 轻量级锁:当有竞争但不激烈的时候,jvm会将偏向锁升级为轻量级的锁,考虑到线程的阻塞与唤醒需要CPU从用户态转换为核心态,所以轻量级的锁不会让线程处于阻塞状态只会让线程不断的自旋尝试获取锁,此情况考虑的是没有多个线程同时竞争的情况下。
- 重量级锁:自旋锁,但是该自旋锁非普通的自旋锁,而是自适应自旋锁。当有多个线程同时对同步代码代码进行竞争时就会从轻量级锁升级为重量级锁,重量级锁获取锁如果没有获取到锁,不会马上进入阻塞状态,因为线程每一次从阻塞到唤醒都需要消耗资源,所以通过让当前线程自旋的获取锁,如果在一定时间内还是没有获取到锁对象才会进入到阻塞状态。但是对于某个同步代码有多个线程自旋获取锁都没有成功的话就不会在进行自旋,而是直接进入阻塞状态,减少资源开销。
- Moniter
- 我们看了java对象的内存模型后可知当为轻量级锁和重量级锁的时候其markword的内部结构除了锁标志位外剩下的就是一个执行锁的引用。
- 每一个Object对象中都内置了一个Moniter对象,每个Moniter其实就是一把锁,在Hatspot虚拟机实现指令重排的时候发现当前有使用synchroized关键字时会调用内置对象Moniter的指令monitorenter和monitorexit来获取锁和释放锁。
- ObjectMonitor监视器,因为jdk不开源Hotspot虚拟机的源码,所以我们不能从下载的jdk查看其源码,而在社区版中是可以的,所以有兴趣的童鞋可以去看看openjdk
- 这篇文章可以直达源码解释:https://blog.youkuaiyun.com/qq_33249725/article/details/104212364
- 我们知道了ObjectMonitor内部中最重要的是关于_WaitSet和_EntryList这两个属性,当多个线程出现竞争阻塞时,线程就会将线程包装成ObjectWaiter对象就会放入_EntryList中,等待获取锁的ObjectWaiter释放锁后再竞争锁。而_WaitSet则是结合Object的wait、notify、notifyAll三个方法使用的,所以现在知道为什么这三个方法为什么要放到Object类中了吧,因为Object是所有类的基类,而我们提到每个对象都有一个Moniter对象;每一次调用wait方法都会讲当前线程的包装成ObjectWaiter对象放入等待对列中;而获取了锁的对象会有_owner指向该对象,还有的就是synchroized也是一个重入锁,由 _recursions管理,每一次获取锁对象_recursions都会自增1,跟AQS的state是一个道理。是不是其实感觉和reentrantLock很相像。
- 这篇文章可以直达源码解释:https://blog.youkuaiyun.com/qq_33249725/article/details/104212364