Java 多线程知识点

线程和进程的概念、并行和并发的概念

进程

进程是程序的一次执行,进程是一个程序及其数据在处理机上顺序执行时所发生的活动,进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位

  • 进程是系统进行资源分配和调度的独立单位,每一个进程都有它自己的内存空间和系统资源

为使程序能并发执行,系统必须进行以下的一系列操作:

  • 创建进程,系统在创建一个进程时,必须为它分配其所必需的、除处理机以外的所有资源,如内存空间、I/O 设备,以及建立相应的 PCB;
  • 撤消进程,系统在撤消进程时,又必须先对其所占有的资源执行回收操作,然后再撤消 PCB;
  • 进程切换,对进程进行上下文切换时,需要保留当前进程的 CPU 环境,设置新选中进程的 CPU 环境,因而须花费不少的处理机时间

可以看到进程实现多处理机环境下的进程调度,分派,切换时,都需要花费较大的时间和空间开销

引入线程主要是为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理。使OS具有更好的并发性

  • 简单来说:进程实现多处理非常耗费CPU的资源,而我们引入线程是作为调度和分派的基本单位(取代进程的部分基本功能【调度】)

    进程与线程

    进程作为资源分配的基本单位

线程作为资源调度的基本单位,是程序的执行单元,执行路径(单线程:一条执行路径,多线程:多条执行路径)。是程序使用CPU的最基本单位

线程

线程的属性:

  • 轻型实体
  • 独立调度和分派的基本单位
  • 可并发执行
  • 共享进程资源

线程有两个基本类型:

  • 用户级线程:管理过程全部由用户程序完成,操作系统内核心只对进程进行管理
  • 系统级线程(核心级线程):由操作系统内核进行管理。操作系统内核给应用程序提供相应的系统调用和应用程序接口 API,以使用户程序可以创建、执行以及撤消线程

值得注意的是:多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率

并行与并发

并行:

  • 并行性是指同一时刻内发生两个或多个事件
  • 并行是在不同实体上的多个事件
    并发:
  • 并发性是指同一时间间隔内发生两个或多个事件
  • 并发是在同一实体上的多个事件
    由此可见:并行是针对进程的,并发是针对线程的

创建线程的方式及实现

创建多线程有两种方法:

  • 继承 Thread 类,重写 run 方法
  • 实现 Runnable 接口,重写 run 方法

需要注意的是通过实现 Runnable 接口来创建多线程时需要将 Runnable 实现类的对象传入 Thread 类的构造函数

1
Thread t1 = new Thread(new MyRunnable());

jvm 虚拟机的启动是多线程的。不仅仅是启动 main 线程,至少还会启动垃圾回收线程

run() 方法和 start() 方法

run() :仅仅是封装被线程执行的代码,直接调用是普通方法(不会启动一个新的线程)
start() :首先启动了线程,然后再由 jvm 去调用该线程的 run() 方法


进程间通信的方式

(分为同一个端系统和不同的端系统进程间通信的情况)

  • 管道 pipeline
  • 信号 signal
  • 消息队列 message queue
  • 共享内存 shared memory (Java 主要采用的多线程通信的方式)
  • 信号量 semaphore
  • Socket

AQS 同步队列

AQS 的核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的(AQS 是 CLHLock 的一种变种),即将暂时获取不到锁的线程加入到队列中

CLH 队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系)。AQS 将每条请求共享资源的线程封装成一个 CLH 锁队列的一个节点(Node)来实现锁的分配

AQS 定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如 ReentrantLock)和 Shared(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)

要实现 AQS 的获取和释放功能,至少需要考虑三方面

  • 共享状态的原子修改,因为是在并发情况下
  • 线程的阻塞和唤醒,使用了 Unsafe 的 park 机制
  • 队列的管理,使用了两个队列,同步队列和条件队列。同步队列进行获取和释放操作,条件队列进行阻塞和唤醒操作

AQS 和各种同步器实现的关系,AQS 提供了同步队列和条件队列的管理,包括各种情况下的入队出队操作。而同步器子类实现了 tryAcquire 和 tryRelease 方法来操作状态,来表示什么情况下可以直接获得锁而不需要进入 AQS,什么情况下获取锁失败则需要进入 AQS 队列等待


Semaphore

Semaphore 表示了一种可以同时有多个线程进入临界区的同步器,它维护了一个状态表示可用的票据,只有拿到了票据的线程可以进入临界区,否则就等待,直到获得释放出的票据。Semaphore 常用在资源池中来管理资源。当状态只有 1 和 0 两个值时,它退化成了一个互斥的同步器,类似锁


CountDownLatch 闭锁

CountDownLatch 闭锁也是基于 AQS 实现的一种同步器,它表示了“所有线程都等待,直到锁打开才继续执行”的含义。它和 Semaphore 的语意不同, Semaphore 的获取和释放操作都会修改状态,都可能让自己或者其他线程立刻拿到锁。而闭锁的获取操作只判断状态是否为 0,不修改状态本身,闭锁的释放操作会修改状态,每次递减 1,直到状态为 0

所以正常情况下,闭锁的获取操作只是等待,不会立刻让自己获得锁,直到释放操作把状态变为 0

闭锁可以用来实现很多场景,比如:

  • 某个服务依赖于其他服务的启动才能启动,就可以让这个服务在其他服务状态的闭锁上等待
  • 某个游戏,必须等所有就绪者都到达才能开始游戏
  • 启动一组相关的线程
  • 等待一组相关线程结束

CyclicBarrier 栅栏

