Java 多线程 --- 线程同步 内部锁synchronized
Intrinsic Lock (Monitor)
- synchronized锁机制是基于monitor对象(也被叫做
Monitor 或 Monitor Lock
监视器锁或 Intrinsic Lock
)实现的,每个对象都存在一个monitor对象与之关联,对象头中有一块专门的内存区域用于存储与之关联的monitor对象的地址。
-
The Owner
: 表示目前锁的持有者, 如果为null则表示是无锁状态
-
Entry Set:
记录等待获得相应内部锁的线程. 多个线程申请同一个锁的时候, 只有一个申请者能够成为该锁的持有线程, 其他申请失败者会继续保留在Entry Set.
-
Wait Set:
当一个线程获得锁之后, 因为没有满足某些条件而不得不放弃锁 (调用wait方法). 会被放入Wait Set并进入阻塞状态
- 比如在HotSpot虚拟机中,monitor是由ObjectMonitor实现的,其主要数据结构如下(源码ObjectMonitor.hpp文件,C++实现):
//只列举出部分关键字段
ObjectMonitor() {
_object; = NULL; //当前monitor关联的锁对象
_header = NULL; //当前monitor关联的锁对象的原始对象头
_count = 0; //抢占该monitor的线程数
_owner = NULL; //占用当前monitor的线程
_WaitSet = NULL; //处于wait状态的线程,会被加入到该列表
_EntryList = NULL ; //处于block状态的线程,会被加入到该列表
}
- 内部锁是非公平锁. 所以在wait set的线程被唤醒时, 会有其他的活跃线程(处于Runnable状态, 并且是第一次竞争该锁) 来一起竞争. 所以不是内部锁不是先到先得, 允许线程插队获得锁.
- 内部锁是可重入锁, 有一个计数器记录目前线程的所有权, 为0时代表无锁, 为1时代表已被抢占, 当拥有锁的线程再次申请时, 计数器会进行加一操作.
- 内部锁是重量级锁 因为monitor锁机制依赖于底层操作系统的Mutex Lock实现,挂起线程和恢复线程都需要从用户态切换到内核态去完成,状态转换耗费的成本非常高,所以synchronized是Java语言中的一个重量级操作
synchronized 关键字
- Intrinsic Lock是通过synchronized关键字触发的
- synchronized修饰
实例方法
上,锁对象是当前的this对象
- synchronized修饰
静态方法
上,锁对象是方法区中的类对象,是一个全局锁
- synchronized修饰
代码块
,也就是synchronized(object){},锁对象是()中的对象
- synchronized保证原子性, 可见性, 有序性
-
原子性
: 基于 monitorenter 和 monitorexit 字节码指令,保证同步块只有单一线程执行。
-
可见性
: synchronized 的可见性是由“对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store、write 操作)
”这条规则获得的。
-
有序性
:
具体实现
- 在修饰代码块时,字节码层面上是通过
montiorenter
和 monitorexit
指令来实现的锁获取与释放动作. 当线程进入到monitorenter
指令后, 线程将会持有monitor对象. 退出monitorenter
指令后,线程将会释放monitor对象
-
monitorenter
指令: 获取monitor对象的所有权, 并会发生如下3中情况之一:
- monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
- 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加 (可重入性)
- 第三种情况就是这把锁已经被别的线程获取了,等待锁释放
-
monitorexit
指令:释放对于monitor的所有权,
- 释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权
- 如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁.
- 在修饰方法时, JVM通过
ACC_SYNCHRONIZED
这个标志区分一个方法是否为同步方法. 如果有ACC_SYNCHRONIZED
标志, 则会先持有方法所在的monitor对象,然后再执行方法. 在该方法执行时间,其它任何线程均无法再获取到这个monitor对象,当线程执行完该方法后,他会释放掉这个monitor对象
- You cannot interrupt a thread that is trying to acquire a lock.
- You cannot specify a timeout when trying to acquire a lock.
synchronized 修饰实例方法
- 用synchronized修饰的实例方法叫做
同步实例方法
, 锁对象是当前的this对象
- 同步方法的整个方法体就是一个临界区
Example 1: 循环递增序列号生成器
//只有一个线程可以更新序列号, 保证多线程情况下序列号正常更新
public class SafeCircularSeGenerator {
private short sequence = -1;
public synchronized short nextSequence() {
if (sequence >= 999) {
sequence = 0;
}
else {
sequence++:
}
return sequence;
}
}
Example 2: 使用synchronized重写银行账户的transfer方法:
class Bank
{
private double[] accounts;
public synchronized void transfer(int from, int to, int amount) throws InterruptedException {
//余额不足, 等待入账
while (accounts[from] < amount)
wait(); // wait on intrinsic object lock's single condition
accounts[from] -= amount;
accounts[to] += amount;
notifyAll(); // notify all threads waiting on the condition
}
public synchronized double getTotalBalance() { . . . }
}
synchronized 修饰代码块
- synchronized修饰的代码块就是临界区,
- synchronized(object),锁对象是()中的对象叫锁句柄, 是对应的锁 (可以是任何类型的Object或者this指针)
- 具体格式如下
synchronized(锁句柄 handle) {
//critical section
}
- 锁句柄的变量通常采用
private final
修饰. 这是因为锁句柄变量的值一旦改变, 会导致执行同一个同步块的多线程实际上使用不同的锁, 从而产生混乱 如: private final Object lock = new Object()
Example 1: 使用sychronized代码块 实现循环递增序列号生成器
//只有一个线程可以更新序列号, 保证多线程情况下序列号正常更新
public short nextSequence() {
synchronied(this) {
if (sequence >= 999) {
sequence = 0;
}
else {
sequence++:
}
return sequence;
}
}
Example 2: 使用synchronized代码块重写银行账户的transfer方法:
class Bank {
private final Object lock = new Object();
public void transfer(int from, int to, int amount) {
synchronized (lock) {
accounts[from] -= amount;
accounts[to] += amount;
}
System.out.println(. . .);
}
}
synchronized 修饰静态方法
- synchronized 关键字可以修饰静态方法, 也就是对 类 加锁, 也叫做类锁
- 用static修饰的同步函数使用的锁为.class文件
public class Car {
public static synchronized void staticRuning1(Thread thread){
System.out.println(thread.getName()+ " static car1 得到锁");
System.out.println("------ static car1 is running ------");
working();
System.out.println(thread.getName()+ " static car1 释放锁");
System.out.println();
}
public static synchronized void staticRuning2(Thread thread){
System.out.println(thread.getName()+ " static car2 得到锁");
System.out.println("------ static car2 is running ------");
working();
System.out.println(thread.getName()+ " static car2 释放锁");
System.out.println();
}
public static void working(){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
public class test02 {
public static void main(String[] args) {
//线程1 类
Thread t1 = new Thread(){
@Override
public void run() {
Car.staticRuning1(Thread.currentThread()); //同步类方法1
}
};
t1.start();
//线程2 类
Thread t2 = new Thread(){
@Override
public void run() {
Car.staticRuning2(Thread.currentThread()); //同步类方法2
}
};
t2.start();
}
}
output --- Car类不能同时访问两个静态方法
Thread-0 static car1 得到锁
------ static car1 is running ------
Thread-0 static car1 释放锁
Thread-1 static car2 得到锁
------ static car2 is running ------
Thread-1 static car2 释放锁
sychronized的可重入性
- 内部锁是可重入锁, monitor有一个计数器记录目前线程的所有权, 为0时代表无锁, 为1时代表已被抢占, 当拥有锁的线程再次申请时, 计数器会进行加一操作
- 当释放时, monitor会进行减一操作
- 可重入锁的好处是可以避免一定程度的死锁情况(自己调用自己), 可以递归调用
- Example 1: 同一类的同一方法
- 因为方法被synchronized修饰,如果不可重入的话,无法执行递归,输出0和1代表进入了两次method方法,说明了Synchronized的可重用性。
public class SynchronizedReusing {
public static void main(String[] args) {
SynchronizedReusing reusing = new SynchronizedReusing();
reusing.method(0);
}
public synchronized void method(int i){
if(i==1){
System.out.println(i++);
return;
}
System.out.println(i++);
method(i);
}
}
Output:
0
1
Process finished with exit code 0
- Example 2: 同一类的不同方法
- 两个方法都被synchronized修饰,如果不可重入的话,method是无法访问method1的(锁对象都是this, 会有死锁),说明了Synchronized的可重用性
public class SynchronizedReusing {
public static void main(String[] args) {
SynchronizedReusing reusing = new SynchronizedReusing();
reusing.method(0);
}
public synchronized void method(int i){
System.out.println("method " + i++);
method1(i);
}
public synchronized void method1(int i){
System.out.println("method1 " + i);
}
}
Output:
method 0
method1 1
Process finished with exit code 0
public class SynchronizedReusing {
public synchronized void method(){
System.out.println("父类的方法");
}
}
class SynchronizedReusingSon extends SynchronizedReusing {
@Override
public synchronized void method() {
System.out.println("子类的方法");
super.method();
}
public static void main(String[] args) {
SynchronizedReusingSon son = new SynchronizedReusingSon();
son.method();
}
}
Output:
子类的方法
父类的方法
Process finished with exit code 0
``