深入理解Java虚拟机—Java内存模型与线程

上一篇:深入理解Java虚拟机—后端编译与优化

下一篇:深入理解Java虚拟机—线程安全与锁优化

Java内存模型与线程

一. 硬件的效率和一致性

让计算机并发执行若干个运算任务和更充分地利用计算机处理器的效能之间看起来是因果关系,但实际上没那么简单。CPU的运算能力十分强大,但是任何任务都不太可能单单靠CPU就能够完成,比如读取内存,存储的数据,网络的请求等IO操作是很难消除的,更糟糕的是存储设备与CPU之间的运算差距过大,所以在内存和CPU之间又会加一道高速缓存介质进行提速。为了充分利用CPU的性能,通常会采取异步IO的方式进行,这样CPU可以去处理其他的运算请求,如果是同步IO,则会阻塞在等待数据准备完成上,浪费CPU的时间。多线程技术也是为了更好的利用CPU的资源。

上面说到高速缓存,这个将内存的数据读取到缓存,再提供给CPU,当CPU修改后写回高速缓存,最后又同步到内存中。高速缓存会带来缓存一致性的问题,因为在多处理器系统中,每个处理器都有一个高速缓存,但是它们共享内存,这个时候就存在不同的CPU读取、修改内存数据不一致的问题,比如A读取变量a是1,B之前修改过这个值,但是没有来得及同步到内存中。为了解决这个一致性问题,需要处理器访问缓存时遵守一些协议,有:MSI、MESI、MOSI、Synapse、Firefly、Dragon Protocol等。
  
在这里插入图片描述
 这里说的内存模型可以理解成,在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。另外,除了增加高速缓存外,为了使得处理器的运算单元能充分利用,可能对输入的指令进行乱序执行优化,执行后将结果重组成顺序的结果。所以,如果一个任务依赖另一个任务的结果,顺序性并不能靠代码的先后顺序来保证,Java中也有类似的指令重排序

二. Java内存模型

1. 主内存与工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中读取变量的底层细节。这里的变量包括了类的实例字段,静态字段和构成数组对象的元素,不包括局部变量与方法参数,那个是线程私有的,不会有并发问题。为了获得更好的执行效能,Java内存模型没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。

内存模型规定所有的变量存储在主内存中,每条线程有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量和主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中完成,不能直接读写主内存中的变量(对volatile也不例外)。线程之间也无法直接访问对方工作内存中的变量,传递需要通过主内存完成。
这个内存模型和Java内存区域中的堆、栈、方法区划分层次不同,没有关系
在这里插入图片描述

主内存 在虚拟机中对应 java堆中的对象实例数据部分,在物理机中对应 物理硬件的内存RAM

2. 内存间交互操作

主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下8种操作来完成:

  1. lock(锁定):作用于主内存的变量,它把一个变量标识成一条线程独占的状态。
  2. unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程锁定。
  3. read(读取):作用于主内存的变量,它把一个变量的值从主存中传输到线程的工作内存中,以便随后的load动作。
  4. load(载入):作用于工作内存的变量,把read的变量放入内存的变量副本中。
  5. use(使用):作用于工作内存,把工作内存的一个变量的值交给执行引擎。
  6. assign(赋值):作用于工作内存,它把一个从执行引擎接收到的值赋值给工作内存的变量。
  7. store(存储):作用于工作内存,将内存变量传给主内存,随着方便后面wirite一同使用。
  8. wirte(写入):把Store的值传递到主内存中,方便以后随时write操作。

如果把一个变量从内存复制到工作内存,要顺序调用read和load操作,如果同步回去,要顺序执行store和write操作。但是只保证了顺序执行,而没有保证连续执行,比如read a、read b、load b、load a这种情况。

另外执行这8种基本操作需要满足以下规则:

  1. 不运行read和load、store和write操作之一单独出现。
  2. 不允许一个线程丢弃最近的assign操作,即修改必须同步变化
  3. 不允许一个线程无原因的把数据从线程工作内存同步回主内存中
  4. 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化的变量
  5. 一个变量同一个时刻只允许一条线程对其进行lock操作,但可以被一个线程重复lock,lock多少次,需要执行多少次unlock。
  6. 对一个变量进行lock,会清除工作内存中此变量的值,在使用的时候,需要重新执行load或者assign操作。
  7. 如果一个变量没有被lock,不允许进行unlock,也不允许unlock其他线程lock的变量
  8. 对变量进行unlock的时候,必须将数据回写到主内存中。

