synchronized
关键字解决的是多个线程之间访问资源的同步性,synchronized
关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
另外,需要注意的是,在 Java 早版本中,synchronized
属于 重量级锁 ,效率低下。
具体原因是什么呢?
因为监视器锁(monitor)是依赖于底层的操作系统 Mutex Lock
(互斥锁)来实现的,Java 的线程是映射到操作系统的原生线程上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换需要从用户转换到内核态,这个状态的切换需要相对比较长的时间,时间成本相对较高。
比较好的是,在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized
较大优化,所以现在的 synchronized
锁效率也优化的很不错了。JDK 1.6 对锁的实现引入了大量的优化,如:自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
所以,目前的各种开源框架还是 JDK 源码,都大量使用了 synchronized
关键字。
synchronized 关键字最主要有三种使用方式:
1.修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要先获得 当前对象实例的锁 。
synchronized void method(){
// 业务代码
}
2.修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,进入同步代码前要先获得 当前class 的锁 。
因为静态成员不属于任何一个实例对象,是类成员(static 表明这是该类的一个静态资源,不管new了多少对象,只有一份)。所以,如果一个线程 A 调用了一个实例对象的 非静态 synchronized
方法 ,而线程 B 需要调用这个实例对象所属类的 静态 synchronized
方法 ,是允许的,不会发生互斥现象,因为访问静态 synchronized
方法占用的锁是当前类的锁,访问非静态 synchronized
方法占用的锁是当前实例对象锁。
synchronized static void method(){
// 业务代码
}
3.修饰代码块: 指定加锁对象,对给定对象 / 类加锁。 synchronized(this | object)
表示进入代码库需要获得给定对象的锁。
synchronized(类.class)
表示进入同步代码前要获得 当前class的锁。
synchronized(this){
// 业务代码
}
总结:
synchronized
关键字加到static
静态方法 和synchronized(class)
代码块上都是给 Class类上锁。synchronized
关键字加到实例方法上就是给对象实例上锁。- 尽量不要使用
synchronized(String a)
,因为 JVM 中,字符串常量池具有缓存功能。
public class Singleton{
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
// 先判断对象是否已经实例过,没有实例化才进入加锁代码
if(this.instance == null){
// 类对象加锁
synchronized(Singleton.class){
if(this.instance == null){
instance = new Singleton();
}
}
}
return this.instance;
}
}
另外,需要注意的是,instance
采用 volatile
修饰是非常有必要的,具体原因是:instance = new Singleton(); 这行代码其实是分三步执行的:
- 为
instance
分配内存空间- 初始化
instance
- 将
instance
指向分配的内存地址
但是由于JVM具有指令重排的特性,执行顺序很有可能变成 1 -> 3 -> 2
,指令重排在单线程环境下不会出现问题,但是多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 步骤1和3,此时 T2调用了 getInstance() 后发现instance
不为空,因为返回 instance
,但是此时 instance
还未被初始化。
使用 volatile
可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
结论: 构造方法不能使用synchronized关键字修饰。
构造方法本身就属于线程安全的,不存在同步的构造这一说。
synchronized关键字底层原因属于 JVM 层面。
public class SychronizedDemo{
public void method(){
synchronized(this){
System.out.println("synchronized 代码块");
}
}
}
通过 JDK 自带的 javap
,命令查看 SychronizedDemo
类的相关字节码信息:首先切换到类的对应目录执行 :javac SychronizedDemo.java
,命令生成编译后的 .class 文件,然后执行 javap -c -s -v -l SychronizedDemo.class
。
从上面我们可以看出:
synchronized
同步语句块实现的是 monitorrenter
和 monitorexit
执行,其中,monitorrenter
指令指向同步代码块的开始位置,monitorexit
指令则指向同步代码块的结束位置。
当执行 monitorenter
指令时,线程试图获取锁也就是获取 对象监视器 monitor
的持有权。
在 Java虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由
ObjectMonitor
实现的。每个对象内都内置了一个ObjectMonitor
对象。另外,
wait / notofy
等方法也依赖于monitor
对象,这就是为什么只有在同步的方法中才能调用wait / notify
等方法,否则会抛出java.lang,IllegalMonitorStateException
的异常的原因。
在执行 monitorrenter
时,会尝试获取对象的锁,如果锁的计数器为0,则表示锁可以被获取,获取后将锁计数器设为1,也就是加1.
在执行 monitorexit
指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
public class SynchronziedDemo2{
public synchronzied void method(){
System.out.println("synchronized 方法");
}
}
synchronzied
修饰的方法并没有 monitorrenter
指令和 monitorexit
指令,取而代之的却是 ACC_SYNCHRONIZED
标识,该标识表明了该方法是一个同步方法, JVM 通过 ACC_SYNCHRONIZED
访问标志来判断一个方法是否声明为同步方法,从而执行相应的同步调用。
synchronized
同步代码块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识表明了该方法是一个同步方法。
不过两者的本质都是对对象监视器 monitor 的获取。
可重入锁: 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到这个锁的计数器降为0时才能释放。
synchronized
是依赖于 JVM 实现的,前面说到的虚拟机团队在 JDK 1.6 为 synchronized
关键字做了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。
ReentrantLock
是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try / finally 代码块来完成)。
相比 synchronized
, ReentrantLock
增加了一些高级功能,主要有三点:
ReentrantLock
提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly()
来实现这个机制,也就是说在等待的线程可以选择放弃等待,改为处理其他的事情。ReentrantLock
可以指定是公平锁还是非公平锁。而 synchronized
只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock
默认情况是非公平的,可以通过 ReentrantLock
类的ReentrantLock(boolean fair)
构造方法来制定是否是公平的。synchronized
关键字与 notify / notifyAll
方法相结合可以实现 等待 / 通知 机制。ReentrantLock
类当然也可以实现,但是需要借助 Condition
接口与 newCondition()
方法。Condition :是 JDK 1.5 之后才有的,它具有的很好的灵活性,比如可以实现多路通知功能也就是在一个
Lock
对象中可以创建多个Condition
实例(即对象监视器),线程对象可以注册在指定的Condition
中,从而可以有选择性的进行线程通知。在使用notify / notifyAll
方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock
类结合Condition
实例可以实现“选择性通知”,这个功能非常重要,而且是 Condition 接口默认提供的。而
synchronized
关键字就相当于整个Lock 对象
中只有一个Condition
实例,所有的线程都注册在它一个身上。如果执行notifyAll()
方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition
实例的singalAll()
方法只会唤醒注册在该Condition
实例中的所有等待线程。
如果想要使用上述功能,那么选择 ReentrantLock
是一个不错的选择,性能已经不是选择标准。
JDK 1.6 对锁的实现引入了大量的优化,如:偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。
注意: 锁可以升级,不可以降级,这种策略是为了提高获得锁 和 释放锁 的效率。
关于这几种优化的详细信息,小伙伴可以查看下这篇文章哦:《Java 1.6 到底对synchronized做了什么?》
本篇文章讲解了Java多线程进阶面试题-synchronized关键字。代码和笔记由于纯手打,难免会有纰漏,如果发现错误的地方,请第一时间告诉我,这将是我进步的一个很重要的环节。以后会定期更新算法题目以及各种开发知识点,如果您觉得写得不错,不妨点个关注,谢谢。