Java内存模型与线程
Java内存模型
Java内存模型(JMM)的定义是为了屏蔽硬件和操作系统的内存的访问差异,通过定义主内存和工作内存来定义各个变量的访问规则。
- 主内存中保存了所有的变量,工作内存是每个线程自己的私有内存,工作内存中的变量需要从主内存中读取变量,写变量的时候只能在工作内存中完成,不能直接写到主内存。不同的线程的工作内存之间是隔离的,线程之间的变量传递需要通过主内存完成。
Volatile变量的访问规则
Java内存模型的三大特性:可见性、原子性、有序性
-
Volatile可以保证的特性有:可见性和有序性
-
可见性:当工作线程中的变量更新之后,会立即写回主内存,并将当前主内存的变量加锁,当更新完成之后,将其他线程的工作内存中的变量失效,然后当使用该变量时重新从主内存中读取变量,即读取到最新的值
-
有序性:通过内存屏障来保证有序性
volatile无法保证原子性,如果需要保证多个操作的原子性,那么扔需要通过加锁的方式保证原子性
-
-
volatile在同步方面比其他的同步机制更快,提高并发量,因为volatile在读操作时与普通变量基本没有什么差别,写操作相对会 慢一些,因为需要插入多个内存屏障来保证有序性,和其他的同步机制比较,相对更快一些,所以volatile又被称为轻量级的同步机制
hapens-before规则
Java内存中所有的有序性并不是都依赖volatile和sychronized来保证的,而是定义了"先行发生"(happens-before)规则,主要是保证两个操作之间的执行顺序关系,保证程序执行的正确性
-
程序次序规则
单线程中,按照程序的代码顺序执行,写在前面的操作先行发生于后面的操作
-
管程锁定规则
一个unlock操作先行发生于后面的对同一个锁的lock操作
-
volatile变量规则
对一个volatile变量的写操作先行发生于后面对这个变量的读操作
即前面有一行代码是写volatile变量,后面有一行代码是读取相同的volatile变量,那么写操作是优先于读操作,不会乱序
-
线程启动规则
Thread对象的
start操作优先于对此线程的每一个操作 -
线程终止规则
线程中所有的操作都先行发生于此线程的终止操作
-
线程中断规则
对线程的
interrput()方法调用先行发生于被中断线程的中断事件检测 -
对象终结规则
一个对象的初始化完成(调用构造函数)先行发生于该对象的
finalize()方法 -
传递性
如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A一定也先行发生于操作C
Java线程
线程如何实现
线程是比进程更轻量级的实现,可以实现独立的CPU调度,提高多核CPU的优势。那么Java中的线程是如何实现的呢?通过Java中的Thread类可以看到,start方法等大部分操作线程的方法都是native的。Java的线程只是对操作系统的线程进行封装,屏蔽了在不同平台下对线程操作的差异性。
线程有3中主要实现的方式:使用内核线程实现、使用用户线程实现、使用用户线程加轻量级进程混合实现
Java线程调度
线程调度就是系统为不同的线程分配不同的CPU使用时间的过程。Java中的线程调度主要有两种方式:抢占式线程调度、协同式线程调度,Java使用的调度方式为:抢占式线程调度
- 抢占式线程调度:每个线程的执行时间由系统分配,线程的切换也通过系统分配,但是每个线程的执行时间分配可以通过线程的优先级来进行调整
- 协同式线程调度:线程的执行时通过线程自身控制,每个线程执行完成之后,通过其他线程执行。
线程状态
线程具有5种状态,在同一时间,线程只能处于其中一种状态中。
5种线程状态由特定的触发方式从一种状态转变为另一种状态。
- 新建:当线程被创建之后但是未启动的状态为新建状态
- 运行:当线程调用
start方法之后,即处于运行状态(因为调用start方法之后并不一定立即开始执行,所以将运行中、等待CPU分配都称为运行状态) - 阻塞:当线程在遇到
synchronized、lock等同步方法,需要等待获取锁时,会从运行状态转变为阻塞状态,等待锁的获取之后,重新分配CPU时间,进入运行状态执行 - 等待:程序中调用
sleep()、wait()方法都会从运行状态转换为等待状态,不会被CPU分配执行时间,直到线程被唤醒之后才会被分配CPU时间,进入运行状态 - 终止:当线程执行结束之后,从运行状态转换为终止状态,进行线程的销毁
彩蛋
-
DCL(双重锁校验)的单例模式中volatile作用是什么?不使用可不可以
双重锁校验的单例模式代码:
public class DCLSingleleton { private static volatile DCLSingleleton instance = null; private DCLSingleleton() {} public static DCLSingleleton getInstance() { if(instance == null) { synchronized (DCLSingleleton.class) { if(instance == null) { instance = new DCLSingleleton(); } } } return instance; } }volatile的作用有2个:- 保证单例对象
instance变量的可见性,在多线程的环境下,当instance变量第一次实例化之后,可以让其他的线程可见(其实通过synchronized已经可以保证了,因为在synchronized释放锁之后,也会将线程工作内存的变量刷新到主内存) new DCLSingleleton();操作并不是一个原子性操作,其中主要分为3步:1.分配内存空间、2.调用构造函数初始化对象、3.将内存空间的地址值指向instance变量。其中需要通过volatile的有序性,防止这3个步骤发生指令重排序,否则在多线程的情况下会出现其他线程获取到未完全初始化完成的对象
- 保证单例对象
-
Java中创建线程有几种方式?
-
继承
Thread重写run方法 -
实现
Runnable接口并实现run方法 -
实现
Callable接口并实现call方法
其中
Thread和Runnable的run方法都是没有返回值的,Callable的call方法可以有返回值当然,线程池也可以作为创建线程的方式
-