弄浪的鱼

我们知道当堆中内存满了的时候,JVM就会使用可达性分析算法,检查对象是否有被 GC Root 引用。如果一个对象没有被任何 GC Root 引用,那就说明它是一个无效的对象,就会被垃圾回收器回收。此外,即使是被一个 GC Root 引用,但如果是弱引用,那这个对象任然有被回收的风险。

现在我们知道了什么情况下一个对象会被回收,那 JVM 是怎么回收一个对象的呢?

我们知道多个线程同时读写同一共享变量会导致并发问题。

一种解决方案是使用 Immutability 模式,如果共享变量在初始化之后就不会改变,只能读取,那么无论多少个线程同时读这个共享变量都不会出现并发问题。比如说 Java 中的 Long、Integer、Short、Byte 等基本数据类型的包装类的实现。

另一种解决方案是突破共享变量,没有共享变量就不会有并发问题。那么如何避免共享呢?思路其实很简单,就是每个线程拥有自己的变量,彼此不共享,就不会有共享问题。

具体来说有两种方法:线程封闭和线程本地存储(ThreadLocal)。

在正式了解 Java 类加载机制的细节之前,我们有必要了解一下 JVM 整体的运行原理,对 JVM 运行机制的整体脉络进行一次梳理。

JVM 整体运行原理

首先我们会有一系列以 .java 结尾的源文件。要把这些源文件发布到线上,我们需要将其打成 jar 包,或者打成 war 包。这个打包的过程就是将 .java 文件编译.class 文件。接着就可以使用 Tomcat 这样的容器,或者 java 命令来运行一个 jar 包中的文件。

这里有一个问题,编译好的 .class 字节码是怎么运行起来的呢?

在并发编程中有两个核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信和协作。解决这两个问题的方法前人已经替我们总结出来理论模型,分别是管程模型信号量模型.

MESA管程模型

Java中实现管程有两种方式

  1. 一是使用synchronized关键字给代码块添加隐式锁实现互斥,同时使用notify()notifyAll()实现同步
  2. 二是使用 Java SDK 并发包下的 Lock 和 Condition 两个接口,来实现管程.
    1. Lock 的特性包括:能够响应中断、支持超时和非阻塞地获取锁
    2. Condition 实现了管程模型里面的条件变量。

注:Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。Java 并发包下的 Lock&Condition则则支持多个条件变量。

在 Java 领域,实现并发程序的主要手段是多线程。线程是操作系统的概念,虽然不同语言对线程操作进行了不同的封装,但是万变不离其综。

通用的线程模型可以用「五态模型」来描述,它们分别是:初始状态,可运行状态,运行状态,休眠状态和终止状态。

通用线程转换图——五态模型

这五种状态在不同编程语言里会有简化合并。例如,C 语言的 POSIX Threads 规范,就把初始状态和可运行状态合并了;Java 语言里则把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而 JVM 层面不关心这两个状态,因为 JVM 把线程调度交给操作系统处理了。

除了简化合并,这五种状态也有可能被细化,比如,Java 语言里就细化了休眠状态。

题图:https://unsplash.com/photos/FjRNGCLnmV0

我们来想象一个场景。现在你是清朝末年账房里的一个先生,张三要给李四转4两银子,你从柜台取过张三的账本,发现李四的账本不在了,于是你想先留着张三的账本,等会儿再去柜台看看李四的账本别人用完没。

没成想,李四想着给张三转5两银子,另一个账房先生和你同时取了李四的账本。你俩想法一样,他留着李四的账本,等着一会儿去拿张三的账本。

现在的局面正是俩人互相「死等」对方的账本,在代码的世界,这种局面就叫做「死锁」。死锁用书面点的方法来说就是:一组互相竞争的线程互相等待,导致「永久」阻塞的现象。