线程和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虚拟机》