iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > Python >java多线程:基础详解
  • 965
分享到

java多线程:基础详解

2024-04-02 19:04:59 965人浏览 八月长安

Python 官方文档:入门教程 => 点击学习

摘要

目录Java内存模型主内存和工作内存的交互命令内存模型的原子性内存模型的可见性内存模型的有序性指令重排优化的底层原理valatile原理volatile与加锁的区别先行发生原则线程的

Java内存模型

  • Java内存模型与Java内存结构不同,Java内存结构指的是JVM内存分区。Java内存模型描述的是多线程环境下原子性,可见性,有序性的规则和保障。
  • Java内存模型提供了主内存和工作内存两种抽象,主内存指的是共享区域 ,工作内存指的是线程私有工作空间。
  • 当一个线程访问共享数据时,需要先将共享数据复制一份副本到线程的工作内存(类比操作系统中的高速缓存),然后在工作内存进行操作,最后再把工作内存数据覆盖到主内存。主内存和工作内存交互通过特定指令完成。
  • 如下为并发内存模型图

在这里插入图片描述

多线程环境下原子性,可见性,有序性分别指的是

  • 原子性:程序执行不会受到线程上下文切换的影响。
  • 可见性:程序执行不会受到CPU缓存影响。
  • 有序性:程序执行不会受到CPU指令并行优化的影响。

主内存和工作内存的交互命令

  • lock:把主内存的一个变量标记为一个线程锁定状态。
  • unlock:把主内存中处于锁定状态的变量释放出来。
  • read:把主内存的变量读取到线程工作内存。
  • load:把工作内存的值放入工作内存变量副本中。
  • use:把工作内存变量的值传递给执行引擎。
  • assign:把执行引擎接收到的值赋值给工作内存变量。
  • store:把工作内存的值传送到主内存中。
  • write:把工作内存的值写入到工作内存变量。

内存模型的原子性

Java内存模型只保证store和write两个命令按顺序执行,但不保证连续执行,因此多个线程同时写入共享变量可能出现线程安全问题。

诸如i++的操作,首先将主存中的变量i的值拷贝一份拿到线程的本地内存,在本地内存进行自增操作,然后将新的i值写回主存。
但是涉及到多线程环境下的线程上下文切换就会出现问题,可能线程1将i值拿来进行自增操作,然后还来不及写回主存,时间片用完,轮到线程2执行,线程2对i进行自减操作,然后轮到线程1时,线程1将上一次的值写回内存,就会将线程2上一步的计算结果覆盖,就会产生错误的结果。

通过多线程的学习我们知道,对共享数据加锁可以保证操作的原子性,相当于i++操作对应底层命令是原子化绑定的,这样就不会出现线程安全问题,但是会导致程序性能降低。

内存模型的可见性

  • 对于频繁从主存取值的操作,JIT可能会将其进行优化,以后每次操作不从主存取值,而是从CPU缓存中取值。一旦线程1每次从寄存器取值,那么此时主存中变量值的变化对于线程1来说就是不可见的。
  • 如下,子线程是无法感知主存中flag的修改的,子线程就无法停止。
    
    public class Test {
        static boolean flag = true;
        public static void main(String[] args) throws InterruptedException {
            //3秒后线程无法停止
            new Thread(()->{
                while(flag){
                }
            }).start();
            Thread.sleep(3000);
            System.out.println("flag = false");
            flag =false;
        }
    }
    
  • 有两种方法可以保证主存中数据的可见性,方法1是加锁。加锁既可以保证原子性,又可以保证可见性。
    
    public class Test {
        static boolean flag = true;
        public static void main(String[] args) throws InterruptedException {
                    new Thread(()->{
                        while(flag){
                            synchronized (Test.class){}
                        }
                    }).start();
                    Thread.sleep(3000);
                    System.out.println("flag = false");
                    flag =false;
                }
    }
    
  • 还有一种方法是使用volatile关键字,它可以保证当前线程对共享变量的修改对另一个线程是一直可见的。volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但是volatile关键字只能保证可见性,不能保证原子性。volatile适用于一个线程写多个线程读的应用场景,保证各个线程可以实时感知到其他线程更新的数据。
    
    public class Test {
        static volatile boolean flag = true;
        public static void main(String[] args) throws InterruptedException {
                    new Thread(()->{
                        while(flag){
                        }
                    }).start();
                    Thread.sleep(3000);
                    System.out.println("flag = false");
                    flag =false;
                }
    }
    
  • 对于多线程同时操作共享变量的情况,使用volatile关键字依然会出现线程安全问题,因为原子性无法保证。

