synchronized和volatile相关知识

Posted by shuyou on Monday, July 26, 2021

总结下synchronized和volatile关键字相关八股文

在多线程环境下,线程是交替执行的,多线程竞争共享资源容易产生线程不安全的问题,甚至产生死锁的问题。

使用多线程,一定要保证程序是线程安全的。通常会采用以下一些方法来保证线程安全:

  • 多线程之间没有共享变量,即考虑使用线程私有变量
  • 加锁
  • CAS

synchronized

Synchronized是Java的关键字,它可以将代码块锁起来。

	public void synchronized lock(){
		//do something
	}

可以修饰普通方法、代码块、静态方法等。

Synchronized是可重入锁、互斥锁、内置锁(监视器锁)

  • 可重入锁:是指允许同一个线程多次获取同一把锁
  • 互斥锁:是指同一时刻只允许一个线程进入锁
  • 内置锁:是值使用对象的内置锁(监视器)来实现的锁

Synchronized是由相关字节码指令实现的:monitorenter和monitorexit,而这两个指令依赖于底层操作系统Mutex Lock来实现的。但是使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。在jdk1.6后,对synchronized关键字进行了大量的优化,如锁升级、偏向锁、轻量级锁、重量级锁等。

在Java SE 1.6里Synchronied同步锁,一共有四种状态:无锁、偏向锁、轻量级锁、重量级锁,它会随着竞争情况逐渐升级。锁可以升级但是不可以降级,目的是为了提供获取锁和释放锁的效率。

锁膨胀方向: 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 (此过程是不可逆的)

偏向锁

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁。只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果成功,表示线程已经获取到了锁。

偏向锁使用了一种等待竞争出现才会释放锁的机制。所以当其他线程尝试获取偏向锁时,持有偏向锁的线程才会释放锁。

但是偏向锁的撤销需要等到全局安全点(就是当前线程没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,让你后检查持有偏向锁的线程是否活着。如果线程不处于活动状态,直接将对象头设置为无锁状态。如果线程活着,JVM会遍历栈帧中的锁记录,栈帧中的锁记录和对象头要么偏向于其他线程,要么恢复到无锁状态或者标记对象不适合作为偏向锁。

轻量级锁

轻量级加锁:线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并 将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失 败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级解锁:会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成 功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

volatile

在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的 synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程 修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当 的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

为了性能优化,JMM 在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序。JMM 提供了内存屏障阻止这种重排序。

Java 编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。

volatile保证"可见性",但不保证"原子性"。

「真诚赞赏,手留余香」

ShuYou's Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付