线程创建的两种方式

继承Thread类

class MyThread extends Thread{......@Overridepublic void run(){......}
}MyThread mt = new MyThread();  //创建线程
mt.start();                    //启动线程

实现Runnable接口

class MyThread implements Runnable{......@Overridepublic void run(){......}
}
MyThread mt = new MyThread();    //创建Runnable对象
Thread td = new Thread(mt);      //创建线程
td.start();                      //启动线程

两种方式的区别:
1.继承Thread类受限于JAVA单继承的特性,而实现Runnable接口则没有这种限制;
2.Runnable的代码可以被多个Thread共享,适用于多个线程处理同一资源(同一个Runnable对象)的情形。

线程的生命周期

创建

  • new一个线程对象。

就绪

  • 创建线程之后,调用了start()方法,此时线程进入就绪队列,等待CPU调度。或者阻塞状态的线程被唤醒。

运行

  • 获取到了CPU资源,执行run()方法。

终止

  • 线程的run()方法执行完毕,或者线程调用了相关的终止方法。

阻塞

  • 运行状态下,由于某种原因让出CPU资源,暂停自己的执行,就进入了阻塞状态,如调用了sleep()方法。

守护线程

JAVA线程有两类

  • 用户线程
    运行在前台,执行具体的任务,程序的主线程、连接网络的子线程等等都是用户线程
  • 守护线程
    运行在后台,为其他前台线程服务,一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作。数据库连接池中的监测线程,JVM虚拟机启动后的检测线程还有最常见的垃圾回收线程等等都是守护线程。
  • 设置守护线程
    通过调用Thread类的setDaemon(true)方法来设置当前线程为守护线程。

注意事项

  • setDaemon(true)必须在start()方法之前调用,否则会抛出异常。
  • 守护线程中产生的新线程也是守护线程。
  • 不是所有的任务都可以分配给守护线程来执行,比如读写操作或计算逻辑。
  • 前面提到用户线程结束之后,守护线程就没有守护对象了,会随JVM一起结束工作,所以一旦在守护线程里面读写操作,那如果用户线程都结束了,可能会读写异常。

下面就是一个主线程结束,打断了守护线程的写操作的实例,我们本来是想写到word1到word999的,主线程scanner读取键盘输入,阻塞等待,一旦键盘输入完成,即scanner结束阻塞解除之后,主线程运行下去就结束了,守护线程即便没有写到999,也还是结束了。

class DaemonThread implements Runnable{@Overridepublic void run() {// TODO Auto-generated method stubFile file = new File("D:" + File.separator+"word.txt");try {writeFile(file);} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}}private void writeFile(File file) throws Exception {OutputStream oStream =new FileOutputStream(file);int i = 1;while(i < 1000) {Thread.sleep(1000);oStream.write(("\r\nword" + i).getBytes());System.out.println(Thread.currentThread().getName() + ": word" + i++);}oStream.close();}
}
public class ThreadTest {public static void main(String[] args) {System.out.println("进入主线程" + Thread.currentThread().getName());Thread thread = new Thread(new DaemonThread());thread.setDaemon(true);thread.start();Scanner scanner = new Scanner(System.in);scanner.nextLine();System.out.println("退出主线程");}
}

在word6时候,控制台输入结束:

文件内容是这样的:

可见性

基本概念

