多线程的知识梳理

多线程的知识梳理

线程的创建方式

  • Lambda 最常用(推荐)

    Thread thread = new Thread(() -> {
    	System.out.println("hello thread");
    });
    thread.start();
    

    同时,在变量捕获中,lambda / 匿名内部类的捕获的变量中,必须是 final / 事实final 变量
    在这里插入图片描述

    但是在 C++ / Python / JS 这种语言的lambda变量捕获的时候就没有这种限制,Java的变量捕获是将变量进行“拷贝”一份,如果拷贝了导致前后的值不一致,可能就会出现bug或者安全问题,所以Java禁止这样的行为

  • 继承Thread,使用匿名内部类——创建Thread的子类的实例,面向对象,重写了run

    Thread thread = new Thread() {
    	@Override
        public void run() {
           System.out.println("hello thread");
        }
    };
    thread.start();
    
  • 创建子类,继承Thread,重写run方法

    class myThread extends Thread {
    	@Override
        public void run() {
            System.out.println("hello thread");
        }
    }
    public static void main(String[] args) {
            Demo5 demo5 = new Demo5();
            demo5.run();
        }
    
    
  • 实现Runnable接口,重写run方法

    class myThread implements Runnable{
    	@Override
        public void run() {
            System.out.println("hello run");
        }
    } 
    public static void main(String[] args) {
            myThread thread = new myThread();
            thread.run();
        }
    
  • 实现Runnable接口,使用匿名内部类

    Thread thread = new Thread(new Runnable {
    	@Override
        public void run() {
           System.out.println("Ciallo thread");
        }
    });
    thread.start();
    

另外Java规定,一个线程只能start一次,一个start对应一个线程

Thread.start()内部调用start0()

private native​​ void start0();

一般的 Java写的代码一般先编译成.class文件,再通过JVM解释运行

native代表底层是由==C++==写的,在JVM内部通过C++实现的方法,调用系统的原生API在系统内部创建线程,系统内部通过PCB来描述线程,通过链表来组织线程的调度,系统通过PCB结构体来对线程进行调度执行。当需要调度的时候就添加到链表上

每个PCB有独自的ID,每一个ID代表着一个线程

在这里插入图片描述

Thread的几个常见方法

  • 构造方法

在这里插入图片描述

  • 属性方法

    ID——getId()

    名称——getName()

    状态——getState()

    优先级——getPriority()

    是否为后台线程——isDaemon()

    是否存活——isAlive()

    是否被中断——isInterrupted()

查看Thread.State的枚举类型中,规定了线程的以下状态

线程状态

public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called {@code Object.wait()}
         * on an object is waiting for another thread to call
         * {@code Object.notify()} or {@code Object.notifyAll()} on
         * that object. A thread that has called {@code Thread.join()}
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }
  1. NEW

    线程还未开始的状态

  2. RUNNABLE

    可运行的线程状态,正在JVM中运行的或者正在等待其他来自系统的资源

  3. BLOCKED

    阻塞状态,因为等待监视锁而被阻塞的状态

  4. WAITING

    等待线程的状态,等待被唤醒或者等到其他线程结束

    1. wait()<—> notify()/ notifyAll()
    2. join()
  5. TIMED_WAITING

    限时等待状态,超过特定的时间就会继续执行下去

  6. TERMINATED

    终止状态,线程结束

    1. 线程正常运行结束

    2. 线程异常终止,如果线程出现异常可能会带动其他的线程一起崩

    3. 终止线程 Interrupt

      在Java中,终止线程不意味着“强行终止”,而是结束这个线程的入口方法

      Java不提供强制终止线程的方法,这种做法并不安全


线程安全问题

当某个逻辑一个线程运行的时候通常不会出现问题,但出现多个线程执行可能就会出现问题了。

多个线程同时运行,共占同一份资源,当对某个变量同时被两个或多个线程修改,可能就会出现冲突,有bug

如以下例子

public static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0;i < 5000;i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0;i < 5000;i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        System.out.println();
        System.out.println("count = " + count);

在这里插入图片描述在这里插入图片描述

发现每一次的执行结果都不一样,没有得到正确的数字,这是为什么?

  • 就把刚才的count++操作来说,CPU的指令一般有三条,分别是load、add、save三部分,完成一次count++操作,如果t1线程与t2线程的count++操作没有被打扰,则可以正常进行,但如果在load、add、save的任意一部分出现“插队”的情况呢?

