synchronized 关键字解决的是多个线程间访问同一资源的同步性问题。
早期版本中,synchronized 属于重量级锁,效率低下,这是由于监视器锁(monitor)是依赖于底层操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上。如果要挂起或者唤醒线程,都需要操作系统帮忙完成,而操作系统进行线程间的切换时,需要从用户态转换到内核态,这个状态的转换需要相对长的时间,时间成本较高,这也就是早期 synchronized 效率低下的主要原因。庆幸的是,Java1.6 以后官方对 synchronized 做了较大的优化,因此现在的 synchronized 锁效率也是比较高的(推荐先使用)。
synchronized 关键字最主要的三种使用方式:
- 修饰实例方法: 给当前对象实例加锁,进入同步代码前要获得当前对象实例的锁;
- 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,(static 修饰表明是类成员,是该类的一个静态资源,所以不管 new 了多少个对象,应有中也只有一份)。如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 调用这个实例对象所属的类的静态 synchronized 方法,这是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象的锁;
- 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。
synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是对当前 Class 类上锁;
synchronized 关键字加到实例方法上是给对象实例上锁;
尽量不要使用 synchronized(String s),因为 JVM 中,字符串常量池具有缓存功能。
- 代码示例:双重校验锁(DCL)实现的对象单例(线程安全)
public class Singleton {
// DCL 方式的单例需确保使用 volatile
private volatile static Singleton uniqueInstance;
// 私有化构造器
private Singleton() {
}
public static Singleton getUniqueInstance() {
// 先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
使用 volatile 关键字可以禁止 JVM 的指令重排,保证多线程环境下代码也能正常执行.。
以上 uniqueInstance = new Singleton();
这段代码其实是分为三步执行:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
由于 JVM 具有指令重排的特性,所以上述执行顺序有可能变成 1->3->2,指令重排在单线程环境下不会有问题,但多线程下会导致线程可能得到还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
网友评论