0%

初探Java多线程

基础理论篇学习笔记

什么是进程

  • 进程是程序运行和资源分配的基本单位,一个程序至少一个进程,一个进程至少一个线程。

  • 多个进程的*内部数据和状态是完全独立的*,而多个线程是共享一个内存空间和一组系统资源,有可能互相影响


什么是线程

  • 线程是程序内部的控制流,只能使用分配给线程 的资源和环境
  • 线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换比进程切换负担要小。
  • 线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小能独立运行的基本单位。

多线程编程的目的

  • 多线程编程的目的,就是 最大限度地利用CPU资源,当某一线程的处理不需要占用CPC而和I/O等资源打交道时,让需要占用CPU资源的其他线程有机会获得CPU资源,从根本上,这就是多线程编程的最终目的。
  • 线程(Thread)也是程序的最小单元,它依托进程而存在。
  • 多个线程可以共享一块内存空间和一组系统资源因此线程之前的切换更加节省资源、更加轻量化,也因此被称为轻量级的进程。

多线程概念介绍

  • 一个进程可以包含多个线程。

  • 一个程序实现多个代码同时交替运行就需要产生多个线程。

  • CPU随机的抽出时间,让我们的程序一会做这件事情,一会做另外一件事情。

  • 同期其他大多数编程语言不同,Java内置支持多线程编程(multithreaded progranmming)。多线程程序包包含两个或两条以上并发运作的部分,吧程序中每个这种的部分都叫做一个线程(thread),每个线程都独立的执行路经,因此多线程是多任务处理的一种特殊形式。

  • 多任务处理被所有的现代操作系统所支持。然后,多任务处理有两种截然不同的类型,基于进程和基于线程。

    • 基于进程
      • 基于进程的多任务处理是更熟悉的形式,进程(process)本质上是一个执行的程序。因此基于进程的多线程任务处理的特点是允许你的计算机同时运行两个或更多的程序。
    • 基于线程
      • 基于线程(thread-based)的多任务处理环境中,线程是最小的执行单位。这意味着一个程序可以执行两个或则多个任务的功能。
    • 多线程程序比多进程程序需要更少的管理资源。
      • 进程是重量级的任务,需要分配给他们的独立空间地址。进程之间通信是昂贵和受限的。进程之间的转换也是很需要花费的。另一方面,线程是轻量级的选手,它们共享相同的地址空间并且共享同一进程,线程之间通信是轻量级的,线程的转换也是轻量级的。
    • 线程的实现
      • 1.两种方法均需要执行线程start方法作为线程分配必须的系统资源、调度线程运行并执行线程的run方法。
      • 2.在具体应用中,采用哪种方法来构造线程体要试情况而定。通常,当一个线程继承了另一个类,应该应该使用第二种方法,即使实现runnable接口。
      • 3.线程的消亡不能通过调用一个Trread.stop方法,而是让线程自然消亡。

线程的生命周期

一个线程的消亡过程。

  • 新建(New)

    • 创建后尚未启动。
  • 可运行(Runnable)

    • 可能正在运行,也可能正在等待 CPU 时间片。
    • 包含了操作系统线程状态中的 运行(Running ) 和 就绪(Ready)。
  • 无限期等待(Waiting)

    • 等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。

      进入方法 退出方法
      没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
      没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕
      LockSupport.park() 方法 -
  • 限期等待(Timed Waiting)

    • 无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。

    • 调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用 “使一个线程睡眠” 进行描述。

    • 调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用 “挂起一个线程” 进行描述。

    • 睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态

    • 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。

      进入方法 退出方法
      Thread.sleep() 方法 时间结束
      设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll()
      设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕
      LockSupport.parkNanos() 方法 -
      LockSupport.parkUntil() 方法 -
  • 阻塞(Blocking)

    • 这个状态下,是在多个线程有同步操作的场景,比如正在等待另一个线程的 synchronized 块的执行释放,或者可重入的 synchronized 块里别人调用 wait() 方法,也就是线程在等待进入临界区。
    • 阻塞可以分为:等待阻塞,同步阻塞,其他阻塞
  • 死亡(Terminated)

    • 线程因为 run 方法正常退出而自然死亡。
    • 因为一个没有捕获的异常终止了 run 方法而意外死亡。