在这里插入图片描述
在这里插入图片描述

刚才的线程安全问题,是跟系统的线程调度有关的~~,但我们无法干预系统的调度,它是随机的,所以故引出了

原因

  1. [ 根本原因 ] 系统的随机调度

  2. 多个线程同时对同一个变量进行修改

    注意区分修改 <=> 读取

    • 一个线程针对一个变量修改 🆗
    • 多个线程针对不同变量修改 🆗
    • 多个线程针对同一个变量**读取** 🆗
  3. 修改操作不是原子的

    引入了锁的概念

  4. 内存可见性

    本质还是由编译器优化导致的bug

  5. 指令重排序

    本质还是由编译器优化导致的bug

wait & notify / notifyAll

wait 就是与notify搭配的,而且要在synchronized内使用(locker.wait() <=> locker.notify() & notifyAl )

由于线程的随即调度的特性,所以wait与notify最大的特点是

  • 控制各个线程的逻辑执行顺序:T1线程执行逻辑1后,需要等待T2线程执行逻辑2,再继续下面的操作

在这里插入图片描述

可以理解为wait是让线程打了个小瞌睡😪💤

注意唤醒的问题
在这里插入图片描述
在这里插入图片描述

总结

  • 必须用 while,保证唤醒后再次判断条件,防止虚假唤醒和错误唤醒。
  • if 只能防一次,while 是防到位

notify是唤醒随机一个等待的线程,而notifyAll是唤醒所有的

wait与sleep的区别

  • wait需要在锁里面操作,而sleep不需要

  • wait一定要搭配notify唤醒,sleep过了规定的时间就会继续往下执行下去,而wait需要等待通知

    wait的设计初衷就是被notify,超时时间只是“后手”

  • wait与sleep一样都能起到阻塞效果

    sleep是按照一定的时间阻塞

    wait更偏向于执行逻辑的阻塞

  • wait会暂时地释放锁,然后再获取锁,而sleep放在锁内部,执行睡眠时不会释放锁

  • –另外sleep(0)并不是阻塞等待0ms,而是触发一次线程调度,主动让出一次线程,而具体的调度要看CPU

但interrupt又不同于sleep与wait,它是可能终止程序的~~

Thread类中run与start的区别

  • run方法是线程的执行入口,且Thread类本身 继承 实现了Runnable抽象接口,故创建线程时候需要重写run方法

  • start方法是线程启用的入口,一个线程对应一个start且只能start一次,start执行的时候会自动调用run方法

  • 创建线程后如果调用线程的run方法而不是start方法,并没有真正地创建线程,只是在main线程中执行了run中的重写的代码

  • start方法会真正地创建线程并实现重写的run方法

更加完善…

  1. 从调度上看

    start方法由JVM调度,使得新线程处于就绪状态,并执行run方法

    run只是普通方法,不会触发线程调度,完全由当前线程顺序执行

    在这里插入图片描述

  2. 可重复调度上

    start方法只能被调用一次,一个线程有且仅有一次start,调用多次会抛出异常

    run方法可以被当前线程重复调用

  3. 线程状态

    start方法使得线程处于new-runnable-waiting等一系列状态,具体要看CPU的调度

    run方法只是一个主线程调用的方法,不会有状态变化

单例模式

Java实例化需要消耗资源,本质上有些逻辑与场景只需要存在一个实例,如果随意创建多个就会导致资源开销的浪费、状态混乱甚至功能出错

饿汉模式

class mySingleton {
	private volatile static mySingletonLazy instance = new mySingletonLazy();
    
    public mySingletonLazy getInstance() {
        return instance;
    }
    
    private mySingletonLazy() {
        
    }
}

🎗️懒汉模式

这是普通的懒汉模式

class mySingletonLazy {
	private static mySingleton instance;

    private mySingleton() {

    }

    public mySingleton getInstance() {
        if (instance == null) {
           instance = new mySingleton();
        }
        return instance;
    }
}

这是加锁优化后的懒汉模式

class mySingletonLazy {
	private volatile static mySingleton instance;

    private mySingleton() {

    }