  • 可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到。
  • 共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量。

JAVA内存模型(JMM)

Java Memory Model描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取出变量这样的底层细节。

JMM

所有共享变量都存储在主内存中

每个线程都有自己独立的工作内存,里面保存该线程使用到的变量副本(是主内存中该变量的一份拷贝)

工作内存和主内存交互的八种原子操作
关于一个变量如何从主内存中拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型定义了以下八种操作来完成,JVM实现时候必须保证下面提及的每一种操作都是原子的、不可分的。

①用于主内存变量:

锁定(lock):把变量标识为一条线程独占的状态。
解锁(unlock):把处于锁定状态的变量释放出来。
读取(read):把变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
载入(load):把read操作从主内存中得到的变量值放入工作内存的变量副本中。

②用于工作内存变量:

使用(use):把工作内存中一个变量的值传递给执行引擎。
赋值(assign):把从执行引擎接收到的值赋给工作内存的变量。
存储(store):把工作内存中变量的值传送到主内存中,以便随后的write操作使用。
写入(write):把store操作从工作内存中得到的变量的值放入主内存的变量中。

确保并发操作安全的原则:

1.顺序但不连续:必然是先read后load以及先store后write,有先后顺序的要求,但是并没有要求连续执行,比如read和load,store和write之间是要插入其他指令的,比如read a,read b,load a,load b就是合理的;
2.读了一定载,存了一定写,读或者存之后不允许拒绝载入和写操作,即read\load,store\write不允许他们之一单独出现;
3.线程不能没有assign,就同步回主内存,想同步回去总归要做点不一样的事情吧hhh
4.新的变量只能在主内存“诞生”,不允许在工作内存中使用未初始化的变量,即use和store这种涉及工作内存变量的操作,都要求这个变量是先执行过assign和load操作的(初始化操作)
5.一个变量只允许一个线程lock操作,但是可以同一个线程lock套Lock,如果多次Lock,自然也需要多次unlock才能解锁变量
6.加锁之前先重新读取值,Lock操作会清空工作内存中此变量的值,use之前需要重新load或者assign进行初始化
7.不能解空锁,不能解别人的锁unlock不能解锁没有lock的变量,也不能在一个线程unlock另一个线程中lock的变量
8.解锁之前先同步,unlock前,必须要先store、write,把变量同步回主内存。
我们下面所讲的所有内容,都可以在这些规则中找到对应的点。
以上8种内存操作以及8种规则限定,再加上其他volatile的一些特殊规定,完全确定了JAVA中哪些内存访问操作在并发下是安全的,由于定义相当严谨但又十分繁琐,很麻烦,后面我们会介绍一个先行发生原则来确定一个访问在并发环境下是否安全。

两条规定:

  • 1.线程不能直接与主内存交互。
    线程对共享变量的所有操作必须在自己的工作内存中进行,不能直接从主内存读写。
  • 2.各线程工作内存之间相互独立。
    不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量

共享变量可见性实现的原理:
以线程1对共享变量的修改要被线程2及时看到为例,要经过以下步骤:

  • 1.把工作内存1中更新过的共享变量刷新到主内存中;
  • 2.从主内存中将刷新过的共享变量读取工作内存2中。

那么要实现可见性,必须保证:

  • 1.线程修改后的共享变量值及时从工作内存刷新到主内存(store and write);
  • 2.其他线程能及时把共享变量新值从主内存更新到自己的工作内存(load and read)。

可见性实现方式:
语言层面:

  • synchronized
  • volatile

先了解两个小的基本概念之后我们再来介绍synchronized和volatile。
指令重排序
代码书写的顺序与实际执行的顺序不同,是编译器或者处理器为了提高程序性能而做的优化。原因是重排序之后的指令可能更加符合CPU的执行特点,最大限度发挥CPU性能。
int num1 = 1;
int num2 = 2;

可能实际的执行顺序是相反的:
int num2 = 2;
int num1 = 1;

as-if-serial
无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致。
一个例子:
int num1 = 1;
int num2 = 2;
int sum = num1 + num2;

在单线程中前两句随便重排序(数据依赖关系不允许重排序),但是第三句绝对是在前两句之后,这样不会影响sum的最终结果是3。指令重排序不会影响单线程中变量可见性。
而多线程中,指令重排序很有可能导致变量可见性问题。后面详细叙述。

下面详细叙述synchronized和volatile。
synchronized
synchronized实现互斥锁,能够实现原子性和可见性。

实现可见性原理

JMM关于synchronized的两条规定:

线程解锁unlock前(退出sychronized代码块时候),必须把共享变量的最新值刷新到主内存中store and write

线程加锁lock时(进入sychronized代码块时候),将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存重新读取最新的值load and read(加锁和解锁需要同一把锁)

这两条规定也就保证了synchronized能够实现共享变量的可见性。

过程
1.获得互斥锁
2.清空工作内存
3.从主内存拷贝变量的最新副本到工作内存
4.执行代码
5.更改后的共享变量值刷新到主内存
6.释放互斥锁
synchronized可见性分析代码示例
下面是一个线程不安全的代码:

public class Test {private boolean ready = false;private int result = 0;private int number = 1;public void write() {ready = true;//1.1number = 2;//1.2}public void read() {if (ready) {result = number * 3;//2.1}System.out.println("result的值为:" + result);//2.2}private class ReadWriteThread extends Thread{private boolean flag;public ReadWriteThread(boolean flag) {this.flag = flag;}@Overridepublic void run() {if (flag) {//构造方法传入true执行写操作write();}else {//构造方法传入false执行读操作read();}}}public static void main(String[] args) {Test fuck = new Test();fuck.new ReadWriteThread(true).start();//启动写线程fuck.new ReadWriteThread(false).start();//启动读线程}
}

以上程序的逻辑很简单,就是一个Test类里面,有个result和number,有write和read方法。它内部有个线程类,通过传给线程类的构造器的boolean值来控制这个线程是读线程还是写线程。
然后在main里面分别启动一个读和写线程,运行后结果的输出可能有很多种情形。
result的值可能为:0,6,3
导致这种情况(共享变量不可见)出现的原因:

线程的交叉执行
比如最朴素的情况,先写后读,则输出结果6.写线程先启动1.1执行完ready=true,number此时是1,然后读线程得到CPU,进入if语句,最终打印result是3,而如果读线程直接执行完毕,result就是0。

重排序结合线程交叉执行
比如写线程先启动,重排序先执行1.2,然后读线程就执行了,这时候即使number值变了,result的结果也是0。
共享变量更新后的值没有在工作内存与主内存间及时更新

而安全的代码则是在write和read方法都加上synchronized关键字。

