首先抛出一个问题
public class SynchronizedDemo {
public static int count = 0;
public static void main(String[] args) {
for(int i= 0; i < 1000;i++){
new Thread(()->SynchronizedDemo.incr(),"线程t1").start();
}
try {
//保证线程执行结束
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("运行结果:"+count);
}
public static void incr(){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
上述代码的运行结果不管执行多少次一定是一个小于1000的值,这是问什么呢?
这里边涉及到线程的原子性和可见性
所谓线程的原子性就是:一系列或者一个指令操作是不可中断的,一但开始就不允许其他线程或者cpu中断。
上述代码的原子性体现在 count++;这个操作
我们可以通过字节码文件看到
我们可以通过javav -p SynchronizedDemo.class文件看到,count++对应的字节码
12: getstatic #17 // Field count:I 访问静态变量
15: iconst_1 将常量1压入到栈中
16: iadd 通过这个进行自增
17: putstatic #17 // Field count:I 设置静态变量
线程运行上述指令,要不全部运行,要么全不运行,不可中断也不可分割,但是在多核cpu运行的时候可能存在切换,因为多个线程是通过cpu时间片进行切换的。
看一张解释线程原子性的图
我们实际场景中可能还会遇到多个线程去访问同一个共享资源
Synchronized有三种锁类型
类锁在JVM中只又一个,对象锁在JVM中可以有多个
1、实例锁
2、类锁,静态锁、对象锁
3、代码块加锁
互斥的锁的本质是:共享资源
锁的存储,锁存储在对象头
可以通过这个包查看类的内存布局,里边包含对象头信息。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
1、无锁状态下
这是无锁状态下的内存布局,可以看到经过压缩之后是96位,如果不压缩就是128位
synchronizedDemo.ClasslayoutDemo object internals:
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) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
value涉及到大端和小端存储
1).大端存储:大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放。
2).小端存储:小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。
十六进制(0X) 00 00 00 00 00 00 00 01
对应的二进制:00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000 0 01
可以看到最后三位的含义 红色代表是否偏向锁,蓝色代表锁标志
2、轻量级锁 加 Synchronized的状态下
public class ClasslayoutDemo {
public static void main(String[] args) {
ClasslayoutDemo classlayoutDemo = new ClasslayoutDemo();
synchronized (classlayoutDemo){
System.out.println("locking");
System.out.println(ClassLayout.parseInstance(classlayoutDemo).toPrintable());
}
}
}
输出结果(轻量级锁)
locking
synchronizedDemo.ClasslayoutDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 88 f7 04 03 (10001000 11110111 00000100 00000011) (50657160)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
00000000 00000000 00000000 00000000 00000011 00000100 11110111 100010 00 可以看到为轻量级锁。
jdk1.6之后优化了Synchronized的锁,增加了偏向锁和轻量级锁,可以自动进行锁升级,如果一个线程没有获得偏向锁,就通过CAS(自适应自旋锁)去尝试获取锁,如果自旋失败就升级为轻量级锁,线程就通过CAS去尝试获取轻量级锁,如果自旋失败就升级为重量级锁,将该线程假如到阻塞队列中。
思考:但是加锁一定会带来性能开销。优化的最好方式是不加锁,不加锁怎么保证线程安全呢?
偏向锁:不存在竞争,
CAS是乐观锁(自旋锁)
思考:用到偏向锁之后HashCode就没法在对象头里边存储了?
JVM默认是关闭偏向锁的,可以通过 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 开启偏向锁
synchronizedDemo.ClasslayoutDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 48 4c 03 (00000101 01001000 01001100 00000011) (55330821)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
00000000 00000000 00000000 00000000 00000011 01001100 01001000 00000 1 01
计算了HashCode是无法用到偏向锁的,锁会升级为重量级锁。因为无法存储HashCode了,所以无法使用偏向锁。