示例一:
public class A {
int num = 0;
public int getNum() {
return num;
}
public void increase() {
num ++;
}
}
public class LockTest {
public static void main(String[] args) throws InterruptedException {
A a = new A();
long start = System.currentTimeMillis();
Thread t1 = new Thread(()->{
for (int i = 0; i < 10000000; i++) {
a.increase();
}
});
t1.start();
for (int i = 0; i < 10000000; i++) {
a.increase();
}
t1.join();
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end-start));
System.out.println(a.getNum());
}
}
打印结果不为20000000:
为自增方法添加synchronized关键字
public class A {
int num = 0;
public int getNum() {
return num;
}
public synchronized void increase() {
num ++;
}
}
或者为自增方法添加同步代码块
public class A {
int num = 0;
public int getNum() {
return num;
}
public void increase() {
synchronized (this) {
num ++;
}
}
}
打印结果为20000000:
在jdk1.6之前,synchronized实现方式为如下图所示:
线程1拿到锁之后,其他线程直接放到等待队列中,直到线程1释放锁
锁:互斥锁,悲观锁,同步锁,重量级锁(线程阻塞,上下文切换,操作系统线程调度)
改进代码,使用AtomicInteger原子操作类:
public class A {
int num = 0;
AtomicInteger atomicInteger = new AtomicInteger();
public int getNum() {
return atomicInteger.get();
}
public void increase() {
// synchronized (this) {
// num ++;
// }
atomicInteger.incrementAndGet();
}
}
打印结果为20000000,并且性能得到了提升:
原子操作类实现原理(CAS):
public class A {
int num = 0;
AtomicInteger atomicInteger = new AtomicInteger();
public int getNum() {
return atomicInteger.get();
}
public void increase() {
// synchronized (this) {
// num ++;
// }
atomicInteger.incrementAndGet();
// 以上atomicInteger.incrementAndGet()代码实现原理
while (true) {
int oldValue = atomicInteger.get();
int newValue = oldValue + 1;
if (atomicInteger.compareAndSet(oldValue, newValue)) {
break;
}
}
}
}
多线程原子操作类保证线程安全原理图解:
源码:
CAS:无锁,自旋锁,乐观锁,轻量级锁(compareAndSet、compareAndSwap)
类比汽车驾驶:
公路好比是重量级锁,而山路好比是轻量级锁
CAS的存在的问题:
1、原子性问题
在以上源码中compareAndSwapInt()方法是否有原子性呢?
查看源码:可以看到该方法是个native方法,调用底层c++实现的代码
jdk底层c++源码:
以上代码大致可以理解为:
1、判断cpu是否是多核
2、如果是多核那么就加上lock汇编指令(硬件级别加锁)
lock cmpxchgq 缓存行锁/总线锁
缓存是由最小的存储区块-缓存行(cacheline)组成,缓存行大小通常为64byte。
如果值比较小,没有超过64byte,则会加缓存行锁。
如果值比较大,那么会加总线锁,所有与总线交互的线程都得等着。
因此,jdk底层已经实现了锁保证了原子性
2、ABA问题
现象:线程1修改新值时,线程2在线程1执行的过程中,进行设置新值,并且将原来的值还原了回去,但是线程2执行完毕后,回到线程1执行,线程1由于判断值相等,将线程2的值改成了新值,导致了ABA问题。正常情况应该在线程2再次修改了值之后,线程1不用再修改了
解决方案:每次修改完值后添加版本
虽然两者结果值都相同,但实际上其他线程已经将值进行了修改,那么就应该重新循环,拿到最新的值做修改。
扩展:查看jdk源码目录发现,jdk底层c++源码为我们兼容了多种操作系统,所以这就是jvm跨平台的原因
在jdk1.6之后,官方对synchronized做了很多优化:
对象的内存布局
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 对象头:比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等。Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
- 实例数据:存放类的属性数据信息,包括父类的属性信息
- 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
对象头:
引入jol依赖包,查看对象内部组成结构:
<dependencies>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
</dependencies>
打印对象内部组成:
package com.laoxu.tuling.class1;
public class User {
private int id;
private String name;
public User() {
}
public User(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
无状态锁
public class LockUpgrade {
public static void main(String[] args) {
User userTemp = new User();
System.out.println("无状态(001):" + ClassLayout.parseInstance(userTemp).toPrintable());
}
}
打印结果:发现二进制对象头末尾编程了001
无状态(001):com.laoxu.tuling.class1.User object internals:
// 占用空间(byte) 类型描述(object header对象头) 十六进制 十六进制对应的二进制
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
12 4 int User.id 0
16 4 java.lang.String User.name null
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
// alignment 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
// 由于 4+4+4+4+4 = 20 不能被8整除,那么就会填充4个字节进行填充
对齐填充:为了提高计算机寻址的性能,根据大量实验得出的结论是8个字节寻址性能最佳,jvm专门做了对齐填充,使得计算机牺牲较小的空间来提高计算机寻址的效率。
启用偏向锁:要想升级为偏向锁,必须先启用偏向锁,不然直接升级为轻量级锁,详见上面流程图
// jvm默认延时4s自动开启偏向锁,可用过-XX:BiasedLockingStartupDelay=0取消延时
// 如果不要偏向锁,可通过-XX:-UseBiasedLocking = false 来设置
Thread.sleep(5000);
User user = new User();
System.out.println("启用偏向锁(101):" + ClassLayout.parseInstance(user).toPrintable());
打印结果:发现二进制对象头末尾变成了101
启用偏向锁(101):com.laoxu.tuling.class1.User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
12 4 int User.id 0
16 4 java.lang.String User.name null
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
偏向锁(101)(带线程id)
for (int i = 0; i < 2; i++) {
synchronized (user) {
System.out.println("偏向锁(101)(带线程id):" + ClassLayout.parseInstance(user).toPrintable());
}
System.out.println("偏向锁释放(101)(带线程id):" + ClassLayout.parseInstance(user).toPrintable());
}
偏向锁(101)(带线程id):发现二进制对象头末尾还是101,并且在二进制后3个字节记录了线程id等信息
偏向锁释放(101)(带线程id)打印结果:发现释放后对象头末尾的偏向锁标记并没有消失
偏向锁(101)(带线程id):com.laoxu.tuling.class1.User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 28 d6 02 (00000101 00101000 11010110 00000010) (47589381)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
12 4 int User.id 0
16 4 java.lang.String User.name null
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
偏向锁释放(101)(带线程id):com.laoxu.tuling.class1.User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 28 d6 02 (00000101 00101000 11010110 00000010) (47589381)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
12 4 int User.id 0
16 4 java.lang.String User.name null
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
释放synchronized后,偏向锁还是存在,就是方便下次如果还是这个线程调用,那么直接使用偏向锁
轻量级锁
在上面的基础上添加一个线程并且对user加锁
new Thread(()->{
synchronized (user) {
System.out.println("轻量级锁(00)(带线程id):" + ClassLayout.parseInstance(user).toPrintable());
}
}).start();
打印结果:发现二进制对象头末尾编程了00
轻量级锁(00)(带线程id):com.laoxu.tuling.class1.User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) b8 f4 76 1f (10111000 11110100 01110110 00011111) (527889592)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
12 4 int User.id 0
16 4 java.lang.String User.name null
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
轻量-->重量(10)
在上面的基础上添加一个线程并且对user加锁,并且睡眠3秒钟
如果是一个线程占用user资源,则会变成偏向锁,但是一旦又有一个线程想要占用user资源,那么就会马上升级为轻量级锁。
new Thread(()->{
synchronized (user) {
System.out.println("轻量级锁(00)(带线程id):" + ClassLayout.parseInstance(user).toPrintable());
try {
System.out.println("睡眠3秒钟========================");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("轻量-->重量(10):" + ClassLayout.parseInstance(user).toPrintable());
}
}).start();
Thread.sleep(1000);
new Thread(()-> {
synchronized (user) {
System.out.println("重量级锁(10):" + ClassLayout.parseInstance(user).toPrintable());
}
}).start();
打印结果:发现二进制对象头末尾编程了10
睡眠3秒钟========================
轻量-->重量(10):com.laoxu.tuling.class1.User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) fa c0 20 1c (11111010 11000000 00100000 00011100) (471908602)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
12 4 int User.id 0
16 4 java.lang.String User.name null
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
重量级锁(10):com.laoxu.tuling.class1.User object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) fa c0 20 1c (11111010 11000000 00100000 00011100) (471908602)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 92 c3 00 f8 (10010010 11000011 00000000 11111000) (-134167662)
12 4 int User.id 0
16 4 java.lang.String User.name null
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
如果是一个线程占用user资源,则会变成偏向锁,但是一旦又有一个线程想要占用user资源,那么就会马上升级为轻量级锁,如果再来一个线程又要占用user资源,那么就会升级为重量级锁。
补充:CAS一定比重量级锁性能高吗?答案是不一定的。
还是类比汽车行驶:
如果是上面这种情况,山路太曲折了,导致路程太长,那么公路就有可能更快的到达终点。
参考文档: