synchronized用法加锁原理

目录

  • 使用场景
  • 不同场景加锁对象结论验证实验
    • 实验1: synchronized 修饰方法,加锁对象是类实例,不同实例之间的锁互不影响
    • 实验2: synchronized 加在静态方法上,加锁对象是方法所在类,不同类实例之间相互影响
    • 实验3:synchronized 加在代码块上,加锁对象为括号中对象,而不是所在类实例对象
  • 实现原理
  • Bibliography

使用场景

  1. 修饰方法;
  2. 修饰静态方法
  3. 修饰代码块;
    当修饰普通方法时,加锁对象是方法所在类实例对象;修饰静态方法,加锁对象是方法所在类;修饰代码块,加锁对象是synchronized(objXXX)括号中的对象。为了证明synchronized在不同三种场景中加锁对象总结一致,进行了下面三个验证实验。

不同场景加锁对象结论验证实验

实验1: synchronized 修饰方法,加锁对象是类实例,不同实例之间的锁互不影响

实验步骤: 一个类中定义两个同步方法, 创建类的一个实例对象,开启两个线程,在两个线程中使用同一个对象实例分别调用不同的同步方法。

假设猜想: 线程B由于无法获得实例对象锁而无法进入同步方法2,需要线程A退出方法1之后,线程B才可以进入方法2。

// 同步方法类(所有实验公用)
public class BankAccount{
    String accountName;
    Double balance;

    public BankAccount(String accountName,double balance){
        this.accountName = accountName;
        this.balance = balance;
    }

    public synchronized double deposit(double amount){
        System.out.println(Thread.currentThread().getName()+ " deposit begin");
        balance = balance + amount;
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+ "deposit end");
        return balance;
    }

    public synchronized double  withdraw(double amount){
        System.out.println(Thread.currentThread().getName() + "withdraw begin");
        balance = balance - amount;
        System.out.println(Thread.currentThread().getName() + "withdraw end");
        return balance;
    }

    public static synchronized void staticMethod(double amount){
        System.out.println(Thread.currentThread().getName()+ "staticDeposit begin");

        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+ "staticDeposit end");

    }

    public void synchronizedCodeBlock(double amount){
        System.out.println(Thread.currentThread().getName()+ "synchronizedCodeBlock1 method begin");
        synchronized (accountName) {
            System.out.println(Thread.currentThread().getName()+ "synchronizedCodeBlock1  begin");
            try {
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+ "synchronizedCodeBlock1  end");
        }

        System.out.println(Thread.currentThread().getName()+ "synchronizedCodeBlock1 method end");
    }
}
// 自定义线程池(所有实验公用)
public class MyThreadFactory {
    public static ThreadPoolExecutor threadPool() {
       return new ThreadPoolExecutor(5,
               10,
               1,
               TimeUnit.SECONDS,
               new ArrayBlockingQueue<>(985),
                new ThreadPoolExecutor.AbortPolicy());
    }
}
// 测试类
public class SynchronizedUsage {
    public static void main(String[] args) {
        SynchronizedUsage usage = new SynchronizedUsage();
        usage.test1();
    }
   public void test1() {
    BankAccount myBankAccount = new BankAccount("稻草", 0);
    ThreadPoolExecutor executor = MyThreadFactory.threadPool();
    executor.execute(()-> myBankAccount.deposit(100));
    executor.execute(()-> myBankAccount.withdraw(100));
    }
}

实验结果: 线程B尝试获得锁失败,等线程A退出方法1之后才进入了方法2。
synchronized用法加锁原理_第1张图片
实验分析: 线程A虽然睡眠,但这个过程依然持有对象锁,所以线程B虽然尝试调用实例对象不同的同步方法,但由于对象已经被加锁,所以调用同步方法2失败。

实验2: synchronized 加在静态方法上,加锁对象是方法所在类,不同类实例之间相互影响

