分析JVM线程同步原理

线程和共享数据

Java 的一个优势就是在语言层面支持多线程,这种支持集中在协调多线程对数据的访问上。java

JVM 将运行时数据划分为几个区域:一个或多个栈,一个堆,一个方法区。数组

在 JVM 中,每一个线程拥有一个栈,其余线程没法访问,里面的数据包括:局部变量,函数参数,线程调用的方法的返回值。栈里面的数据只包含原生数据类型和对象引用。在 JVM 中,不可能将实际对象的拷贝放入栈。全部对象都在堆里面。多线程

JVM 只有一个堆,全部线程都共享它。堆中只包含对象,把单独的原生类型或者对象引用放入堆也是不可能的,除非它们是对象的一部分。数组也在堆中,包括原生类型的数组,由于在 Java 中,数组也是对象。函数

除了栈和堆,另外一个存放数据的区域就是方法区了,它包含程序中使用到的全部类(静态)变量。方法区相似于栈,也只包含原生类型和对象引用,可是又跟栈不一样,方法区中类变量是线程共享的。this

对象锁和类锁

正如前面所说,JVM 中的两个区域包含线程共享的数据,分别是:spa

  1. :包含全部对象
  2. 方法区:包含全部类变量

若是多个线程须要同时使用同一个对象或者类变量,它们对数据的访问必须被恰当地控制。不然,程序会产生不可预测的行为。线程

为了协调多个线程对共享数据的访问,JVM 给每一个对象和类关联了一个锁。锁就像是任意时间点只有一个线程可以拥有的特权。若是一个线程想要锁住一个特定的对象或者类,它须要向 JVM 请求锁。线程向 JVM 请求锁以后,可能很快就拿到,或者过一会就拿到,也可能永远拿不到。当线程不须要锁以后,它把锁还给 JVM。若是其余线程须要这个锁,JVM 会交给该线程。code

类锁的实现其实跟对象锁是同样的。当 JVM 加载类文件的时候,它会建立一个对应类java.lang.Class对象。当你锁住一个类的时候,你其实是锁住了这个类的Class对象。对象

线程访问对象实例或者类变量的时候不须要获取锁。可是若是一个线程获取了一个锁,其余线程不能访问被锁住的数据,直到拥有锁的线程释放它。同步

管程

JVM 使用锁和管程协做。管程监视一段代码,保证一个时间点内只有一个线程能执行这段代码。

每一个管程与一个对象引用关联。当线程到达管程监视代码段的第一条指令时,线程必须获取关联对象的锁。线程不能执行这段代码直到它获得了锁。一旦它获得了锁,线程能够进入被保护的代码段。

当线程离开被保护的代码块,无论是如何离开的,它都会释放关联对象的锁。

屡次锁定

一个线程被容许锁定一个对象屡次。对于每一个对象,JVM 维护了一个锁的计数器。没有被锁的对象计数为 0。当一个线程第一次获取锁,计数器自增变为 1。每次这个线程(已经获得锁的线程)请求同一个对象的锁,计数器都会自增。每次线程释放锁,计数器都会自减。当计数器变为 0 时,锁才被释放,能够给别的线程使用。

同步块

在 Java 语言的术语中,协调多个线程访问共享数据被称为同步(synchronization)。Java 提供了两种内建的方式来同步对数据的访问:

  1. 同步语句
  2. 同步方法

同步语句

为了建立同步语句,你须要使用synchronized关键字,括号里面是同步的对象引用,以下所示:

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引用的对象锁。若是不是锁住this引用,而是锁住其余对象,在线程执行同步块语句以前,它须要得到该对象的锁。

有两个字节码monitorentermonitorexit,被用来同步方法中的同步块

字节码 操做数 描述
monitorenter 取出对象引用,请求与对象引用关联的锁
monitorexit 取出对象引用,释放与对象引用关联的锁

monitorenter被 JVM 执行时,它请求栈顶对象引用关联的锁。若是该线程已经拥有该对象的锁,计数器自增。每次monitorexit被执行,计数器自减。当计数器变为 0 时,该锁被释放。

注意:当同步块中抛出异常时,catch语句保证对象锁被释放。无论同步块是如何退出的,JVM 保证线程会释放锁。

同步方法

为了同步整个方法,你只须要在方法声明前面加上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 要求获得该实例对象的锁。对于类方法,JVM 要求获得类锁。在同步方法完成以后,无论它是正常返回仍是抛出异常,锁都会被释放。

相关文章
相关标签/搜索