线程的状态

线程的基本方法

  • wait线程等待

    • 调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方法一般用在同步方法或同步代码块中。
  • sleep线程睡眠

    • sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法会导致当前线程进入 WATING 状态。
  • yield线程让步

    • yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。
  • interrupt线程中断

    • 中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。

      • 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。

      • 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException,从而使线程提前结束 TIMED-WATING 状态。

        许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。

      • 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。

  • join等待其它线程终止

    • join() 方法,等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
    • 很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。
  • notify线程唤醒

    • Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
  • 其它方法

    • isAlive(): 判断一个线程是否存活。
    • activeCount(): 程序中活跃的线程数。
    • enumerate(): 枚举程序中的线程。
    • currentThread(): 得到当前线程。
    • isDaemon(): 一个线程是否为守护线程。
    • setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
    • setName(): 为线程设置一个名称。
    • setPriority(): 设置一个线程的优先级。
    • getPriority()::获得一个线程的优先级。

线程优先级

  • 设置优先级是为了在多线程环境中便于系统对线程调度,优先级高的线程将优先执行。

    • 一个线程的优先级遵从以下原则。
      • 线程创建时,子继承父的优先级。
      • 线程创建后,可用过调用setPriority()方法改变优先级。
      • 线程的优先级是1-10之间的正整数。
        • 1 - MIN_PRIORITY
        • 10 - MAX_PRIORITY
        • 5 - NORM_PRIORITY
  • 线程的优先级策略

    • 线程调度器选择优先级最高的线程运行.但是,如果发生以下情况,就会终止线程的运行.
      • 线程体中调用了yieid()方法,让出了对CPU的占用权。
      • 线程体中调用sleep()方法,使线程进入了睡眠状态。
      • 线程由I/O操作而受阻塞。
      • 另一个更高优先级的线程出现。
      • 在支持时间片段系统中,该线程的时间片用完。

上下文切换

  • 是指某一时间点 CPU 寄存器和程序计数器的内容。

    • 寄存器
      • 是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。
    • 程序计数器
      • 是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
    • 上下文切换的活动
    1. 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
    2. 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
    3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序中。
    • 引起线程上下文切换的原因
    1. 当前执行任务的时间片用完之后,系统 CPU 正常调度下一个任务。
    2. 当前执行任务碰到 IO 阻塞,调度器将此任务挂起,继续下一任务。
    3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务。
    4. 用户代码挂起当前任务,让出 CPU 时间。
    5. 硬件中断。

