当前位置: 首页 > 科技观察

如果有人问你什么是synchronized,就把这篇文章发给他

时间:2023-03-16 00:14:04 科技观察

当有人问你Java内存模型是什么的时候,把这篇文章发给他。我们已经介绍过,Java语言提供了一系列与并发处理相关的关键字,如synchronized、volatile、final、concurrentpackages等。在中,有这样一段话:synchronized关键字可以作为其中之一需要原子性、可见性和有序性三个特性时的解决方案,好像是“***”。事实上,大多数并发控制操作都可以使用synchronized来完成。海明威在他的《午后之死》中说:“冰山运动的磅礴,是因为它只有八分之一在水面上。”对于程序员来说,synchronized只是一个关键字,使用起来非常简单。之所以我们可以不用多想就可以处理多线程的问题,是因为这个关键字帮助我们屏蔽了很多细节。然后,本文围绕synchronized展开,主要介绍它的用法、原理,以及如何提供原子性、可见性和顺序保证。Synchronized的用法Synchronized是Java提供的并发控制关键字。主要有两个用途,分别是同步方法和同步代码块。也就是说,synchronized既可以修饰方法,也可以修饰代码块。代码如下:/***@authorHollis18/08/04.*/publicclassSynchronizedDemo{//同步方法publicsynchronizedvoiddoSth(){System.out.println("HelloWorld");}//同步代码块publicvoiddoSth1(){synchronized(SynchronizedDemo.class){System.out.println("HelloWorld");}}}synchronized修饰的代码块和方法只能同时被单线程访问。synchronized的实现原理是synchronized,是Java中解决并发情况下数据同步访问的一个非常重要的关键字。当我们要保证某个共享资源一次只能被一个线程访问时,我们可以在代码中使用synchronized关键字来锁定类或对象。在深入理解多线程(一)——Synchronized的实现原理中,我已经介绍过它的实现原理。为了保证知识的完整性,这里简单介绍一下。详情请阅读原文。我们反编译上面的代码得到如下代码:publicsynchronizedvoiddoSth();descriptor:()Vflags:ACC_PUBLIC,ACC_SYNCHRONIZEDCode:stack=2,locals=1,args_size=10:getstatic#2//Fieldjava/lang/System.out:Ljava/io/PrintStream;3:ldc#3//StringHelloWorld5:invokevirtual#4//Methodjava/io/PrintStream.println:(Ljava/lang/String;)V8:returnpublicvoiddoSth1();descriptor:()Vflags:ACC_PUBLICode:stack=2,locals=3,args_size=10:ldc#5//classcom/hollis/SynchronizedTest2:dup3:astore_14:monitorenter5:getstatic#2//Fieldjava/lang/System.out:Ljava/io/PrintStream;8:ldc#3//StringHelloWorld10:invokevirtual#4//Methodjava/io/PrintStream.println:(Ljava/lang/String;)V13:aload_114:monitorexit15:goto2318:aload_219:aload_120:monitorexit21:aload_222:athrow23:return可以从反编译代码可以看出,对于同步方式,JVM使用了ACC_SYNCHRONIZED标签来实现同步。对于同步代码块。JVM使用monitorenter和monitorexit两条指令来实现同步。在爪哇?VirtualMachineSpecification,里面有介绍同步方法和同步代码块的实现原理。我翻译成中文是这样的:method-levelsynchronizationisimplicit。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当一个线程要访问一个方法时,它会检查是否有ACC_SYNCHRONIZED。如果设置了,需要先获取监听锁,然后开始执行方法,执行完方法后释放监听锁。这时候,如果其他线程请求执行该方法,就会因为无法获得监听锁而被阻塞。值得注意的是,如果在方法执行过程中出现异常,并且在方法内部没有处理异常,那么在方法外部抛出异常之前,监听锁会自动释放。同步代码块使用两条指令实现,monitorenter和monitorexit。执行monitorenter命令可以理解为加锁,执行monitorexit可以理解为释放锁。每个对象都维护一个计数器,记录它被锁定的次数。解锁对象的计数器为0。当一个线程获得锁(执行monitorenter)时,计数器递增为1。当同一个线程再次获得该对象的锁时,计数器再次递增。当同一个线程释放锁(执行monitorexit指令)时,计数器再次递减。当计数器为0时,锁将被释放,其他线程可以获得锁。ACC_SYNCHRONIZED、monitorenter、monitorexit都是基于Monitor实现的。在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现。ObjectMonitor类中提供了enter、exit、wait、notify、notifyAll等几个方法,当sychronized被锁定时,会调用objectMonitor的enter方法,解锁时会调用exit方法。(关于Monitor的更多细节参见深入理解多线程(四)——Monitor的实现原理)全部。我们分析过Java并发编程中的多线程问题:线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片后开始执行,时间片用完后,它就会失去CPU的使用权。因此,在多线程场景下,由于时间片在线程之间轮换,会出现原子性问题。在Java中,为了保证原子性,提供了两条高级字节码指令monitorenter和monitorexit。前面说过,这两条字节码指令在Java中对应的关键字是synchronized。通过monitorenter和monitorexit指令,可以保证synchronized修改的代码一次只能被一个线程访问,在锁释放之前不能被其他线程访问。因此在Java中可以使用synchronized来保证方法和代码块内的操作是原子的。当线程1执行monitorenter命令时,会锁住Monitor。加锁后,其他线程无法获得锁,除非线程1主动解锁。即使在执行过程中,由于某种原因,比如CPU时间片用完了,线程1让出CPU,但他并没有解锁。并且由于synchronized锁是可重入的,下一个时间片只能自己获取,代码会继续执行。直到所有代码执行完毕。这保证了原子性。同步和可见性可见性是指当多个线程访问同一个变量时,一个线程修改了该变量的值,其他线程可以立即看到修改后的值。当有人问你什么是Java内存模型时,我们发给他这篇文章进行分析:Java内存模型规定所有的变量都存放在主存中,每个线程都有自己的工作内存。线程中使用的变量的主内存副本保存在线程的工作内存中。线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存。不同的线程不能直接访问对方工作内存中的变量,线程间变量的传递需要自己的工作内存和主存之间进行数据同步。因此,可能会出现线程1改变了某个变量的值,而线程2不可见的情况。前面我们提到过,被synchronized修饰的代码在开始执行时会被锁定,执行完毕后会解锁。为了保证可见性,有这样一个规则:在解锁变量之前,必须将该变量同步回主存。这样解锁后,后续线程就可以访问修改后的值了。因此,被synchronized关键字锁定的对象的值是可见的。同步有序有序是指程序执行的顺序按照代码执行的顺序执行。当有人问你什么是Java内存模型时,我们发了这篇文章给他分析:除了时间片的引入,由于处理器优化和指令重排,CPU还可能对输入的代码进行重新排序。执行,比如load->add->save可能优化为load->save->add。这是可能存在有序问题的地方。这里要注意,synchronized不能禁止指令重排和处理器优化。也就是说,synchronized无法避免上面提到的问题。那么,为什么说synchronized也提供顺序保证呢?这是为了扩大秩序的概念。Java程序中的自然顺序可以用一句话来概括:如果在这个线程中观察,所有的操作都是自然有序的。如果一个线程观察另一个线程,则所有操作都是无序的。上面这句话也是《深入理解Java虚拟机》中的原句,但是怎么理解呢?周志明没有详细解释。这里简单展开一下,其实和as-if-serial语义有关。as-if-serial语义的含义是:无论怎么重排序(编译器和处理器提高并行度),单线程程序的执行结果都不会改变。编译器和处理器无论如何优化都必须遵守似串行语义。这里不详细描述as-if-serial语义。简单的说,as-if-serial语义保证单线程中指令重排有一定的限制,只要编译器和处理器遵守这个语义,那么就可以认为单线程程序执行完毕顺序地。当然,其实是有重排的,只是我们不需要关心这种重排的干扰。因此,由于synchronized修饰的代码,在同一时间只能被同一个线程访问。然后就是单线程执行。因此,它的顺序是可以保证的。synchronized和锁优化前面介绍了synchronized的用法、原理和对并发编程的影响。是一个很好用的关键字。Synchronized其实是借助Monitor来实现的。锁定时会调用objectMonitor的enter方法,解锁时会调用exit方法。其实只有在JDK1.6之前,synchronized的实现才会直接调用ObjectMonitor的enter和exit。这种锁称为重量级锁。所以在JDK1.6中,对锁做了很多优化,然后有轻量级锁,偏向锁,锁消除,自适应自旋锁,锁粗化(自旋锁在1.4有,但是默认关闭,jdk1.6默认开启),这些操作是为了更高效地在线程间共享数据,解决竞争问题。好了,关于synchronized关键字,我们介绍了它的用法、原理,以及如何保证原子性、顺序、可见性,也扩展了锁优化相关的信息和思考。后面我们会继续介绍volatile关键字以及他和synchronized的区别。敬请关注。【本文为专栏作家霍利斯原创文章,作者微信公众号Hollis(ID:hollishuang)】点此阅读更多本作者好文