  public synchronized void write() {ready = true;//1.1number = 2;//1.2}public synchronized void read() {if (ready) {result = number * 3;//2.1}System.out.println("result的值为:" + result);//2.2}

加了一把锁,锁内的代码在一段时间内只能有一个线程可以执行,只有当前线程释放锁,其他线程才能进入这块代码执行。

synchronized解决可见性的解决方案:

  • 原子性
    避免了线程交叉执行锁内代码,同样由于保证锁内只有一个线程执行,那么重排序对单个线程来讲as-if-serial,始终结果是不变的,也不存在由于指令重排序导致的不可见问题了。

  • 可见性
    上面提到了synchronized实现可见性的原理,在加锁和释放锁时候都是要刷内存的。也就保证了共享变量能够及时更新。不加synchronized也能更新,但是并不能保证是及时更新,那么高并发情况下就可能出现意外。

加了synchronized之后(这里是对象锁),就只有两种结果,0或者6了~
在写读之间加个sleep,让主线程休眠上1000ms,那么基本可以保证先写后读,最终结果只有6。
volatile
synchronized保证原子性和可见性,volatile保证了可见性。

volatile实现内存可见性的原理:
从编译器和处理器角度来讲,通过加入内存屏障和禁止重排序优化实现

对volatile变量执行write操作时候,处理器会在写操作前加入一条store屏障指令,会把工作内存中共享变量副本的值强制刷新到主内存共享变量中去,以便进行后续的write,主内存中就是写的最新的值。还能防止处理器,把volatile前面的变量重排序到volatile写操作之后。
对volatile变量执行read操作时候,处理器会在读操作前加入一条load屏障指令,会把主内存中共享变量的值放入工作内存的共享变量副本中去,以便进行后续的read,工作内存中就是读的最新的值。还能防止重排序。

volatile没有lock,不能实现原子性。最简单的例子就是count++,这个实在不想说了…算了还是写个代码看一下,这段代码是保证了race的可见性的,我们预期的race的结果应该是200000:

public class Test {public static volatile int race = 0;public static void increase() {race ++;}public static void main(String[] args) {Thread[] threads = new Thread[20];for(int i = 0; i < 20; i++) {threads[i] = new Thread(new Runnable() {@Overridepublic void run() {for (int j = 0; j < 10000; j++) {Test.increase();}}});threads[i].start();}while(Thread.activeCount() > 1) {Thread.yield();}System.out.println(race);}
}

不过最终的运行结果小于200000,就是由于volatile并不能保证原子性,race++的底层机器指令是分几步完成的,不同的线程就会进行交叉操作。

自增操作原子性的解决方案

使用synchronized关键字
使用ReentrantLock(java.until.concurrent.locks包下)
使用AtomicInteger(vava.util.concurrent.atomic包下)

使用synchronized示例:

public synchronized static void increase() {race ++;
}

使用ReentrantLock示例:
首先需要一个ReentrantLock对象,调用这个对象的lock方法,就加上了锁,这里推荐用try…finally写法,finally中调用unlock释放锁,因为锁中的操作可能抛出异常,当然这里race++显然没有异常,我们只是在这里演示一下推荐写法。

    public static ReentrantLock lock = new ReentrantLock();public static void increase() {lock.lock();try {race ++;}finally {lock.unlock();}}

volatile使用场合

  • ①对变量的写入操作不依赖当前值或者能够确保只有单一线程修改变量的值

不满足:number ++、 count = count *5等等
满足:boolean变量等

  • ②该变量没有包含在具有其它变量的不变式中。

不满足:程序中有两个volatile变量low和up,还有个不变式low<up

不满足上面两种情形的还是要通过加锁来保证原子性。第一种情形不再讨论了,第二个其实没有看懂。。网上搜了个例子。

public class A {private volatile int low=0;private volatile int up=100;public int getLow() { return low; }public int getUp() { return up; }public void setLow(int value) { if (value > upper) system.out.print(".......");low = value;}public void setUp(int value) { if (value < low) system.out.print(".......");up = value;}
}

这个例子里面是没有a++之类的东西的,这里面有两个VOLATILE变量,它包含了一个不变式就是LOWupper) 和 if (value < low) )初始值是0和100,如果有两个线程同时操作set函数,一比如A线程操作setlow(4),B线程setup(3),最后结果是4和3。所以这里我们是需要加锁的。volatile并没有卵用。

volatile和synchronized的比较

补充两点:

  • ①即使没有保证可见性的操作,如加锁或者volatile,很多时候共享变量依然能够得到主内存和工作内存之间的及时更新,出现不可见问题一般都是高并发情况下。
  • ②之前的那八个操作都必须是原子操作,而对64位的数据类型(long和double),模型中特别定义了宽松的规定,允许虚拟机将没有被volatile修饰的64位数据的读写划分为两次32位的操作分开进行,这叫long和double的非原子性协定(Noatomic Treatment of double and long Variables)这样就有可能导致线程交叉操作,出现某些线程修改“半个变量”的情形。但现在这样实现的JVM还是很罕见的。

本文作者:啦啦哇哈哈,
原文链接:https://www.jianshu.com/p/0ecca8ab9cb4

欢迎关注我的微信公众号「码农突围」,分享Python、Java、大数据、机器学习、人工智能等技术,关注码农技术提升•职场突围•思维跃迁,20万+码农成长充电第一站,陪有梦想的你一起成长

Java线程基础回顾及内存模型,看你还记得多少?相关推荐

  1. JAVA线程两种方法内存,JAVA线程基础回顾及内存模型(一)

    线程创建的两种方式 继承Thread类 class MyThread extends Thread{ ...... @Override public void run(){ ...... } } My ...

  2. java和硬件交互_Java内存模型

