synchronized用法加锁原理
目录
- 使用场景
- 不同场景加锁对象结论验证实验
- 实验1: synchronized 修饰方法,加锁对象是类实例,不同实例之间的锁互不影响
- 实验2: synchronized 加在静态方法上,加锁对象是方法所在类,不同类实例之间相互影响
- 实验3:synchronized 加在代码块上,加锁对象为括号中对象,而不是所在类实例对象
- 实现原理
- Bibliography
使用场景
- 修饰方法;
- 修饰静态方法
- 修饰代码块;
当修饰普通方法时,加锁对象是方法所在类实例对象;修饰静态方法,加锁对象是方法所在类;修饰代码块,加锁对象是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。
实验分析: 线程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退出方法之后才可以。
实验分析: 线程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退出同步代码块之后才进入了同步代码块。
实验分析: 由于方法没有设置锁,两个线程都可以进入方法内,线程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
-
深入分析Synchronized原理(阿里面试题)
-
Java对象的内存布局
-
synchronized底层实现原理(保证看懂)
-
[java] synchronized关键字用法及实现原理详解