synchronized的分析与使用
- 同步 机制: synchronized是Java同步机制的一种实现,即互斥锁机制,它所获得的锁叫做互斥锁
- 互斥锁: 指的是每个对象的锁一次只能分配给一个线程,同一 时间只能由一个线程占用
- 作用: synchronized用于保证同一时刻只能由一个线程进入到临界区,同时保证共享变量的可见性、原子性和有序性
- 使用: 当一个线程试图访问同步代码方法(块)时,它首先必须得到锁,退出或抛出异常时必须释放锁
synchronized关键字的使用:
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的class对象
- 同步方法块,锁是括号里面的对象
代码1:
/**
* 一个变量类类
* @author yang
*/
public class NewNum {
/**
* 一个静态变量
*/
static int num = 0;
public static void main(String[] args) {
for(int j = 0; j < 1000; j++) {
SetNumThread setNumThread = new SetNumThread();
Thread timeOne = new Thread(setNumThread);
timeOne.start();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(NewNum.num);
}
}
class SetNumThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
NewNum.num++; //1
System.out.println(NewNum.num);
}
}
}
以上代码块的理想输出结果应当是10000,但是实际上却是每一次输出都不一样。造成这种结果,正是由于线程对共享变量的操作是不安全的。应当对共享变量num加锁。将1处代码变为:
synchronized (SetNumThread.class){
NewNum.num++;
}
synchroized 的实现原理
同步代码块是使用monitorenter和monitorexit指令实现的,同步方法(在这看不出来需要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。
同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;
同步方法:synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。
锁优化
jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
锁的实现机制与java对象头息息相关,锁的所有信息,都记录在java的对象头中。用2字(32位JVM中1字=32bit=4baye)存储对象头,如果是数组类型使用3字存储(还需存储数组长度)。对象头中记录了hash值、GC年龄、锁的状态、线程拥有者、类元数据的指针。
偏向锁:
在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。
那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步,退出同步也,无需每次加锁解锁都去CAS更新对象头,如果不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候需要锁膨胀为轻量级锁,才能保证线程间公平竞争锁。
轻量级锁:
轻量锁与偏向锁不同的是: 1. 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁 2. 每次进入退出同步块都需要CAS更新对象头 3. 争夺轻量级锁失败时,自旋尝试抢占锁
自旋锁:
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
重量级锁:
当竞争线程尝试占用轻量级锁失败多次之后,轻量级锁就会膨胀为重量级锁,重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。
Synchronized的可重入性
- 重入锁: 当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功
- 实现: 一个线程得到一个对象锁后再次请求该对象锁,是允许的,每重入一次,monitor进入次数+1
Synchronized与String锁
- 隐患: 由于在JVM中具有String常量池缓存的功能,因此 相同字面量是同一个锁!!!
- 注意: 严重不推荐将String作为锁对象,而应该改用其他非缓存对象
- 提示: 对字面量有疑问的话请先回顾一下String的基础,这里不加以解释
参考文章:
https://blog.csdn.net/noble510520/article/details/78834224