Java Thread

进程 vs 线程

进程(process) 与 线程(thread) 最大的区别是进程拥有自己的地址空间,某进程内的线程对于其他进程不可见,即进程A不能通过传地址的方式直接读写进程B的存储区域。进程之间的通信需要通过进程间通信(Inter-process communication,IPC)。与之相对的,同一进程的各线程间之间可以直接通过传递地址或全局变量的方式传递信息。

此外,进程作为操作系统中拥有资源和独立调度的基本单位,可以拥有多个线程。通常操作系统中运行的一个程序就对应一个进程。在同一进程中,线程的切换不会引起进程切换。在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。相比进程切换,线程切换的开销要小很多。线程于进程相互结合能够提高系统的运行效率。

从概念上来说:

  • 进程:一个程序对一个数据集的动态执行过程,是分配资源的基本单位。
  • 线程:一个进程内的基本调度单位。一个进程包含一个或者多个线程。

Thread Category

线程可以分为两类:

  1. 用户级线程(user level thread)
  2. 内核级线程(kernel level thread)

用户级线程(user level thread)
对于这类线程,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。在应用程序启动后,操作系统分配给该程序一个进程号,以及其对应的内存空间等资源。应用程序通常先在一个线程中运行,该线程被成为主线“程。在其运行的某个时刻,可以通过调用线程库中的函数创建一个在相同进程中运行的新线程。 用户级线程的好处是非常高效,不需要进入内核空间,但并发效率不高。

内核级线程(kernel level thread)
对于这类线程,有关线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只能调用内核线程的接口。内核维护进程及其内部的每个线程,调度也由内核基于线程架构完成。内核级线程的好处是,内核可以将不同线程更好地分配到不同的CPU,以实现真正的并行计算。

事实上,在现代操作系统中,往往使用组合方式实现多线程,即线程创建完全在用户空间中完成,并且一个应用程序中的多个用户级线程被映射到一些内核级线程上,相当于是一种折中方案。

什么是守护线程(daemon thread)?

守护线程是运行在后台的一种特殊进程,它独立于控制终端,并且周期性地执行某种任务或着等待处理某些发生的事件。也就是在程序运行的时候在后台提供一种通用服务的线程。

daemon是相于user线程而言的,可以理解为一种运行在后台的服务线程,比如时钟处理线程、idle线程、垃圾回收线程等都是daemon线程。

daemon线程有个特点就是”比较次要”,程序中如果所有的user线程都结束了,那这个程序本身就结束了,不管daemon是否结束。而user线程就不是这样,只要还有一个user线程存在,程序就不会退出。

在Java中java.lang.Thread.isDaemon()方法用来测试线程是否为守护线程

1
public final boolean isDaemon() //  if return true, the thread is daemon thread

注意:

  1. Thread.Daemon(true) 必须在Thread.start()方法之前设置,否则会出现IllegalThreadStateException异常
  2. 不能把正在运行的常规线程设置为守护线程
  3. 守护线程应该永远不去访问固有资源,如:数据库、文件等。因为它会在任何时候甚至在一个操作的中间发生中断。

Context Switch

对于单核单线程CPU而言,在某一时刻只能执行一条CPU指令。上下文切换(Context Switch)是一种将CPU资源从一个进程分配给另一个进程的机制。从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。在切换的过程中,操作系统需要先存储当前进程的状态(包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。

Semaphore / Mutex

当用户创立多个线程/进程时,如果不同线程/进程同时读写相同的内容,则可能造成读写错误,或者数据不一致。此时,需要通过加锁的方式,控制核心区域(critical section)的访问权限。对于semaphore而言,在初始化变量的时候可以控制允许多少个线程/进程同时访问一个critical section,其他的线程/进程会被堵塞,直到有人解锁。

Mutex相当于只允许一个线程/进程访问的semaphore。此外,根据实际需要,人们还实现了一种读写锁(read-write lock),它允许同时存在多个阅读者(reader),但任何时候至多只有一个写者(writer),且不能于读者共存。

Deadlock

死锁是指两个或多个线程/进程之间相互阻塞,以至于任何一个都不能继续运行,因此也不能解锁其他线程/进程。例如,线程A占有lock A,并且尝试获取lock B;而线程2占有lock B,尝试获取lock A。此时,两者相互阻塞,都无法继续运行。

总结产生死锁的四个条件(只有当四个条件同时满足时才会产生死锁):

  1. Mutual Exclusion – Only one process may use a resource at a time
  2. Hold-and-Wait – Process holds resource while waiting for another
  3. No Preemption – Can’t take a resource away from a process
  4. Circular Wait – The waiting processes form a cycle

How to solve deadlock?

  1. 检测死锁并且恢复。
  2. 仔细地对资源进行动态分配,以避免死锁。
  3. 通过破除死锁四个必要条件之一,来防止死锁产生

在 MySQL 中是如何处理死锁?

Producer-Consumer

场景说明:

  • 有一个或多个生产者生产某种数据,生产的数据放在某个缓冲区;
  • 有一个消费者从缓冲区读取数据,每次只能取一项;
  • 系统负责保证在任何时候只有一个主体(生产者或消费者)可以访问缓冲区。
  • 当缓冲区已满时,生产者不会继续向缓冲区添加数据;当缓冲区为空时,消费者不会继续从缓冲区读取数据;

IPC - Inter-Process-Communication

一个进程不能直接读写另一个进程的数据,两者之间的通信需要通过进程间通信(inter-process communication, IPC)进行。进程通信的方式通常遵从生产者消费者模型,需要实现数据交换和同步两大功能。

  • Shared-memory + semaphore: 不同进程通过读写操作系统中特殊的共享内存进行数据交换,进程之间用semaphore实现同步。
  • Message passing: 进程在操作系统内部注册一个port,并且监测有没有数据,其他进程直接写数据到该port。该通信方式更加接近于网络通信方式。事实上,网络通信也是一种IPC,只是进程分布在不同机器上而已。

实现方式:

  1. 管道(pipe):管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。
  2. 信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  3. 信号量(semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  4. 消息队列(message-queu):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  5. 共享内存(shared-memory):共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。
  6. 套接字(socket):套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。

Pipe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
fun main() {
val inputPipe = PipedInputStream()
val outputPipe = PipedOutputStream()
outputPipe.connect(inputPipe)

thread {
/* Input */
while (true) {
val read = inputPipe.read()
println("read is $read")
}
}

thread {
/* Output */
var writeData = 0
while (true) {
writeData += 1
outputPipe.write(writeData)
println("write is $writeData")
Thread.sleep(500)
}
}

while (true) {
Thread.yield()
}
}

// write is 1
// write is 2
// read is 1
// read is 2
// write is 3
// write is 4
// read is 3
// read is 4

Semaphore

What is Semaphore?

Semaphore acts as a permit. The thread that wants access to the shared resource tries to acquire a permit. If the semaphore’s count is greater than zero, then the thread is able to get a permit. Once a thread gets the permit, the semaphore’s count decrements. Otherwise, the thread will be blocked until a permit can be acquired.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fun main() {
val semaphore = Semaphore(0, true)

thread {
try {
while (true) {
Thread.sleep(1500)
println("Producer --> ${System.currentTimeMillis()}")
semaphore.release()
}
} finally {
}
}

thread {
try {
while (true) {
semaphore.acquire()
println("Consumer --> ${System.currentTimeMillis()}")
}
} finally {
}
}

while (true) Thread.yield()
}

Reference