public class Test {
    static volatile int a = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            for(int i=0;i<100000;i++){
                a++;
            }
        }).start();
        new Thread(()->{
            for(int i=0;i<100000;i++){
                a--;
            }
        }).start();
        Thread.sleep(1000);
        System.out.println(a); //不能保证a为0
    }
}

内存模型的有序性

有序性是指在单线程环境中, 程序是按序依次执行的。而在多线程环境中, 程序的执行可能因为指令重排而出现乱序。

指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序。这种排序(比如两个变量的定义顺序)不会影响单线程的结果,但是会对多线程程序产生影响。

比如 a=1 b=2两条语句就可能发生指令重排。而 a=1,b=a+1 不会发生指令重排。

示例:线程1执行f1方法,线程2执行f2方法。两个线程同时执行,可能发生如下结果: f1中发生指令重排 flag=true先执行,a=1后执行。线程1先执行flag=true,然后轮到线程2执行,此时flag为true,执行if语句,i=1。这就是指令重排造成的程序错乱。


class Test{
    int a = 0;
    boolean flag = false;
    public void f1() {
        a = 1;                   
        flag = true;           
    }
    public void f2() {
        if (flag) {                
            int i =  a +1;      
        }
    }
}

可以用volatile修饰flag来禁用指令重排达到有序性。

加锁也可以避免指令重排带来的混乱,但是本身并没有禁止指令重排,因为保证了原子性,所以即使指令重排在同步代码块中依然相当于单线程执行,也不会有逻辑上的错误。

指令重排优化的底层原理

一个指令的执行被分成:取指、译码、访存、执行、写回 5个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。
指令流水线并不是串行的,并不会因为一个耗时很长的指令在“执行”阶段呆很长时间,而导致后续的指令阻塞。相反,流水线是并行的,多个指令可以同时处于同一个阶段,只要CPU内部相应的处理部件未被占满即可。

比如,依次有两条指令a和b需要执行,如果是串行执行,它们的执行过程如下


指令a                          指令b
阶段1 阶段2 阶段3 阶段4 阶段5    阶段1 阶段2 阶段3 阶段4 阶段5

但是,假如阶段2耗时很长,使用串行的方式就无法在一个阶段阻塞的时候去执行其他阶段。

如下就是流水线的方式来执行,当指令a的阶段2阻塞时,完全可以去执行指令b的阶段1,这样就提高了程序执行效率,最大程度利用CPU各个部件。


指令a                         
阶段1 阶段2 阶段3 阶段4 阶段5   
指令b
      阶段1 阶段2 阶段3 阶段4 阶段5

因此指令重排就是对于一个线程中的多个指令,可以在不影响单线程执行结果的前提下,将某些指令的各个阶段进行重排序和组合,实现指令级并行。

valatile原理

如下,假设对变量a用valatile关键字修饰。


valatile int a = 0;

那么,对变量a的写指令之后都会插入写屏障,对变量a的读指令之前都会插入读屏障。


a++;
//写屏障
//读屏障
int b = a;

写屏障会保证写屏障之前的所有对共享数据的改动都会同步到主存中。读屏障会保证读屏障之后对共享数据的读取操作都会到主存去读取。这样就保证了,每次对valatile变量的修改对其他线程始终是可见的,从而保证了可见性。

另外,写屏障会保证写屏障之前的指令不会被排到写屏障后面。读屏障会保证读屏障之后的代码不会排到读屏障前面。这样就保证了有序性。

如下,由于写屏障的存在,int b=1;语句只能排在 a++前面,不能颠倒顺序。


int b=1;
a++;
//写屏障

volatile与加锁的区别

volatile只能保证可见性和有序性,不能保证原子性,加锁既可以保证可见性 原子性 有序性都可以保证。

volatile只适用于一个线程写,多个线程读的情况,对于多个线程写的情况,必须要加锁。

加锁相对于volatile是更加重量级的操作,所以一般能用volatile解决的问题就不要加锁。

先行发生原则

