基础概念

进程与线程

进程是所有线程的集合,每一个线程是进程中的一条执行路径

线程分类

  • 用户线程
    • 主线程
    • 子线程
  • 守护线程
    • 守护线程当进程不存在或主线程停止,守护线程也会被停止
    • GC线程

HotSpot的每一个Java线程都是直接映射到一个操作系统原生线程来实现的

创建线程

继承Thread类

class MyThread extends Thread{
    @Override
    public void run() {}
}

多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈

批注 2019-08-02 115159

Thread类

  • public String getName() :获取当前线程名称。
  • public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
  • public void run() :此线程要执行的任务在此处定义代码。
  • public void yield():让出CPU,返回到就绪状态
  • join:等待被调用join的线程执行完毕再继续运行
  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用

InterruptedException

调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞(sleep)、限期等待或者无限期等待(wait)状态,那么就会抛出 InterruptedException,从而提前结束该线程

interrupted()

在自定义线程执行任务使,可以使用这个方法作为一个flag,作为是否继续运行的依据

while(interrupted()){
    // do
}
// end

实现Runnable接口

实现Runnable接口比继承Thread类所具有的优势:

  • 适合多个相同的程序代码的线程去共享同一个资源。
  • 可以避免java中的单继承的局限性。
  • 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  • 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

线程调度

  • Java 使用的抢占式调度多线程系统

可以通过Thread实例setPriority来调整优先级,不过此举总体而言不是一个文档的调节手段

线程状态

批注 2020-06-17 111804

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操 作系统处理器。
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状 态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个 状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态 将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、 Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

线程安全

当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的

不可变

不可变的对象一定是线程安全的

这种对象的接口一般需要精心设计 最简单的方式是所有成员变量设置为final

绝对线程安全

不管运行时环境如何,调用者都不需要任何额外的同步措施

相对线程安全

要保证对这个对象单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性

线程兼容

对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用

线程对立

指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码

线程安全的实现

互斥同步

  • synchronized
  • Lock

非阻塞同步

  • CAS

无同步方案

  • 可重入代码:类似于函数式编程
  • 线程本地存储

synchronized

使用了锁对象,这个锁对象一瞬间只能被一个线程所持有

synchronized(this){ // 可以是任意一个对象
  // 需要同步操作的代码
}
public synchronized void method(){ // 也可以同步静态方法,等同于上面的synchronize(this)
  // 可能会产生线程安全问题的代码   
}

synchronized是可重入锁

重入:某个线程试图获得一个已经由它持有的锁

程序执行过程中发生异常,锁会被释放

不能使用String常量,以及int long等原始类型

synchronized底层

JDK早期的 使用的重量级实现 也就说在 OS 层面 后来的进行了改进

synchronized实现过程

  • java代码:synchronized
  • 字节码: monitorenter monitorexit
  • 执行过程中会进行锁升级
  • lock comxchg

锁升级:

在 markword 中记录记录获取锁的这个线程ID 此时是偏向锁。线程通过判断这个ID是否为自身来判断自己是不是获得了偏向锁。偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不做了,提高了程序的运行性能

但如果应用程序里所有的锁通常处于竞争状态,那么偏向锁就会是一种累赘

-XX:UseBiasedLocking=false

可以关闭偏向锁

如果发现markword的不是自己的线程ID 则升级为 自旋锁

在自旋一定次数以后,不能获得锁,则升级为OS层面的重量级锁

执行时间短(加锁代码),线程数少,用自旋 执行时间长,线程数多,用系统锁

锁降级:锁降级发生的条件会比较苛刻,锁降级发生在Stop The World期间,当JVM进入安全点的时候,会检查是否有闲置的锁,然后进行降级

volatile

任何对被volatile关键字修饰的变量都会在主内存操作 不会操作副本

volatile变量操作时需要同步给内存变量 所以一定会使线程的执行速度变慢

而锁机制通过读入副本 释放锁写入主内存来包装可见性

  • 保证线程可见性
    • MESI 缓存一致性协议
  • 禁止指令重排序
    • DCL(double check lock) 单例

重排序的3种类型:

  1. 编译器重排
  2. 指令并行重排
  3. 内存系统重排

可见性

在没有同步的情况下,编译器或者处理器都会对一些上下文无关的指令进行 重排序 ,这可能会导致一个线程修改了某一个数值,而另一个线程无法马上读取到修改后的数值

  • 失效数据
  • 非原子的64位操作

    在java当中,一个64位大小的数值可以被分为2个32位的操作

在Java内存结构中,既然堆是共享的,为什么在堆中会有内存不可见问题。现在计算机CPU为了高效,往往会在高速缓存区中缓存共享变量

为什么要重排序?还是为了性能,流水线技术的原理是指令1还没有执行完,就可以开始执行指令2,而不用等到指令1执行结束之后再执行指令2

加锁与可见性

之所以要在访问某个共享的可变变量时要求所有线程在锁上同步,就是为了确保读写可见性。 加锁的含义不局限与互斥行为,还包括内存可见性

volatile是比synchronized更为轻量级的同步机制,它无法进行互斥操作,但能保证内存可见性

  • 典型用法
voatile boolean f;

while (f){
    // do something
}

CAS

  • AtomicInteger 等原子类的实现

批注 2020-05-12 135356

它包含三个参数CAS(V,E,N): V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程进行自旋重复上述操作或者什么都不做。最后,CAS返回当前V的真实值

CAS是CPU原语支持 Java 通过native方法调用汇编指令来实现

ABA问题

如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过

解决方法:版本号

在大多数情况下 ABA问题并不会影响到程序的正确性

使用 AtomicStampedReference 实现

自旋开销

CAS多与自旋结合。如果自旋CAS长时间不成功,会占用大量的CPU资源。

让JVM支持处理器的pause指令可以解决这个问题。

在Java层,也可以通过手动yield线程或者sleep来解决

unsafe类

直接操作JVM里的内存

JDK9之后无法使用了

  • allocateMemory 直接分配内存
  • freeMemory 释放内存
  • compareAndSet CAS操作

Java 与协程

  • Java 内核线程的局限性

内核线程1:1映射到Java上,当面对大量请求时,线程切换的成本开销远远大于计算本身的开销

协程的主要优势是轻量,一个协程的实现特例被称之为纤程

新并发模型下,一段使用纤程并发的代码会被分为两部分——执行过程(Continuation)和调度器(Scheduler)。执行过程主要用于维护执行现场,保护、恢复上下文状态,而调度器则负责编排所有要执行的代码的顺序

results matching " "

No results matching " "

results matching " "

No results matching " "