一篇文章弄懂Java多线程基础和Java内存模型

浪人 等级 361 0 0

文章目录

一、多线程的生命周期及五种基本状态
二、Java多线程的创建及启动
1.继承Thread类,重写该类的run()方法
2.通过实现Runnable接口创建线程类
3.通过Callable和Future接口创建线程
三、Java内存模型概念
四、内存间的交互操作
五、volatile和synchronized的区别
写在前面:提起多线程大部门同学可能都会皱起眉头不知道多线程到底是什么、什么时候可以用到、用的时候是不是有共享变量问题等等一大堆问题。本篇文章将分为两部分第一部分是讲解多线程基础、第二部分讲解Java内存模型。

一篇文章弄懂Java多线程基础和Java内存模型

一、多线程的生命周期及五种基本状态

Java多线程生命周期,首先看下面这张经典的图,图中基本上囊括了Java中多线程重要知识点。

一篇文章弄懂Java多线程基础和Java内存模型

Java线程具有五中基本状态

  • 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

  • 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

  • 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

  • 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

    1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

    2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

    3.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

二、Java多线程的创建及启动

Java中线程的创建常见有如三种基本形式

1.继承Thread类,重写该类的run()方法

继承Thread类,重写该类的run()方法

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0 ;i < 50;i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
    public static void main(String[] args) {
        for (int i = 0;i<50;i++) {
            //调用Thread类的currentThread()方法获取当前线程
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 10) {
                new MyThread().start();
                new MyThread().start(); 
            }
        }
    }
} 

运行结果:

...
main 48
main 49
Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
Thread-1:0
... 

这是代码运行后的结果,从图中可以看出:

1、有三个线程:main、Thread-0 、Thread-1

2、Thread-0 、Thread-1两个线程输出的成员变量 i 的值不连续(这里的 i 是实例变量而不是局部变量)。因为:通过继承Thread类实现多线程时,每个线程的创建都要创建不同的子类对象,导致Thread-0 、Thread-1两个线程不能共享成员变量 i ;

3、线程的执行是抢占式,并没有说Thread-0 或者Thread-1一直占用CPU(这也与线程优先级有关,这里Thread-0 、Thread-1线程优先级相同,关于线程优先级的知识这里不做展开)

2.通过实现Runnable接口创建线程类

定义一个类实现Runnable接口;创建该类的实例对象obj;将obj作为构造器参数传入Thread类实例对象,这个对象才是真正的线程对象

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0 ;i < 50 ;i++) {
            System.out.println(Thread.currentThread().getName()+":" +i);
        }
    }

    public static void main(String[] args) {
       for (int i = 0;i < 50;i++) {
            System.out.println(Thread.currentThread().getName() + ":" +i);
            if (i == 10) {
                MyRunnable myRunnable = new MyRunnable();
                new Thread(myRunnable).start();
                new Thread(myRunnable).start();
            }
        }
        //java8 labdam方式
         new Thread(() -> {
            System.out.println(Thread.currentThread().getName());
        },"线程3").start();
    }
} 

运行结果:

...
main:46
main:47
main:48
main:49
Thread-0:28
Thread-0:29
Thread-0:30
Thread-1:30
... 

1、线程1和线程2输出的成员变量i是连续的,也就是说通过这种方式创建线程,可以使多线程共享线程类的实例变量,因为这里的多个线程都使用了同一个target实例变量。但是,当你使用我上述的代码运行的时候,你会发现,其实结果有些并不连续,这是因为多个线程访问同一资源时,如果资源没有加锁,那么会出现线程安全问题(这是线程同步的知识,这里不展开);
2、java8 可以使用lambda方式创建多线程。

3.通过Callable和Future接口创建线程

创建Callable接口实现类,并实现call()方法,该方法将作为线程执行体,且该方法有返回值,再创建Callable实现类的实例;使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值;使用FutureTask对象作为Thread对象的target创建并启动新线程;调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

