【JavaSE】多线程与并发编程(总结)


一、多线程概述

参考:https://www.cnblogs.com/zsql/p/11144688.html

进程与线程

进程与线程的区别

  1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位

  2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线

  3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间 (包括代码段,数据集,堆等) 及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见

  4. 调度和切换:线程上下文切换比进程上下文切换要快得多

在这里插入图片描述
线程(栈+PC+TLS)

  • 栈:用于存储该线程的局部变量,这些局部变量是该线程私有的,除此之外还用来存放线程的调用栈祯。
    我们通常都是说调用堆栈,其实这里的堆是没有含义的,调用堆栈就是调用栈的意思。
    那么我们的栈里面有什么呢?
    我们从主线程的入口main函数,会不断的进行函数调用,
    每次调用的时候,会把所有的参数和返回地址压入到栈中。

  • PC:是一块内存区域,用来记录线程当前要执行的指令地址 。
    Program Counter 程序计数器,操作系统真正运行的是一个个的线程,
    而我们的进程只是它的一个容器。PC就是指向当前的指令,而这个指令是放在内存中。
    每个线程都有一串自己的指针,去指向自己当前所在内存的指针。
    计算机绝大部分是存储程序性的,说的就是我们的数据和程序是存储在同一片内存里的
    这个内存中既有我们的数据变量又有我们的程序。所以我们的PC指针就是指向我们的内存的。

  • 缓冲区溢出
    例如我们经常听到一个漏洞:缓冲区溢出
    这是什么意思呢?
    例如:我们有个地方要输入用户名,本来是用来存数据的地方。
    然后黑客把数据输入的特别长。这个长度超出了我们给数据存储的内存区,这时候跑到了
    我们给程序分配的一部分内存中。黑客就可以通过这种办法将他所要运行的代码
    写入到用户名框中,来植入进来。我们的解决方法就是,用用户名的长度来限制不要超过
    用户名的缓冲区的大小来解决。

  • TLS:
    全称:thread local storage
    之前我们看到每个进程都有自己独立的内存,这时候我们想,我们的线程有没有一块独立的内存呢?答案是有的,就是TLS。
    可以用来存储我们线程所独有的数据。
    可以看到:线程才是我们操作系统所真正去运行的,而进程呢,则是像容器一样他把需要的一些东西放在了一起,而把不需要的东西做了一层隔离,进行隔离开来。

并行与并发

并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。

并行同一时刻上多个任务同时在执行 。

在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。

线程安全问题

多个线程同时操作共享变量1时,会出现线程1更新共享变量1的值,但是其他线程获取到的是共享变量没有被更新之前的值。就会导致数据不准确问题。

在这里插入图片描述

共享内存不可见性问题

  1. Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量 。(如下图所示)

在这里插入图片描述

在这里插入图片描述

上图中所示是一个双核 CPU 系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。CPU的每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。 那么Java内存模型里面的工作内存,就对应这里的 Ll或者 L2 缓存或者 CPU 的寄存器

  1. 线程A首先获取共享变量X的值,由于两级Cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0的值缓存到两级缓存,线程A修改X的值为1,然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache内和主内存里面的X的值都是1。

  2. 线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X=1;到这里一切都是正常的,因为这时候主内存中也是X=l。然后线程B修改X的值为2,并将其存放到线程2所在的一级Cache和共享二级Cache中,最后更新主内存中X的值为2,到这里一切都是好的。

  3. 线程A这次又需要修改X的值,获取时一级缓存命中,并且X=l这里问题就出现了,明明线程B已经把X的值修改为2,为何线程A获取的还是l呢?这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见。

synchronized 的内存语义:

这个内存语义就可以解决共享变量内存可见性问题。进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。会造成上下文切换的开销,独占锁,降低并发性

Volatile的理解:

该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时-,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。volatile的内存语义和synchronized有相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。不能保证原子性

二、实现多线程

方式1:继承Thread类

需求:我们要实现多线程的程序。 如何实现呢?

由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。
而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。


Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。 但是呢? Java可以去调用C / C++写好的程序来实现多线程程序。

由C/C++去调用系统功能创建进程,然后由Java去调用这祥的API, 然后提供一些类供我们使用。我们就可以实现多线程程序了。


那么Java提供的类是什么呢?

  • Thread类
    步骤
  • A : 自定义类MyThread继承Thread
  • B : 重写run () 方法
    • 为什么是run ()方法呢?
  • C :创建对象
  • D : 启动线程

注意:单独调用 run() 方法其实和调用一个类的普通方法是没有区别的,要想启动线程实际上应该调用的是 start() 方法,这是为什么呢?


start() 和 run() 的区别?

  • start() :
    使该线程开始执行;Java 虚拟机调用该线程的 run() 方法。
    用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体中的代码执行完毕而直接继续执行后续的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里的run()方法 称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

  • run() :
    如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回
    Thread 的子类应该重写该方法。
    run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

总结:
在这里插入图片描述
在这里插入图片描述
两个小问题

  • 为什么要重写 run()方法?
    • 因为run()是用来封装被线程执行的代码
  • run() 方法和 start() 方法的区别?
    • run():封装线程执行的代码,直接调用,相当于普通方法的调用
    • start():启动线程;然后由JVM调用此线程的run()方法

创建自定义线程代码:

//定义一个线程类
public class MyThread extends Thread {
   
  @Override
  public void run() {
   
    for(int i=0; i<100; i++) {
   
      System.out.println(i);
   }
 }
}

//调用方法执行线程
public class MyThreadDemo {
   
  public static void main(String[] args) {
   
    MyThread my1 = new MyThread();
    MyThread my2 = new MyThread();
//    my1.run();
//    my2.run();
    //void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法
    my1.start();

匿名内部类的方式创建一个线程

new Thread(new Runnable() {
   
    @Override
    public void run() {
   
        System.out.println("多线程");
    }
}).start();

获取和设置线程对象名称

在这里插入图片描述

//void setName(String name):将此线程的名称更改为等于参数 name
    my1.setName("高铁");
    my2.setName("飞机");
    
    
    
    //Thread(String name)
    MyThread my1 = new MyThread("高铁");
    MyThread my2 = new MyThread("飞机");
    
    
    System.out.println(Thread.currentThread().getName());
    
    

线程优先级
在这里插入图片描述
线程控制

在这里插入图片描述

在这里插入图片描述
等待线程死亡

public class ThreadTest {
   
    public static void main(String[] args)  {
   
        ThreadDemo th1=new ThreadDemo();
        ThreadDemo th2=new ThreadDemo();
        ThreadDemo th3=new ThreadDemo();

        th1.setName("康熙");
        th2
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_popo_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值