博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
jvm(12)-java内存模型与线程
阅读量:5108 次
发布时间:2019-06-13

本文共 13033 字,大约阅读时间需要 43 分钟。

【0】README
0.1)本文部分文字描述转自“深入理解jvm”,旨在学习
“java内存模型与线程” 的基础知识;
 
【1】概述
1)并发处理的广泛应用是使得 Amdahl 定律代替摩尔定律称为计算机性能发展源动力的根本原因;
2)Amdahl 定律:该定律通过系统中并行化与串行化的比重来描述多处理器系统能获得的运算加速能力;
3)摩尔定律:该定律用于描述处理器晶体管数量与运行效率间的发展关系;
Conclusion)这两个定律的更替代表了近年来硬件发展从追求处理器频率到追求多核心并行处理的发展过程;
 
【2】硬件的效率与一致性
1)高速缓存(干货——引入高速缓存)
  • 1.1)problem:由于计算机的存储设备与处理器的运算速率有几个数量级的差距;
  • 1.2)solution:引入一层读写速度尽可能接近处理器速度的高速缓存(cache) 来作为内存与处理器间的缓冲: 将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓冲同步回内存中,这样处理器就无须等待缓慢的内存读写了;
2)缓存的引入产生了一个新问题——缓存一致性: 在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存,如下图所示:
  • 2.1)问题描述(problem): 当多个处理器的运算任务都涉及到同一块内存区域时,将可能导致各自的缓存数据不一致,那同步到内存时以谁的数据为准呢?
  • 2.2)solution: 需要各个处理器遵循一些协议,在读写时要根据协议来进行操作,这类协议有 MSI, MESI,等。
  • 2.3)内存模型: 可以理解为 在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象;(干货——java内存模型定义)
3)乱序执行: 为了使得处理器内部的运算单元能够被尽量使用,处理器可能对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的;
 
【3】java内存模型
0)intro to java内存模型:java虚拟机规范试图定义一种java内存模型(java memory model)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果;
【3.1】主内存与工作内存
1)java内存模型的主要目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中读取变量这样的底层细节;
2)java内存模型规定了:所有的变量都存储在主内存中。
  • 2.1)每条线程还有自己的工作内存: 线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值)都必须在工作内存中进行,而不能直接读写内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量;(干货——每条线程还有自己的工作内存,工作内存定义)
  • 2.2)线程间变量值的传递均需要通过主内存来完成:线程、内存、工作内存三者关系如下所示:
3)这里所讲的主内存,工作内存与前面讲的 java内存区域中的java堆,栈,方法区等并不是同一个层次的内存划分,这两者基本没有关系;
  • 3.1)如果硬要扯上关系,则:主内存主要对应于java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域;
  • 3.2)更低层次上说:主内存就直接对应于物理硬件的内存,而为了获得更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存;
【3.2】内存间交互操作
1)关于主内存与工作内存间的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存;java内存模型中定义了以下8种操作(operations):(干货——java内存模型中定义了以下8种操作)
  • o1)lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态;
  • o2)unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
  • o3)read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load 动作使用;
  • o4)load(载入):作用于工作内存的变量, 它把 read 操作从主内存中得到的变量放入工作内存的变量副本中;
  • o5)use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;
  • o6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;
  • o7)store(存储):作用于工作内存的变量, 它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用;
  • o8)write(写入):作用于主内存的变量, 它把store操作从工作内存中得到的变量的值放入主内存的变量中;
2)相关操作:
  • 2.1)把变量从主内存复制到工作内存:顺序执行read和load操作(目的地是工作内存);
  • 2.2)把变量从工作内存同步回主内存:顺序执行store和write操作(目的地是主内存);
  • Attention)java内存模型只要求上述两个操作必须按顺序执行,没有保证是连续执行;即read和 load 之间,store和 write之间可以插入其他指令;(干货——java内存模型只要求上述两个操作必须按顺序执行,没有保证是连续执行,它们之间还可以插入其他指令)
