我们来看下对象的内存布局
Object o=new Object();
Object在方法区;
o在栈里;
new Object在堆;
在HotSpot虚拟机里,对象在堆内存中的存储布局,可以划分为三个部分:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)(保证8个字节的倍数);
对象头里面包含对象标记(Mark Word),类元信息(又叫类型指针);
对象标记里面包含哈希码,GC标记,GC次数,同步锁标记,偏向锁持有者;
类型指针指向方法区的类元信息Klass
如果只有class A没有数据,那么就是只有一个对象头的实例对象;
class A{
}
如果class A有数据,那么不但有对象头,还有实例数据
class A{
int age;
}
实例数据:存放类的属性(Field)数据信息,包括父类的属性信息;
对齐填充
虚拟机要求对象起始地址位置必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐这部分内存按8字节补充对齐;
一句话就是,这个字段如果不是8个字节的,给他补齐。
我们在代码加入
<!--分析对象在jvm的大小和布局-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
public static void main(String[] args) throws Exception {
//对象的详细信息
System.out.println(VM.current().details());
System.out.println("------------------");
// Objects are 8 bytes aligned. 对象是8字节对齐的。
System.out.println(VM.current().objectAlignment());
}
可以看到分析出来的字节信息
![](https://i-blog.csdnimg.cn/blog_migrate/2cc8569a7951035c7a28bb15284aff57.png)
Object o=new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
我们可以看到loss due to the next object alignment 下一次对象对齐造成的损失,就是不够,补齐
![](https://i-blog.csdnimg.cn/blog_migrate/fe4bb585341b9e1b280979b2e690d1ce.png)
在看下面的代码
class A{
char age;
double num;
}
public class Producer {
public static void main(String[] args) throws Exception {
A o=new A();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
alignment/padding gap 对齐/填充间隙
![](https://i-blog.csdnimg.cn/blog_migrate/f5f998681fd394eec18a0999c9ed15ce.png)
接下来我们来看下Synchronized与锁升级
谈谈你对Synchronized的理解?
在高并发时,能用无锁数据结构,就不要用锁,能锁代码块,就不要锁整个方法,能用对象锁,就不要使用类锁;
Synchronized的锁升级你聊聊?
用锁能够实现数据的安全性,但是会带来性能下降,无锁能够基于线程并行提升程序性能,但是会带来安全性下降;
锁的升级过程:无锁-》偏向锁-》轻量级锁-》重量级锁;
Synchronized锁:由对象头中的Mark word根据锁标志位的不同而被复用及锁升级策略;
无锁标志位: 0 0 1
偏向锁标志位:1 0 1
轻量级锁 标志位:0 0
重量级锁 标志位: 1 0
偏向锁:Mark word存储的是偏向的线程id;
轻量级锁:Mark word存储的是指向线程栈中Lock Record的指针;
重量级锁: Mark word 存储的是指向堆中的monitor对象的指针;
我们可以看到在无锁的状态下,对象头Mark word标记的就是001
![](https://i-blog.csdnimg.cn/blog_migrate/52979e002757e2452bee36eda141f18f.png)
当执行hashcode的时候下面的部分发生了变化,也就是hashcode存在对象头中
![](https://i-blog.csdnimg.cn/blog_migrate/b23d3e152edc0148e476d8e011eef5e2.png)
程序不会有锁的竞争
无锁:初始状态,一个对象被实例化后,如果还没有被任何线程竞争锁,那么他就为无锁状态(001);
偏向锁:单线程竞争
当线程A第一次竞争到锁时,通过操作修改Mark word中的偏向线程id,偏向模式。
如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步;
当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后序访问时便会自动获得锁;
锁总是同一个线程持有,很少发送竞争,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程;
说白了就是一个线程抢到的资源比其他线程抢到的资源多,就是偏向锁。
在Git bash里面可以看到偏向锁的信息
java -XX:+PrintFlagsInitial |grep BiasedLock*
![](https://i-blog.csdnimg.cn/blog_migrate/62ba1a0a4e4f5e7f81d6033dcd48b61a.png)
UseBiasedLocking=true 就是开启偏向锁
BiasedLockingStartupDelay=0 就是程序在启动的时候立刻启动,没有延迟
我们在代码看到,当前是轻量级锁 0 0
![](https://i-blog.csdnimg.cn/blog_migrate/7a0a8ec59648fa07103e501e799fcf5e.png)
那么我们在vm中设置一下-XX:BiasedLockingStartupDelay=0 ,再次启动
![](https://i-blog.csdnimg.cn/blog_migrate/f5697f05d8928e2cfd593abc575f89f9.png)
可以看到变成了101 偏向锁
![](https://i-blog.csdnimg.cn/blog_migrate/3b0baa34aff9e2b7f8d7bf902f0e26b1.png)
我们也可以在代码中加入延迟时间5秒,不设置vm参数,他的默认时间是4秒,所以我们要超过4秒
才会变成偏向锁1 0 1
注意,要在对象new之前加sleep
![](https://i-blog.csdnimg.cn/blog_migrate/25121825f51fe7d96d195321c6526d66.png)
我们在看下不加锁,加了延迟5秒时间
可以看到,锁状态为101是偏向锁状态了,只是由于o对象未用synchronized加锁,所以线程id是空的,其余数据跟上述无锁状态一样
![](https://i-blog.csdnimg.cn/blog_migrate/08307b084002442f5940b9de17e4304e.png)
偏向锁带线程id的情况,第一行中后面不再是0了,有了线程id的值
![](https://i-blog.csdnimg.cn/blog_migrate/93ab93f33ef5d485f483ea2f52113703.png)
偏向锁的撤销
偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销;
撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行;
1.第一个线程正在执行sync方法处于同步块,他还没有执行完,其他线程来抢夺,该偏向锁会被取消掉并出现锁升级;
此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得改轻量级锁。
2.第一个线程执行完成sync方法退出同步块,则将对象头设置成无锁状态并撤销偏向锁,重新偏向;
一句话说明:如果A线程正在运行,B线程进来了,同时争抢资源,锁升级为轻量级锁,如果A退出了,那么先变成无锁状态,然后再重新成为偏向锁
轻量级锁:多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在竞争太过激烈的情况,也就没有线程阻塞;
轻量级锁的主要作用
有线程来参与锁的竞争,但是获取锁的冲突时间极短,本质就是自旋锁CAS
当轻量级的自旋锁达到了一定的自旋次数还没有成功,那么升级为重量级锁
可以看到,在多个线程抢夺一个对象的时候,变成了重量级锁1 0
![](https://i-blog.csdnimg.cn/blog_migrate/688c474dbd4879ee410b22c20f9c0630.png)
当调用了hashcode立马就会从偏向锁变成轻量级锁0 0
![](https://i-blog.csdnimg.cn/blog_migrate/251e2a370d6f51704c74e0fb06adccbf.png)
在同步代码块中写hashcode方法,会升级为重量级锁
![](https://i-blog.csdnimg.cn/blog_migrate/96fca1ff89d195d24989de9ea8d5bcba.png)
当没有加锁的时候,就是无锁状态,当加了锁之后,如果开启5秒延迟就是偏向锁。
如果存在另一个线程进来抢占资源,那么升级为轻量级锁,当轻量级自旋次数达到上限之后,
升级为重量级锁,这就是锁升级的过程。
接下来我们来看下AQS
AQS 是抽象的队列同步器 AbstractQueuedSynchronizer的简写
和AQS有关的类
ReentrantLock;
CountDownLatch;
ReentrantReadWriteLock;
Semaphore;
可以看到ReentrantLock里面有一个Sync类继承了AQS
![](https://i-blog.csdnimg.cn/blog_migrate/c70fa042ad6d50d3443b1b5165650ffa.png)
![](https://i-blog.csdnimg.cn/blog_migrate/576d1e503b334660da10baa935097af5.png)
![](https://i-blog.csdnimg.cn/blog_migrate/3b4c5fad4aa0ef7bd57fab6ac6f0bcb6.png)
![](https://i-blog.csdnimg.cn/blog_migrate/cd03807b2135fbf6e4c2d1f88db7a00a.png)
可以看到上面的类里面都有一个抽象的类Sync继承了AQS
在ReentrantLock,CountDownLatch,ReentrantReadWriteLock,Semaphore的源码中,
都有一个抽象的类Sync继承了AbstractQueuedSynchronizer,简称(AQS)抽象队列同步器,
加锁的时候会抢占资源,抢到资源的往下进行,抢不到资源的就会阻塞,阻塞就会排队,
排队那么就会进入我们的AQS,AQS底层的数据结构就是Node链表,在Node排队;
AQS的state 0是空闲,大于0就是有人在使用;
通过自旋等待;
state变量判断是否阻塞;
从尾部入队;
从头部出队;
我们来看下非公平锁
new ReentrantLock();
![](https://i-blog.csdnimg.cn/blog_migrate/f64034e12e25f8a5158043df185a62e1.png)
可以sync对应的就是Sync类
![](https://i-blog.csdnimg.cn/blog_migrate/f1fcca9cb49e1b772d4f198018283c7f.png)
我们可以看到加锁的方法对应的也是sync
![](https://i-blog.csdnimg.cn/blog_migrate/a49de8fceace11fad0299fca69501cea.png)
可以看到我们实现的lock锁都是由sycn的lock来实现的
![](https://i-blog.csdnimg.cn/blog_migrate/ca225d7fad503aacb9c038e8b338be9b.png)
可以看到公平锁的lock也是Sync的lock
![](https://i-blog.csdnimg.cn/blog_migrate/f05960795f8d90cc357ed6b8227a53e0.png)
不管是公平锁还是非公平锁都会进入acquire方法
![](https://i-blog.csdnimg.cn/blog_migrate/2592068e891466ec8749dfde8802f676.png)
在公平锁里面就是多了一个hasQueuedPredecessors方法
具有排队的前置任务
![](https://i-blog.csdnimg.cn/blog_migrate/1e882d9c25c2bffed8a1f5686329de30.png)
如果为true,那么说明前面有排队的,如果为false说明前面没有排队的
公平锁加锁时判断等待队列中是否存在有效节点的方法
![](https://i-blog.csdnimg.cn/blog_migrate/901caf723b6a78b6b54772468e67550a.png)
接下来我们来看下ReentrantReadWriteLock 读写锁
读写锁定位为:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程;
我们来看下,在可重入锁的场景下
class A{
Lock lock=new ReentrantLock();
public void write(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+",开始写入");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",写完了");
}finally {
lock.unlock();
}
}
public void read(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+",开始读取");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",读完了");
}finally {
lock.unlock();
}
}
}
public class Producer {
public static void main(String[] args) throws Exception {
A a=new A();
for (int i = 0; i <5 ; i++) {
new Thread(()->{
a.write();
},String.valueOf(i)).start();
}
for (int i = 0; i <5 ; i++) {
new Thread(()->{
a.read();
},String.valueOf(i)).start();
}
}
}
可以看到重入锁场景下,写锁执行完了,读锁才开始读取,做不到读读共享
就是线程0读完了,线程1才能读
![](https://i-blog.csdnimg.cn/blog_migrate/f616c122203b5d4a24417bfb1296004a.png)
我们来看下读写锁的代码
class A{
// Lock lock=new ReentrantLock();
ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
public void write(){
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+",开始写入");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",写完了");
}finally {
lock.writeLock().unlock();
}
}
public void read(){
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+",开始读取");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",读完了");
}finally {
lock.readLock().unlock();
}
}
}
public class Producer {
public static void main(String[] args) throws Exception {
A a=new A();
for (int i = 0; i <5 ; i++) {
new Thread(()->{
a.write();
},String.valueOf(i)).start();
}
for (int i = 0; i <5 ; i++) {
new Thread(()->{
a.read();
},String.valueOf(i)).start();
}
Thread.sleep(1000);
//再次写入
for (int i = 0; i <3 ; i++) {
new Thread(()->{
a.write();
},String.valueOf(i)).start();
}
}
}
可以看到读读共享了,就是0,1,2,3,4线程同时读取了
![](https://i-blog.csdnimg.cn/blog_migrate/778101b9e0ce9959c65777bfc97563be.png)
写锁饥饿问题 就是读锁的次数比较多,写锁的次数比较少;
在写锁之间 我们还可以获取到读锁,我们来看下代码
public class Producer {
public static void main(String[] args) throws Exception {
ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
readWriteLock.writeLock().lock();
System.out.println("写锁");
readWriteLock.readLock().lock();
System.out.println("读锁");
readWriteLock.writeLock().unlock();
readWriteLock.readLock().unlock();
}
}
在写锁里面 包含这读锁,在写锁外面 释放读锁,这就是锁降级 降级为读锁
也叫做写后读
![](https://i-blog.csdnimg.cn/blog_migrate/83bcf2ecce84b1480f72d355dc0ac252.png)
我们把读锁放在写锁上面
public static void main(String[] args) throws Exception {
ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
readWriteLock.readLock().lock();
System.out.println("读锁");
readWriteLock.writeLock().lock();
System.out.println("写锁");
readWriteLock.writeLock().unlock();
readWriteLock.readLock().unlock();
}
可以看到读锁升级写锁的时候,卡死了,所以在读锁没有读完之前,不能进行写锁
![](https://i-blog.csdnimg.cn/blog_migrate/d02c4714c0101bf320be536f194ad5b2.png)
有没有比读写锁更快的锁?
邮戳锁StampedLock,也叫票据锁,是对读写锁的优化;
stamp(戳记,long类型)
代表了锁的状态,当stamp返回零时,表示线程获取锁失败,并且当释放锁或者转换锁的时候,都要传入最初获取的stamp值;
所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功;
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁);
StampedLock有三种访问模式:
1.Reading(读模式悲观):功能和读写锁的读锁类似;
2.Writing(写模式):功能和读写锁的写锁类似;
3.Optimistic reading(乐观读模式):无锁机制,类似数据库中的乐观锁,支持读写并发,很乐观的认为读取时没人修改,假如被修改再实现升级为悲观读模式;
接下来我们来看下StampedLock的代码,操作读写锁
class A{
//邮戳锁 票据锁
StampedLock stampedLock=new StampedLock();
public void write(){
//写锁 stamp戳记 当stamp返回0时,表示线程获取锁失败
long stamp=stampedLock.writeLock();
try {
System.out.println(Thread.currentThread().getName()+",开始写入");
System.out.println(Thread.currentThread().getName()+",写完了");
}finally {
//释放写锁 当释放写锁的时候都要传入最初的戳记
stampedLock.unlockWrite(stamp);
}
}
public void read(){
long stamp=stampedLock.readLock();
try {
System.out.println(Thread.currentThread().getName()+",开始读取");
for (int i = 0; i <4 ; i++) {
try {
//阻塞1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",正在读取中");
}
System.out.println(Thread.currentThread().getName()+",读完了");
}finally {
stampedLock.unlockRead(stamp);
}
}
}
public class Producer {
public static void main(String[] args) throws Exception {
A a=new A();
new Thread(()->{
a.read();
},"t1").start();
new Thread(()->{
a.write();
},"t2").start();
}
}
可以看到,读完了才能写锁
![](https://i-blog.csdnimg.cn/blog_migrate/9d9c590e222ad8fe4928c5ec1fd05df3.png)
我们在来看下邮戳锁的乐观读模式
在读锁的情况下,也能进行写锁
validate
如果自发出给定标记后未完全获取锁,则返回true,如果标记为0,则始终返回false
如果图章代表当前持有的锁,则始终返回true.
返回true,就代表没有修改
tryOptimisticRead 尝试乐观读
一个有效的乐观读取标记,如果是完全锁定则为0;
class A{
//邮戳锁 票据锁
StampedLock stampedLock=new StampedLock();
int num=0;
public void write(){
//写锁 stamp戳记 当stamp返回0时,表示线程获取锁失败
long stamp=stampedLock.writeLock();
try {
System.out.println(Thread.currentThread().getName()+",开始写入");
num=10;
System.out.println(Thread.currentThread().getName()+",写完了");
}finally {
//释放写锁 当释放写锁的时候都要传入最初的戳记
stampedLock.unlockWrite(stamp);
}
}
public void read(){
long stamp=stampedLock.readLock();
try {
System.out.println(Thread.currentThread().getName()+",开始读取");
for (int i = 0; i <4 ; i++) {
try {
//阻塞1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",正在读取中");
}
System.out.println(Thread.currentThread().getName()+",读完了");
}finally {
stampedLock.unlockRead(stamp);
}
}
//乐观读
public void aa(){
long stamp=stampedLock.tryOptimisticRead();
for (int i = 0; i < 4; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+",正在读取,是否有人修改过:"+stampedLock.validate(stamp));
}
if(!stampedLock.validate(stamp)){
//有人修改过 乐观读 变为悲观读
try {
//重新赋值
stamp=stampedLock.readLock();
System.out.println("结果为:"+num);
}finally {
stampedLock.unlockRead(stamp);
}
}
System.out.println("读完毕结果为:"+num);
}
}
public class Producer {
public static void main(String[] args) throws Exception {
A a=new A();
new Thread(()->{
a.aa();
},"t1").start();
Thread.sleep(3000);
new Thread(()->{
System.out.println("写进入");
a.write();
},"t2").start();
}
}
可以看到t1在读取的时候,t2也能写入了
![](https://i-blog.csdnimg.cn/blog_migrate/99705431401f8bc06cf554759c543ab5.png)
StampedLock不支持重入;
StampedLock的悲观读锁和写锁都不支持条件变量Condition;
使用StampedLock一定不要调用中断操作,不要调用interrupt方法;