Java内存模型——JMM
Java内存模型定义了一种多线程访问java内存的规范
- java内存模型将内存分为主内存和工作内存。类的状态存储在主内存中,每次java线程用到主内存中的变量时需要读取一次主内存中的变量值,并拷贝到自己的工作内存中。运行线程代码时,操作的是自己工作内存中的数据。在线程执行完毕后,会将最新值更新到主内存。
- 规范中定义了几个原子操作,用于操作主内存和工作内存中的变量
- 内存规范中定义了volatile变量的使用规范
- happens-before先行发生原则,只要符合这些原则,则不需要额外进行同步处理,如果不符合规则,这段代
码就是线程非完全的
硬件模型
- 线程在读取主内存中的数据到 CPU 缓存时,就会产生一个数据存放在不同的位置,这种多个备份就会有 2 个问题: 可见性和静态条件 。
- 多线程之间是可以通过使用 PipedInputStream/PipedOutputStream 互相传递数据,但是他们之间的沟通只能通 过共享变量来实现。
- 主内存是多个线程共享的 。
- 当 new 一个对象时,也就是被分配到主内存中,每个线程又有自己的工作内存,工作内存中存储了主存中操 作对象的副本。
堆和栈
栈帧
每个线程都有自己的栈内存,用于存储本地变量、方法参数和栈调用。一个线程中存储的变量对其他线程是不可见的。
堆
堆是所有线程共享的一块公共内存区域,jdk1.6+引入逃逸分析,对象都在堆中创建,为了提高线程的执行效率,会从堆中创建一个缓存到自己的栈中,此时,如果多个县城使用该变量就会引发问题,这时需要引入volatile变量要求线程从主存中读取变量的值。
线程操作某个对象时的执行顺序
- 从主内存中赋值变量到当前工作内存中(read和load)
- 执行代码,改变共享变量值(use和assign)
- 用工作内存中的数据刷新主内存中的相关内容(store和write)
volatile关键字
volatile 关键字实际上是 Java 提供的一种轻量级的同步手段,因为 volatile 只能保证多线程内存可见性,不能保证 操作的原子性。
任何被volatile修改的变量都不会进行副本拷贝,任何操作都在主内存中,所有线程都可以立刻看到。
务必注意:volatile保证可见性,并不保证原子性
使用 volatile 的限制:
1、对变量的写操作不依赖于当前值
2、该变量没有包含在具有其它变量的不变式中
volatile的特性
- 保证可见性
- 保证有序性,JMM可以禁止读写volatile变量前后语法的大部分重排序优化,可以保证变量的赋值操作顺序和程序中的执行顺序一致
- 部分原子性,针对volatile变量的符合操作不具备原子性
特例
public class Test3 {
private static boolean flag = false;
private static int i = 0;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(100);
flag = true;
System.out.println("flag被修改了!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
while(!flag) {
i++;
}
System.out.println("程序执行结束");
}
}
//问题:程序不能执行结束,主线程一直处于死循环状态
//解决方法:给flag添加关键字volatile
总结
JMM在不同的内存构架和操作系统中有相同的明确的行为,JMM对于一个线程所做的变动能给其它线程可见提供了保证。
- 程序次序规则:线程内代码可以按照先后顺序执行。
- 管程锁定规则:对于同一个锁,一个解锁操作一定发生在另一个线程锁定之前。
- volatile变量规则:前一个valatile写操作在后一个volatile读操作之前。
- 线程启动规则:一个线程内的任何操作都必须在线程start调用之后。
- 线程终止规则:一个线程的所有操作都会在线程终止之前。
- 对象终结规则:一个对象的终结操作都必须在这个对象构造完成之后。
- 可传递性:如果操作A先于B,操作B先于C,则A先于C。
线程状态
线程状态切换
线程从创建并启动到消亡经历了5种状态:新建、就绪、运行、阻塞和死亡。
- 初始状态(new新建态):new Thread(…)
- 可运行状态(就绪态):线程初始化后,调用start方法,只能针对新建态线程对象调用start方法,否则出现异常illegalThreadStateException
- 运行状态(执行态):就绪态获取了CPU,执行线程run()。注意:有多个CPU就会有多个线程并行执行。目前采用的是基于时间片轮转法的抢占式调度策略,在选择可以运行线程时会考虑线程的优先级
- 运行态的线程可以调用yield()使运行态的线程转入就绪态,重新竞争CPU资源
- 阻塞态:由于某些原因使线程对象放弃CPU使用权,临时停止运行,直到线程重新进入就绪态,才有机会执行。
- 终止状态(死亡态):程序运行完成或者因为异常退出run(),该线程结束生命周期。直接调用stop()也可以结束进程(但不建议使用)。主线程main()结束时,其它线程不受影响。
注意:不要试图针对一个死亡态的线程对象调用start()重新启动。
阻塞可以分为3种:
- 等待阻塞:执行的线程执行了wait()(Object类定义的),JVM就会将线程放入到等待队列中,直到调用notify或者notifyAll进行唤醒,重新申请锁进入锁池中。
- 同步阻塞:执行线程在获取对象的同步锁时,如果锁已经被其它线程占用,则JVM将线程放入到锁池中。
- 其它阻塞:执行sleep()或者join(),或者发出IO请求时。JVM会将线程对象置为阻塞状态。当sleep()状态超时、join()等待的线程终止或者超时、或者IO处理完毕,线程再次转入就绪态。
线程状态
Java 线程的状态可以从 java.lang.Thread 的内部枚举类java.lang.Thread$State 得知
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
常见问题
- 如何强制启动一个线程
在 Java 中没有办法强制启动一个线程,线程的运行是由线程调度器负责控制的,java 没有公布相关的 API - 垃圾回收
System.gc()或者 Runtime.getRuntime().gc()可以通知执行垃圾回收,但是垃圾回收器是一个低优先级线程,所以 具体运行实际不确定 - 如何提前唤醒休眠的线程
当线程进入休眠态时,它会定时结束休眠(注意时间不是很靠谱,因为休眠时间需要依靠系统时间和调度器)。如 果需要提前唤醒,则需要通过 interrupt 方法实现,所谓的 interrupt 就是产生一个异常 InterruptedException - 不推荐使用的方法
①stop():不要使用 stop 方法停止一个线程,因为 stop 方法过于极端,可能会出现同步问题,导致数据不一致。一般最佳软件实践可以考虑通过设置标志值,通过 return、break、异常等手段来控制线程中的执行流程自然停止
Thread t1=new Thread(()->{
for(int i=0;i<100;i++)
System.out.println(Thread.currentThread()+"---"+i);
});
t1.start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.stop();//使线程 t1 立即终止
建议通过其它方法实现线程自然终止,而不是立即终止
public class Test4 {
public static void main(String[] args) {
MyThread t1=new MyThread();
t1.start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// t1.stop();
t1.setFlag(false);
}
static class MyThread extends Thread {
private boolean flag = true;// 这个标志值用于实现自然终止
@Override public void run() {
for(int i=0;i<100 && flag;i++)
System.out.println(Thread.currentThread() + "---" + i);
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
}
②suspend():suspend 方法用于暂停线程的执行,该方法容易导致死锁问题,因为该线程在暂停时并不释放所占有的资源,从而 出现其它需要该资源的线程与当前线程出现环路等待问题,从而引发死锁。
resume 方法用于恢复 suspend 挂起线程的运行,resume 和 suspend 两个方法是配套使用,suspend 使得线程 进入阻塞状态,并且不会自动恢复,必须通过对应的 resume 才能使得线程重新进入可运行状态
sleep方法
Thread 类中定义的方法,可以阻塞当前线程,是静态方法
sleep(long)让当前线程休眠等待指定的时间,时间单位为 ms。休眠时间的准确性依赖于操作系统时钟和 CPU 调 度机制。如果需要可以通过 interrupt 方法来唤醒休眠的线程
sleep(long 毫秒数,int 纳秒数)
目前比较流行的写法为 TimeUnit.SECONDS.sleep(1) 等价于 Thread.sleep(1000)
案例:实现一个时间显示,每隔一秒钟更新一次显示。例如倒计时显示分析:
DateFormat 用于实现日期时间的格式化 date–string 或者解析 string–date 。如果需要使用自定义格式,一般使用 SimpleDateFormat
DateFormat df = new SimpleDateFormat("yyyy-MM-ddE hh:mm:ss");
Date now = new Date();
String ss = df.format(now);
System.out.println(ss);
try{
//按照指定的格式解析字符串,将字符串转换为日期类型
Date nn = df.parse(ss);
System.out.println(nn);
}catch(ParseException e){
e.printStackTrace();
}
代码实现
package com.yan.test1;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class Test3 {
public static void main(String[] args) {
DateFormat df = new SimpleDateFormat("yyyy-MM-ddE HH:mm:ss");
for (;;) {
Date now = new Date();
String ss = df.format(now);
System.out.println(ss);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}