由于这些规定,和volatile的特殊规定,可以确定哪些内存访问操作在并发下是安全的。这种定义十分繁琐,实践很麻烦,所以采取happen before原则来确定一个访问是否在并发环境下安全

3. volatile的特殊规则

关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制

当一个变量被定义成volatile后,它将具备两种特性:

  • 第一是保证对所有线程的可见性,“可见性”指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的
  • 禁止指令重排序优化:对volatile变量增加一个内存屏障,不能把后面的指令重排到内存屏障之前

关于volatile变量的可见性的误解:“volatile变量对所有线程立即可见的,对volatile变量所有的写操作都能立刻反映到其他线程中,换句话说,volatile变量在各个线程中是一致的,所以基于volatile变量的运算在并发下是安全的”。这句话的论据部分并没有错,但是其论据并不能得出“基于volatile变量的运算在并发下是安全的”这个结论

volatile变量在各个线程中的工作内存中不存在一致性问题(在各个线程的工作内存中volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在不一致问题),但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的

package com.jvm.thread;

/**
 * volatile变量自增运算测试
 * @author xl69628
 *
 */
public class VolatileTest {
    public static volatile int race = 0;
    private static final int THREADS_COUNT = 20;
    
    public static void increase(){
        race++;
    }
    
    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for(int i = 0; i < THREADS_COUNT; i++){
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i = 0; i < 10000; i++){
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        
        //等待所有累加线程都结束
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        
        System.out.println(race);
        //如果代码正确并发,输出结果为200000。但是每次运行都不会得到期望的结果。
    }
}
G:\javaee\workspace\Test\bin\com\jvm\thread>javap -c VolatileTest
警告: 二进制文件VolatileTest包含com.jvm.thread.VolatileTest
Compiled from "VolatileTest.java"
public class com.jvm.thread.VolatileTest {
  public static volatile int race;

  static {};
    Code:
       0: iconst_0
       1: putstatic     #13                 // Field race:I
       4: return

  public com.jvm.thread.VolatileTest();
    Code:
       0: aload_0
       1: invokespecial #18                 // Method java/lang/Object."<init>":
()V
       4: return

  public static void increase();
    Code:
       0: getstatic     #13                 // Field race:I
       3: iconst_1
       4: iadd
       5: putstatic     #13                 // Field race:I
       8: return

  public static void main(java.lang.String[]);
    Code:
       0: bipush        20
       2: anewarray     #25                 // class java/lang/Thread
       5: astore_1
       6: iconst_0
       7: istore_2
       8: goto          37
      11: aload_1
      12: iload_2
      13: new           #25                 // class java/lang/Thread
      16: dup
      17: new           #27                 // class com/jvm/thread/VolatileTest
$1
      20: dup
      21: invokespecial #29                 // Method com/jvm/thread/VolatileTes
t$1."<init>":()V
      24: invokespecial #30                 // Method java/lang/Thread."<init>":
(Ljava/lang/Runnable;)V
      27: aastore
      28: aload_1
      29: iload_2
      30: aaload
      31: invokevirtual #33                 // Method java/lang/Thread.start:()V

      34: iinc          2, 1
      37: iload_2
      38: bipush        20
      40: if_icmplt     11
      43: goto          49
      46: invokestatic  #36                 // Method java/lang/Thread.yield:()V

      49: invokestatic  #39                 // Method java/lang/Thread.activeCou
nt:()I
      52: iconst_1
      53: if_icmpgt     46
      56: getstatic     #43                 // Field java/lang/System.out:Ljava/
io/PrintStream;
      59: getstatic     #13                 // Field race:I
      62: invokevirtual #49                 // Method java/io/PrintStream.printl
n:(I)V
      65: return
}

问题就出现在自增运算“race++”之中,用javap反编译这段代码,发现只有一行代码的increase()方法在Class文件中由4条字节码指令构成(return指令不是由race++产生的,这条指令可以不计算),从字节码层面容易分析并发失败的原因了:当getstatic指令把race的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但是在执行iconst_1, iadd这些指令时,其他线程可能已经把race的值加大了,而操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同步回主内存中

客观地说,此时使用字节码来分析并发问题,仍然是不严谨的,因为即使编译出来只有一条指令,也并不意味执行这条指令就是一个原子操作。一条字节码指令在解释执行时,解释器将要运行许多行代码才能实现它的语义,如果是编译执行,一条字节码指令也可能转化成若干条本地机器码指令,此处使用 -XX:+PrintAssembly 参数输出反编译来分析会更加严谨一些

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性:

  1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  2. 变量不需要与其他的状态变量共同参与不变约束。

