当问到多线程时候如何解决线程安全的问题时候,大部分人都知道加锁。提到锁最先接触到的就是Synchronized关键字。
当我们想要保证一个共享资源在同一时间只会被一个线程访问到时,我们可以在代码中使用synchronized关键字对类或者对象加锁
synchronized常用的有以下三种使用方式:
▪修饰代码块,即同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
▪ 修饰普通方法,即同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
▪ 修饰静态方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象。
/**
* 对象锁
*/
public class Test{
// 对象锁:形式1(方法锁)
public synchronized void method1(){
System.out.println("对象锁也是方法锁");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
}
// 对象锁:形式2(代码块形式)
public void method2(){
synchronized (this){
System.out.println("对象锁2");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
/**
* 方法锁(即对象锁中的形式1)
*/
public synchronized void method1(){
System.out.println("对象锁也是方法锁");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
}
/**
* 类锁
*/
public class Test{
// 类锁:形式1 :锁静态方法
public static synchronized void method1(){
System.out.println("类锁1");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
}
// 类锁:形式2 :锁静态代码块
public void method2(){
synchronized (Test.class){
System.out.println("类锁2");
try{
Thread.sleep(500);
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
使用javap -c Test可以查看编译之后的具体信息
/修饰方法
public synchronized void method1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String 对象锁也是方法锁
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
//类锁
public void method2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/demo/Test
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String 类锁2
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
可以看到在同步块的入口和出口分别有monitorenter和monitorexit指令。当执行monitorenter指令时,线程试图获取锁也就是获取monitor(monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行monitorexit指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
在synchronized修饰方法时是添加ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
要想真正了解Monitor怎么实现加锁和解锁的,需要先了解对象头。让我们看看对象模型。
HotSpot虚拟机中,设计了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。