线程的同步

  • 在多线程环境中,可能会出现两个甚至多个线程试图同时访问一个有限的资源。必须对这种潜在的资源冲突进行预防。
    • 解决方案:在线程使用一个资源时为其加锁即可。访问资源的第一个线程为其上锁以后,其他线程比便不能在使用那个资源。除非被解锁。
  • 在线程环境中,关于成员变量与局部变量;如果一个变量是成员变量,那么多个线程对同一个对象的成员变量进行操作时候,他们对该成员变量是彼此影响到(也就是说一个线程对成员变量的改变会影响另一个到另一线程)。
  • 不能依靠线程优先级来决定线程的执行。
  • 同步到实现方式
    • synchronized 关键字;当synchronized关键字修饰一个方法的时候,该方法叫做同步方法。
    • Java中方锁。
      • java中每个对象都有一个锁(lock)或者叫监视器(monitor),当访问一个对象synchronized方法时,表示将该对象上锁,此时其他任何线程在去访问该synchronized方法了,直到之前那个线程执行方法完毕后(或者抛出异常),那么该对象的锁释放掉。其他线程才有可能再去访问synchronized方法。
      • 如果一个对象中有多个synchronized方法,某一时刻某个线程进入了该对象中的synchronized方法,那么在该方法没有执行完成之前或者抛出异常之前,其他线程是无法访问该对象的任何synchronized方法。
      • 被synchronized保护的变量应该是private修饰的。
      • 如果某个synchronized方法被static修饰的,那么当线程访问该方法时候,它锁定并不是对象(实例),而是synchronized方法所以在对象所对应的Class对象,因为Java中无论一个类有多少个对象,这些对象都会对应唯一一个Class对象,因此当线程分别访问一个类的两个对象的两个static synchronized方法时,他们的执行顺序是顺序的,也就是说一个线程先执行方法,执行完毕后另一个线程才开始执行。
      • synchronized方法是一个粗粒度的控制,某一个时刻只能有一个方法执行synchronized方法;sysnchronized块则是一种细粒度的控制方;只会将代码块同步。位于方法内、synchronized块之外的代码是可以被多个线程同时访问的。

volatile关键字

volatile 关键字的主要作⽤就是保证变量的可⻅性然后还有⼀个作⽤是防⽌指令重排序。

  • 并发编程的三个重要特性

    • 原⼦性 : ⼀个的操作或者多次操作,要么所有的操作全部都得到执⾏并且不会收到任何因素的

      ⼲扰⽽中断,要么所有的操作都执⾏,要么都不执⾏。 synchronized 可以保证代码⽚段的原

      ⼦性。

    • 可⻅性 :当⼀个变量对共享变量进⾏了修改,那么另外的线程都是⽴即可以看到修改后的最新

      值。 volatile 关键字可以保证共享变量的可⻅性。

    • 有序性 :代码在执⾏的过程中的先后顺序,Java 在编译器以及运⾏期间的优化,代码的执⾏顺

      序未必就是编写代码时候的顺序。 volatile 关键字可以禁⽌指令进⾏重排序优化。

使用场景

  • 读写锁

    • 如果需要实现一个读写锁,每次只能一个线程去写数据,但是有多个线程来读数据,就synchronize同步锁来对set方法加锁,get方法不加锁, 使用volatile来修饰变量,保证内存可见性,不然多个线程可能会在变量修改后还读到一个旧值。
  • 状态位

    • 用于做状态位标志,如果多个线程去需要根据一个状态位来执行一些操作,使用volatile修饰可以保证内存可见性。

      用于单例模式用于保证内存可见性,以及防止指令重排序。

synchronized关键字

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰

的⽅法或者代码块在任意时刻只能有⼀个线程执⾏。

synchronized 关键字加到 static 静态⽅法和 synchronized(class)代码块上都是是给 Class

类上锁。synchronized 关键字加到实例⽅法上是给对象实例上锁。尽量不要使⽤

synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!

常用场景

1. 同步一个代码块

1
2
3
4
5
public void func() {
synchronized (this) {
// ...
}
}

它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。

对于以下代码,使用 ExecutorService 执行了两个线程,由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。

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

public void func1() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e1.func1());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

对于以下代码,两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。

1
2
3
4
5
6
7
8
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e2.func1());
}
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9

2. 同步一个方法

1
2
3
public synchronized void func () {
// ...
}

它和同步代码块一样,作用于同一个对象。

3. 同步一个类

1
2
3
4
5
public void func() {
synchronized (SynchronizedExample.class) {
// ...
}
}

作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SynchronizedExample {

public void func2() {
synchronized (SynchronizedExample.class) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func2());
executorService.execute(() -> e2.func2());
}
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

4. 同步一个静态方法

1
2
3
public synchronized static void fun() {
// ...
}

作用于整个类。

Java实现多线程的方式

有三种使用线程的方法:

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

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

实现 Runnable 接口