3)java内存模型还规定了在执行上述8种基本操作时必须满足如下规则(rules):
  • r1)不允许read和 load,store 和 write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现;
  • r2)不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存;
  • r3)不允许一个线程无原因地(没有发生过任何 assign操作)把数据从线程的工作内存同步回主内存中;
  • r4)一个新变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化的变量,换句话说,对一个变量实施 use,store操作前,必须先执行过 assign 和 load 操作;
  • r5)一个变量在同一个时刻只允许一个线程对其进行lock 操作,但lock操作可以被同一条线程重复执行多次,多次执行 lock后,只有执行相同次数的unlock 操作,变量才会被解锁;
  • r6)如果对一个变量执行lock 操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值;
  • r7)如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去 unlock 一个被其他线程锁定住的变量;
  • r8)对一个变量执行unlock 变量前,必须先把此变量同步回主内存中(执行store, write操作);
  • Conclusion) 这8种内存访问操作以及上述规则限定,再加上稍后介绍的对 volatile 的一些特殊规定,就已经完全确定了java 程序中哪些内存访问操作在并发下是安全的;
【3】 对于volatile型变量的特殊规则
1)当一个变量定义为volatile后,它具备两种特性(characters):
  • c1)保证此变量对所有线程的可见性,这里的可见性指:当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量做不到这一点,其在线程间传递需要通过主内存来完成;(干货——这就是为什么会出现数据的脏读)
  • c2)对于volatile变量的可见性,有一些误解: volatile变量对所有线程都是可见的,对volatile变量所有的写操作都能立刻反应到其他线程中,即,volatile变量在各个线程中是一致的,所有基于 volatile变量的运算在并发下是安全的。上述语句中的错误在于并不能得出“基于 volatile变量的运算在并发下是安全的”这个结论。
2)虽然volatile变量在各个线程的工作内存中不存在一致性问题,但java里面的运算并非原子操作,导致 volatile变量的运算在并发下一样是不安全的。
  • 2.1)看个荔枝:
  • [java] 
     
     
     
    1. public class VolatileTest {  
    2.     public static volatile int race = 0;  
    3.       
    4.     public static void increase() {  
    5.         race++;  
    6.     }  
    7.       
    8.     public static final int THREADS_COUNT = 20;  
    9.       
    10.     public static void main(String[] args) {  
    11.         Thread[] threads = new Thread[THREADS_COUNT];  
    12.         for (int i = 0; i < threads.length; i++) {  
    13.             threads[i] = new Thread(new Runnable() {  
    14.                 @Override  
    15.                 public void run() {  
    16.                     for (int j = 0; j < 10000; j++) {  
    17.                         increase();  
    18.                     }  
    19.                 }  
    20.             });  
    21.             threads[i].start();  
    22.         }  
    23.           
    24.         // 等待所有累计线程都ending  
    25.         while(Thread.activeCount() > 1) {  
    26.             Thread.yield();  
    27.         }  
    28.         System.out.println(race);  
    29.     }  
    30. }  
  • 对以上执行结果的分析(Analysis):
    • A1)以上代码的正确输出结果是20000, 而执行的结果每次都不一样,且都小于20000;
    • A2)用javap 反编译命令得到如下字节码,发现increase()方法对应4条字节码指令(return指令不算):
  • 对以上字节码的分析(Analysis):
    • A1)当 getstatic指令 把 race 的值取到操作栈顶时,volatile关键字保证了 race的值在此时是正确的,但是在执行 iconst_1, iadd 这些指令的时候,其他线程可能已经把race 的值加大了,而在操作栈顶的值就变成了过期的数据,所以 putstatic 指令执行后就可能把较小的race 值同步回主内存中;
    • A2)客观上说,在这里使用 字节码来分析并发问题,不是很严谨。因为即使编译出来只有一条字节码指令,也不意味着执行这条指令就是一个原子操作。一条字节码指令也可能会转化成若干条本地机器码指令,此处使用 -XX:+PrintAssembly 参数输出反汇编来分析会更加严谨;(干货——因为即使编译出来只有一条字节码指令,也不意味着执行这条指令就是一个原子操作)
3)由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁来保证原子性:
  • scene1)运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值;
  • scene2)变量不需要与其他的状态变量共同参与不变约束;