    public mySingleton getInstance() {
        //减少加锁开销
        if (instance == null) {
            synchronized (this) {
                //保证实例化过程原子性
                if (instance == null) {
                    instance = new mySingleton();
                }
            }
        }
        return instance;
    }
}

通常来说,我们都认为懒汉的行为是褒义,饿汉是贬义——因为饿汉开始就消耗资源了,不管使不使用,而懒汉是想要就创建,不想要就暂时搁置。

但对于线程安全的角度来说,懒汉的单例创建模式还是好的吗?


饿汉 vs 懒汉 线程安全

先下结论,饿汉是线程安全的,而懒汉是线程不安全的,但懒汉的不安全只出现在实例化之前,

  • 饿汉模式是天然的线程安全的,它的方法都是实例的读操作,当多个线程对同一变量进行读操作的时候是没有问题的

  • 懒汉模式的修改操作,原理上是原子的,但由于JVM优化的指令重排序问题,可能会使得懒汉模式的创建实例化的时候会出现线程安全问题

    • 来看普通的懒汉模式是如何触发线程安全问题的( ̄︶ ̄)↗ 
      在这里插入图片描述

      虽然在t1第二次创建实例后,t2的instance指向空,后续也会被GC回收,实际上已经消耗了两次实例化的资源,但如果这个对象的构造数据很大呢?那就消耗了非常多的时间


    • 可以选择加锁解决这个问题

      synchronized (this) {
         	if (instance == null) {
         		instance = new mySingleton();
         	}
      	return instance;
      }
      

      在这里插入图片描述

      进一步分析~~

      可以发现只要一进入方法就会加锁,而这个代码只有在实例化之前会出现线程安全问题,实例化之后就没有这个问题了

      此处即使把instance实例化成功之后还是频繁的上锁解锁 => 阻塞 => 效率问题

      可以给加锁增加判定的条件,减少加锁的开销

      //判定是否要加锁
      if (instance == null) {
        	synchronized (this) {
        		if (instance == null) {
         			instance = new mySingleton();
        		}
         	}
      }
      return instance;
      

但这样就真的没问题了吗?这其实还有指令重排序引起的问题

这行代码涉及的指令比较多,new mySingleton()实例化的代码分为了三个阶段

  1. 分配内存空间
  2. 针对空间进行初始化
  3. 分配内存首地址,赋值到引用变量中

但这个逻辑在指令重排序可能是1 => 3 => 2,这种执行顺序就会出现问题了,还是这个代码

在这里插入图片描述

所以此处应该禁止指令重排序,关掉编译器优化,还是用到volatile修饰~~

此处标注的饿汉式代码与考点

在这里插入图片描述

  1. 指令重排序问题
  2. 单例模式有且只有一份 static修饰
  3. 由于是在static中使用锁对象,所以锁对象也需要是static
  4. 锁对象最好是final / 事实final ,确保锁不会被错误引用或修改,保证线程安全不会被破坏
  5. 类static方法可以直接用类名直接引用,避免直接创建实例
  6. 单例模式两个if与上锁都是重要的,上锁保证线程安全
  7. 单例模式不能让你直接实例化,只能间接拿到对象

阻塞队列

基于队列“先进先出”的特点的同时加上线程安全,当队列满的时候就线程阻塞,当完成一个线程任务退出后便线程唤醒,后续阻塞的线程任务就能进入到阻塞队列中

因为阻塞队列的功能好用,所以就基于这个阻塞队列结构封装了一组服务器,这就是“消息队列”(Message Queue)

消息队列不仅可以降低两个服务器的耦合性,还能起到“削峰填谷”的作用,降低服务器的负担,让服务器始终能维持在一个合适的强度工作,是经典的**生产者消费者模型**

可以总结以下特点:

  1. 线程安全

  2. 阻塞功能

    1. 队列为空,尝试出队列,产生阻塞,直至队列不空
    2. 队列为满,尝试入队列,产生阻塞,直至队列不满

工作原理图belike

在这里插入图片描述

生产者消费者模型

  • 降低资源的竞争

  • 解耦合

    在这里插入图片描述

  • 削峰填谷

    belike三峡大坝,使得服务器能够以自己的速度节奏来处理任务。不同的服务器它能承受的工作量是不同的,当请求量比较高时,就能够按照

  • 自己的速度来读取速度,会花更多的时间消费队列中的内容,总体能保证自己能正常工作,不至于请求量太大卡死

