线程和Java内存模型

硬件的效率和一致性

  CPU的性能 = IPC * MHz

  IPC主要是指的利用进程间的通信来提升CPU的性能(Amdahl定律,通过系统中的串行和并行的比重来提升系统西能),而MHz则是指信号交换频率(摩尔定律,处理器的晶体管数量越多,运行效率就越高)

  CPU -->Registers -->Cache-->RAM—>HardDisk

  因为计算机的CPU计算速度远远高于计算机存储设备的信息,所以加了一层cache,来缓冲数据的获取。但是引入了新的问题,缓存一致性。每个处理器都有自己的高速缓存区,还有一个共享主内存区域,如果数据同步在主内存中的时候,一致性怎么处理,是按照那个处理器的呢?在现在的实现里是各个处理器都遵守一些协议来保证数据的一致性,如MSI、MESI等。

Java的内存模型

简介

java 也是分为主内存和工作内存。java中所有的变量都存在于主内存中,此处变量主要是实例字段,静态的字段,构成数组对象的字段,不包括局部变量和方法参数,因为后两者都是线程私有的,不会被共享,所以不会出现竞争问题。每个线程都有自己的工作内存,每个线程的工作内存中保存了主内存中的变量的副本拷贝,每次线程执行到更新操作的时候,需要先更新副本变量,再同步到主内存中,也就是说,读写操作都在工作内存中进行,不能直接读写主内存中的变量,即使volatile也不例外。不同线程之间的变量访问也是通过主内存进行的。需要注意的是,这个主内存和工作内存的划分和堆、栈、方法区的内存区域划分是两个不同层次的划分,两者基本没有关系。从变量、主内存和工作内存的定义看来,主内存更像是堆,工作内存像是栈。但实际上堆里面还有其他的信息,比如GC标志,年龄,同步锁,存储对象的HashCode等。从更低层的角度看的话,主内存对应于物理硬件的内存,虚拟机首先保证执行的应该是工作内存,甚至工作内存的优先存储级别应该在寄存器和高速缓存之上,因为程序运行时主要访问读写的操作就是在工作内存中进行的。

内存间的交互

也就是主内存和工作内存之间具体的交互方式。一个变量如何从主内存读取到工作内存,又如何更新到主内存中的实现?

这个实际上是java内存模型中定义了8种操作:{lock,unlock},{read,load,use},{assign,store,write}。这8种操作需要保证是原子性操作。凡事容易有例外,long,double的load,store,read,write在32位的操作系统上可能就会被划分为2次32位的操作,所以,java虚拟机也允许未被volatile修饰的变量的上述四种操作不保持原子性。假设多个线程同时对一个没有volatile修饰的变量进行赋值和读取,就可能会出现读取的值既不是原值也不是新的值,而是一个啥也不是的东西。

java内存模型规定了在执行上述8种基本操作的时候的一些规则:

1、不允许read和load,store和write的操作之一单独出现,也就是不能出现读取了主内存的值,但是不给工作内存的副本变量或者写入到主内存,但是主内存不接受的情况。

2、不允许线程丢弃最近的assign操作,一旦在工作内存中将变量改变了,就需要同步回主内存中。

3、如果一个线程中的变量没有被assign,不允许写入到主内存中。

4、新的变量只能在主内存中出现,然后才能到工作内存,也就是一个变量在use和store之前肯定是要先执行assign和load操作。

5、一个变量同一时间只能有一个线程对其进行lock操作,如果重复执行多次,需要执行同样多次的unlock才能解锁成功。

6、一个变量执行了lock操作,就会在工作内存中将其清空值,这样再使用的时候就需要再重新load或assign操作初始化变量的值。

7、一个变量事先没有被lock锁定,就不能被unlock。

8、对一个变量执行unlock操作之前,必须先把变量同步回主内存。

上面的8种情况基本上已经确定了java那些内存访问操作在并发下是安全的。不过还需要volatile变量的一些描述。

volatile关键字

什么是线程不安全的?也就是一个线程A在操作时间上先发生于线程B,但是无法确认B执行后的值。比如setter,getter

volatile是java提供的最轻量级的同步机制。被volatile修饰的变量有两种特性:①保证变量对所有的线程都是立即可见的。②禁止指令重排序。

volatile修饰的变量在不同的线程工作内存中也是可以不一致的,这一点和上面的第一种特性并不冲突,因为读取和赋值是多个指令操作,我们看一段字节码,就可以看出问题。

package main;

/**
 * <看看volatile是怎么回事>
 * <详细介绍>
 *
 * @author lihaitao
 * @since 设计wiki | 需求wiki
 */
public class VolatileTest {
    public static volatile int race = 0;

    public static void increase() {
        race++;
        /*
            Java 字节码的指令
            public static void increase();
            Code:
               0: getstatic     #2                  // Field race:I
               3: iconst_1
               4: iadd
               5: putstatic     #2                  // Field race:I
               8: return
         */
    }