先行发生是Java内存模型中定义的两项操作之间的偏序关系。如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响被操作B察觉。

先行发生原则–是判断是否存在数据竞争、线程是否安全的主要依据。先行发生原则主要用来解决可见性问题的。

如下代码


//以下操作在线程A中执行
i = 1;
//以下操作在线程B中执行
j = i;
//以下操作在线程C中执行
i = 2

如果A先行发生于B,B先行发生于C,那么必然j的值为1。如果A先行发生于B,B和C没有先行发生关系,那么j的值可能为1也可能为2。

Java内存模型存在一些天然的先行发生关系,这些先行发生关系不需要任何的同步操作,就可以保证其线程安全。

1、程序次序规则。在一个线程内,书写在前面的代码先行发生于后面的。确切地说应该是,按照程序的控制流顺序,因为存在一些分支结构。

2、Volatile变量规则。对一个volatile修饰的变量,对他的写操作先行发生于读操作。

3、线程启动规则。Thread对象的start()方法先行发生于此线程的每一个动作。

4、线程终止规则。线程的所有操作都先行发生于对此线程的终止检测。

5、线程中断规则。对线程interrupt()方法的调用先行发生于被中断线程的代码所检测到的中断事件。

6、对象终止规则。一个对象的初始化完成(构造函数之行结束)先行发生于发的finilize()方法的开始。

7、传递性。A先行发生B,B先行发生C,那么,A先行发生C。

8、管程锁定规则。一个unlock操作先行发生于后面对同一个锁的lock操作。

线程的三种实现方式

  • 使用内核线程实现
  • 内核线程就是直接由操作系统内核支持的线程,通过内核完成线程的切换。
  • 通过线程调度器来负责线程调度,即将线程任务分配到指定处理器。
  • 在用户态,每个内核级线程会一 一对应一个轻量级进程,就是通常所说的用户级线程,多个用户级线程可以组成一个用户进程。
  • 如下所示:p进程 LWP用户线程 KLT内核线程 Thread Scheduler 线程调度器

在这里插入图片描述

  • 由于内核线程的支持,每个用户线程都是独立调度单位,即使有一个用户线程阻塞了,也不会影响当前进程其他线程执行。但是用户线程切换 创建 终止都要内核支持,内核与用户态切换代价较高。
  • Java就是使用内核线程实现的,无论是windows还是linux都是基于内核线程实现的。
  • 使用用户线程实现
  • 操作系统内核只能感知到用户进程,用户进程为操作系统内核的基本调度单位。
  • 基于用户进程实现的用户线程,线程的创建 切换 销毁都是进程自己管理,与内核没有关系。因为操作系统只能把处理器资源分配到进程,那么线程的运行 阻塞 生命周期管理都要用户进程自己来实现。
  • 内核不参与线程调度,因此线程的上下文切换开销比较小,但是实现起来非常复杂,而且当一个用户级线程阻塞整个进程都会阻塞,并发度不高。

在这里插入图片描述

  • 混合模式实现
  • 用户线程和内核线程使用M对N的映射来实现,兼顾两者的优点。

在这里插入图片描述

总结

本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注编程网的更多内容!

--结束END--

本文标题: java多线程:基础详解

