文章目录
概念和作用
- 概念:
synchronized
是java
中的一个关键字,翻译成中文是同步的意思,主要解决了多个线程之间访问资源的同步性。 - 作用:
synchronized
可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程来执行.
sychronized的特性
互斥
sychronized
会起到互斥的效果,即某个线程执行到某个对象的synchronized
中时,其他线程如果也执行到同一个对象的synchronized
,就会发生阻塞等待.
可重入
sychronized
同步块对于同一条线程来说是可重入的,所以不会出现自己把自己锁死的问题.
class Lock{
Object lock=new Object();
public void test(){
synchronized (lock){
System.out.println("进入第一个synchronized");
try { TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
synchronized (lock) {
System.out.println("进入第二个synchronized");
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
}
public static void main(String[] args) {
Lock lock=new Lock();
lock.test();
}
运行结果
可重入锁总结
Java
中的所有锁都是可重入锁,Java
并没有内置不可重入锁,常见的ReentrantLock
和Synchronized
都属于可重入锁- 可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
- 隐式锁(即
synchronized
关键字使用的锁)默认是可重入锁,显式锁(即Lock
)也有ReentrantLock
这样的可重入锁。
可重入锁的工作原理
可重入锁的工作原理很简单,就是用一个计数器来记录锁被获取的次数和一个持有者标识
- 获取锁:
当一个线程尝试获取锁时,首先会检查当前锁的持有者是否是自己。- 如果是,则将计数器
+1
,表示再次获取成功。 - 如果不是,则检查锁是否被其他线程持有,如果没有被持有,则将当前线程设置为持有者,并将计数器设为
1
; 如果 已经被其他线程持有,则该线程会被阻塞,直到锁被释放。
- 如果是,则将计数器
- 释放锁:
- 当持有锁的线程调用释放锁的方法时,计数器
-1
。 - 如果计数器减到
0
,表示该线程已经完全释放了锁,此时会清空持有者标识,允许其他线程获取该锁。
- 当持有锁的线程调用释放锁的方法时,计数器
使用sychronized
修饰实例方法
锁定当前对象的实例,适用于需要保护实例变量的情况
public class SynchronizedExample {
public synchronized void synchronizedMethod() {
// 线程安全的代码
System.out.println(Thread.currentThread().getName() + " is executing synchronizedMethod");
}
}
修饰静态方法
通过在静态方法前加上 synchronized
关键字,可以确保同一时刻只有一个线程可以执行该静态方法。由于静态方法是类级别的,因此锁是针对类的
public class SynchronizedExample {
public static synchronized void synchronizedStaticMethod() {
// 线程安全的代码
System.out.println(Thread.currentThread().getName() + " is executing synchronizedStaticMethod");
}
}
修饰代码块
通过在代码块前加上 synchronized
关键字,可以锁定特定的对象。这种方式提供了更细粒度的锁控制,避免了不必要的锁竞争。
public class SynchronizedExample {
private final Object lock = new Object();
public void synchronizedBlockMethod() {
synchronized (lock) {
// 线程安全的代码
System.out.println(Thread.currentThread().getName() + " is executing synchronizedBlockMethod");
}
}
}
synchronized的底层原理
从字节码的角度来分析
synchronized
同步代码块:
在windows
系统下进行反编译(指令是javap -c
)
可以得到这样的字节码指令:
所以,sychronized
同步代码块的实现使用的是monitorenter
和monitorexit
这两个指令来实现的;monitorenter
用来实现获得锁,而monitorexit
用来实现释放锁.
synchronized
普通同步方法
在windows
系统下进行反编译(指令是javap -v
)
可以得到这样的字节码指令:
调用指令将会检查方法ACC_SYCHRONIZED
访问标志是否被设置.如果设置了,执行线程会将先持有的monitor
锁,然后再执行方法,最后在方法完成(无论是否是正常完成)时释放monitor
.
synchronized
静态同步方法
在windows
系统下进行反编译(指令是javap -v
)
可以得到这样的字节码指令:
总结:
- 在
Java
编译器将Java
源代码编译为字节码时,synchronized
关键字会被转换为特定的字节码指令。以下是与synchronized
相关的字节码指令:
monitorenter
:用于获取锁,当线程执行到monitorenter
指令时,它会尝试获取与对象关联的监视器(monitor
)。如果锁被其他线程持有,当前线程将被阻塞,直到锁可用。monitorexit
:用于释放锁,当线程执行到monitorexit
指令时,它会释放与对象关联的监视器。monitorexit
必须在monitorenter
成功获取锁后执行,否则会抛出IllegalMonitorStateException
。
- 异常处理
在字节码中,synchronized
还处理了异常情况。为了确保在发生异常时也能释放锁,字节码会使用try-catch
结构:
如果在monitorenter
和monitorexit
之间的代码抛出异常,控制流会跳转到catch
块,确保monitorexit
被调用,从而释放锁。
什么是管程(Monitors
)
概念
管程(monitors
,也称之为**“监视器”),是一种操作系统中的同步机制**,它的引入是为了解决多线程或多进程环境下的并发控制问题。
定义:“管程是一种机制,用于强制并发线程对一组共享变量的互斥访问(或等效操作)。此外,管程还提供了等待线程满足特定条件的机制,并通知其他线程该条件已满足的方法。”
这个定义描述了管程的两个主要功能:
- 互斥访问:管程确保多个线程对共享变量的访问互斥,即同一时间只有一个线程可以访问共享资源,以避免竞态条件和数据不一致性问题。
- 条件等待和通知:管程提供了等待线程满足特定条件的机制,线程可以通过条件变量等待某个条件满足后再继续执行,或者通过条件变量通知其他线程某个条件已经满足。
管程中包含什么
- 共享变量:管程中包含了共享的变量或数据结构,多个线程或进程需要通过管程来访问和修改这些共享资源。
- 互斥锁:互斥锁是管程中的一个关键组成部分,用于确保在同一时间只有一个线程或进程可以进入管程。一旦一个线程或进程进入管程,其他线程或进程必须等待,直到当前线程或进程退出管程。
- 条件变量:条件变量用于实现线程或进程之间的等待和通知机制。当一个线程或进程需要等待某个条件满足时(比如某个共享资源的状态),它可以通过条件变量进入等待状态。当其他线程或进程满足了这个条件时,它们可以通过条件变量发送信号来唤醒等待的线程或进程。
- 管程接口:管程还包括了一组操作共享资源的接口或方法。这些接口定义了对共享资源的操作,并且在内部实现中包含了互斥锁和条件变量的管理逻辑。其他线程或进程通过调用这些接口来访问共享资源,从而确保了对共享资源的有序访问。
#include <iostream>
#include <mutex>
#include <condition_variable>
class Monitor {
private:
int count; // 共享变量
std::mutex mtx; // 互斥锁
std::condition_variable cond; // 条件变量
public:
Monitor() : count(0) {}
void enter() {//进入某个管程
std::unique_lock<std::mutex> lock(mtx);
}
void exit() {//退出某个管程
mtx.unlock();
}
void wait() {//进入等待序列
count++;
cond.wait(lock);
count--;
}
void notify() {//唤醒某个线程
if (count > 0) {
cond.notify_one();
}
}
void notifyAll() {//唤醒全部线程
if (count > 0) {
cond.notify_all();
}
}
};
那么我们之前会存在一个疑问:为什么每个Object对象都可以充当一个锁,现在有了答案
因为:每个Object
对象天生都带着一个管程,每个被锁住的对象都会和Monitor
关联起来
在HotSpot
虚拟机中,monitor
采用的是ObjectMonitor
来实现的.其主要数据结构如下:
加锁/解锁的过程图:
总结:每个对象中都有一个指针指向 Monitor
对象(也称之为管程或者监视器)的真实地址.每个对象都存在一个monitor
与之相关联.当一个monitor
被某个线程持有之后,它便处于锁定的状态.