    public static final int THREAD_COUNT = 20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREAD_COUNT];
        for (Thread thread : threads) {
            thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            thread.start();
        }
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println(race);
    }
}


public main.VolatileTest();

    Code:

       0: aload_0       

       1: invokespecial #1                  // Method java/lang/Object."<init>":()V

       4: return        

  public static void increase();

    Code:

       0: getstatic     #2                  // Field race:I

       3: iconst_1      

       4: iadd          

       5: putstatic     #2                  // Field race:I

       8: return        
上面的increase方法中race++成了4条字节码指令,如果在第二条和第三条执行的时候,其他的线程进行了操作,这样该线程就可能会把小的值写回主内存。当然,字节码的执行指令只有一条也不意味着是原子性操作,因为到解释器进行解释执行的时候可能需要执行多行代码实现。但现在已经能反映问题了,
我也试着进行反汇编,但是oracle的官网对一些命令包已经不支持,所以也不了了之。

第二条特性是禁止重新排序,也就是禁止代码执行顺序和代码编写顺序不一致。虚拟机或者物理机为了性能都有可能进行乱序执行的操作,比如说下面的代码

package main;

import java.util.LinkedList;
import java.util.List;

/**
 * <一句话描述>
 * <java代码只是伪代码,因为排序问题是在反汇编之后执行的时候可能会出现问题,这里只是用java代码进行演示>
 *
 * @author lihaitao
 * @since 设计wiki | 需求wiki
 */
public class FieldResolution {

    String a;
    String b;
    List<String> list;
    boolean aBoolean = false;

    String c = "c";

    {
        /**
         * 假设下面的赋值由线程A操作
         * 是一些初始化的操作...
         */
        a = "a";
        b = "b";
        list = new LinkedList<>();
        aBoolean = true;

        String c = "c";
    }

    {
        /**
         * 该代码块由线程B操作
         */
        if (aBoolean) {
            // do something
        }
    }
}
volatile是怎么实现的其他线程能获取到最新的值呢?在volatile修饰的变量进行assign的操作的时候,会进行一个lock指令,该指令会禁止后面的指令进行重排序,也会让该缓存写入到内存中,顺便让其他的线程中该变量的缓存失效,这样,其它线程再去读取该变量的时候需要重新的进行load,use操作。
volatile有好处,也有不好的地方,不能完全保证数据的一致性,那volatile的应用场景是什么?答案是不需要依赖数据的原值的情况,也就是我对这个变量的写操作不需要依赖于该变量的原值,我只需要赋值,其他线程再进行读取的时候就是一个正确的值。

 

Java内存模型的三个特性:可见性、原子性、有序性。

可见性就是一旦一个变量被修改了,其他线程能立即知道这个变量的最新值。

原子性是指的操作一次性完成,不会存在一部分完成的情况,虽然long,double是有非原子性操作协定,但是一般的虚拟机实现都会将这两个实现成原子性操作。所以我们不需要太关心。

有序性就是一个线程本身执行是有序的,但是别的线程观察该线程的时候,就会发现其执行可能是乱序的。

Java与线程

并发并不一定是线程粒度,可能是进程(PHP),java中的并发一般都是和线程挂钩的。

实现线程的3种方式和Java中的线程实现

1、利用内核线程

关于内核线程就是由操作系统内核直接支持的线程。这种线程切换由内核进行处理,操作线程调度器进行调度线程,将线程的任务映射到各个处理器上。用户的程序一般不是直接使用内核线程,而是使用实现LWP(light weight process)轻量级进程方式,轻量级进程是内核线程的一种高级接口。一般的情况是用户的程序一个进程分成多个线程,每个线程都继承LWP,每个LWP对应一个支持的KLT(kernel-level Thread),先支持内核线程,才有轻量级进程。轻量级线程和内核线程是1:1的关系。

2、利用用户线程

也就是不用内核线程进行调度维护,完完全全是用户程序去操作线程的切换调度,这样不需要内核额外的开销。看起来不错,但是用户的程序如果线程切换、创建、调度都是很麻烦的甚至不能处理,因为处理器只会向进程进行资源分配。所以java,ruby等都放弃了这个方式。

3、内核线程和用户线程共用

这种方式比较好,既有内核线程进行调度,又因为有很多用户线程执行任务,消耗内核资源较少。用户线程和LWP的比例为N:M。但是没有成为主流,不知道是实现比较难还是有其他的问题。

java中实现的方式是第一种

Java线程的调度

两种调度方式:协同式调度,抢占式调度。

java采用的是抢占式调度。

Java线程的状态

新建状态。

运行状态。

无限等待状态。wait()、join()、LockSupport.park()

有限等待状态。sleep(),wait(),join()还有LockSupport的几个method

阻塞状态。阻塞状态只出现在竞争的时候

死亡状态。挂了。

 

参考资料:周老师的《深入java虚拟机》

posted @ 2017-12-19 15:40  TT的宝藏  阅读(200)  评论(0编辑  收藏  举报