Java并发编程基础

作者:碳水怪兽👾 发布于:2021/10/10

相关概念

进程

进程是关于数据集合的一次运行活动,是操作系统资源分配的基本单位

线程

线程是进程的一条执行路径,是CPU调度的基本单位,线程继承其隶属进程的资源

Java中线程的创建方式

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口

线程的启动

创建Thread后调用其start方法

new Thread(() -> System.out.println("hello world")).start();

守护线程

线程分为2类,一类是守护线程、一类是非守护线程,在Java中,当所有的非守护线程推出后,JVM进程也会退出。常见的守护线程有JVM的垃圾回收线程

线程的生命周期

Java.lang.Thread中有一个内部枚举,用来表示线程的状态,代码如下:

public static enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;

        private State() {
        }
}

这几种状态说明如下:

  • NEW:线程刚刚创建时的状态
  • Runnable:线程处于就绪状态或运行状态,就绪状态指的是获取了除CPU以外的所有资源,一旦获取到CPU资源就可以运行
  • BLOCKED:阻塞状态,等待获取监视器锁(synchronized)
  • WAITING:等待状态,等待所需要的资源
  • TIMED_WAITING:超时等待,等待所需要的资源,超时后自动退出
  • TERMINATED:终止状态,线程运行完毕的状态

线程生命周期图如下:

多线程带来的问题

  • 1.内存可见性

为了解决CPU执行速度和内存读写速度不匹配的问题,计算机引入了多级缓存,CPU的每个核都有自己的缓存,当一个线程修改缓存变量中的值,不一定能立刻刷新到内存,同时其他的线程不一定能立刻从内存中读取这个变量的值,所有可能会产生可见性问题。

  • 2.重排序

为了提升性能,编译器和CPU可能会做重排序,虽然重排序的结果在单线程下是一样的,但是在多线程的情况下不一定能保证结果是一样的

  • 3.原子性

原子性表示一系列的操作要么全做,要么全不做

Java内存模型(JMM)的happen-before原则可以帮我们解决这些问题,happen-before表示如果步骤A在步骤B前执行,那么步骤A的结果对步骤B可见。

happen before原则如下:

  • 单线程中的每个操作,happen before该线程中任意后续操作
  • 对volatile、final变量的写,happen before与后续对这个变量的读
  • synchronized的解锁,happen before对于这个锁的加锁
  • .....
  • 4.死锁

死锁是由于多个线程竞争临界资源,而造成的一种相互阻塞的情况, 如果没有外力作用,这些线程都无法继续推进下去。

死锁的4个必要条件:

  • 互斥条件:一个资源在同一时间内只能被一个进程占用
  • 不剥夺条件:进程获取到资源后,其他的进程不能强行剥夺
  • 占用并等到条件:进程在等待获取资源时,不会放弃已经获取到的资源
  • 循环等待条件:多个进程形成一种头尾相接的等待状态

如何优雅的关闭线程

  • 设置关闭线程的标志位,在线程中的run方法中的循环中判断标识位

    当线程在循环中阻塞时,无法在执行判断,也就无法结束线程

  • 循环判断中断标识

    通过调用interrupt方法,该方法会唤醒由于sheep、join、wait方法阻塞的线程并设置中断标识

Synchronized

Synchronized原理

synchronized加在方法或对象在编译成字节码时会加入monitorenter和monitorexit指令,执行monitorenter时会对对象监视器(Monitor)进行获取,而这个获取过去具有排他性,这样就达到了同一时刻只有一个线程访问的目的

Java内存模型

Java内存模型屏蔽了各种硬件和操作系统的内存操作差异,实现Java在各个平台上能够采用一致的方法操作内存。Java内存模型规定所有的变量都保存在主内存中,其中包含实例变量、静态变量,不包含局部变量和方法参数,每个线程都有自己的工作内存,线程的工作内存保存了该线程用的变量和主内存的副本拷贝,线程对变量的操作都放在主内存中。