synchronized 关键字的了解
synchronized 解决的是多个线程访问资源的同步性问题,synchronized 可以保证被它修饰的方法或者代码块在同一时间只有一个线程可以执行。
-
另外,在 Java 早期版本中, synchronized 属于 重量级锁,效率低下。
-
为什么呢?
-
因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原⽣线程之上的。如果要挂起或者唤醒⼀个线程,都需要操作系统帮忙完成,⽽操作系统实现线程之间的切换时需要从⽤户态转换到内核态,这个状态之间的转换需要相对⽐⻓的时间,时间成本相对⾼。
-
庆幸的是在 Java 6 之后 Java 官⽅对从 JVM 层⾯对 synchronized ⼤优化,所以现在的synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引⼊了⼤量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
-
所以,你会发现⽬前的话,不论是各种开源框架还是 JDK 源码都⼤量使⽤了 synchronized 关键字。
1. synchronized的三种使用方法
1.1 修饰实例方法
- 作用于当前对象实例加锁,进入同步代码前需要先获得 当前对象实例的锁。
synchronized void method() {
//业务代码
}
1.2 修饰静态方法
- 也就是给当前类加锁,会作用于当前对象的所有实例,进入同步代码前需要获得当前class的锁。
- 因为静态成员不属于任何实例对象,是类成员(static表示的是这是该类的一个静态资源,不管new了多少个对象,只有一份)。
- 所以,如果⼀个线程 A 调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程 B 需要调⽤这个实例对象所属类的静态 synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁。
synchronized void staic method() {
//业务代码
}
1.3 修饰代码块
- 指定加锁对象,对给定对象或者类加锁。synchronized(this | Object) 表示进入同步代码块之前需要获取给定对象的锁。synchronized(类名.class)表示进入同步代码块之前需要获取当前class的锁。
synchronized(this) {
//业务代码
}
2. 总结
- synchronized关键字加到static静态方法或者synchronized(类名.class)上,都是给Class类上锁。
- synchronized关键字加到实例方法上是给对象实例上锁。
- 尽量不要使⽤ synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能。
3. “单例模式了解吗?
/**
* @author LanceQ
* @version 1.0 2021/4/11
*/
public class SingletonDemo {
//禁止指令重排volatile
private static volatile SingletonDemo instance=null;
// private static SingletonDemo instance=null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t 我是构造方法SingleDemo()");
}
//DCL(double check lock双端检锁机制)
public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class){
if(instance==null)
instance = new SingletonDemo();
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i <20; i++) {
new Thread(()->{
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
- 需要注意 instance 采⽤ volatile 关键字修饰也是很有必要。
- 采⽤ volatile 关键字修饰也是很有必要的, instance = new Singleton(); 这
段代码其实是分为三步执⾏:
- 为 instance 分配内存空间
- 初始化 instance
- 将 instance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。
例如,线程 T1 执⾏了 1 和 3,此时 T2 调⽤ getInstance() 后发现 instance 不为空,因此返回instance,但此时 instance 还未被初始化。
使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏。
4. 额外
-
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。
-
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法。
参考:JavaGuide面试突击版