Java编程语言的优势之一是它支持语言级的多线程。大部分支持都集中在协调对多个线程之间共享的数据的访问。
JVM将正在运行的Java应用程序的数据存放到多个运行时数据区域中:一个或多个Java堆栈,堆和方法区。
在JVM内部,每个线程都被分配一个虚拟机栈,其中包含其他线程无法访问的数据,包括线程调用的每个方法的局部变量,参数和返回值。虚拟机栈上的数据仅限于基本类型和对象引用。在JVM中,无法将实际对象的存放在虚拟机栈上,所有对象都分配在堆上。
JVM中只有一个堆,并且所有线程都共享它。堆只包含对象。无法在堆上分配单独的原始类型或对象引用 - 这些东西必须是对象的一部分。数组也分配在堆上,包括基本类型的数组,但在Java中,数组也是对象。
除了虚拟机栈和堆之外,还有方法区,它包含程序使用的所有类(或静态)变量。方法区域类似于堆栈,因为它只包含基本类型和对象引用。但是,与虚拟机栈不同,方法区域中的类变量由所有线程共享。
- 对象和类锁如上所述,JVM中的两个存储区包含所有线程共享的数据。这些是:堆,包含所有对象
- 方法区域,包含所有类变量
如果多个线程需要同时使用相同的对象或类变量,则必须很好的控制对数据的访问。否则,程序将出现不可预测的问题。
为了协调多个线程之间的共享数据访问,Java虚拟机将锁与每个对象和类相关联。锁就像一个特权,任何时候只有一个线程可以“拥有”。如果线程想要锁定特定对象或类,它会询问JVM。在线程向JVM发出锁定之后JVM会为线程提供锁定。当线程不再需要锁时,它会将锁归还给JVM。如果另一个线程请求了相同的锁,则JVM将锁传递给另外一个线程。
类锁在实现上为对象锁。当JVM加载类文件时,它会创建一个类实例java.lang.Class。锁定类时,实际上是锁定该类的Class对象。
线程无需获取锁来访问实例或类变量。但是,如果一个线程确实获得了一个锁,那么没有其他线程可以访问锁定的数据,直到拥有该锁的线程释放它为止。
监视器
JVM将锁与监视器结合使用。监视器相当于是监护人,它监视一系列代码,确保一次只有一个线程执行代码。
每个监视器都与对象引用相关联。当线程到达监视器监视下的代码块中的第一条指令时,线程必须获取对引用对象的锁定。在获得锁之前,不允许线程执行代码。一旦获得锁定,线程就会进入受保护代码块。
当线程离开块时,无论它如何离开块,它都会释放相关对象的锁定。
多个锁
允许单个线程多次锁定同一对象。对于每个对象,JVM维护对象被锁定的次数。解锁对象的计数为零。当线程第一次获取锁定时,计数增加到1。每次线程获取对同一对象的锁定时,计数都会递增。每次线程释放锁定时,计数都会递减。当计数达到零时,锁被释放并可供其他线程使用。
同步块
在Java语言术语中,必须访问共享数据的多个线程的协调称为同步。该语言提供了两种内置方式来同步对数据的访问:使用synchronized语句或同步方法。
同步语句
要创建synchronized语句,请使用synchronized带有表达式的关键字,该表达式的计算结果为对象引用,如reverseOrder()下面的方法所示:
class KitchenSync { private int[] intArray = new int[10]; void reverseOrder() { synchronized (this) { int halfWay = intArray.length / 2; for (int i = 0; i < halfWay; ++i) { int upperIndex = intArray.length - 1 - i; int save = intArray[upperIndex]; intArray[upperIndex] = intArray[i]; intArray[i] = save; } } } }
在上面的例子中,在当前对象(this)上获取锁之前,不会执行synchronized块中包含的语句。如果代替this引用,表达式产生对另一个对象的引用,则在该线程继续之前将获取与该对象相关联的锁。
看一下该类方法生成的字节码信息
两个操作码monitorenter和monitorexit,用于方法中的同步块。
当JVM遇到monitorenterJ时,它获取虚拟机栈上对象引用的对象的锁。如果线程已拥有该对象的锁,则递增计数。
当JVM遇monitorexit时,计数递减。当计数达到零时,监视器被释放。
请注意,即使从同步块中抛出异常,catch子句也可确保锁定的对象将被解锁。无论退出同步块如何,当线程进入块时获取的对象锁定肯定会被释放。
同步方法
要同步整个方法,只需将synchronized关键字包含在方法限定符之一中,如下所示:
class HeatSync { private int[] intArray = new int[10]; synchronized void reverseOrder() { int halfWay = intArray.length / 2; for (int i = 0; i < halfWay; ++i) { int upperIndex = intArray.length - 1 - i; int save = intArray[upperIndex]; intArray[upperIndex] = intArray[i]; intArray[i] = save; } } }
JVM不使用任何特殊操作码来调用或从同步方法返回。当JVM解析对方法的符号引用时,它确定方法是否已同步。如果是,则JVM在调用方法之前获取锁。对于实例方法,JVM获取与调用该方法的对象关联的锁。对于类方法,它获取与方法所属的类关联的锁。在synchronized方法完成后,无论是通过返回还是通过抛出异常来完成,都会释放锁。
希望大家能够支持我的文章,点个关注不迷路,点完关注的小伙伴可以找我私聊领取Java学习资料、大厂面试题库等福利!也欢迎大家在评论区留言交流JVM见解。