使用volatile变量的第二个语义是禁止指令重排序优化,普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点,这也就是Java内存模型中描述的所谓的“线程内表现为串行的语义”(WithinThread As-If-Serial Semantics)
指令重排很难理解,举个例子就很清楚了:

 A: boolean init = false
    // do something
    init = true
  B :  while(!init) {
      sleep()
    }
    // do something

A线程准备开启一个服务,设置了一个boolean变量为flase,执行了一串操作,最终将这个变量设置成了true,开启服务成功。B线程在此过程中一直在请求判断这个变量是否是true,当判断服务开启开始执行其他的操作。这么描述一看上去并没有什么问题,但如果这个变量不是volatile类型,就可能因为指令重排而出问题。原因就在于根据指令重排的规则,A线程的变量没有被其他地方使用,所以其最后赋值为true可能被提前执行,也就是初始化动作没做完就设置成true了,这个时候对B线程来说就麻烦大了,B线程必须等服务初始化启动好了才能进行下面的操作,由于指令重排造成B线程的判断失误

另外volatile禁止指令重排是在JDK5才完全正确,之前的版本是有bug的。使用volatile会多执行一个lock addl $0x0, (%esp)操作,这个相当于一个内存屏障,重排序时不能将后面的操作,排在这个操作之前。addl $0x0 (%esp)是一个空操作,作用是使得本CPU的Cache写入了内存,该写入动作会引起别的CPU或者内核无效化其Cache,这个操作保证了volatile变量的修改对其他CPU立即可见。禁止指令重排的原理在于:指令重排指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理,但不是说任意重排,而是要正确处理指令依赖情况保证程序能够得到正确的执行结果。比如x=x+10;x*=2;y-=3;这3条指令,前两条的顺序就不能改变,但是3可能在1前面执行或者12中间执行。结果看起来是一致的。所以通过lock指令将修改同步到内存时,意味着之前的所有操作都已经执行完毕了,就形成了指令重排无法越过的内存屏障了

java内存模型中对volatile变量定义的特殊规则,假定T表示一个线程,V和W分别表示volatile型变量,那么在进行read、load、use、assign、store和write操作时需要满足如下规则:

  • 只有当线程T对变量V执行的前一个动作为load时,T才能对V执行use;并且,只有T对V执行的后一个动作为use时,T才能对V执行load。T对V的use,可以认为是和T对V的load。read动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对V修改后的值)。

  • 只有当T对V的前一个动作是assign时,T才能对V执行store;并且,只有当T对V执行的后一个动作是store时,T才能对V执行assign。T对V的assign可以认为和T对V的store、write相关联,必须连续一起出现(这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程看到自己对V的修改)。

  • 假定动作A是T对V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对V的read或write动作;类似的,假定动作B是T对W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对W的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令的重排序优化,保证代码的执行顺序与程序的顺序相同)。

最后一个问题,volatile比其他同步工具要快吗?这个很难说,只能是某些场景会优于锁,虚拟机对锁有很多的优化,volatile变量读操作的性能消耗与普通变量基本没什么差别,但是写会慢一些,因为需要在代码中插入许多内存屏障保证处理器不发生乱序执行。不过即便如此,大多数场景开销还是比锁低。选择依据只是volatile语义是否满足需求。

4. long和double类型的特殊规则

允许虚拟机将没有被volatile修饰的64位数据类型(long和double)的读取操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read和write这4个操作的原子性,就点就是long和double的非原子协定(Nonatomic Treatment of double and long Variables)。

如果多个线程共享一个为声明为volatile的long或double类型变量,并同时对他们进行读取和修改操作,那么有些线程可能会读取到一个即非原值,也不是其他线程修改值得代表了“半个变量”的数值。

不过这种读取带“半个变量”的情况非常罕见(在目前商用虚拟机中不会出现),因为Java内存模型虽然允许虚拟机不把long和double变量的读写实现成原子操作,但允许虚拟机选择把这些操作实现为具有原子性的操作,而且还“强烈建议”虚拟机这样实现

5. 原子性、可见性和有序性

