一个问题引发思考
public static int count=0;
public static void incr(){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main( String[] args ) throws InterruptedException {
for(int i=0;i<1000;i++){
new Thread(()->App.incr()).start();
}
//保证线程执行结束
Thread.sleep(3000);
System.out.println("运行结果:"+count);
}
结果是小于等于1000的随机数。
原因: 可见性、原子性
count++的指令
14: getstatic #5 // Field count:I
15: iconst_1
16: iadd
17: putstatic #5
getstatic 获取到的i,可能不是最新的,线程之间的不可见导致结果出现不一致。
锁(Synchronized)
互斥锁的本质是什么.
共享资源
加锁逻辑图:
锁的使用
可以修饰在方法层面和代码块层面
class Test {
// 修饰非静态方法
synchronized void demo() {
// 临界区
}
// 修饰代码块
Object obj = new Object();
void demo01() {
synchronized(obj) {
// 临界区
}
}
}
锁的作用范围
synchronized有三种方式来加锁,不同的修饰类型,代表锁的控制粒度:
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
- 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
- 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
锁的存储(对象头)
- 32位、64位对象头
// --------
# 32 bits:
# hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
# JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
# size:32 ------------------------------------------>| (CMS free block)
# PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//-----------
// --------
# 64 bits:
# unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
# JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
# PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
# size:64 ----------------------------------------------------->| (CMS free block)
//-------
看图说话:根据对象头信息判断加锁的类型。
- 偏向锁直接在对象头中存储线程信息,注意偏向锁已无法存储hashCode,所以一旦调用对象的hashcode,偏向锁立刻会升级成重量级锁
- 轻量级锁和重量级锁会在栈中保存线程信息。
打印类的布局
打印对象头信息->ClassLayout.parseInstance(object).toPrintable()
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
如何查看锁状态位
- 16进制: 0x 00 00 00 00 00 00 00 01
(64位)2进制: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000 0
01 (无锁状态)
通过最后三位来看锁的状态和标记。
- 【大端存储和小端存储】
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)
通过打印加锁类来查看对象头
public static void main(String[] args) {
ClassLayoutDemo classLayoutDemo=new ClassLayoutDemo();
synchronized (classLayoutDemo){
System.out.println("locking");
System.out.println(ClassLayout.parseInstance(classLayoutDemo).toPrintable());
}
}
输出结果. (轻量级锁) 最后三位为 000 表示轻量级锁
org.example.ClassLayoutDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 88 f1 bb 02(10001000 11110001 10111011 00000010) (45871496)
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
锁的升级
偏向锁
- 在大多数情况下,锁不仅仅不存在多线程的竞争,而且总是由同一个线程多次获得。在这个背景下就设计了偏向锁。
- 偏向锁,顾名思义,就是锁偏向于某个线程。当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。
- 如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。(偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。)
偏向锁的对象头
org.example.ClassLayoutDemo object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 e8 45 03 (00000101 11101000 01000101 00000011) (54913029)
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
轻量级锁
如果偏向锁被关闭或者当前偏向锁已经已经被其他线程获取,那么这个时候如果有线程去抢占同步锁时,锁会升级到轻量级锁。
重量级锁
- 多个线程竞争同一个锁的时候,虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程;
- Java 线程的阻塞以及唤醒,都是依靠操作系统来完成的:os.pthread_mutex_lock()
- 升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态
重量级锁的案例
public static void main(String[] args) throws Exception {
TestDemo testDemo = new TestDemo();
Thread t1 = new Thread(() -> {
synchronized (testDemo){
System.out.println("t1 lock ing");
System.out.println(ClassLayout.parseInstance(testDemo).toPrintable());
}
});
t1.start();
synchronized (testDemo){
System.out.println("main lock ing");
System.out.println(ClassLayout.parseInstance(testDemo).toPrintable());
}
}
- 每一个JAVA对象都会与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized修饰的同步方法或者代码块时,该线程得先获取到synchronized修饰的对象对应的monitor。
- monitorenter表示去获得一个对象监视器。monitorexit表示释放monitor监视器的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器
monitor依赖操作系统的MutexLock(互斥锁)来实现的,线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能- 任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
总结
- 偏向锁只有在第一次请求时采用CAS在锁对象的标记中记录当前线程的地址,在之后该线程再次进入同步代码块时,不需要抢占锁,直接判断线程ID即可,这种适用于锁会被同一个线程多次抢占的情况。
- 轻量级锁才用CAS操作,把锁对象的标记字段替换为一个指针指向当前线程栈帧中的LockRecord,该工件存储锁对象原本的标记字段,它针对的是多个线程在不同时间段内申请通一把锁的情况。
- 重量级锁会阻塞、和唤醒加锁的线程,它适用于多个线程同时竞争同一把锁的情况。
线程的通信(wait/notify)
在Java中提供了wait/notify这个机制,用来实现条件等待和唤醒。这个机制我们平时工作中用的少,但是在很多底层源码中有用到。比如以抢占锁为例,假设线程A持有锁,线程B再去抢占锁时,它需要等待持有锁的线程释放之后才能抢占,那线程B怎么知道线程A什么时候释放呢?这个时候就可以采用通信机制。