一. 浅谈java内存模型
Java内存模型(JMM)规定了jvm有主内存,主内存是多个线程共享的。当new一个对象的时候,也是被分配在主内存中,每个线程都有自己的工作内存,不同线程之间不能访问对方的工作内存。,加入一个工作内存的目的很明显,就是为了加快在内存中的操作数据的速度,因为工作内存优先存储在寄存器和高速缓存中,这两个操作的速度都远远快于主内存。
工作内存存储了主存的某些对象的副本,当然线程的工作内存大小是有限制的。当线程操作某个对象时,执行顺序如下:
(1)从主存复制变量到当前工作内存(read and load)
(2)执行代码,改变共享变量值(use and assign)
(3)用工作内存数据刷新主存相关内容(store and write)
A在工作内存改了变量值+1,而B此时并不能看到这个操作,B还以为没人改动这个值,就认为自己是再原来的值上进行操作-1。算出来的值变成了多少呢?噢,并不知道,因为可能是3,也可能是1。这时,就出现了并发访问的线程安全问题。
如下图,展现了线程、主内存、工作内存之间的交互关系:
主内存和工作内存之间交互的主要操作为:
Lock(锁定):作用于主内存的变量,将主内存该变量标记成当前线程私有的,其他线程无法访问。
Unlock(解锁):作用于主内存的变量,解除主内存中该变量的锁定状态,让他变成线程共享变量。
Read(读取):作用于主内存的变量,将该变量读取到当前线程的工作内存中,以便进行load操作。
Load(加载):作用于工作内存中的变量,将read获取到的变量载入工作内存的变量副本中。
Use(使用):作用于工作内存中的变量,虚拟机执行引擎在执行字节码指令的时候,碰到了一个变量就会执行该操作,使用该变量。
Assign(赋值):作用于工作内存中的变量,虚拟机执行引擎在执行字节码指令的时候,碰到了变量赋值的指令就会执行该操作。
Store(存储):作用于工作内存中的变量,将工作内存中的变量放入主内存,以便进行write操作。
Write(写入):作用于主内存中的变量,将store得到的变量放入主内存的变量中。
二. volatile关键字
1.概述:
Volatile是jvm提供的最轻量级的同步机制,。被volatile修饰的变量具有两种特性。其一,变量的可见性,即当变量被修改时,修改后的值对所有线程来说是立即可见的。其二,禁止指令重排序优化。基于volatile变量的运算在并发操作下并不一定是安全的,因为java的运算操作并不具有原子性(即如果操作不是原子的,依然没法保证volatile同步的正确性)。
2.使用volatile进行同步需满足的两条规则:
①、运算结果并不依赖变量的当前值,或能够确保只有单一的线程修改变量值。
l 对变量的写入操作不依赖于该变量的当前值(比如a=0;a=a+1的操作,整个流程为a初始化为0,将a的值在0的基础之上加1,然后赋值给a本身,很明显依赖了当前值),或者确保只有单一线程修改变量。
②、变量不需要与其他状态变量共同参与不变约束。
l 该变量不会与其他状态变量纳入不变性条件中。(当变量本身是不可变时,volatile能保证安全访问,比如双重判断的单例模式。但一旦其他状态变量参杂进来的时候,并发情况就无法预知,正确性也无法保障)
volatile适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。这是一种很简单的同步场景,这时候使用volatile的开销将会非常小。
4.Volatile和synchronized区别:
1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
3.volatile仅能实现变量的修改可见性,并能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
三. Happens-Before原则
说了这么多,发现可以使用volatile和synchronized关键字进行同步。但是,你不会无缘无故就使用它们,存在竞争、线程安全问题的时候才应该考虑使用,但是如何判断是否存在这些问题呢?下面介绍java内存模型中的一个重点原则——先行发生原则(Happens-Before),使用这个原则作为依据,来指导你判断是否存在线程安全和竞争问题。
程序顺序规则:在程序中,如果A操作在B操作之前(比如A代码在B代码上面,或者由A程序调用B程序),那么在这个线程中,A操作将在B操作之前执行。
管理锁定规则:一个unlock操作先于后面对同一个锁的lock操作之前执行。
Volatile变量规则:对一个volatile变量的写操作必须在对该变量的读操作之前发生。
线程启动规则:线程的Thread.start必须在该线程所有其他操作之前发生
线程终止规则:线程中所有操作都先行发生于该线程的终止检测。可以通过Thread.join()方法结束、Thread.isAlive()的返回值判断线程是否终止。
线程中断规则:对线程interrupt()方法的调用必须在被中断线程的代码检测到interrupt调用之前执行。
对象终结规则:对象的初始化(构造函数的调用)必须在该对象的finalize()方法完成。
传递性:如果A先行发生于B,B先行发生于C,那么A先行发生于C。
这些操作是无需使用任何同步手段就能保证成立的先行发生规则。如果要线程A、B,需要B能看到A操作的结果(无论两者是否在一个线程当中),需要A、B满足Happens-Before关系,如果两个操作不存在Happens-Before关系,JVM会对他们进行任意重排序。当A和B在同一个线程中,或者两个线程使用同样的锁,他们就能满足Happens-Before,如果使用不同锁,就不满足。
四. 原子性、可见性、有序性
Java内存模型就是围绕着在并发过程中如何处理这三个特性来建立的。
1.原子性:
除了long型字段和double型字段外,java内存模型确保访问任意类型字段所对应的内存单元都是原子的。这包括引用其它对象的引用类型的字段。此外,volatile long 和volatile double也具有原子性。(虽然java内存模型不保证non-volatile long 和 non-volatile double的原子性,当然它们在某些场合也具有原子性。)(译注:non-volatile long在64位JVM,OS,CPU下具有原子性)
2.可见性:
可见性就是指当一个线程修改了共享变量的值,其他线程能立即得知这个修改。可见性是通过当变量每次修改后都将其同步回主内存,每次使用变量之前都从主内存中加载变量值来实现。普通变量与volatile变量的区别在于,volatile变量规则保证新值能立即同步回主内存以及每次使用前立即从主内存刷新。除了volatile之外synchronized关键字和final关键字也能实现可见性。
3.有序性:
Java语言提供了volatile和synchronized两个关键字来保证线程间操作的有序性。在java中,有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程内观察另一个线程,所有操作都是无序的。前半句讲的是“线程内表现为串行语义”,后半句指“指令重排序”和“工作内存和主内存同步延迟”。
五. 线程
1定义
线程也叫作轻量级进程,是大多现代操作系统的基本调度单位。在同一个进程中,多个线程共享内存空间,因此需要足够的同步机制才能保证正常访问。每个线程本身都有各自的程序计数器、栈和局部变量等。在java中使用线程调度的方式是抢占式的,需要由操作系统分配执行时间,线程本身无法决定(例如java中,只有Thread.yield()可以让出自己的执行时间,但是并没有提供可以主动获取执行时间的操作)。虽然java中线程调度由系统执行,但是还是可以通过设置线程优先级来“建议”操作系统多给某些线程分配执行时间(然后,这并不一定就能保证高优先级的先执行,所以不太靠谱...)。
Java定义了如下几种线程状态,一个线程有且仅有一个:
- sleep,wait区别:通过上图看到sleep结束后立即进入可运行状态,而wait后唤醒的线程还要竞争对象锁;所以sleep不会释放对象锁(如果有synchronized块话),而wait会释放对象锁。
- 两个队列:等待池和锁池,Java中每个对象都会有一个等待池和一个锁池,进入等待池的对象被唤醒后进入锁池竞争资源。
- join:主线程等待调用join方法的子线程执行结束后再继续执行。分布式处理框架fork/join
- yield:让出CPU资源给其他的任务,由yeild后直接进入可运行状态可以看到,yeild不会释放对象锁(如果有synchronized块话)