• 【高并发基石】多线程、守护线程、线程安全、线程同步、互斥锁


    前言

    很久之前就听过互联网架构中有三高,高可用、高并发、高性能,多线程是处理高并发问题的基石,起步阶段一定要对线程有一个系统深刻的印象,为以后做准备

    一、进程与线程

    线程(Thread):一个程序内部的一条执行路径。

    调用main方法的过程也是线程执行的一种体现。在程序中,如果同一时刻只有一条执行路径,这个程序就是一个单线程程序,同一时刻,可以执行多个线程这个程序就是一个多线程程序

    进程(Process):是计算机中的程序关于某数据集合上的一次运行活动,进程由线程创建,线程的容器也是程序的实体
    在这里插入图片描述

    多进程是指操作系统能同时运行多个任务(程序)
    多线程是指在同一程序中有多个顺序流在执行,当main线程中同时执行多个子线程就是多线程的体现

    在我们平时使用IDEA的过程中,执行完一段程序,运行框最后都会弹出这样的一段话,表示这个进程结束标志着程序退出
    在这里插入图片描述

    二、线程的创建

    在Java中通过java.lang.Thread来代表线程,按照面向对象的思想,Thread类必须给咱提供实现多线程的方式,所以对于程序员来说就派生了以下几种实现方式

    1.继承Thread类

    ①先定义一个子类MyThread继承线程类Java.lang.Tread(也就是Thread类),并重写他的内部run()方法
    ②创建MyThread类对象,然后调用线程对象的start()方法启动线程

    就像这样:

    public static void main(String[] args) {
         MyThread m1 = new MyThread();
         m1.start();//启动线程
       }
    class MyThread extends MyThread{...}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    优缺:在Java面向对象中的继承是单继承模式,通过上述方式创建线程虽然编码简单,但是会导致无法继承其他类,不利于扩展

    2.实现Runable接口

    ①定义一个线程任务类(MyThread)实现Runnable接口,重写run()方法
    ②创建任务类(MyThread)对象
    ③把任务对象交给Thread处理,调用线程对象的start()方法启动线程

    就像这样

    public static void main(String[] args) {
           MyThread m1 = new MyThread();
           new Thread(m1).start();//实现接口启动线程的方式
        }
    class MyThread extends MyThread{...}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性更强
    缺点:多了一层对象包装,如果线程有执行结果是不可以直接返回的

    3.匿名内部类实现

    本质还是实现接口 ,只不过是一种简写形式

    new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("通过匿名内部类方式启动的线程");
                }
       }).start();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    4.实现Callable、FutureTask接口

    ①得到任务对象
    1.定义类实现Callable接口,重写call方法
    2.用EutureTask把Callable对象封装成线程任务对象
    ②把线程任务对象交给Thread处理
    ③调用Thread的start()方法启动线程,执行任务
    ④线程执行完毕后、通过Eutureask的get方法去获取任务执行的结果
    这个方法比较复杂繁琐,现在使用的很少,了解就行

    三、线程创建的本质(🚩)

    通过上述三种方式,不难发现都需要通过start()方法启动线程,然后执行重写的run()方法,大家千万不要以为实现线程效果的是run()方法啊,他就是一个普通的不能再普通的方法,我们只是根据规则重写了一下,接口的目的是用来规范,这里的重写run()方法就是一种规范性的体现,甚至你去利用ctrl+B查看它的源码结果都是本地找不到用法
    在这里插入图片描述
    其实,实现线程效果的本质是 start0()方法而不是run(),start0()方法才是本地方法,是JVM机在调用,底层是通过C++来实现的,感兴趣的可以去debug追一下源码private native void start0();

    四、Thread常用API、构造器

    1.API: 比较常用的有,获取线程名称——getName()、设置名称——setName()、获取当前线程对象currentThread()、启动线程——start()、终止线程——interrupt()

    public class Test {
        public static void main(String[] args) {
            MyThread myThread = new MyThread();
            myThread.start();
        }
    }
    class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("Hello world!");
            System.out.println("当前线程名称:"+Thread.currentThread().getName());
        }
    }
    运行结果:
      Hello world !
      当前线程名称:Thread-0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2.线程休眠——sleep(),让线程休眠指定时间(单位是毫秒)

    Thread.sleep(1000);
    
    • 1

    3.构造器

    构造器说明
    public Thread( string name)可以为当前线程指定名称
    public Thread(Runnable target)把Runnable对象交给线程对象
    public Thread(Runnable target ,String name )把Runnable对象交给线程对象,并指定线程名称

    五、用户线程与守护线程

    Java线程主要分为用户线程守护线程
    理解这两个概念通过顾名思义的方式再适合不过了,用户线程是一个用户,守护线程是一个守护者时时刻刻守护用户,当用户程序执行完毕守护者就默默退出
    最常见的:main线程退出后,里面的子线程还在运行,当开启守护线程后,子线程会随着main线程的结束而退出
    创建守护线程只需通过该对象来调用setDaemon(true)实现即可

     public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() -> {
                while (true) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("用户线程在运行");
                }
            });
            thread.setDaemon(true);  //开启守护线程
            thread.start();
            Thread.sleep(1000);
            System.out.println("main线程结束~");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了要守护的人,守护线程没有工作可做,也就没有继续运行程序的必要了
    守护线程开启前后对比:
    在这里插入图片描述
    垃圾回收机制就是典型的守护线程!

    六、线程安全

    当多个线程同时操作同一个共享资源的时候可能出现业务安全问题,称为线程安全
    生活中取钱就是一个经典的线程安全问题
    在这里插入图片描述
    懒羊羊、美羊羊同时去取沸羊羊的钱,两个线程同时进行都符合判断条件,结果银行还亏了十万,这就是线程安全引起的生产事故
    线程安全问题出现的原因是因为存在多线程并发、同时访问共享资源存在修改共享资源的动作
    因此我们需要使线程同步!

    七、加锁实现线程同步(🚩)

    同步机制: 多线程编程情况中,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。
    互斥锁: Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
    关键字synchronized来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问

    在这里插入图片描述

    1.同步代码块

    作用:把出现线程安全问题的核心代码给上锁
    原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程进来执行

    synchronized(同步锁对象){
         操作共享资源的代码(核心代码)
    }
    
    • 1
    • 2
    • 3

    2.同步方法

    作用:把出现线程安全的核心方法给上锁
    原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程再来执行

    修饰符 synchronized 返回值类型 方法名称(形参列表){
             操作共享资源的代码(核心代码)
    }
    
    • 1
    • 2
    • 3

    给对象上锁之后可以实现线程同步就不会出现多个线程同时访问同一个共享资源的情况(高速路上的收费站就可以抽象成一把锁)
    注意:同步也有相应的局限性,会导致程序的执行效率降低,因为单位时间内启动的线程变少了

    八、线程死锁

    多个线程都占用了对方的锁资源,但不肯相让,导致了线程死锁
    模拟一个场景:
    在这里插入图片描述
    代码模拟:

    class DeadLockDemo extends Thread {
      static Object o1 = new Object();//资源1
      static Object o2 = new Object();//资源2
      boolean flag;
      public DeadLockDemo(boolean flag) {
      this.flag = flag;
     }
    @Override
    public void run() {
    if (flag) {
       synchronized (o1) { //对象互斥锁,  下面就是同步代
       System.out.println(Thread.currentThread().getName() + " 进入 1"); 
       synchronized (o2) { //  这里获得 li 对象的监视权
       System.out.println(Thread.currentThread().getName() + " 进入 2");
         }
      }
    } else {
       synchronized (o2) {
       System.out.println(Thread.currentThread().getName() + " 进入 3"); 
       synchronized (o1) { //  这里获得 li 对象的监视权
       System.out.println(Thread.currentThread().getName() + " 进入 4");
             }
          }
        }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    1. 如果 flag 为 T, 线程 A 就会先得到/持有 o1 对象锁, 然后尝试去获取 o2 对象锁
    2. 如果线程 A 得不到 o2 对象锁,就会 Blocked
    3. 如果 flag 为 F, 线程 B 就会先得到/持有 o2 对象锁, 然后尝试去获取 o1 对象锁
    4. 如果线程 B 得不到 o1 对象锁,就会 Blocked

    九、线程面试八股文整理(🚩)

    面试官:请你说一下进程和线程有什么区别
    答:
    1.进程是线程的执行单元,一个进程里至少会有一个线程
    2.进程有独立的内存空间,同一个进程内的线程是可以共享内存的
    3.线程是内存处理器的调度单元而进程不是
    4.不管是线程和进程都可以并发执行
    5.每个独立的进程都会有一个对应的程序入口,但一个独立的线程是不能独立运行的,必须要依赖一个对应的应用程序

  • 相关阅读:
    (附源码)spring boot新闻管理系统 毕业设计 211113
    Linux 上的 .NET 如何自主生成 Dump
    SPI协议的verilog实现(spi master slave联合实现)
    安装IDEA -- MacBook点击IDEA意外退出
    Haproxy 透传IP配置方法及测试
    mysql添加字段,如果有则不添加
    java实现富文本转word并下载,部分功能
    U-Boot与kernel之间的参数传递机制完全解析
    【就业必备知识】大学毕业如何处理档案和户口,小心变成死档和黑户
    python 基础知识(面向对象)
  • 原文地址:https://blog.csdn.net/weixin_57535055/article/details/125423657