文章目录
synchronized是java语言最常用的加锁指令。
1. Synchronized的使用
在java运行环境中,每个实例都对应有自己的一把锁,不同的锁之间互不影响,相同的锁互相排斥。
通一把锁,在同一时间,只能被一个线程获取,没有获得锁的线程只能等待。
1.1 修饰实例方法
实际上是在this对象上加锁。参照1.2
public void synchronized dosomething() {
}
1.2 在代码块里显式给this对象加锁
synchronized (this) {
}
1.3 在代码块里显示给对象加锁
建议在这个指定对象上加上final修饰。因为 o
对象如果指向变化了,锁对象就会变化,会导致问题。
final Object o = new Object();
synchronized (o) {
}
1.4 修饰静态方法
实际上是在这个类的class对象上加锁。参照1.5
举例来说,这个类的名称是A,这个方法等同于在A.class
这个对象上加锁。
public static synchronized void dosomething() {
}
1.5 显式锁定class对象
在这里需要注意,在jvm里每个类加载后就会创建一个class对象,比如A类,在jvm里就有且只有一个A.class对象,因此,如果对A.class上锁,那么其他争抢A.class对象的线程都会等待与争抢
synchronized(A.class){
}
2.Synchronized的原理
在java中,锁的实现机制是通过以下三个机制协调实现的。
- 1.锁对象
- 2.锁对象所关联的Monitor对象。
- 3.Monitor对象锁关联的同步队列。
其流程如下,线程在获取锁时,会根据锁对象的头信息,查找monitor对象,检查monitor对象是否被其他线程占用。
-
- 如果monitor对象没有被占用,则标记monitor对象为本线程占用。并且monitor内部的锁计数器+1。如果锁方法完成,在解锁时锁计数器-1,并且清除monitor对象的所有者信息为空。 由于锁有可重入机制,如果monitor对象的所有者是自己,那么会允许该线程重复获取锁。
-
- 如果monitor对象被占用,且所有者不是本线程。则会进入monitor关联的同步队列。在monitor对象被解锁时,同步队列上的线程会被唤醒,重新抢锁。注意wait方法也可以进入到这个同步队列。
在java程序中,synchronized方法编译为java字节码,实际上就是用
monitorenter 方法
和monitorexit 方法
来包裹住synchronized方法。
3.Synchronized的优化
在早期jdk版本中,synchronized使用的是重量级锁(悲观锁),效率相比较低。随着jdk升级,synchronized进行了优化,内部会对锁进行逐步升级,因此提升了适应性,效率已经大幅提升了。
3.1 Synchronized加锁膨胀过程
synchronized在加锁的时候一开始是偏向锁,当发生锁冲突的时候,会升级为轻量级锁即CAS,在重试10次后,如果仍然有线程无法获取锁将升级为重量级锁。
- 偏向锁:在对象头记录所属线程,当新线程访问对象头时会比较对象头记录的线程id,如果不一致,将触发锁升级。
- 自旋锁:第一次升级为自旋锁。默认10次尝试。
- 重量级锁:自旋锁尝试多次失败后升级为重量级锁,想要获取锁的线程将进入等待队列。
注意:synchronized加锁的对象不能使用String常量、Integer、Long等基础类型。
3.2 锁消除
public String lockcancel() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 10; i++) {
sb.append("i:" + i);
}
return sb.toString();
}
在编译时,编译器就会判断出sb这个对象并不会被这段代码块以外的地方访问到,更不会被其他线程访问到,这时候的加锁就是完全没必要的,编译器就会把这里的加锁代码消除掉,于是,编译器会将程序中的sb对象替换为线程不安全的StringBuilder 类型,以提升效率。
3.3 锁的粗化和细化
- 锁粗化
如果一系列的连续操作都对同一个对象反复加锁解锁,甚至加锁操作时出现在循环体中,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
public void method(String s1, String s2){
synchronized(this){
System.out.println("s1");
}
synchronized(this){
System.out.println("s1");
}
}
会被优化为
public void method(String s1, String s2){
synchronized(this){
System.out.println("s1");
System.out.println("s1");
}
}
- 锁细化
由于加锁后程序进入临界区,为提升系统整理效率,应当减少加锁范围。尽量不要对一整个方法进行加锁,而是抽离出一个方法中的共享数据,只对方法中的这部分数据代码加锁。
总结
Synchronized的运行过程有许多值得了解的细节。
多线程系列在github上有一个开源项目,主要是本系列博客的实验代码。
https://github.com/forestnlp/concurrentlab
如果您对软件开发、机器学习、深度学习有兴趣请关注本博客,将持续推出Java、软件架构、深度学习相关专栏。
您的支持是对我最大的鼓励。