1、简述
在Java中,多线程编程是一项常见的任务,然而,它也伴随着一系列潜在的问题,比如竞态条件(Race Condition)和数据不一致性。为了解决这些问题,Java提供了一种同步机制,即synchronized关键字。本文将深入探讨Java中synchronized技术,介绍它的基本概念、用法和一些最佳实践。
2、关键特性
synchronized关键字可以保证并发编程的三大特性:原子性、可见性、有序性,而volatile关键字只能保证可见性和有序性,不能保证原子性,也称为是轻量级的synchronized。
- 原子性:一个或多个操作全部执行成功或者全部执行失败。synchronized关键字可以保证只有一个线程拿到锁,访问共享资源。
- 可见性:当一个线程对共享变量进行修改后,其他线程可以立刻看到。 执行synchronized时,会对应执行lock、unlock原子操作,保证可见性。
- 有序性:程序的执行顺序会按照代码的先后顺序执行。
synchronized关键字可以实现什么类型的锁?
-
悲观锁:synchronized关键字实现的是悲观锁,每次访问共享资源时都会上锁。
-
非公平锁:synchronized关键字实现的是非公平锁,即线程获取锁的顺序并不一定是按照线程阻塞的顺序。
-
可重入锁:synchronized关键字实现的是可重入锁,即已经获取锁的线程可以再次获取锁。
-
独占锁或者排他锁:synchronized关键字实现的是独占锁,即该锁只能被一个线程所持有,其他线程均被阻塞。
3、基本使用
3.1 synchronized关键字的基本概念
synchronized是Java中用于实现同步的关键字,它可以用来修饰方法或代码块。当一个线程进入一个由synchronized修饰的方法或代码块时,它将自动获得锁,其他线程必须等待直到该线程释放锁。这确保了在同一时刻只有一个线程可以执行被synchronized修饰的代码。
public synchronized void synchronizedMethod() {
// 同步的代码块
}
3.2 对象级别的锁和类级别的锁
在synchronized中,锁可以是对象级别的,也可以是类级别的。对象级别的锁是基于对象实例的,而类级别的锁是基于类的Class对象的。
// 对象级别的锁
public synchronized void objectLevelLock() {
// 同步的代码块
}
// 类级别的锁
public static synchronized void classLevelLock() {
// 同步的代码块
}
3.3 同步代码块
除了修饰方法,synchronized还可以用于同步代码块。这使得我们可以更加精细地控制同步的范围,提高程序的性能。
public void someMethod() {
// 非同步的代码
synchronized (lockObject) {
// 需要同步的代码块
}
// 非同步的代码
}
4、实现原理
Synchronized的底层实现原理是完全依赖JVM虚拟机的,所以谈synchronized的底层实现,就不得不谈数据在JVM内存的存储:Java对象头,以及Monitor对象监视器。
以下我们来看一下Monitor的实现原理:
ObjectMonitor() {
_header = NULL;
_count = 0; //锁的计数器,获取锁时count数值加1,释放锁时count值减1
_waiters = 0, //等待线程数
_recursions = 0; // 线程重入次数
_object = NULL; // 存储Monitor对象
_owner = NULL; // 持有当前线程的owner
_WaitSet = NULL; // wait状态的线程列表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 阻塞在EntryList上的单向线程列表
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁状态block状态的线程列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
4.1 运行流程
通过以下代码添加同步锁来解析当前线程运行流程:
public void synch1(){
synchronized(this){
try {
synchronized (this){
TimeUnit.MINUTES.sleep(2);
}
System.out.println(Thread.currentThread().getName()+" is runing");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
通过JAR 自带的jconsole.exe 我们可以很清楚的看到当前运行的6个线程状况:
也可以通过Java自带的Jstack + PID 来查看线程运行情况:
jstack 21836
4.2 如何加锁
首先我们来看看对方法同步的运行情况,可以通过反编译class类来查看当前Java代码运行的情况:
public synchronized static void synch0(){
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+" is runing");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
通过Javap指令来解析当前class类:
javap -v SynchronizedDemo
找到反编译指定的代码块,我们可以很清楚的看到同步的方法,是通过flags的ACC_SYNCHRONIZED来实现加锁和解锁的:
接下来我们对代码块实现加Synchronized操作:
public void synch1(){
synchronized(this){
try {
synchronized (this){
TimeUnit.MINUTES.sleep(2);
}
System.out.println(Thread.currentThread().getName()+" is runing");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
同理反编译,我们可以很清楚的看到同步代码块是通过Monitorenter和Monitorexit来实现同步加锁的:
小结:Java虚拟机是通过进入和退出Monitor对象来实现代码块同步和方法同步的,代码块同步使用的是monitorenter和 monitorexit 指令实现的,而方法同步是通过Access flags后面的标识来确定该方法是否为同步方法
5. 锁优化
在Java多线程编程中,synchronized关键字是一种常见的同步机制,用于保护共享资源,防止多个线程同时访问而导致数据不一致的问题。
jDK对synchronized锁进行升级:
5.1 偏向锁(Biased Locking)
偏向锁是为了解决大多数情况下都是由同一线程多次获得锁的场景。当一个线程第一次访问一个同步代码块时,JVM会将对象头的Mark Word设置为偏向锁,并将线程ID保存其中。之后,该线程再次进入同步代码块时,无需再进行同步操作,直接获得锁。
这种优化避免了多线程竞争的开销,提高了单线程执行同步代码块的性能。
5.2 轻量级锁(Lightweight Locking)
当多个线程竞争同一个锁时,偏向锁会失效,这时JVM使用轻量级锁进行优化。轻量级锁的核心思想是,使用CAS操作尝试在对象头的Mark Word中存储锁记录,而不是直接争夺锁。
第一个获得锁的线程会在对象头中记录锁信息,后续线程通过CAS更新锁记录。如果CAS成功,线程获得锁;否则,升级为重量级锁。
5.3 重量级锁(Heavyweight Locking)
在极端情况下,多个线程同时竞争同一个锁,轻量级锁无法解决竞争,JVM会将锁升级为重量级锁。这时,JVM使用操作系统提供的互斥量来保证同一时刻只有一个线程能够获得锁。
虽然重量级锁提供了最强的同步性,但相对于偏向锁和轻量级锁,它的性能开销较大。
6、结论
synchronized是Java中用于解决多线程同步问题的基本机制之一。通过深入理解synchronized的基本概念、对象级别的锁和类级别的锁、同步代码块、避免死锁以及性能考虑,我们可以更好地编写安全且高效的多线程程序。在实际应用中,根据具体场景选择合适的同步机制是至关重要的。