3.1)看个荔枝(使用volatile变量来控制并发)
[java] 
 
 
 
  1. // 使用volatile变量来控制并发  
  2. public class VolatileVariableTest {  
  3.     volatile boolean shutdownRequested; // volatile变量  
  4.       
  5.     public void shutdown() {  
  6.         shutdownRequested = true;  
  7.     }  
  8.     public void doWork() {  
  9.         while(!shutdownRequested) {  
  10.             // do sth.  
  11.         }  
  12.     }  
3.2)使用volatile变量的第二个语义是禁止指令重排序优化:普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的 方法执行过程中无法感知到这点,这也就是java 内存模型中描述的所谓的 “线程内表现为串行的语义”;
(干货——volatile可以禁止指令重排序优化)
[java] 
 
 
 
  1. // 指令重排序演示(伪代码)  
  2.     public void instruct_reorder() {  
  3.         Map configOptions;  
  4.         char[] configText;  
  5.         //  此变量必须为 volatile  
  6.         volatile boolean initialized = false;  
  7.           
  8.         // 假设以下代码在线程A 中执行  
  9.         // 模拟读取配置信息,当读取完成后将 initialized 设置为true 已通知其他线程配置可用  
  10.         configOptions = new HashMap();  
  11.         configText = readConfigFile(filename);  
  12.         processConfigOptions(configText, configOptions);  
  13.         initialized = true;  
  14.           
  15.         // 假设以下代码在线程B 中执行  
  16.         // 等待initialized 为true,代表线程A 已经把配置信息初始化完成  
  17.         while(!initialized) {  
  18.             sleep();  
  19.         }  
  20.         // 使用线程A 中初始化好的配置信息  
  21.         do_sth_with_config();  
  22.     }  
对以上代码的分析(Analysis):
  • 如果定义initialized变量没有使用volatile修饰:就可能会由于指令重排序的优化,导致位于线程A 中最后一句代码“initialized=true”被提前执行;这样在线程B中使用配置信息的代码就可能出现错误,而volatile关键字则可以避免此类情况的发生;
3.3)再看个荔枝:(分析volatile关键字是如何禁止指令重排序优化的),以下代码是一段单例模式代码,可以观察加入 volatile和未加入volatile关键字时 所生成汇编代码的差别(如何获得JIT的汇编代码,参考4.2.7)
[java] 
 
 
 
  1. // 单例模式(分析volatile关键字是如何禁止指令重排序优化的)  
  2. public class Singleton {  
  3.     private volatile static Singleton instance;  
  4.       
  5.     public static Singleton getInstance() {  
  6.         if(instance == null) {  
  7.             synchronized (Singleton.class) { // 同步块  
  8.                 if(instance == null) {  
  9.                     instance = new Singleton();  
  10.                 }  
  11.             }  
  12.         }  
  13.         return instance;  
  14.     }  
  15.       
  16.     public static void main(String[] args) {  
  17.         Singleton.getInstance();  
  18.     }  
  19. }  
(这个反汇编荔枝待研究)
【3.4】对于long 和 double 型变量的特殊规则
1)java内存模型要求lock, unlock, read, load, assign, use,store,write这8个操作都具有原子性: 但对于64位的数据类型(long和double),在模型中特别定义了一条相对宽松的规定:允许虚拟机将没有被 volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load, store,read和write这4个操作的原子性,这点就是所谓的 long 和double 的非原子性协定;
2)这项宽松的规定所导致的problem:如果有多个线程共享一个并未声明为 volatile的long 或 double类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读取到一个既非原值,也不是其他线程修改值的代表了半个变量的数值;
3)不过这种读取到的“半个变量”的case非常罕见:因为java内存模型虽然允许虚拟机不把long 和 double 变量的读写实现成原子操作,但允许虚拟机选择把 这些操作实现为具有原子性的操作,而且还强烈建议虚拟机这样实现;
(干货——不过这种读取到的“半个变量”的case非常罕见)
 
