Java 多线程 --- 线程同步 内部锁synchronized

Java 多线程 --- 线程同步 内部锁synchronized

  • Intrinsic Lock (Monitor)
  • synchronized 关键字
    • synchronized 修饰实例方法
    • synchronized 修饰代码块
    • synchronized 修饰静态方法
  • sychronized的可重入性

Intrinsic Lock (Monitor)

  • synchronized锁机制是基于monitor对象(也被叫做Monitor 或 Monitor Lock监视器锁或 Intrinsic Lock)实现的,每个对象都存在一个monitor对象与之关联,对象头中有一块专门的内存区域用于存储与之关联的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 操作)”这条规则获得的。
  • 有序性:

具体实现

  • 在修饰代码块时,字节码层面上是通过 montiorentermonitorexit 指令来实现的锁获取与释放动作. 当线程进入到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对象
  • Intrinsic Lock的一些限制
  • 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
``

你可能感兴趣的:(#,Java,---,多线程并发,java,开发语言)