Java内存模型——JMM

Java内存模型定义了一种多线程访问java内存的规范

  1. java内存模型将内存分为主内存和工作内存。类的状态存储在主内存中,每次java线程用到主内存中的变量时需要读取一次主内存中的变量值,并拷贝到自己的工作内存中。运行线程代码时,操作的是自己工作内存中的数据。在线程执行完毕后,会将最新值更新到主内存。
  2. 规范中定义了几个原子操作,用于操作主内存和工作内存中的变量
  3. 内存规范中定义了volatile变量的使用规范
  4. happens-before先行发生原则,只要符合这些原则,则不需要额外进行同步处理,如果不符合规则,这段代
    码就是线程非完全的

硬件模型

java 大量驻留线程池 java多线程保存数据_java

  • 线程在读取主内存中的数据到 CPU 缓存时,就会产生一个数据存放在不同的位置,这种多个备份就会有 2 个问题: 可见性和静态条件 。
  • 多线程之间是可以通过使用 PipedInputStream/PipedOutputStream 互相传递数据,但是他们之间的沟通只能通 过共享变量来实现。
  • 主内存是多个线程共享的 。
  • 当 new 一个对象时,也就是被分配到主内存中,每个线程又有自己的工作内存,工作内存中存储了主存中操 作对象的副本。

堆和栈

栈帧

每个线程都有自己的栈内存,用于存储本地变量、方法参数和栈调用。一个线程中存储的变量对其他线程是不可见的。

堆是所有线程共享的一块公共内存区域,jdk1.6+引入逃逸分析,对象都在堆中创建,为了提高线程的执行效率,会从堆中创建一个缓存到自己的栈中,此时,如果多个县城使用该变量就会引发问题,这时需要引入volatile变量要求线程从主存中读取变量的值。

线程操作某个对象时的执行顺序

java 大量驻留线程池 java多线程保存数据_后端_02

  1. 从主内存中赋值变量到当前工作内存中(read和load)
  2. 执行代码,改变共享变量值(use和assign)
  3. 用工作内存中的数据刷新主内存中的相关内容(store和write)

volatile关键字

volatile 关键字实际上是 Java 提供的一种轻量级的同步手段,因为 volatile 只能保证多线程内存可见性,不能保证 操作的原子性。

任何被volatile修改的变量都不会进行副本拷贝,任何操作都在主内存中,所有线程都可以立刻看到。

务必注意:volatile保证可见性,并不保证原子性

使用 volatile 的限制:

1、对变量的写操作不依赖于当前值

2、该变量没有包含在具有其它变量的不变式中

volatile的特性

  1. 保证可见性
  2. 保证有序性,JMM可以禁止读写volatile变量前后语法的大部分重排序优化,可以保证变量的赋值操作顺序和程序中的执行顺序一致
  3. 部分原子性,针对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对于一个线程所做的变动能给其它线程可见提供了保证。

  1. 程序次序规则:线程内代码可以按照先后顺序执行。
  2. 管程锁定规则:对于同一个锁,一个解锁操作一定发生在另一个线程锁定之前。
  3. volatile变量规则:前一个valatile写操作在后一个volatile读操作之前。
  4. 线程启动规则:一个线程内的任何操作都必须在线程start调用之后。
  5. 线程终止规则:一个线程的所有操作都会在线程终止之前。
  6. 对象终结规则:一个对象的终结操作都必须在这个对象构造完成之后。
  7. 可传递性:如果操作A先于B,操作B先于C,则A先于C。

线程状态

线程状态切换

线程从创建并启动到消亡经历了5种状态:新建、就绪、运行、阻塞和死亡。

  1. 初始状态(new新建态):new Thread(…)
  2. 可运行状态(就绪态):线程初始化后,调用start方法,只能针对新建态线程对象调用start方法,否则出现异常illegalThreadStateException
  3. 运行状态(执行态):就绪态获取了CPU,执行线程run()。注意:有多个CPU就会有多个线程并行执行。目前采用的是基于时间片轮转法的抢占式调度策略,在选择可以运行线程时会考虑线程的优先级
  4. 运行态的线程可以调用yield()使运行态的线程转入就绪态,重新竞争CPU资源
  5. 阻塞态:由于某些原因使线程对象放弃CPU使用权,临时停止运行,直到线程重新进入就绪态,才有机会执行。
  6. 终止状态(死亡态):程序运行完成或者因为异常退出run(),该线程结束生命周期。直接调用stop()也可以结束进程(但不建议使用)。主线程main()结束时,其它线程不受影响。
    注意:不要试图针对一个死亡态的线程对象调用start()重新启动。

阻塞可以分为3种:

  • 等待阻塞:执行的线程执行了wait()(Object类定义的),JVM就会将线程放入到等待队列中,直到调用notify或者notifyAll进行唤醒,重新申请锁进入锁池中。
  • 同步阻塞:执行线程在获取对象的同步锁时,如果锁已经被其它线程占用,则JVM将线程放入到锁池中。
  • 其它阻塞:执行sleep()或者join(),或者发出IO请求时。JVM会将线程对象置为阻塞状态。当sleep()状态超时、join()等待的线程终止或者超时、或者IO处理完毕,线程再次转入就绪态。

java 大量驻留线程池 java多线程保存数据_System_03

线程状态

Java 线程的状态可以从 java.lang.Thread 的内部枚举类java.lang.Thread$State 得知

public enum State { 
    NEW, 
    RUNNABLE, 
    BLOCKED, 
    WAITING, 
    TIMED_WAITING, 
    TERMINATED; 
}

java 大量驻留线程池 java多线程保存数据_java 大量驻留线程池_04

常见问题

  • 如何强制启动一个线程
    在 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();
			}
		}

	}
}