【3.5】原子性,可见性与有序性
0)intro: java内存模型是围绕着在并发过程中如何处理原子性, 可见性和有序性这3个特征来建立的;
1)原子性:由于java内存模型来直接保证的原子性变量操作包括 read,load,assign,use,store和write,我们大致认为基本数据类型的访问读写数据是具备原子性的。
  • 1.1)同步块——synchronized关键字:如果应用场景需要一个更大范围的原子性保证,java内存模型还提供了lock 和 unlock 操作来满足这些需求,尽管虚拟机没有把lock 和 unlock 操作直接开放给用户使用,但是却提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐式地使用这两个操作;
  • 1.2)这两个字节码指令反映到java代码中就是同步块——synchronized关键字:因此在synchronized块之间的操作也具备原子性;
2)可见性:可见性指当一个线程修改了共享变量的值,其他能够立即得知这个修改。
  • 2.1)java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此;
  • 2.2)普通变量与 volatile变量的区别是:volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新;所以volatile保证了多线程操作时变量的可见性,普通变量则不能保证这一点;
  • 2.3)java还有两个关键字实现可见性: synchronized 和 final;
    • 2.3.1)同步块的可见性: 是由对一个变量执行unlock 操作前,必须先把此变量同步回主内存中;
    • 2.3.2)而final关键字的可见性:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this 的引用传递出去,那在其他线程中就能看见final 字段的值。
    • 2.3.3)看个荔枝:
    • [java] 
       
       
       
      1. // final 可见性测试  
      2. public class FinalVisibilityTest {  
      3.     public static final int i;  
      4.       
      5.     public final int j;  
      6.       
      7.     static {  
      8.         i = 0;  
      9.         // do sth  
      10.     }  
      11.     {  
      12.         // 也可以选择在构造函数中初始化  
      13.         j = 0;  
      14.         // do sth  
      15.     }  
      16. }     
  • 对以上代码的分析(Analysis):变量i 和 j 都具备可见性,它们无须同步就能被其他线程正确访问;
3)有序性
  • 3.1)java程序中天然的有序性总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指: 线程内表现为串行的语义,后半句是指:指令重排序现象和工作内存与主内存同步延迟现象;
  • 3.2)volatile和 synchronized关键字保证了线程间操作的有序性:volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由 一个变量在同一时刻只允许一条线程对其进行lock 操作这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入;
【3.6】先行发生原则
1)先行发生原则定义:先行发生是 java内存模型中定义的两项操作之间的偏序关系,如果说操作A 先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到, 影响包括 修改了内存中共享变量的值,发送了消息,调用了方法等;
(干货——先行发生原则定义)
2)看个荔枝(如何理解 “影响包括 修改了内存中共享变量的值,发送了消息,调用了方法等”)
[java] 
 
 
 
  1. // 先行发生 原则  
  2. public class AheadOccurTest {  
  3.     int i = 0;  
  4.     int j = 0;  
  5.       
  6.     public void test() {  
  7.         // 以下操作在线程A中执行  
  8.         i = 1;  
  9.         // 以下操作在线程 B 中执行  
  10.         j = i;  
  11.         // 以下操作在线程 C 中执行  
  12.         i = 2;  
  13.     }  
  14. }  
对以上代码的分析(Analysis):线程C 出现在 线程A 和 B之间, 但线程C 和 B 并没有先行发生关系,那j的值 会是多少,答案是不确定的;
3)下面是java内存模型下一些天然的先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用:
  • 3.1)程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作;准确地说,应该是控制流顺序;
  • 3.2)管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作;这里必须强调的是同一个锁,而后面是指时间上的先后顺序;
  • 3.3)volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的后面是指时间上的先后顺序;
  • 3.4)线程启动规则:Thread对象的start() 方法先行发生于此线程的每一个动作;
  • 3.5)线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,可以通过Thread.join() 方法结束,Thread.isAlive() 的返回值等手段检测到线程已经终止运行;
  • 3.6)线程中断规则:对线程interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrrupted() 方法检测到是否有中断发生;
  • 3.7)对象终结规则:一个对象的初始化完成先行发生于它的finalize() 方法的开始;
  • 3.8)传递性:如果操作A 先行发生于操作B, 操作B 先行发生于操作C,那就可以得出操作A 先行发生于 操作C的结论;
