关键字:synchronized
介绍
synchronized
是Java语言中的一个关键字,用于实现线程同步,以确保在多线程环境下对共享资源的访问是安全且一致的。它通过提供一种互斥机制来防止多个线程同时执行特定的代码区域,从而避免了数据不一致性和其他并发问题。以下是关于synchronized
的一些关键点:
基本用途
- 同步方法:当
synchronized
应用于方法时,该方法成为同步方法。这意味着同一时间只有一个线程可以访问该方法。对于实例方法,锁是当前实例对象(this
);对于静态方法,锁是类的Class对象。 - 同步代码块:允许更细粒度的控制,你可以指定一个对象作为锁。只有获得了这个特定对象锁的线程才能执行该代码块。这对于减少锁的范围和提高并发性非常有用。
实现原理
- 监视器锁(Monitor Lock):每个对象都有一个关联的监视器锁。当线程进入
synchronized
代码块或方法时,它会自动获取对象的监视器锁,并在退出时释放锁。 - 锁升级:Java 6以后,为了提高效率,synchronized经历了锁升级的过程,包括偏向锁、轻量级锁和重量级锁,以适应不同的竞争程度。
特性
- 原子性:确保被
synchronized
保护的代码块作为一个不可分割的单位执行,即操作不会被线程调度机制打断。 - 可见性:通过监视器锁的规则,保证了变量的修改能够及时地被其他线程看到,因为线程在退出同步代码块之前会将修改的数据刷新到主内存中。
- 不可中断性:一旦线程获得了
synchronized
锁,它会一直持有直到执行完毕或抛出异常,期间不会响应中断请求。
使用注意事项
- 死锁:不当的使用
synchronized
可能导致死锁,特别是在多个锁依赖的场景中。 - 性能考量:虽然
synchronized
在Java中已经过优化,但在高并发场景下,过度使用仍可能成为性能瓶颈。 - 可重入性:同一个线程可以多次获取同一个对象的锁,不会造成自我阻塞。
与其他并发工具比较
相较于synchronized
,Java并发包(java.util.concurrent
)提供了更多高级并发工具,如ReentrantLock
, Semaphore
, CountDownLatch
等,它们提供了更灵活的锁机制和更高的并发性能,但synchronized
因其简单易用和编译器支持而在很多场景下仍然是首选。
使用
public class PreloadSingleton {
public static PreloadSingleton instance = new PreloadSingleton();
//其他的类无法实例化单例类的对象
private PreloadSingleton() {
};
public static PreloadSingleton getInstance() {
return instance;
}
}
public class Singleton {
private static Singleton instance=null;
private Singleton(){
};
public static Singleton getInstance()
{
if(instance==null)
{
instance=new Singleton();
}
return instance;
}
}
单例模式和线程安全
(1)预加载只有一条语句return instance,这显然可以保证线程安全。但是,我们知道预加载会造成内存的浪费。
(2)懒加载不浪费内存,但是无法保证线程的安全。首先,if判断以及其内存执行代码是非原子性的。其次,new Singleton()无法保证执行的顺序性。
不满足原子性或者顺序性,线程肯定是不安全的,这是基本的常识,不再赘述。我主要讲一下为什么new Singleton()无法保证顺序性。我们知道创建一个对象分三步:
memory=allocate();//1:初始化内存空间
ctorInstance(memory);//2:初始化对象
instance=memory();//3:设置instance指向刚分配的内存地址
jvm为了提高程序执行性能,会对没有依赖关系的代码进行重排序,上面2和3行代码可能被重新排序。我们用两个线程来说明线程是不安全的。线程A和线程B都创建对象。其中,A2和A3的重排序,将导致线程B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象(线程不安全)。
synchronized用法1-同步方法
public class Singleton {
private static Singleton instance = null;
private Singleton() {
};
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
如果要经常的调用getInstance()方法,不管有没有初始化实例,都会唤醒和阻塞线程。为了避免线程的上下文切换消耗大量时间,如果对象已经实例化了,我们没有必要再使用synchronized加锁,直接返回对象。
synchronized用法2-同步代码块
我们把sychronized加在if(instance==null)判断语句里面,保证instance未实例化的时候才加锁
public class Singleton {
private static Singleton instance = null;
private Singleton() {
};
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
顺序性考虑
上述讨论得知new一个对象的代码是无法保证顺序性的,因此,我们需要使用另一个关键字volatile保证对象实例化过程的顺序性。
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
};
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (instance) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
到此,我们就保证了懒加载的线程安全。
关键字:volatile
volatile
是Java中的一个关键字,它用于修饰变量,以确保在多线程环境中的可见性和一定程度的有序性,但不保证原子性。以下是volatile
关键字的主要作用和特性:
可见性(Visibility)
当一个变量被声明为volatile
时,任何对其的写操作都会立即刷新到主内存中,而任何读操作都会直接从主内存中读取最新的值。这意味着,一旦一个线程修改了volatile
变量的值,其他线程可以立即看到这个变化,消除了缓存不一致的问题,保证了共享变量的可见性。
有序性(Ordering)
volatile
除了保证可见性外,还禁止了指令重排序(Instruction Reordering)。在没有volatile
修饰的情况下,编译器和处理器为了优化性能可能会对指令进行重新排序,这在单线程环境中没有问题,但在多线程环境下可能导致不一致的行为。使用volatile
可以确保对volatile变量的读/写操作不会与其他内存操作重排序,从而保证了操作的有序性。
不保证原子性(Non-Atomicity)
尽管volatile
提供了可见性和一定的有序性,但它并不能保证复合操作(如i++
)的原子性。例如,一个线程执行i++
操作实际上包含了读取、修改、写回三个步骤,即使i
是volatile
的,这个操作仍然不是原子的,因此在多线程环境下可能会出现问题。为了确保原子性,通常需要配合synchronized块或使用java.util.concurrent
包下的原子类(如AtomicInteger
)。
使用场景
- 适用于状态标记量,如
flag
变量,用于控制线程的启动、停止或某个状态的开关。 - 读多写少的场景,当一个变量被频繁读取,但很少修改时,使用
volatile
可以减少锁的开销。 - 双重检查锁定(Double-Checked Locking)模式中,用于确保实例化过程的可见性。
锁升级
锁升级是Java中针对synchronized
关键字实现的一种优化策略,旨在减少线程间竞争锁的开销,提高并发性能。这一机制主要发生在Java 6及之后的版本中,主要包括以下几个阶段:
1. 偏向锁(Bias Locking)
- 目的:大多数情况下,锁只会被一个线程反复获得,偏向锁就是为了这种场景设计的优化。它会偏向于第一个获得它的线程,之后的运行过程中,如果该锁没有被其他线程尝试获取,持有偏向锁的线程不需要进行同步操作。
- 升级:当其他线程尝试获取这个偏向锁时,偏向锁就会升级为轻量级锁。
2. 轻量级锁(Lightweight Locking)
- 目的:如果偏向锁被另一个线程竞争,那么偏向锁就会升级为轻量级锁。轻量级锁是为了解决锁竞争较轻的情况,它通过在堆栈上创建锁记录(Lock Record)来避免传统的重量级互斥锁带来的系统调用开销。
- 操作:线程会尝试将对象头的Mark Word复制到自己的栈帧中创建的Lock Record中,并尝试用CAS操作将对象头的Mark Word更新为指向Lock Record的指针。如果成功,表示获取锁成功;如果失败,则说明有竞争,会进一步升级。
- 升级:当锁竞争进一步加剧,轻量级锁会膨胀为重量级锁。
3. 重量级锁(Heavyweight Locking)
- 目的:当多个线程同时竞争锁,且通过轻量级锁无法有效解决竞争时,锁就会膨胀为重量级锁。这时,Java虚拟机会使用操作系统的互斥量(Mutex)来实现线程同步,这会导致线程阻塞和唤醒,开销较大。
- 特点:重量级锁是传统意义上的互斥锁,它会导致未获取到锁的线程阻塞,等待操作系统调度。
锁降级
除了锁升级,还存在锁降级的概念,即从重量级锁降级为轻量级锁,甚至是偏向锁。但是,实际应用中锁降级并不常见,主要还是关注于锁的升级路径来优化并发性能。
CAS
CAS(Compare and Swap)操作是一种在多线程环境下的非阻塞同步技术,用于实现原子性的更新操作。这项技术广泛应用于Java的并发编程中,尤其是在java.util.concurrent.atomic
包下的原子类中,比如AtomicInteger
、AtomicLong
等。下面是关于CAS操作的关键点:
工作原理
CAS操作包含三个操作数:
- 内存值(V):要更新的变量在内存中的当前值。
- 预期值(A):执行CAS操作前,线程期望V应该具有的值。
- 新值(B):如果V的值确实等于A,那么需要更新的新值。
执行过程如下:
- 比较:CAS首先比较内存位置V的当前值与预期值A是否相等。
- 交换:如果相等,说明没有其他线程改变过这个值,此时将内存值更新为新值B,并返回true,表示更新成功。如果不相等,说明已经有其他线程改变了这个值,此时不进行任何操作,并返回false,表示更新失败。
特点
- 原子性:CAS操作是CPU级别的原子指令,保证了读-改-写的整个过程不会被中断,从而避免了数据的不一致性。
- 乐观锁:CAS基于一种乐观的并发策略,它总是认为自己可以成功完成操作,而不像悲观锁那样每次操作前都先锁定资源。
- 非阻塞:失败的线程不会被挂起或阻塞,而是被告知失败后可以再次尝试,这减少了线程上下文切换的开销,提高了系统的吞吐量。
应用场景
- 原子变量更新:如原子类的递增、递减操作。
- 无锁数据结构:构建如无锁队列、无锁栈等高性能并发数据结构。
- 轻量级锁实现:在Java中的锁实现中,轻量级锁的获取和释放也使用了CAS操作。
注意事项
- ABA问题:如果一个值从A变为B再变回A,CAS操作无法识别这种变化,可能会导致错误的结果。通常通过添加版本号或使用AtomicStampedReference来解决。
- 循环开销:在高竞争的场景下,CAS可能需要多次重试,这可能导致大量的循环(自旋),消耗CPU资源。
- 非阻塞不代表无等待:失败的线程虽然没有被阻塞,但需要不断地重试,这也是一种等待状态。