弊端

像这样的任务请求,他的响应时间会受到影响,因为MQ会出现阻塞的情况,你不知道它前面还有多少个请求在排队~~

所以生产者消费者模型更适合应用在“异步”的场景,不适合“同步”的场景。像需要数据高速反馈的场景就不适合了,例如像FPS游戏的服务器

——那什么是同步 和 异步???

  • 同步:A做完了事情需要跟B汇报,B通知了A,A再继续忙后续的事情
  • 异步:A做完了事情跟B汇报,不管B回不回应,A都不等了,B把结果做好了再通知A

Java封装了阻塞队列

BlockingQueue
在这里插入图片描述
在这里插入图片描述

只有put 和 take 才带有阻塞效果~~

线程池

听着,所谓线程池…🍅

线程 相比于进程轻量,线程池就是提前把一些线程创建好,放到一个“池”里,想用就直接拿出来,比从用户态 —> 内核态 —> 用户态的操作系统步骤创建要快,属于是纯用户态的代码

在这里插入图片描述

如果我们采用线程池的方案,提前把线程通过操作系统的API创建好了,想要直接从用户态里去取就好了

优点

  • 降低资源消耗:减少线程的创建和销毁带来的资源开销
  • 提高响应速度:能当成任务直接调度,不用等待线程创建
  • 可管理性:进行统一的分配,监控,避免因大量线程间的因互相抢占系统资源导致的线程阻塞

🎗️ThreadPoolExecutor类

Java的标准库中也提供了线程池的类

构造方法

构造方法是经典的面试题

在这里插入图片描述

  1. 线程数

    在这里插入图片描述

    核心线程数 与 最大线程数:核心线程数是不能销毁的,非核心线程数是当任务太多了系统自动创建新的线程。

    最大线程数 = 核心线程数+非核心线程数

  2. 等待最大时间

    在这里插入图片描述

  3. 任务队列

    在这里插入图片描述

    直接用阻塞队列,队列没有任务或爆满时能阻塞等待

  4. 线程工厂

    在这里插入图片描述

    工厂模式就是针对对象创建的一种设计模式,通过工厂类专门实现创建对象,将创建对象的过程与方法解耦合,便于扩展与维护,屏蔽内部对象的构造细节,根据输入的请求返回构造好的一个空的产品

  5. 拒绝策略💫

    在这里插入图片描述

    拒绝策略是当任务队列长度达到上限时,如果再尝试往里加入任务时就会触发“拒绝策略”,而阻塞就是拒绝策略的其中一种,但“死等”是不好的,设置超时时间就是典型的措施。但还有其他的拒绝策略,开发中通常认为新任务是比较重要的,其他的策略是尽力让新任务执行

    一共有四种拒绝策略

    在这里插入图片描述

    • AbortPolicy:直接抛出异常
    • CallerRunsPolicy:某个线程调用submit提交任务,但由于任务队列已满了,尝试让调用submit的那个线程负责执行这个任务
    • DiscardOldestPolicy:把任务队列中最老的任务(最早加入的)直接抛弃,然后添加新任务进来
    • DiscardPolicy:把任务队列中最新的任务(最晚加入的)直接抛弃,然后添加新任务进来

    总结

    在这里插入图片描述

Executors类

对ThreadPoolExecutor的进一步封装,更加方便

在这里插入图片描述

协程

是比线程更加轻量的线程——java21才引用的

ThreadLocal

是轻量线程级的变量,生命周期与线程相同

因为多个线程修改同一个变量是不安全的,而多个线程修改不同的变量是安全的。所以ThreadLocal就是给每个线程都分配了一个变量,让他们自己玩自己的,互不干扰

	public static ThreadLocal<Integer> tl = new InheritableThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                tl.set(i);
            }
            System.out.println("t1: " + tl.get());
			//可手动删除
            tl.remove();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                tl.set(i);
            }
            System.out.println("t2: " + tl.get());
			//不手动删除就等线程结束
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        tl.set(88);
        System.out.println(tl.get());
    }
  • List item

在这里插入图片描述
当线程第一次使用tl的时候就创造了“副本”,而且修改副本不影响tl本体

‍希望能帮助到你们,祝身体健康,欢迎评论区指正错误

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值