本文链接: https://www.lsjlt.com/news/132462.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • java多线程:基础详解
    目录Java内存模型主内存和工作内存的交互命令内存模型的原子性内存模型的可见性内存模型的有序性指令重排优化的底层原理valatile原理volatile与加锁的区别先行发生原则线程的...
    99+
    2024-04-02
  • java——多线程基础
    目录多线程使用场景:线程和进程区别:创建线程的方式:Thread类的有关方法:线程的同步:模拟火车站售票程序线程的同步:synchronized1. 同步代码块:2. synchro...
    99+
    2024-04-02
  • Java多线程基础
    目录一、线程二、创建多线程的方式1、继承Thread类实现多线程2、实现Runnable接口方式实现多线程3、Callable接口创建线程三、线程的生命周期与状态四、线程的执行顺序1...
    99+
    2024-04-02
  • 【Java】Java多线程编程基础
    文章目录 1. 进程与线程1.1 进程与线程的基本认识1.1.1 进程(Process)1.1.2 线程(Thread) 1.2 为什么会有线程1.2.1 以看视频为例 2. ...
    99+
    2023-10-03
    java python 开发语言
  • 怎样解析Java基础多线程
    怎样解析Java基础多线程,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。 多线程是Java学习的非常重要的方面,是每个Java程序员必须掌握的基本技能。一、进程...
    99+
    2023-06-02
  • Java多线程——基础概念
    目录java多线程并发与并行:多线程使用场景:创建线程的方式:Thread类的有关方法:线程的同步:       ...
    99+
    2024-04-02
  • JAVA多线程线程安全性基础
    目录线程安全性什么是线程安全的代码什么是线程安全性 总结线程安全性 一个对象是否需要是线程安全的,取决于它是否被多个线程访问,而不取决于对象要实现的功能 什么是线程安全的代码 核心:...
    99+
    2024-04-02
  • Java多线程Thread基础学习
    目录1. 创建线程   1.1 通过构造函数:public Thread(Runnable target, String name){}  或:publ...
    99+
    2023-05-17
    Java多线程 Java 多线程Thread
  • 新手了解java 多线程基础知识
    目录一、线程的生命周期JDK中用Thread.State类定义了线程的几种状态:二、线程同步1、为什么要有线程同步2、synchronized2.1同步代码块2.2同步方法3、Loc...
    99+
    2024-04-02
  • Java基础:彻底搞懂java多线程
    目录进程与线程使用多线程的优势线程的状态创建线程线程中断总结进程与线程 进程 进程是操作系统结构的基础,是程序在一个数据集合上运行的过程,是系统进行资源分配和调度的基本单位。进程可...
    99+
    2024-04-02
  • python多线程基础
    一、python多线程基础    python多线程主要涉及两个类:thread和threading,后者实际是对前者的封装,由于threading提供了更完善的锁机制,运用场景更多,重点学习了这个类的使用。threading.Thread...
    99+
    2023-01-31
    多线程 基础 python
  • c#多线程之线程基础
    目录一、简介二、创建线程三、暂停线程四、线程等待五、终止线程六、检测线程状态七、线程优先级八、前台线程和后台线程九、向线程传递参数十、使用C# Lock 关键字十一、使用Monito...
    99+
    2024-04-02
  • python多进程基础详解
    目录进程开启一个进程JOIN方法进程之间空间隔离进程的常用方法current_process 查看pid(进程id)os.getpid() 查看进程id进程其他方法和属性守护进程互斥...
    99+
    2024-04-02
  • Java多线程基础概念是什么
    本篇内容主要讲解“Java多线程基础概念是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java多线程基础概念是什么”吧!并发与并行并行,表示两个线程同时做事情。并发,表示一会做这个事情,一...
    99+
    2023-06-17
  • Java多线程编程基石ThreadPoolExecutor示例详解
    目录前言为什么用线程池参数介绍核心线程数和最大线程数设置使用示例线程池执行任务的流程线程池执行流程图源码解读基础属性和变量execute(Runnable command)addWo...
    99+
    2023-05-16
    Java多线程ThreadPoolExecutor Java ThreadPoolExecutor
  • py基础---多线程、多进程、协程
    目录 Python基础__线程、进程、协程 1、什么是线程(thread)? 2、什么是进程(process)? 3、进程和线程的区别...
    99+
    2023-01-31
    多线程 进程 基础
  • Java多线程基础知识点有哪些
    这篇文章主要为大家展示了“Java多线程基础知识点有哪些”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“Java多线程基础知识点有哪些”这篇文章吧。一、线程什么是线程:线程是进程的一个实体,是CP...
    99+
    2023-06-25
  • Java零基础学习多线程的示例
    这篇文章给大家分享的是有关Java零基础学习多线程的示例的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。守护线程从线程分类上可以分为:用户线程(以上讲的都是用户线程),另一个是守护线程。守护线程是这样的,所有的用户...
    99+
    2023-06-06
  • java线程的基础实例解析
    目录一、线程初步认识1、什么是线程2、Java本身就是多线程3、为什么要使用多线程4、线程的优先级5、线程的状态​6、Daemon线程二、线程启动和终止1、构造线程2、什...
    99+
    2024-04-02
  • 深入了解Python的多线程基础
    目录线程多线程Python多线程创建线程GIL锁线程池总结线程 线程(Thread),有时也被称为轻量级进程(Lightweight Process,LWP),是操作系统独⽴调度和分...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作