这3个特性之前陆陆续续提到过,这里总结一下:

  • 原子性:由内存模型直接保证原子性变量操作包括read、load、assign、use、store、write,大致可以认为基本数据类型的访问读写是具备原子性的。至于long和double的非原子协定,了解一下即可,实际上无须过多关注。如果需要更大范围的原子性,可以使用lock和unlock,这两个虚拟机没有直接开放给用户使用,但是提供了字节码指令monitorenter和monitorexit来隐式地使用这两个操作,反映到Java代码中就是synchronized关键字,所以synchronized块是具备原子性的

  • 可见性:指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。详情见上面的volatile说明。除了这个外,还有两个关键字能实现可见性:synchronized和final。同步块的可见性是通过规则”对一个变量执行unlock操作之前必须把这个变量同步回主内存中“实现的。final的可见性指,被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this的引用传递出去,那么其他线程就能看见final字段的值
    除了volatile,Java还有两个关键字能实现可见性,synchronized和final。同步块的可见性是由“对一个变量执行unlock操作之前,必须把此变量同步回主内存中(执行store和write操作)”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么其他线程中就能看见final字段的值

//变量i与j都具备可见性,它们无须同步就能被其他线程正确访问
    public static final int i;
    public final int j;
    
    static{
        i = 0;
        //do something
    }
    
    {
        //也可以选择在构造函数中初始化
        j = 0;
        //do something
    }
  • 有序性:volatile中提到的指令重排也说过。总结就是:在本线程中观察,所有的操作都是有序的(即结果与正常顺序是一致的),在其他线程中观察,所有的操作都是无序的(即发生了重排)。前半句指的是“线程内表现为串行语义”,后面句指的是指令重排现象和工作内存与主内存同步延迟现象。Java提供了volatile和synchronized关键字保证两个线程之间的操作有序,synchronized是由规则“一个变量在同一个时刻只允许一条线程对其进行lock操作”,决定了持有一个锁的两个块只能串行进入
    Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入

6. happens-before原则(先行发生原则)

有序性如果只靠volatile和synchronized,那么并发编程会非常麻烦,但是实际上写代码的时候却并没有这么复杂。原因在于Java语言中有一个happens-before原则。这个原则很重要,是判断数据是否存在竞争、线程是否安全的主要依据。

先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值/发送了消息/调用了方法等
举个例子:

    i = 1;//在线程A中执行
    j = i;//在线程B中执行
    i = 2;//在线程C中执行
    //A先于B,但是C与B没有先行关系,B存在读取过期数据风险,不具备多线程安全性

如果A先行发生于B,则j的值一定等于1,原因有两个,根据定义i=1可以被B观察到,C没有执行。如果C在A、B之间执行,但是并不是先行于B,j的值就无法确定了。因为C对B的影响可能被B观察到了,也可能没有。这样就存在线程安全的问题。

上面的例子简单的说就是如果存在先行关系,就不用担心指令重排对两个线程的影响,不存在先行关系就要特别小心了。Java内存模型中有一些先天的先行发生关系,不需要借助同步可以在编码中直接使用:

  1. 程序次序规则:在一个线程内,按照程序代码顺序执行。准确的说是:控制流顺序而不是程序代码顺序(有选择和循环)
  2. 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。必须是同一个锁,后面指的是时间上的先后顺序
  3. volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。后面指的时间先后顺序
  4. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
  5. 线程终止规则:线程中所有操作都先行发生于对此线程的终止检测。
  6. 线程中断规则:对线程的interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  7. 对象终结规则:一个对象的初始化完成,先行发生于它的finalize方法的开始
  8. 传递性:如果操作A先行发生于B,B先行于C,那么A先行于C

Java语言无须任何同步手段保障就能成立的先行规则就只有上面这些。下面说明一下时间先后和先行的区别,比如有个共享变量x = 0,A线程在时间上先set其为1,线程B获取这个值,这个时候B获取的是1吗?答案是否定的,应该是不知道。因为这个操作没有任何先行规则匹配,虽然set操作先执行,但是不能确保get操作能获得修改后的值。修改方法很简单,加上synchronized或者定义成volatile。

时间上先发生,不一定是先行发生,那么先行发生,一定在时间上是先发生的吗?不一定,因为指令重排。比如int i = 1; j = 2。指令重排后j可能先被执行,但是根据程序次序规则,在一个线程内i = 1是先行于j=2的

三. Java与线程

并发不一定依赖多线程,但是Java里面谈论并发,大多数与线程脱不开关系

1. 线程的实现

线程是比进程更轻量级的调度执行单位,线程的引入可以把一个进程的资源分配和执行调度分开。各个线程既可以共享进程资源(内存地址,文件IO等),又可以独立调度(线程是CPU调度的基本单位)。