public class MyCallable implements Callable<Integer> {
    private int i = 0;
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建MyCallable对象
        Callable<Integer> myCallable = new MyCallable();
        //使用FutureTask来包装MyCallable对象
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable);
        for (int i = 0;i<50;i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            if (i == 30) {
                Thread thread = new Thread(ft);
                thread.start();
            }
        }
        System.out.println("主线程for循环执行完毕..");
        Integer integer = ft.get();
        System.out.println("sum = "+ integer);
    }
} 

call()方法的返回值类型与创建FutureTask对象时<>里的类型一致。

三、Java内存模型概念

在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。

在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。

堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

主内存和工作内存解释

主内存(main memory): 类的实例所存在的区域,所有的实例都存在主存储器内,并且实例的字段也位于这里。主存储器为所有的线程所共享,主内存主要对应于Java堆中对象的实例数据部分。

工作内存(working memory): 每个线程各自独立所拥有的作业区,在working memory中,存有main memory中的部分拷贝,称之为工作拷贝(working copy)。

Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:

一篇文章弄懂Java多线程基础和Java内存模型

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

下面通过示意图来说明这两个步骤:

一篇文章弄懂Java多线程基础和Java内存模型

如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。
1、线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。
2、线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

四、内存间的交互操作

主内存与工作内存之间的交互操作定义了8种原子性操作。具体如下:

  • lock(锁定):作用于主内存的变量,将一个变量标识为一条线程独占状态

  • unlock(解锁):作用于主内存的变量,将一个处于锁定状态的变量释放出来

  • read(读取):作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中

  • load(载入):作用于工作内存的变量,把read传输的变量值放入或者拷贝到工作内存的变量副本

  • use(使用):作用于工作内存的变量,表示线程引用工作内存中的变量值,将工作内存中的一个变量的值传递给执行引擎

  • assign(赋值):作用于工作内存的变量,表示线程将指定的值赋值给工作内存中的某个变量。

  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送给主内存中

  • write(写入):作用于主内存的变量,将store传递的变量值放入到主内存中对应的变量里

下面图片能帮我们加深印象

一篇文章弄懂Java多线程基础和Java内存模型

五、volatile和synchronized的区别

首先需要理解线程安全的两个方面:执行控制和内存可见。

执行控制的目的是控制代码执行(顺序)及是否可以并发执行。

内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。

synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。

volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。

使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性。

对于volatile关键字,当且仅当满足以下所有条件时可使用:

  1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
  2. 该变量没有包含在具有其他变量的不变式中

volatile和synchronized的区别

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
收藏
评论区

相关推荐