    Java内存模型 我们常说的JVM内存模式指的是JVM的内存分区:而Java内存模式是一种虚拟机规范,真实并不存在 Java虚拟机规范中定义了Java内存模型(Java Memory Model,JM ...

  3. Java 线程 - 基础及工具类 (二)

    Java 并发系列文章 Java 线程 - 并发理论基础(一) Java 线程 - 基础及工具类 (二) Java 线程 - 并发设计模式 (三) Java 线程(二) 通用的线程生命周期 Java ...

  4. java内存分配模型优点_高并发实战(二)-并发基础 缓存 MESI 内存模型

    左图为高速缓存 右图为多级缓存 数据的读取和存储都经过高速缓存,CPU核心与高速缓存有一条特殊的快速通道.主存与高速缓存都是连接在系统总线上,当然其他组件也是在此基础上进行通信的. 在高速缓存出现后不 ...

  5. modeler java堆空间,JVM|02内存模型

    JVM内存模型 概述 Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保 ...

  6. Java 线程 基础知识总结

    线程基础 很不严谨的说,线程是什么?线程就是为了让很多个东西并发执行,大大的提高程序执行的效率啊 三个非常重要的概念: 程序:一组写好了的静态代码块(就我们写的那些代码玩意) 进程:正在进行着的程序, ...

  7. java线程基础巩固---线程生命周期以及start方法源码剖析

    上篇中介绍了如何启动一个线程,通过调用start()方法才能创建并使用新线程,并且这个start()是非阻塞的,调用之后立马就返回的,实际上它是线程生命周期环节中的一种,所以这里阐述一下线程的一个完整 ...

  8. c++ 异步下获取线程执行结果_这份阿里技术官强推的java线程池笔记,建议你看一下

    线程池 线程是宝贵的内存资源,单个线程占1MB空间,过多分配易造成内存溢出 频繁的创建及销毁线程会增加虚拟机回收频率.资源开销.造成程序性能下降 因此线程池出现了 线程池的概念 线程容器,可设定线程分 ...

  9. Java线程:保留的内存分析

    本文将为您提供一个教程,使您可以确定活动应用程序Java线程保留Java堆空间的数量和位置. 将提供来自Oracle Weblogic 10.0生产环境的真实案例研究,以使您更好地理解分析过程. 我们 ...

  10. Java线程面试题,值得一看!

    1) 什么是线程? 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速.比如,如果一个线程完成 ...

最新文章

  1. html 表格隐藏 显示出来了,如何在html页面打开时隐藏表格
  2. python怎样判断字符串可以反序列化_从字符串值issu反序列化的Python AWS Lambda Stringargument构造函数/工厂方法...
  3. JAVA中定义常量的几种方式
  4. 开发团队里是否需要测试人员?
  5. 深入解析TRUNCATE TABLE – 手工修复和验证过程
  6. 佳肴_纪中1420_dfs
  7. java中成绩转换_成绩转换
  8. Ubuntu如何修改用户密码
  9. 密封槽设计标准_O型密封圈标准及沟槽设计规范
  10. docker卸载mysql_Docker卸载镜像
  11. GHD官宣全新中国区焕发大使乔欣
  12. linux下查看硬盘型号、大小等信息
  13. Nutch 笔记(二):Craw more urls and Recrawl
  14. 【全栈开发实战小草看书之Web端(一)开发环境】
  15. 雅思大作文写作--Cities
  16. android 禁止媒体扫描,Android通过.nomedia文件禁止多媒体库扫描指定文件夹下的多媒体文件...
  17. c语言查表程序,C语言查表法问题
  18. 【Unity3D应用案例系列】答题系统开发
  19. 自签名SSL证书有风险吗?
  20. Formal Verification (二) FPV、APPs

热门文章

  1. 前端一键复制粘贴插件——clipboard.js的使用
  2. T5: Text-To-Text Transfer Transformer
  3. opencv_找cv::Mat中的最大值和最小值
  4. 自动色彩均衡算法(ACE)原理及实现
  5. [论文评析] ArXiv,2021, Focal Self Attention技术分析
  6. 快到起飞 | PP-LCNet在CPU上让模型起飞,精度提升且比MobileNetV3+快3倍
  7. org.springframework.web.context.ContextLoaderListener 解决方案
  8. RabbitMQ如何解决各种情况下丢数据的问题
  9. 轻量级移动端日期选择器
  10. AFNetworking网络请求的get和post步骤