主流的操作系统都提供了线程实现,Java提供了不同操作系统对线程的统一处理,每个执行了start方法的Thread实例,就开启了一个线程。Thread的大部分API都是native,所以本章介绍的是线程的实现,而不关系Java是如何封装的。
基本的实现方式有3种:使用内核线程实现(1v1实现)、使用用户线程实现(1vN实现)和使用用户线程加轻量级进程混合实现(NvM实现)

1.1 使用内核线程实现

内核线程(Kernel Thread, KLT) 就是直接由操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程都可以看作是内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫多线程内核(Multi-Thread Kernel)。

程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process, LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型
在这里插入图片描述
轻量级进程的局限性:由于是基于内核线程实现的,所以各种进程操作,如创建/析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换;每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程需要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程是有限的

1.2 用户线程实现

狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到线程存在的实现。用户线程的建立/同步/销毁和调度完全在用户态完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作快速且低消耗,也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间1:N的关系称为一对多的线程模型

广义上一个线程只要不是内核线程,就可以认为是用户线程。所以轻量级进程也算是用户线程,但是实现是建立在内核之上。狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知线程的存在。用户线程的创建、同步、销毁、调度都在用户态完成,实现得当都不需要切换到内核态。所以低消耗,支持线程数大
在这里插入图片描述
劣势在于没有内核的支援,所有动作都需要自己处理,由于操作系统只承担了分配资源,那么阻塞如何处理,多处理器系统中如何将线程映射到其他处理器上之类的问题解决会非常困难,甚至办不到。所以程序复杂,现在使用用户线程的程序越来越少了,Java、Ruby等语言曾经使用过,最终放弃了

1.3 使用用户线程加轻量级进程混合实现

这是种混合实现的方式,用户线程还是完全建立在用户空间,所以线程的创建、切换、析构等操作代价小,并且可以支持大规模用户线程并发。而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,可以使用内核提供的线程调度功能及处理映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。许多Unix系列的操作系统,如Solaris、HP-UX等都提供了N:M的线程模型实现
在这里插入图片描述

1.4 Java线程实现

JDK1.2之前,是基于称为“绿色线程”的用户线程实现的。1.2中,线程模型替换成基于操作系统原生线程模型实现。因此,目前操作系统支持怎么样的线程模型,很大程度上决定了Java虚拟机线程是怎么样映射的,这点在不同的平台上无法达成一致。线程模型只对线程并发规模和操作成本产生影响,对Java的编码和运行差异是透明的。

对于Sun JDK而言,其Windows和Linux版本都是使用1对1的线程模型实现的,一条Java线程对应一条轻量级进程。

在Solaris平台中,操作系统支持1对1和多对多,所以Solaris版的JDK提供了平台专有参数-XX:+UseLWPSynchronization(默认这个)和-XX:+UseBoundThreads决定使用哪种线程模型

2. Java线程调度

线程调度是指系统为线程分配处理器使用权的过程。主要调度方式两种:

  • 使用协同调度的多线程系统,线程执行时间由线程本身控制,线程把自己的工作执行完后,要主动通知系统切换到另外一个线程上去。优点:实现简单。缺点:执行时间不可控制。

  • 使用抢占调用的多线程系统,每个线程由系统分配执行时间,线程的切换不由线程本身决定。Java使用的就是这种线程调度方式。

3. 状态转换

Java语言定义了6种线程状态,任意时间只能处于一个状态:

  • 新建new: 创建后未启动
  • 运行runnable: 包括running和ready,可能正在被执行,可能在等待CPU分配时间
  • 无限期等待waiting: 这种状态不会被分配执行时间,需要等待其他线程显示唤醒,陷入waiting的方法:
    Object.wait();
    Thread.join();
    LockSupport.park();
  • 限期等待timed waiting: 这种状态也不会被分配时间,不过不需要唤醒,到时间就会自动唤醒。
    Thread.sleep()
    Object.wait(timeout);
    Thread.join(timeout);
    LockSupport.parkNanos();
    LockSupport.parkUnitl();
  • 阻塞blocked: 线程被阻塞了,阻塞状态与等待状态的区别在于,阻塞状态在等待一个排他锁,这个事件将在另一个线程放弃这个锁时发生。等待只是等待一段时间,或者是唤醒动作发生。在程序等待进入同步区域的时候,线程将进入这种状态。
  • **结束terminated:**已终止线程的线程状态。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/haiyanghan/article/details/109104777
今日推荐