并发编程之:synchronized

大家好,我是小黑,一个在互联网苟且偷生的农民工。

之前的文章中跟大家分享了关于Java中线程的一些概念和基本的使用方法,比如如何在Java中启动一个线程,生产者消费者模式等,以及如果要保证并发情况下多线程共享数据的访问安全,操作的原子性,使用到了synchronized关键字。今天主要和大家聊一聊synchronized关键字的用法和底层的原理。

为什么要用synchronized

相信大家对于这个问题一定都有自己的答案,这里我还是要啰嗦一下,我们来看下面这段车站售票的代码:

/** * 车站开两个窗口同时售票 */ public class TicketDemo { public static void main(String[] args) { TrainStation station = new TrainStation(); // 开启两个线程同时进行售票 new Thread(station, "A").start(); new Thread(station, "B").start(); } } class TrainStation implements Runnable { private volatile int ticket = 10; @Override public void run() { while (ticket > 0) { System.out.println("线程" + Thread.currentThread().getName() + "售出" + ticket + "号票"); ticket = ticket - 1; } } }

上面这段代码是没有做考虑线程安全问题的,执行这段代码可能会出现下面的运行结果:

image

可以看出,两个线程都买出了10号票,这在实际业务场景中是绝对不能出现的。(你去坐火车有个大哥说你占了他的座,让你滚,还说你是票贩子,你气不气)

那因为有这种问题的存在,我们应该怎么解决呢?synchronized就是为了解决这种多线程共享数据安全问题的。

使用方式

synchronized的使用方式主要以下三种。

同步代码块

public static void main(String[] args) { String str = "hello world"; synchronized (str) { System.out.println(str); } }

同步实例方法

class TrainStation implements Runnable { private volatile int ticket = 100; // 关键字直接写在实例方法签名上 public synchronized void sale() { while (ticket > 0) { System.out.println("线程" + Thread.currentThread().getName() + "售出" + ticket + "号票"); ticket = ticket - 1; } } @Override public void run() { sale(); } }

同步静态方法

class TrainStation implements Runnable { // 注意这里ticket变量声明为static的,因为静态方法只能访问静态变量 private volatile static int ticket = 100; // 也可以直接放在静态方法的签名上 public static synchronized void sale() { while (ticket > 0) { System.out.println("线程" + Thread.currentThread().getName() + "售出" + ticket + "号票"); ticket = ticket - 1; } } @Override public void run() { sale(); } } 字节码语义

通过程序运行,我们发现通过synchronized关键字确实可以保证线程安全,那计算机到底是怎么保证的呢?这个关键字背后到底做了些什么?我们可以看一下java代码编译后的class文件。首先来看同步代码块编译后的class。通过javap -v 名称可以查看字节码文件:

public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: ldc #2 // String hello world 2: astore_1 3: aload_1 4: dup 5: astore_2 6: monitorenter // 监视器进入 7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 10: aload_1 11: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 14: aload_2 15: monitorexit // 监视器退出 16: goto 24 19: astore_3 20: aload_2 21: monitorexit 22: aload_3 23: athrow 24: return

注意看第6行和第15行,这两个指令是增加synchronized代码块之后才会出现的,monitor是一个对象的监视器,monitorenter代表这段指令的执行要先拿到对象的监视器之后,才能接着往下执行,而monitorexit代表执行完synchronized代码块之后要从对象监视器中退出,也就是要释放。所以这个对象监视器也就是我们所说的锁,获取锁就是获取这个对象监视器的所有权。

接下来我们在看看synchronized修饰实例方法时的字节码文件是什么样的。

public synchronized void sale(); descriptor: ()V //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法 flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=3, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field ticket:I // 省略其他无关字节码

可以看到synchronized修饰实例方法上之后不会再有monitorenter和monitorexit指令,而是直接在这个方法上增加一个ACC_SYNCHRONIZED的flag。当程序在运行时,调用sale()方法时,会检查该方法是否有ACC_SYNCHRONIZED访问标识,如果有,则表明该方法是同步方法,这时候还行线程会先尝试去获取该方法对应的监视器(monitor)对象,如果获取成功,则继续执行该sale()方法,在执行期间,任何其他线程都不能再获取该方法监视器的使用权,知道该方法执行完毕或者抛出异常,才会释放,其他线程可以重新获得该监视器。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzwzfs.html