转载

理解JVM(五):Java内存模型与线程

JMM(Java Memory Model)是JVM定义的内存模型,用来屏蔽各种硬件和操作系统的内存访问差异。

  • 主内存:所有的变量都存储在主内存(Main Memory,类比物理内存)中。
  • 工作内存:每条线程有自己的工作内存(Working Memory,类比处理器高速缓存),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
理解JVM(五):Java内存模型与线程

内存间的交互操作

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

其中read和load,store和write必须成对使用,顺序但补一定连续的执行。通俗的说,就是执行了read,后面一定会执行load,但不一定read之后立马load;store和write也一样。lock和unlock也是成对出现,一个变量在同一时间点只能有一个线程对其进行lock。

  1. 对于普通变量的操作: 创建变量,是在主内存中初始化。 线程用到的变量,会先从主内存中拷贝( read )出来,加载( load )到工作内存,然后引用( use )变量并运算赋值( assign )。然后存储( store )到工作内存,然后更新( write )掉原来的变量。

    普通变量的值在线程间传递均需要通过主内存来完成。例如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见。

  2. 对于 volatile 修饰的变量:过程和普通变量一样。但保证变量对所有线程的可见性,并且会禁止指令重排序的优化。

    volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

先行发生原则(happens-before)

它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作之间是否可能存在冲突的所有问题。

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

线程

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

实现线程的3种方式:

  • 使用用户线程实现
    1:N
    
  • 使用用户线程加轻量级进程混合实现
    • 既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为 N:M 的关系,这种就是多对多的线程模型。

Java线程实现:JDK1.2之前是用户线程,1.2和之后的版本,使用操作系统原生线程模型(内核线程)。

Java线程调度

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

  • 协同式线程调度(Cooperative Threads-Scheduling):线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。
    • 好处:实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步的问题。
    • 坏处:线程执行时间不可控制,甚至如果一个线程编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。
  • 抢占式线程调度(Preemptive Threads-Scheduling):每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(在Java中, Thread.yield() 可以让出执行时间,但是要获取执行时间的话,线程本身是没有什么办法的)。在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题,Java使用的线程调度方式就是抢占式调度 。

虽然Java线程调度是系统自动完成的,但是我们还是可以“建议”系统给某些线程多分配一点执行时间,另外的线程少分配一点——通过设置线程优先级的方式(两个线程同时处于Ready状态时,优先级越高的线程越容易被系统选择执行),不过这方法不是很可靠,因为系统线程优先级和Java的10种线程优先级不一定一一对应。

原文  https://juejin.im/post/5b31f6b8f265da59b37e7f3d
正文到此结束
Loading...