从零单排Java多线程(1)

2019-03-14  本文已影响0人  Litchi_0424

文章主要参考Java多线程编程指南(核心篇)

线程的基础知识

我们想要创建一个线程,要不就是实现Runable接口,实现run方法,也可以继承Thread类,覆盖run方法,Thread实例是特殊的Runable实现类,所以在创建它的时候Java虚拟机会为其分配调用栈空间,内核线程等资源,成本要相对昂贵一点,我们在使用的时候,如果是要传递给其他API使用,直接使用Runable接口实现就行。

需要注意的是:启动线程的时候,调用Runable实现类的start方法,启动线程的本质是请求Java虚拟机运行相应的线程,这个线程具体何时能够运行时由线程调度器(操作系统的一部分)决定

/**
*Thread类:是Runable接口的实现类,有一个含参
*的构造器Thread(Runable target);其对Runable的run方法的实现如下
**/
@Override
public void run(){
    if(target !=null){
        target.run();
    }
}

Thread类的常用方法

static Thread currentThread() :返回当前线程,即当前代码的执行线程实例
void join() 等待相应线程运行结束,线程A调用线程B的join方法,A线程的运行会暂停,直到线程B运行结束
static void yield:使当前线程主动放弃对处理器的占用,方法不可靠,被调用时当前线程可能仍然能继续运行
static void sleep(long millis);使当前线程休眠指定的时间

线程的生命周期状态

New,Runable(Ready(被线程调度器选中)-->Running),Blocked(发起阻塞式IO,申请由其他线程持有的独占资源),Waiting,Timed_waiting,Terminated;具体的在后面在说

竞态

计算结果的正确性与时间有关的现象称为竞态(Race Condition)

竞态的两种模式:read-modify-write:最常见的例子就是count++操作,

load(count,r1);//将变量读取到r1寄存器
increment(r1);//修改寄存器的值
store(count,r1);//将寄存器的值写到count对应的内存空间里

check-then-act:读取某个共享变量的值,根据该变量的值决定下一步的动作是什么,最常见的例子就是if判断共享变量后进行操作

线程安全

原子性

原子操作:原子操作是针对 共享变量的操作而言的,原子操作是从改 操作的执行以外的线程来描述的。

“不可分割”含义:

  1. 访问某个共享变量的操作从其执行线程以外的任何线程来看,该操作要么已经执行结束要么尚未发生。
  2. 访问同一组共享变量的原子操作是不能够被交错的

实现原子性的方式:CAS(compare-and-swap),锁

java对long和double型以外的任何类型的变量的写操作都是原子操作,对任何变量的读操作都是原子操作。

可见性

  1. JIT编译器优化,导致共享变量更新不可见;

  2. 与计算机的存储系统有关。程序中变量可能会分配到寄存器(Register)而不是主内存中进行存储。每个处理器都有其存储器,一个处理器无法读取另外一个处理器上的寄存器的内容。但是一个处理器可以通过缓存一致性协议(Cache Coherence Protocol),读取其他处理器的高速缓存中的数据,更新到自己的高速缓存中。称为缓存同步(高速缓存,主内存)。为了保证共享变量更新写入该处理器的高速缓存或者内存中(而不是在写缓冲器中),这个过程称为冲刷处理器。同时,如果其他处理器在此之前更新了共享变量,那么该处理器必须从其他处理器的高速缓存或者主内存中对相应的变量进行缓存同步。这个过程称为刷新处理器缓存

    可见性保证是通过更新共享变量的处理器冲刷处理器缓存,并使读取共享变量的处理器执行刷新处理器缓存的动作来实现
    

    java平台中只需要使用volatile关键字声明就可以保障可见性。

    JLS保证:父线程在启动子线程之前对共享变量的更新对子线程可见;
    线程终止后该线程对共享变量的更新对于调用该线程的join方法的线程而言是可见的
    

有序性

重排序

重排序类型 重排序表现 重排序来源
指令重排序 程序顺序与源代码顺序不一致 编译器
指令重排序 执行顺序与程序顺序不一致 JIT编译器、处理器
存储子系统重排序 源代码顺序、程序顺序和执行顺序这三者保持一致,但是感知顺序与执行顺序不一致 高速缓存、写缓冲器
指令重排序

JIT编译器(字节码动态生成机器码)可能会执行指令重排序

处理器也可能会执行指令重排序,使得执行顺序与程序顺序不一致。处理器的乱序(Out-of-order Execution)。哪条指令就绪就执行哪条指令。指令结果被写入重排缓冲器(ROB),ROB会进行顺序提交,所以不会对单线程程序正确性产生影响。

处理器的乱序执行还采用了猜测执行(Speculation)技术。猜测执行可以导致先执行if语句的语句体。将结果临时存放到ROB中,再来判断if的值,如果是true就提交到高速缓存,主内存中,否则就在ROB中丢弃。

存储子系统重排序

从处理器的角度来说,就只有读内存操作(LOAD)(从RAM地址加载数据)和写内存操作(STORE)

所以重排序只有四种(LoadLoad,StoreStore,LoadStore,StoreLoad),这些重排序是指在一个处理器上先后执行操作,而其他处理器对这两个内存操作的感知顺序不同。

貌似串行语义

存在数据依赖关系(两个操作访问同一个变量,且其中一个操作为写操作,包括读后写,写后读,写后写)的语句不会被重排序,而存在控制依赖关系的语句允许被重排序(典型例子:if)

保证内存访问的顺序性

从逻辑上部分禁止重排序,从底层角度来说,禁止重排序是通过调用处理器提供相关指令(内存屏障)来实现的。

volatile,synchronized可以保证有序性。

可见性和有序性的联系

可见性是有序性的基础,有序性影响可见性

上下文切换

进程中的一个线程由于时间片用完或者自身原因被迫或者主动暂停其运行时,另外一个线程可以被操作系统选中占用处理器开始或者继续其运行,这个过程称为线程切入与切出。在切入切出时操作系统需要保存和恢复相应线程的进度信息。这个进度信息称为上下文。一般包括通用寄存器的内容和程序计数器的内容

Thread.sleep(long millis)
Object.wait(long timeout)
Thread.yield();(不稳定)
Thread.join()
LockSupport.park()
I/O操作(读取文件)
等待其他线程持有的锁

一次上下文切换的时间消耗是微妙级别的

资源调度

资源调度的一个常见特性就是它能否保证公平性:如果资源的任何一个先申请者总是能够比任何一个后申请者先获得该资源的独占权,那么相应的资源调度策略就称为是公平的,否则称为不公平。常见策略是维护一个等待队列。

比较:

非公平调度吞吐量大:资源的持有线程释放资源的时候,会有等待队列中的一个线程被唤醒,而该线程从被唤醒到继续运行需要一定时间,在这段时间内,新来的线程可以先被授予该资源的独占权。如果新来的线程占用时间不长,它有可能在被唤醒的线程继续运行之前释放资源,不影响被唤醒的线程申请资源。这样就可以减少上下文切换次数。但是如果多数线程占用资源时间相当长,那么就会使刚唤醒的线程由于被抢占又进入暂停。耗费资源。

所以,在线程占有时间较长或者申请资源频率不高的情况下,可以使用公平调度,可以避免饥饿

但是默认一般采用非公平,提高吞吐率

上一篇下一篇

猜你喜欢

热点阅读