使用synchronized关键字封装一个锁
代码如下: public class Lock { private boolean isLocked false; public void lock() { synchronized (this) { while (isLocked) { try {
JVM--指令重排序+volatile关键字
volatile 关键字 1、 volatile 翻译为 不稳定的,容易改变的。意思很明确,如果使用volatile 定义一个变量,意思就是可能该变量改变频繁,并且设计到多线程访问问题。 2、不过 现在jdk 的synchronized关键字 性能已经足够出色,也提供了多种Lock 类,因此 volatile关键字能实现的功能 jdk 的同步方法都能够实
JVM--虚拟机方法调用
概述 Java能做到一次编译,随处运行,最要是归功于java虚拟机 和class文件,我们知道,计算机是0和1 的世界,并且只认0和1,所以不管是什么语言什么编译类型,最终给计算机的都是0和1,java也不例外,但是我们的java编译成了class文件,class怎么就转换成0和1了呢,或者说机器码呢?其实这一步是虚拟机帮我们干的。当然,虚拟机是建立在不同
1 Java内存区域与内存溢出异常
1 java虚拟机对内存的管理 java虚拟机在执行java程序的时候把内存分为若干个不同的区,这些区各自有不同的用处,以及创建和销毁时间. 有的区随着虚拟机的启动而启动,有的区则依赖用户线程的启动和结束而启动和结束. 根据java虚拟机规范,java虚拟机将内存分为下面几个部分:如下图 image(https://imghelloworld.o
Groovy初探
开始之前 了解本教程的主要内容,以及如何从中获得最大收获。 关于本教程 如果现在有人要开始完全重写 Java,那么 Groovy 就像是 Java 2.0。Groovy 并没有取代 Java,而是作为 Java 的补充,它提供了更简单、更灵活的语法,可以在运行时动态地进行类型检查。您可以使用 Groovy 随意编写 Java 应用程序,连接 Java
《java 核心技术》卷1 学习 概述 第一章Java程序设计概述
从浅面了解Java 1.Java 在语言得地位 现在有所下降 但仍是老大哥 所以值得学习 2.Java特性 1.简单性:从一方面来说 Java可以支持在小型机器上运行 必定不是很复杂得,所以上手不难 2.面向对象:Java有相比于其他的语言 更简单得接口
volatile 关键字说明
volatile 变量修饰的共享变量进行写操作前会在汇编代码前增加 lock 前缀: 1),将当前处理器缓存行的数据写回到系统内存; 2),这个写会内存的操作会使其它 cpu 缓存该内存地址的数据无效。 Java 语言 volatile 关键字可以用一句贴切的话来描述 “ 人皆用之,莫见其形 “。理解 volatile 对理解它对理解 Java
一篇文章弄懂Java多线程基础和Java内存模型
文章目录 一、多线程的生命周期及五种基本状态 二、Java多线程的创建及启动 1.继承Thread类,重写该类的run()方法 2.通过实现Runnable接口创建线程类 3.通过Callable和Future接口创建线程 三、Java内存模型概念 四、内存间的交互操作 五、volatile和synchronized的
Java里面的十万个为什么
Java里面的十万个为什么 1.不是说 JVM 是运行 Java 程序的虚拟机吗?那 JRE 和 JVM 的关系是怎么样的呢?简单地说,JRE 包含 JVM 。JVM 是运行 Java 程序的核心虚拟机,而运行 Java 程序不仅需要核心虚拟机,还需要其他的类加载器,字节码校验器以及大量的基础类库。JRE 除包含 JVM 之外,还包含运行 Java 程序的其
Java学习路线
阶段一 (夯实基础)Java基础语法学习目标:1.熟悉Java等基本概念2.掌握Eclipse/IDEA集成开发工具的安装、配置和应用3.熟悉Java基本语法、基本类型、运算符和表达式4.掌握分支、循环逻辑语句、数组等知识的应用知识点列表:JDK、JRE、JVM基本概念Java环境搭建和配置安装和使用Eclipse/IDEA开发环境Java基本数据类型变量,
Java开发面试高频考点学习笔记(每日更新)
Java开发面试高频考点学习笔记(每日更新) 1.深拷贝和浅拷贝 2.接口和抽象类的区别 3.java的内存是怎么分配的 4.java中的泛型是什么?类型擦除是什么? 5.Java中的反射是什么 6.序列化与反序列化 7.Object有哪些方法? 8.JVM内存模型 9.类加载机制 10.对象的创建和对象的布局 11.Java的四种引用
2021年度最全面JVM虚拟机,类加载过程与类加载器
前言类装载器子系统是JVM中非常重要的部分,是学习JVM绕不开的一关。一般来说,Java 类的虚拟机使用 Java 方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表
2021年度最全面JVM虚拟机,类加载过程与类加载器
前言类装载器子系统是JVM中非常重要的部分,是学习JVM绕不开的一关。一般来说,Java 类的虚拟机使用 Java 方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表
2021年Java常见面试题目,100%好评!
专题1:JavaOOP 1、什么是B/S架构?什么是C/S架构 2、Java都有哪些开发平台? 3、什么是JDK?什么是JRE? 4、Java语言有哪些特点 5、面向对象和面向过程的区别 6、什么是数据结构? 7、Java的数据结构有哪些? 8、什么是OOP? 9、类与对象的关系? 10、Java中有几种数据类型
阿里程序员的Java之路!2021年最新Java面试点梳理
面试题模块介绍: 一、Java 基础 JDK 和 JRE 有什么区别? 和 equals 的区别是什么? 两个对象的 hashCode()相同,则 equals()也一定为 true,对吗? final 在 java 中有什么作用? java 中的 Math.round(1.5) 等于多少? String 属于基础的数据类型吗?