4)看个荔枝:如何使用这些规则去判定操作间是否具备顺序性,对于读写共享变量的操作来说,就是线程是否安全;
[java] 
 
 
 
  1. // 利用先行发生 原则  
  2. // 判定操作间是否具备顺序性,对于读写共享变量的操作来说,就是线程是否安全  
  3. public class AheadOccurTest2 {  
  4.     private int value = 0;  
  5.       
  6.     public void setValue(int value) {  
  7.         this.value = value;   
  8.     }  
  9.   
  10.     public int getValue() {  
  11.         return value;  
  12.     }  
  13.       
  14.     Integer i = 0;  
  15. }     
对以上代码的分析(Analysis):
  • A1)problem:假设线程A 先调用了 setValue(1), 之后线程B 调用了同一个对象的getValue() ,那么线程B 收到的value是什么?
  • A2)可以判定:尽管线程A在操作时间上先于线程B, 但是无法确定线程B 中“getValue()” 方法的返回结果,换句话说,这里面的操作不是线程安全的;
  • A3)solution:我们至少有两种简单的解决方案:
    • solution1)要么把getter 和  setter方法都定义为 synchronized方法,这样就可以套用管程锁定规则;
    • solution2)要么把value定义为 volatile变量,由于setter方法对value的修改不依赖于 value的原值,满足volatile关键字使用场景,这样就可以套用volatile变量规则来实现先行发生关系;
  • A4)得出结论:一个操作时间上的先发生 不代表这个操作会是先行发生,那如果一个操作先行发生是否就能推导出这个操作必定是 时间上的先行发生呢? (显然推导不出来)。一个典型的荔枝就是多次提到的“指令重排序”。
    • A4.1)看个荔枝:
    • [java] 
       
       
       
      1. // 以下操作在同一个线程中执行  
      2. int i = 1;  
      3. int j = 2;  
    • 对上述代码的分析(Analysis): 根据程序次序规则, int i = 1 的操作先行发生于 int j =2,但 int j = 2 完全可能先被处理器执行,这并不影响先行发生原则的正确性;
  • Conclusion)以上两个实例得出结论:时间先后顺序与先行发生原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准;
【4】java 与线程
【4.1】线程的实现
0)intro
  • 0.1)线程是比进程更轻量级的调度执行单位: 线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址,文件IO等),又可以独立调度(线程是CPU 调度的基本单位);
  • 0.2)线程实现的3种方式:使用内核线程实现,使用用户线程实现,使用用户线程加轻量级进程混合实现(干货——线程实现的3种方式:使用内核线程实现+使用用户线程实现+使用用户线程和轻量级进程混合实现)
1)使用内核线程实现
  • 1.1)内核线程(KLT,Kernel-Level Thread):就是直接由操作系统内核(下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。(干货——内核线程和轻量级进程的定义)
  • 1.2)程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process, LWP):轻量级进程就是我们通常意义上讲的线程,由于每个轻量级线程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程间1:1 的关系称为一对一的线程模型,如下图所示:(干货——引入轻量级进程)
  • 1.3)轻量级进程有局限性:
    • 1.3.1)首先:由于是基于内核线程实现的,所以各种线程操作,如创建,析构及同步,都需要进行系统调用, 而系统调用的代价相对较高,需要在用户态和内核态中来回切换;
    • 1.3.2)其次:每个轻量级进程都需要有一个内存线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的;
2)使用用户线程实现(已经被Deprecated)
3)使用用户线程加轻量级进程混合实现
  • 3.1)在该实现方式下,既存在用户线程,也存在轻量级进程;
  • 3.2)用户线程还是完全建立在用户空间中:因此用户线程的创建,切换,析构等操作依然廉价,并且可以支持大规模的用户线程并发;
  • 3.3)操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁:这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞 的风险;(干货——操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁)
  • 3.4)在这种混合模式中:用户线程与轻量级进程的数量比是不定的,即为 N:M 的关系,如下图所示, 这种就是多对多的线程模型;
【4.2】java线程调度
1)线程调度定义:是指系统为线程分配处理器使用权的过程,主要调度方式有:协同式线程调度 和 抢占式线程调度;
(干货——线程调度定义 + 主要调度方式)
(干货——请注意协同式线程调度和抢占式线程调度的区别)
2)使用协同式线程调度的多线程系统:线程的执行时间由线程本身来控制,线程把自己的工作执行完以后,要主动通知系统切换到另外一个线程上;
  • 2.1)其好处是:实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作读线程自己是可知的,所以没有什么线程同步的问题;
  • 2.2)其坏处是:线程执行时间不可控制,甚至如果一个线程编写有问题,一直不告诉系统进行线程切换,那么程序就会一直阻塞在那里;