需要实现 run() 方法。

通过 Thread 调用 start() 方法来启动线程。

1
2
3
4
5
6
7
8
9
10
public class MyRunnable implements Runnable {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}

实现 Callable 接口

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}

继承 Thread 类

同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。

1
2
3
4
5
6
7
8
9
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}

实现接口 VS 继承 Thread

实现接口会更好一些,因为:

  • Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
  • 类可能只要求可执行就行,继承整个 Thread 类开销过大。

三种方式的区别

  • 实现 Runnable 接口可以避免 Java 单继承特性而带来的局限;增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;适合多个相同程序代码的线程区处理同一资源的情况。
  • 继承 Thread 类和实现 Runnable 方法启动线程都是使用 start() 方法,然后 JVM 虚拟机将此线程放到就绪队列中,如果有处理机可用,则执行 run() 方法。
  • 实现 Callable 接口要实现 call() 方法,并且线程执行完毕后会有返回值。其他的两种都是重写 run() 方法,没有返回值。

Lock

Lock对象也可以实现同步,在使用上更加方便。

再Java多线程中,可以使用synchronized关键字来实现线程之间同步互斥,但是再jdk1.5以后增加了ReentrantLock类。并且功能更加强大、灵活。

ReentrantLock

ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。和synchronized也是互斥锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

public class LockExample {

private Lock lock = new ReentrantLock();

public void func() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
public static void main(String[] args) {
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
// 最后输出 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
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
38
39
40
41
42
43
public class ReentrantLockDemo {

private Lock lock = new ReentrantLock();

public void test(){
// 调用ReentrantLock的lock方法获取锁
lock.lock();
for (int i = 0; i < 3; i++) {
System.out.println("ThreadName " + Thread.currentThread().getName() + " number " + i);
}
// 调用ReentrantLock的lock方法释放锁
lock.unlock();
}
}

public class ReentrantLockThread extends Thread{
private ReentrantLockDemo reentrantLockDemo;

public ReentrantLockThread(ReentrantLockDemo reentrantLockDemo) {
this.reentrantLockDemo = reentrantLockDemo;
}

@Override
public void run() {
reentrantLockDemo.test();
}
}

public class ReentrantLockTests {

public static void main(String[] args) {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
ReentrantLockThread reentrantLockThread1 = new ReentrantLockThread(reentrantLockDemo);
ReentrantLockThread reentrantLockThread2 = new ReentrantLockThread(reentrantLockDemo);
ReentrantLockThread reentrantLockThread3 = new ReentrantLockThread(reentrantLockDemo);
ReentrantLockThread reentrantLockThread4 = new ReentrantLockThread(reentrantLockDemo);
reentrantLockThread1.start();
reentrantLockThread2.start();
reentrantLockThread3.start();
reentrantLockThread4.start();
}

}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
ThreadName Thread-0 number 0
ThreadName Thread-0 number 1
ThreadName Thread-0 number 2
ThreadName Thread-2 number 0
ThreadName Thread-2 number 1
ThreadName Thread-2 number 2
ThreadName Thread-3 number 0
ThreadName Thread-3 number 1
ThreadName Thread-3 number 2
ThreadName Thread-1 number 0
ThreadName Thread-1 number 1
ThreadName Thread-1 number 2
  • 当前的线程执行完成之后才能执行其他线程,但是其他线程执行的顺序是随机的。

公平锁 VS 非公平锁

  • 公平锁的含义是线程需要按照请求的顺序来获得锁;而非公平锁则允许“插队”的情况存在,所谓的“插队”指的是,线程在发送请求的同时该锁的状态恰好变成了可用,那么此线程就可以跳过队列中所有排队的线程直接拥有该锁。
  • 而公平锁由于有挂起和恢复所以存在一定的开销,因此性能不如非公平锁,所以 ReentrantLock 和 synchronized 默认都是非公平锁的实现方式。