实验步骤: 一个类中定义一个静态同步方法,创建两个含有静态同步方法对象实例,开启两个线程,在线程A中,实例1调用静态同步方法,并在方法中睡眠5秒钟,同时实例2也调用静态同步方法。

假设猜想: 线程B由于无法获得类锁而无法进入同步静态方法。

public class SynchronizedUsage {

    public static void main(String[] args) {
        SynchronizedUsage usage = new SynchronizedUsage();
        usage.test2();
    }
    public void test2() {
        BankAccount myBankAccount1 = new BankAccount("稻草", 0);
        BankAccount myBankAccount2 = new BankAccount("稻草", 0);
        executor.execute(()-> myBankAccount1.staticMethod(100));
        executor.execute(()-> myBankAccount2.staticMethod(100));
    }
}

实验结果: 线程B尝试获得锁失败,等线程A退出方法之后才可以。
synchronized用法加锁原理_第2张图片

实验分析: 线程A虽然睡眠,并且两个线程中使用的不同实例对象,但是线程A是在类上进行加锁,线程B再尝试调用同步方法失败。

实验3:synchronized 加在代码块上,加锁对象为括号中对象,而不是所在类实例对象

实验步骤: 方法中的一个部分代码使用synchronized修饰,对局部变量进行加锁,启动两个线程,在两个线程中使用相同实例调用分别调用两个同步代码块的方法,两个同步。

实验猜想: 两个线程都可以进入方法,在同一时间,只能有一个线程访问同步代码块。

public class SynchronizedUsage {
    public static void main(String[] args) {
        SynchronizedUsage usage = new SynchronizedUsage();
        usage.test3();
    }
 public void test3() {
    
        ThreadPoolExecutor executor = MyThreadFactory.threadPool();
        BankAccount myBankAccount = new BankAccount("稻草", 0);
        executor.execute(()-> myBankAccount.synchronizedCodeBlock(100));
        executor.execute(()-> myBankAccount.synchronizedCodeBlock(100));
    }
}

实验结果: 线程A进入同步代码块之后,线程B只有在线程A退出同步代码块之后才进入了同步代码块。
synchronized用法加锁原理_第3张图片

实验分析: 由于方法没有设置锁,两个线程都可以进入方法内,线程A进入代码块之后,线程B尝试进入代码块失败,因为线程A已经获得synchronized后括号内对象的锁,线程B只有在线程A退出同步代码块之后,释放对象锁,线程B才可以进入同步代码块,在此之前,线程B只能阻塞等待。

实现原理

  • 同步代码块实现依赖的是JVM的moniterenter和moniterexit两个指令。
  • 同步方法实现依赖的是方法区类元数据中标志位。

被synchronized修饰的方法或者代码块是通过对对象加锁保证并发状态下,共享资源的访问安全,加锁的的实现主要通过monitorenter和monitorexit两个指令实现,经过JVM编译的字节码,会在同步代码块开始和结束分别插入monitorenter和monitorexit,并且每个对象唯一对应一个monitor(存储在对象头的markword中),当线程遇到monitorenter指令,该对象的monitor将被当前线程劫持,对象加锁。monitor的本质是依赖于底层操作系统的Mutex Lock实现,Mutex是操作系统定义信号量的关键字,是操作系统实现同步所使用的技术,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。note:一个synchronized有两个monitorexit,这是保证锁一定可以得到释放,第一个是程序正常执行退出,第二个是程序发生异常,由虚拟机释放。

同步方法是通过对象头和类方法表中access_flags标志位实现代码同步的,被synchronized修饰的方法,对应编译好字节码方法表对应方法修饰access_flags= 1。

Bibliography

  1. 深入分析Synchronized原理(阿里面试题)

  2. Java对象的内存布局

  3. synchronized底层实现原理(保证看懂)

  4. [java] synchronized关键字用法及实现原理详解

你可能感兴趣的:(并发编程,多线程,java,jvm)