从它的名字可以看出,它是可循环使用的。它的功能和 CountDownLatch 类似,也是让一组线程等待,然后一起开始往下执行。但是两者还是有几个区别

  1. 等待的对象不同。CountDownLatch 的一组线程等待的是一个事件,或者说是一个计数器归 0 的事件。而 CyclicBarrier 等待的对象是线程,只有线程都到齐了才往下执行

  2. 使用方式不同,这个也是由等待的对象不同引起的,CountDownLatch 需要调用 await() 来让线程等待,调用 countDown() 来修改状态,直到触发状态为 0 的事件。而 CyclicBarrier 只需要调用 await() 让线程等待,当调用 await() 方法的线程数满足条件,就自动唤醒所有线程往下执行

  3. CyclicBarrier 可以自动循环使用,当一次拦截被打开后,会自动创建下一个拦截。CountDownLatch 的计数器归 0 后不能再次使用

  4. 底层实现不同,CountDownLatch 使用 AQS 来实现底层同步,CyclicBarrier 基于更上层的 ReetrantLock + Condition 条件队列实现

  5. 失效机制不同,在 CountDownLatch 等待的线程如果被中断或者超时取消,不会影响其他线程。而 CyclicBarrier 采用 all-or-none 的机制,要么全部不通过,要么全部都通过,也就是说一旦在 CyclicBarrier 等待的线程有一个被中断或者超时取消,那么其他所有在这个 CyclicBarrier 等待的线程都被唤醒,通过栅栏往下执行

  6. CyclicBarrier 支持线程全部通过之后的回调功能,通过传入一个 Runnable 对象,由最后一个到达的线程来执行。而 CountDownLatch 不支持回调机制


Java 中 sleep() 和 wait() 方法的区别

sleep() 方法使当前线程进入停滞状态(阻塞当前线程),让出 CPU 的使用,目的是不让当前线程独自霸占该进程所获的 CPU 资源。该方法是 Thread 类的静态方法,当在一个 synchronized 块中调用 sleep() 方法时,线程虽然休眠了,但是其占用的锁并没有被释放;当 sleep() 休眠时间期满后,该线程不一定会立即执行,因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。

wait() 方法是 Object 类的,当一个线程执行到 wait() 方法时就进入到一个和该对象相关的等待池中,同时释放对象的锁(对于 wait(long timeout) 方法来说是暂时释放锁,因为超时时间到后还需要返还对象锁),其他线程可以访问。wait() 使用 notify() 或 notifyAll() 或者指定睡眠时间来唤醒当前等待池中的线程。wait() 必须放在 synchronized 块中使用,否则会在运行时抛出 IllegalMonitorStateException 异常。

由此可以看出它们之间的区别如下:

  • sleep() 不释放同步锁,wait() 释放同步锁。

  • sleep(milliseconds) 可以用时间指定来使他自动醒过来,如果时间没到则只能调用 interreput() 方法来强行打断(不建议,会抛出 InterruptedException),而 wait() 可以用 notify() 直接唤起。

  • sleep() 是 Thread 的静态方法,而 wait() 是 Object 的方法。

  • wait()、notify()、notifyAll() 方法只能在同步控制方法或者同步控制块里面使用,而 sleep() 方法可以在任何地方使用。


阻塞队列和生产者-消费者模式

阻塞队列提供了可阻塞的 put 和 take 方法,以及支持定时的 offer 和 poll 方法。队列可以是有界的也可以是无界的,无界队列永远都不会充满,因此无界队列上的 put 方法也永远都不会阻塞

在基于阻塞队列构建的生产者-消费者设计中,当数据生成时,生产者把数据放入队列,而当消费者准备处理数据时,将从队列中获取数据。生产者不需要知道消费者的标识或数量,或者它们是否是唯一的生产者,而只需将数据放入队列即可。同样,消费者也不需要知道生产者是谁,或者工作来自何处。BlockingQueue 简化了生产者-消费者设计的实现过程,支持任一数量的生产者和消费者

一种最常见的该模式就是线程池和工作队列的组合,如 Executor 任务执行框架

使用 BlockingQueue 实现

BlockingQueue 内部已经实现了同步队列,实现的原理是采用 await()和 signal() 方法,其可以在生成对象时指定容量大小,对于生产者容量到达最大时自动阻塞,对于消费者容量为 0 时自动阻塞
生产者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Producer implements Runnable {

BlockingQueue<String> queue;

public Producer(BlockingQueue<String> queue) {
this.queue = queue;
}

@Override
public void run() {
try {
String temp = "A product, producer: " + Thread.currentThread().getName();
System.out.println("I hava made a product: " + Thread.currentThread().getName());
queue.put(temp);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}

消费者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Consumer implements Runnable {

BlockingQueue<String> queue;

public Consumer(BlockingQueue<String> queue) {
this.queue = queue;
}

@Override
public void run() {
try {
String temp = queue.take();
System.out.println(temp);
} catch (Exception e) {
e.printStackTrace();
}

}

}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ProducerConsumer {

public static void main(String[] args) {
BlockingQueue<String> queue = new LinkedBlockingQueue<>(3);

Consumer consumer = new Consumer(queue);
Producer producer = new Producer(queue);

for (int i = 0; i < 5; i++) {
new Thread(producer, "Producer" + (i+1)).start();
new Thread(consumer, "Consumer" + (i+1)).start();
}
}

}

打印结果:

1
2
3
4
5
6
7
8
9
10
I hava made a product: Producer1
I hava made a product: Producer2
A product, producer: Producer1
I hava made a product: Producer3
I hava made a product: Producer4
A product, producer: Producer3
A product, producer: Producer2
I hava made a product: Producer5
A product, producer: Producer4
A product, producer: Producer5