3)使用抢占式线程调度的多线程系统:那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定;
  • 3.1)java使用的方式就是 抢占式线程调度方式;
  • 3.2)虽然java 线程调度是系统自动完成的: 但我们还是可以建议系统给某些线程多分配一点执行时间,另外一些线程则可以少分配一点——这项操作可以通过设置线程优先级来完成;(干货——设置java 线程优先级)
  • 3.3.)不过线程优先级并不是太靠谱:因为java的线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于 操作系统;虽然现在很多os 都提供了线程优先级,但不见得和 能与 java线程的优先级一一对应;如 Solaris中有 2^32 种优先级,而windows只有7种 ;(干货——java的线程优先级并不是太靠谱)
  • 3.4)下表显示了 java线程优先级 与 windows 线程优先级之间的对应关系:
补充-Complementary):
  • C1)上文说到的“java线程优先级并不是太靠谱”,不仅仅是在说一些平台上不同的优先级实际会变得相同这一点,还有其他case 让我们不能太依赖优先级:优先级可能会被系统自行改变。(干货——优先级可能会被系统自行改变)
  • C2)如,在windows 中存在一个称为 “优先级推进器”的功能,作用是 当系统发现一个线程执行得特别勤奋的话,可能会越过线程优先级去为它分配执行时间;
【4.3】状态转换
1)java定义了6种状态(status):(干货——java定义了6种线程状态)
  • 1.1)新建(New):创建后尚未启动的线程处于这个状态;
  • 1.2)运行(Runnable):Runable包括了os 线程状态中的 Running 和 Ready,也就是处于 此状态的线程有可能正在执行,也有可能正在等待着CPU 为它分配执行时间;
  • 1.3)无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式的唤醒。以下方法会让线程陷入无限期的等待状态(methods):
    • m1)没有设置Timeout参数的Object.wait()方法;
    • m2)没有设置Timeout参数的 Thread.join() 方法;
    • m3)LockSupport.park() 方法;
  • 1.4)限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU 执行时间,不过无需等待被其他线程显式唤醒,在一定时间之后,它们会由系统自动唤醒。以下方法会让线程进入限期等待状态(methods):
    • m1)Thread.sleep() 方法;
    • m2)设置了Timeout参数的Object.wait()方法;
    • m3)设置了Timeout参数的 Thread.join() 方法;
    • m4)LockSupport.parkNanos() 方法;
    • m5)LockSupport.parkUntil() 方法;
  • 1.5)阻塞(Blocked):线程被阻塞了, 阻塞状态与等待状态的区别是:阻塞状态在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而等待状态则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候, 线程将进入这种状态;
  • 1.6)结束(Terminated):已经终止线程的线程状态,线程已经结束执行;
2)以上6种状态会相互转换,转换关系如下图所示:

转载于:https://www.cnblogs.com/pacoson/p/5351530.html

你可能感兴趣的文章
layui数据表格table在选项卡tabs中分页条不显示的解决
查看>>
Android中SQLite应用详解
查看>>
在oracle中创建自动增长字段
查看>>
LINQ操作符三:限制操作符
查看>>
Delphi7 [Fatal Error] ClassPas.pas(8): File not found: 'DesignIntf.dcu'
查看>>
正则表达式
查看>>
java格式及运算符
查看>>
Android相对布局
查看>>
RichCopy: The Only File Copying Utility You’ll Ever Need
查看>>
delphi下webbrowser的应用
查看>>
Swift 统计项目中所有按钮的点击次数
查看>>
Django:视图
查看>>
Bye2018, Hi2019
查看>>
C#多线程学习(六) 互斥对象
查看>>
RedHat7 yum的配置
查看>>
笔记1---访问权限修饰符
查看>>
梯度下降法
查看>>
面向切面编程必须了解的概念
查看>>
CodeForces Powers of Two STL
查看>>
apply和call
查看>>