ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

JAVA多线程-内存模型JMM、volatile关键字和线程状态

2022-02-09 23:29:59  阅读:149  来源: 互联网

标签:JAVA 变量 flag volatile 内存 线程 多线程 方法


JAVA内存模型JMM

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

硬件模型

线程在读取主内存中的数据到CPU 缓存时,就会产生一个数据存放在不同的位置,这种多个备份就会有2 个问题:可见性和静态条件。


多线程之间是可以通过使用PipedInputStream/PipedOutputStream 互相传递数据,但是他们之间的沟通只能通过共享变量来实现。
     主内存是多个线程共享的
     当new 一个对象时,也就是被分配到主内存中,每个线程又有自己的工作内存,工作内存中存        储了主存中操作对象的副本

栈和堆
 

每个线程都有自己的栈内存,叫做栈帧,用于存储本地变量、方法参数和栈调用,一个线程中存储的变量对其它线
程是不可见的,而堆是所有线程共享的一块公用内存内存区域。
JDK1.6+引入逃逸分析,对象都在堆中创建,为了提升线程的执行效率,会从堆中创建一个缓存到自己的栈,如果
多个线程使用该变量就可以引发问题。这时需要引入volatile 变量要求线程从主存中读取变量的值


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


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


volatile关键字

volatile 关键字实际上是Java 提供的一种轻量级的同步手段,因为volatile 只能保证多线程内存可见性,不能保证
操作的原子性。
任何被volatile 修改的变量,都不会进行副本的拷贝,任何操作都即使写在主内存中。因此使用volatile 修改的变
量的修改,所有线程都立刻可以看到。务必注意:volatile 保证可见性,但是不保证原子性

public class MyTest{
private volatile int a; //直接操作主内存,没有线程对工作内存和主内存同步
public void add(int count){
this.a=a+count;
}
}

使用volatile 的限制:
1、对变量的写操作不依赖于当前值
2、该变量没有包含在具有其它变量的不变式中


volatile 的特性
1、保证可见性
2、保证有序性,JMM 可以禁止读写volatile 变量前后语法的大部分重排序优化,可以保证变量的赋值操作顺序和程序中的执行顺序一致
3、部分原子性,针对volatile 变量的符合操作不具备原子性

特例:

public class Test2 {
	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
private static volatile boolean flag = false;


线程状态和线程状态切换

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

public static enum State{
		//线程状态.
		NEW,//尚未启动的线程处于此状态
		RUNNABLE,//在JAVA虚拟机中执行的线程处于此状态
		BLOCKED,//被阻塞等待监视器锁定的线程处于此状态
		WAITING,//正在等待另一个线程执行的特定动作的线程处于此状态
		TIMED_WAITING,//正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
		TERMINATED;//已退出的线程处于此状态
}

1、new 新建态(初始态): new Thread(...)
2、就绪态(可运行态): 线程初始化后,调用start 方法,只能针对新建态线程对象调用start 方法,否则出现异常illegalThreadStateException
3、执行态(运行态):就绪态获取了CPU,执行线程run 方法。注意有多CPU 则会有多个线程并行执行。目前采用的是基于时间片轮转法的抢占式调度策略,在选择可以运行线程时会考虑线程的优先级。
4、运行态的线程中可以调用yield 方法使运行态的线程转入就绪态,重新竞争CPU 资源
5、阻塞态:由于某种原因使线程对象放弃CPU 使用权,临时停止运行,直到线程重新进入就绪态,才有机会执行。
阻塞可以分为3 种:1、等待阻塞:执行的线程执行了wait 方法(Object 类定义的),JVM 就会将线程放入到等待队列中,直到调用notify 或者notifyAll 进行唤醒,重新申请锁进入锁池中。2、同步阻塞:执行线程在获取对象的同步锁时,如果锁已经被其它线程占用,则JVM 将线程放入到锁池中。3、其它阻塞:执行执行sleep 或者join方法,或者发出IO 请求时。JVM 会将线程对象置为阻塞状态。当sleep 状态超时、join 方法等待的线程终止或者超时、或者IO 处理完毕,线程再次转入就绪态。
6、死亡态(终止态),程序运行完成或者因为异常退出run 方法,该线程结束生命周期。直接调用stop 方法也可以结束线程,但是这个方法容易导致数据不一致的问题,所以通常不建议使用。主线程main 方法结束时其它线程不受影响。注意:不要试图针对一个死亡态的线程对象调用start 方法重新启动

常见问题
如何强制启动一个线程:在Java 中没有办法强制启动一个线程,线程的运行是由线程调度器负责控制的,java 没有公布相关的API

垃圾回收:System.gc()或者Runtime.getRuntime().gc()可以通知执行垃圾回收,但是垃圾回收器是一个低优先级线程,所以具体运行实际不确定

不推荐的使用的方法:不要使用stop 方法停止一个线程,因为stop 方法过于极端,可能会出现同步问题,导致数据不一致。一般最佳软件实践可以考虑通过设置标志值,通过return、break、异常等手段来控制线程中的执行流程自然停止

建议通过其它方法实现线程自然终止,而不是立即终止:

suspend 方法用于暂停线程的执行,该方法容易导致死锁问题,因为该线程在暂停时并不释放所占有的资源,从而出现其它需要该资源的线程与当前线程出现环路等待问题,从而引发死锁。
resume 方法用于恢复suspend 挂起线程的运行,resume 和suspend 两个方法是配套使用,suspend 使得线程进入阻塞状态,并且不会自动恢复,必须通过对应的resume 才能使得线程重新进入可运行状态 

public class Test2 {
	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;
		}
	}
}

线程状态切换图


线程状态监控工具

如果项目在生产环境中运行,不可能频繁调用Thread#getState()方法去监测线程的状态变化。JDK 本身提供了一些监控线程状态的工具,还有一些开源的轻量级工具如阿里的Arthas

jvisualvm 远程监控
jvisualvm 是JDK 自带的堆、线程等待JVM 指标监控工具,适合使用于开发和测试环境。它位于JAVA_HOME/bin目录之下。其中线程Dump 的按钮类似于下面要提到的jstack 命令,用于导出所有线程的栈信息。

 

jstack
jstack 是JDK 自带的命令行工具,功能是用于获取指定PID 的Java 进程的线程栈信息。例如本地运行的一个IDEA实例的PID 是11376,那么只需要输入:jstack 11376

 

JMC
JMC 也就是Java Mission Control,它也是JDK 自带的工具,提供的功能要比jvisualvm 强大,包括MBean 的处理、线程栈已经状态查看、飞行记录器等

 

标签:JAVA,变量,flag,volatile,内存,线程,多线程,方法
来源: https://blog.csdn.net/m0_56161893/article/details/122850700

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有