线程本质

    科技2024-08-11  30

    多线程本质

    线程是进程执行的基本单位(线程的概念)

    java中创建多线程,就是操作系统的一个子进程,通过clone的系统调用生成,克隆可以共享文件系统

    进程与线程的区别

    进程是一个程序运行起来的状态,是操作系统分配资源的基本单位.线程是执行调度的基本单位,进程分配资源最重要的是独立的内存空间

    线程在linux中的实现就是一个普通进程(fork方法系统调用,c语言写的)

    a父进程调用fork系统调用启动b子进程,它们共享一块内存空间.

    jvm级别的线程Thread与操作系统级别的线程是一一对应的(lwp),重量级线程,去操作系统内核申请资源(系统调用),态切换.

    由于启动一个线程要和操作系统打交道于是,线程中的协程(协程)在线程内部分成几个分支.由线程来调用.线程相当于一个cpu.线程切换不需要与操作系统打交道,直接在用户空间内进行.

    协程的优点

    调度不需要经过os占有系统资源很少,线程要1m,协程需要4k可以启动非常多协程,1条线程对应10000条协程

    协程应用场景

    很短的计算任务,不需要和内核打交道,并发量高

    使用多线程的目的:

    充分利用cpu的资源,并发做多件事

    线程在没有执行完,被别的进程抢走了cpu使用权后,会将现场信息保存,抢到cpu后恢复现场,接着往下执行。进程切换非常耗资源。

    出现异常时,线程会释放锁

    使用实现runnable接口,进行多线程开启的好处

    不需要给变量添加static,每次创建一个对象作为共享对象即可。

    线程类使用代理模式,代理runable目标任务

    问题

    线程是否越多越好呢

    创建线程的方法

    如果正确使用多线程

    创建线程的方法

    new Thread();

    实现runnable并不是一个创建线程的方法,只是创建线程行为的方式.任务指的是run代码块

    任务->线程->cpu

    创建:线程刚刚new就绪:线程调用start,线程进入等待队列运行:线程抢占到cpu正在运行,如果一个属于它的时间片内没有执行完成则换下来,进入就绪队列终止:线程结束完毕阻塞:线程依赖第三方服务(执行sleep,等待I/O设备资源),无法继续执行,让出cpu资源,阻塞状态消除之后 会进入就绪队列,继续抢占cpu,原因无法消除则一直阻塞

    线程api

    join:等待该线程终止,强制调用该方法的线程执行,其他线程都处于阻塞状态,该线程执行完毕后,其他线程再执行。比如在t1线程中调用t2.join(),那么就会让t2执行完毕后再执行t1

    sleep:线程睡着,让别的线程执行,此时线程进入阻塞

    Wait:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,wait是object类的方法.

    ​ 在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的“锁标志”, 从而使别的线程有机会抢占该锁。

    ​ wait()和notify()必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函 数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生异常。

    yield:从cpu退出一下(进入等待队列),谦让别的线程运行

    Object.wait \ notify

    对象有了等待与唤醒的方法。需要写在同步块中

    线程安全

    死锁:a、b两个资源分别被A、B两个线程占用,此时A线程需要B的资源,B线程需要A的资源,两个线程都试图获取对方的锁

    当定义类实现runnable接口,将runnable放入线程中时,线程操作的是同一个对象中的成员变量等。

    synchronized定义为方法修饰符,则表示是一个普通方法,普通方法属于对象,不属于类,如果是synchronized关键字修饰静态方法则属于类。

    线程池

    为什么要线程池?

    线程创建销毁非常消耗系统资源,于是使用线程池来管理线程

    好处

    使用线程池可以重复利用线程执行任务,省去了创建线程与销毁线程。提高系统响应速度

    线程池的组成

    核心线程:永远存在的线程,会一直运行处理任务

    阻塞队列:当任务过多,大于核心线程数量时,用于存储任务的队列

    最大线程池个数:线程池内可以容纳的最大线程数

    饱和策略:最大线程池也满了,是否需要抛弃任务等,对任务的处理

    线程池的分类

    根据核心线程以及队列大小,饱和策略,最大线程配置的不同以应对各种场景。

    cacheThreadPool:线程池的数量没有固定,可以达到integer.max,线程池中的线程可以进行缓存重复利用。没有可用线程时会新建一个线程

    fixedThreadPool:

    forkjoinpool:分而治之,fork系统调用。

    线程池的生命周期

    运行-终止

    Shutdown(当前线程不接受新的任务,执行目前已有的任务)

    -stop(全部停止执行)

    tidying-所有任务都终止,工作线程为0,慢慢进入terminated

    Terminated:线程池中什么都不做

    线程池的阻塞队列

    ArrayBlockQueue 基于数组定长阻塞共用一个锁对象(生产者、消费者) linkBlockWQueue 基于链表不定长阻塞使用两个锁对象,可互相唤醒并发处理

    线程是否越多越好

    任务需要运送到cpu中去运行,线程就相当于一辆卡车运送任务.卡车需要经历打造->运送->回收,运送一次就需要经历以上三步,如果任务执行的时间小于创建+销毁的时间则非常不划算.

    线程存在于内存中,内存相当于马路,马路上最大存在10台.如果车越来越多,则会造成拥堵甚至瘫痪.

    一个线程默认1m内存,这个栈需要从系统空间中去分配,线程过多则会消耗很多内存.

    操作系统需要频繁切换上下文,影响性能

    正确使用线程池

    使用合适数量的线程运输任务到cpu

    线程池的组成

    任务仓库线程(卡车)任务

    任务派发到任务仓库.线程 有序的从仓库中取任务运行.blockingQueue

    仓库

    仓库中使用blockQueue来存放任务,对列为空时获取阻塞,队列满时放入阻塞,多个线程去队列取数据,但是同时只能一个线程进去取,其他线程排队.

    任务用什么表示

    runnable

    callable->是对于runnable的改进,具有返回值,会抛出异常.feature

    有多条线程持续的从仓库中获取任务并且执行

    synchronized

    概念

    syn是同步关键字,用于解决多线程环境下的代码同步问题,syn可以修饰方法,或者作为单独代码块使用参数为一个object对象.这个object对象作为🔒来使用

    早期

    早期的synchronized是重量级的锁,它需要申请用户态与内核态切换,内核程序能干的操作系统不能干,早期的synchronized申请锁需要做内核态与用户态的转换.

    一个对象的内存布局

    一个对象被new出来后,对象的头部8个字节是markword,其他4个字节的类型指针(class pointer)可以找到class,然后instance data,看类型比如int是4个字节,剩下字节对齐…整个对象的大小必须是8的整数倍

    进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块中使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取

    lock指令,lock一个变量时要刷新工作内存

    可见性

    获取锁的时候,会把主内存中的变量重新读到工作内存中,只有竞争某把锁失败后,得知其他线程正在修改变量,于是自己拥有锁后再去读取主内存中的值到工作内存中

    锁释放的时候会将当前工作内存中的变量刷新回主内存中.Thread.sleep也会刷新工作内存,重新去主内存中把值取到工作内存中

    sout也会刷新工作内存中的值

    其实是通过jvm内存模型的对一个变量unlock之前必须同步到主内存中,如果对一个变量lock操作,必须清空工作内存中此变量的值,重新读取

    优缺点

    执行时间长,线程多,用系统锁

    执行时间短,线程少,用自旋锁(如果线程多用自旋锁则cpu压力会非常大)

    syn是可重入的.会升级的

    早期syn关键字需要内核申请锁资源要通过内核,所以是重量级锁。其他都是 轻量级锁,只在用户空间内

    cas操作,自旋锁,会自己一直(旋转),cas操作在用户态解决锁问题不经过操作系统.

    cas操作汇编语言并不是原子性的,但是汇编前面加上了lock-if-mp,能够实现cas操作的为lock-cmlxchg,锁总线,其他cpu都使用不了,过不去总线。

    实现原理

    1)synchronized同步代码块:synchronized关键字经过编译之后,会在同步代码块前后分别形成monitorenter和monitorexit字节码指令,在执行monitorenter指令的时候,首先尝试获取对象的锁,如果这个锁没有被锁定或者当前线程已经拥有了那个对象的锁,锁的计数器就加1,在执行monitorexit指令时会将锁的计数器减1,当减为0的时候就释放锁。如果获取对象锁一直失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

    每个对象都有一个监视器锁(monitor)与之对应。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下: 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1. 3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

    什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象,所有的java对象都是天生的monitor,每个对象从开始就带了一把锁叫做内部锁或者monitor锁.monitor对象指针存在于java对象的对象头中.Monitor由ObjectMonitor实现,其中有等待队列,线程获取锁失败就封装到等待队列中

    方法被调用时,会检查方法的Acc_syn访问标志是否被设置,如果被设置了,线程将优先获取monitor

    还是加上了lock cmlxchg指令

    object.notify

    wait.会对当前线程进行等待并释放锁

    Notify, 唤醒某锁对象上wait的线程但是不释放锁

    layout,java object layout,跟踪锁升级过程

    普通对象布局:markword,类型指针,实例数据,补齐能被8字节整除

    给对象上锁就是给对象头修改了数据,markword,这个对象指的是线程争取的对象如某个Object

    偏向锁:偏向某个线程的锁,不惊动os,增加获取锁的效率 匿名偏向:偏向锁刚刚启动 101普通对象:偏向锁没启动 001 轻量级锁:有线程竞争偏向锁,直接升级。自旋锁。通过cas操作修改markword。重量级锁:os调配的锁

    lock record:锁记录,对象中的lock record可以有多个。入栈lock record,弹栈lock record,新加一个锁,入栈一个lock record

    volatile

    1.功能 2.lock add这条指令 3.内存屏障,mesi协议

    操作volatile修饰的变量,会马上写回到内存中,并使其他cpu缓存的值失效(mesi协议)

    实现原理

    对volatile变量赋值完成后会在后面加上lock addl $0x0,(%esp);指令

    lock addl $0x0 指令的作用是使当前cpu的cache值写入内存

    lock前缀指令其实就相当于一个内存屏障。内存屏障是一组CPU处理指令,用来实现对内存操作的顺序限制。volatile的底层就是通过内存屏障来实现的。

    lock指令功能:

    不会重排序,内存屏障的功能

    将这个变量所在缓存行的数据写回到系统内存(由volatile先行发生原则保证);

    一个处理器的缓存回写到内存会导致其他处理器的缓存无效会去主内存中重新读取

    不会引起线程之间的切换和调度

    lock指令是否就是线程的工作内存呢?,因为线程执行是需要在寄存器以及pc上的寄存器存储程序需要的数据

    缺点

    修饰引用类型时,引用类型里的值改变时无法观察到,只关心引用类型,所以volatile最好修饰基本类型

    java内存模型

    每个线程都有自己的工作内存有一块主内存工作内存数据从主内存读取解决可见性问题与指令重排序问题 public class Singleton { private static Singleton instance = null; public static Singleton getInstance() { if(null == instance) { // 线程二检测到instance不为空,此处为dcl致命点,会有新的线程来争抢,争抢的时候就会判断,结果没有指令重排序,直接返回了 synchronized (Singleton.class) { if(null == instance) { instance = new Singleton(); // 线程一被指令重排,先执行了赋值,但还没执行完构造函数(即未完成初始化) } } } return instance; // 后面线程二执行时将引发:对象尚未初始化错误 } }

    volatile 不能保证变量的"原子性"

    汇编指令为lock adds 指令,lock指令会将紧跟着后面的指令变为原子指令执行

    每个volatile写操作的前面插入一个StoreStore屏障;

    在每个volatile写操作的后面插入一个StoreLoad屏障;

    在每个volatile读操作的后面插入一个LoadLoad屏障;

    在每个volatile读操作的后面插入一个LoadStore屏障。

    指令重排

    cpu乱序执行,为了效率,当第一条指令与第二条指令无关时,会先执行第二条,cpu效率是内存的100倍。

    a=x这个是很多指令的集合

    对象的创建过程

    单例DCL,需要加volatile吗,要加。禁止指令重排。同时对其他线程的可见性

    如果不加,则莫一线程在进行初始化后,另一个线程进入,此时又会进行初始化一次

    (单例模式)

    对象的半初始化时,进行申请空间,指向,设置初始值。cpu也可以把它重新排序进行申请空间,设置初始值,进行指向,结果在代码中,单例模式两条线程,查看对象是否为null。由于是在半初始化,可能对象还未指向内存块。第二条线程已经准备重新new对象了。所以又重新new了一个对象。

    T1 getInstance了(@123对象),t2也getInstance了(@124对象)

    单例模式需要加。

    假设线程一执行到instance = new Singleton()这句,这里看起来是一句话,但实际上其被编译后在JVM执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情:

    1)给instance实例分配内存;

    2)初始化instance的构造器;

    3)将instance对象指向分配的内存空间(注意到这步时instance就非null了)

    如果指令按照顺序执行倒也无妨,但JVM为了优化指令,提高程序运行效率,允许指令重排序。如此,在程序真正运行时以上指令执行顺序可能是这样的:

    a)给instance实例分配内存;

    b)将instance对象指向分配的内存空间;

    c)初始化instance的构造器;

    这时候,当线程一执行b)完毕,在执行c)之前,被切换到线程二上,这时候instance判断为非空,此时线程二直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)。

    线程交替输出问题

    阿里面试,交替输出2,3,3

    wait方法,锁对象调用wait方法,哪个线程持有这个锁对象,然后wait。调用了wait方法的线程进入阻塞状态

    锁对象的wait方法=持有这个锁对象的线程 wait。

    于是各种持有这个锁的线程,叫醒。=锁对象叫醒线程

    syn里面只有一个队列。生产者消费者都放里面。

    condition是一个队列,多个condition是多个队列,同一种类型(生产者、消费者)的队列。

    内存屏障

    加在两条指令中间的屏障,不允许指令越过边界线

    cpu级别(x86)支持的有序性保障,三条元语

    Load,Store指令

    读取,写入

    LL\SS\LS\SL

    jvm层面实现volatile

    ThreadLocal

    threadlocal是线程独有的对象,设置的值set只有自己线程才能访问到,数据库连接就存放在这里

    public void set(T value) { //当前线程 Thread t = Thread.currentThread(); //获取当前线程的threadLocalMap,每个线程都有自身的map ThreadLocalMap map = getMap(t); //没有就初始化,有就拿 if (map != null) //this对象,是当前线程对象 map.set(this, value); else createMap(t, value); }

    Thread里面有

    /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;

    所以每一个Thread,都有一个自身的ThreadLocalMap,里面可以存对象

    volatile

    cpu之间的缓存一致性协议

    volatile被编译成汇编码,指令前带了lock指令

    Processed: 0.010, SQL: 8