浅析多线程

并发编程三大特性:可见性,原子性,有序性

进程(在内存中的程序,运行中的程序)是分配资源的基本单位

线程(进程内部的执行单元)是CPU调度的基本(最小)单位,也叫轻量级进程LWP(Light Weight Process)

分为两种:

用户级线程(ULT):用户程序实现,不依赖操作系统核心,应用提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/内核态切换,速度快。内核对ULT无感知,线程阻塞则进程(包括它所有的线程)阻塞

内核级线程(KLT,JVM使用):系统内核管理线程(KLT),内核保存线程的状态和上下文信息,线程阻塞不会引起进程阻塞。在多处理器系统上,多线程在多处理器上并行运行。线程的创建、调度和管理由内核完成,效率比ULT要慢,比进程操作快

创建:继承thread类

Java线程创建依赖于系统内核,通过JVM调用系统库创建内核线程,内核线程与Java-Thread是1:1的映射关系

实现runnable接口

实现callable接口

创建线程池:实现线程池接口Executor

协程是虚拟机模拟的线程(用户线程)

并发:指两个或多个事件再同一时间段内发生

并行:指两个或多个再同一时刻发生

Java数据原子操作:

Read(读取):从主存中读取数据

Load(载入):从主存读取到的数据写入工作内存

Use(使用):从主存读取数据来计算

Assign(赋值):将计算好的值重新复制到工作内存中

Store(存储):将工作内存数据写入主存

Write(写入):将store过去的变量值复制给主存中的变量

Lock(锁定):将准村变量加锁,便是为线程独占状态

UnLock(解锁):将主存变量解锁,解锁后其他线程可以锁定该变量

早期解决线程之间数据不一致问题的解决方式:

总线加锁(性能太低):CPU从主存读取数据到高速缓存,会对总线对这个数据加锁,这样其他CPU没法去读或写这个数据,直到这个CPU使用完数据释放锁之后其他CPU才能读取该数据

现在解决线程之间数据不一致问题的解决方式:

MESI缓存一致性协议:多个CPU从主存读取同一个数据到各自的高速缓存,当其中某个CPU修改了缓存里的数据,该数据会马上同步回主内存,其他CPU通过总线嗅探机制(监听)可以感知到数据的变化从而将自己缓存里的数据失效

Volatile缓存可见性实现原理

底层实现主要是通过汇编lock前缀指令,它回锁定这块内存区域的缓存(缓存行锁定)并会写到主内存

IA-32架构软件开发者手册对Lock指令的解释:

    (1) 会将当前处理器缓存行的数据立即写回到系统内存(主存)

    (2) 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI缓存一致性协议)

Volatile保证可见性与有序性,但是不保证原子性,保证原子性需要借助Synchronized这样的锁机制

voliate关键字:

保证内存可见性

禁止指令重排序

多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这种资源可能是:对象、变量、文件等

共享:资源可以由多个线程同时访问

可变:资源可以在其生命周期内被修改

虚拟机的指令重排不可转换规则:

    1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;

    2.锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;

    3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;

    4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

    5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;

    6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

    7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;

    8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

JVM的内存屏障都必须实现四种内存屏障:

LoadLoad屏障:对于这样的语句Load1;LoadLoad;Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕

StoreStore屏障:对于这样的语句Store1;StoreStore;Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其他处理器可见

LoadStore屏障:对于这样的语句Load1;LoadStore;Store2,在Store2及后续写入操作执行前,保证Load1要读取的数据被读取完毕

StoreLoad屏障:对于这样的语句Store1;StoreLoad;Load2,在Load2及后续读取操作执行前,保证Store1的写入操作对其他处理器可见

缓存的速度

主存约80ns

L3高速缓存行 12ns

L2高速缓存行 4ns

L1高速缓存行 约1ns

寄存器< 1ns

Synchronized:

加锁:

    (1) 同步实例方法,锁是当前实例对象

    (2) 同步类方法,锁是当前类对象

    (3) 同步代码块,锁是括号里面的对象

原理:JVM内置锁通过synchronized使用,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex Lock(互斥锁)实现,它是一个重量级锁,性能较低

对象锁的不同状态:

偏向锁:

轻量级锁:

重量级锁(JVM使用):mutex,系统的互斥锁

锁是控制线程的前后的执行过程

不对线程进行任何控制,线程之间的进行是并行的

锁代码:持有锁对象的时候才能执行这段代码

原子性:上锁之后,

    1. 代码不可被打断(持有同样锁的其他代码,打断指同时运行)

线程池的意义:线程是稀缺资源,它的创建与销毁是一个相对偏重且耗资源的操作,而Java线程依赖于内核线程,创建线程需要进行操作系统状态切换,为避免小号需要设法重用线程执行多个任务。线程池就是一个线程缓存,负责对线程进行统一分配、调优和监控

什么时候使用线程池:

· 单个任务处理时间比较短

· 需要处理的任务数量大

线程池的优势:

· 重用存在的线程,减少线程创建,消亡的开销,提高性能

· 提高响应速度,当任务达到时,任务可以不需要等到线程创建就能立即执行

· 提高线程的可管理性,可统一分配、调优和监控

阻塞队列(FIFO):

    1. 在任意时刻,不管并发有多高,永远只有一个线程能够进行队列的入队或者出队操作!线程安全队列

    2. 队列满,只能进行出队操作,所有入队的操作必须等待;队列空,只能进行入队操作,所有出队的操作必须等待,也就是阻塞

线程池的五种状态:

线程池用一个int类型的值来记录当前线程:高三位记录线程池的生命状态,低二十九位记录当前工作的线程数

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容