Jean's Blog

一个专注软件测试开发技术的个人博客

0%

Java之多线程

Java之多线程

什么是多线程

进程

打开Windows任务管理器,可以发现我们在操作系统上运行的程序都是进程:

image-20230908101815880

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

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

线程

进程已经是可以进行资源分配和调度了,为什么还要线程呢?是为了使程序并发执行,系统必须执行以下的一系列操作:

  • 创建进程:系统在创建一个进程时,必须为它分配其所必须的、除处理机以外的所有资源,如内存空间、I/O设备,以及建立相应的PCB(进程控制块:Process Control Block)
    • PCB中记录了操作系统所需的,用于描述进程的当前情况以及控制进程运行的全部信息。PCB的作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位,一个能与其他进程并发执行的进程。或者说,OS是根据PCB来对并发执行的进程进行控制和管理的。
    • 多道程序设计技术是在计算机内存中同时存放几道相互独立的程序,使它们在管理程序控制下,相互穿插运行,两个或两个以上程序在计算机系统中同处于开始到结束之间的状态, 这些程序共享计算机系统资源。与之相对应的是单道程序,即在计算机内存中只允许一个的程序运行。对于一个单CPU系统来说,程序同时处于运行状态只是一种宏观上的概念,他们虽然都已经开始运行,但就微观而言,任意时刻,CPU上运行的程序只有一个。
  • 撤销进程:系统在撤销进程时,又必须先对其所占有的资源执行回收操作,然后再撤销PCB
  • 进程切换:对进程进行上下文切换时,需要保存当前进程的CPU环境,设置新选中进程的CPU环境,因而须花费不少的处理机时间

image-20230908101846011

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

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

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

线程在哪呢?例如:打开一个音乐播放器,既能播放音乐,点击音乐播放器点击控件还能响应

image-20230908101912883

在同一个进程内可以执行多个任务,而这每一个任务都可以看出是一个线程。则一个进程会有一个或多个线程

进程和线程

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

线程有三个基本状态:执行、就绪、阻塞

线程有五种基本操作:派生、阻塞、激活、调度、结束

线程的属性

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

线程有两个基本类型

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

image-20230908101939225

多线程的存在,不是提高程序的执行速度,是为了提供应用程序的使用率,程序的执行其实都是在抢CPU的资源CPU的执行权。多个进程时在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。

并行与并发

  • 并行
    • 并⾏性是指同⼀时刻内发⽣两个或多个事件
    • 并⾏是在不同实体上的多个事件
  • 并发
    • 并发性是指同⼀时间间隔内发⽣两个或多个事件
    • 并发是在同⼀实体上的多个事件

并⾏是针对进程的, 并发是针对线程的

Java实现多线程

Java实现多线程是使用了Thread这个类,主要看下Thread类的解释(注释)

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
* A <i>thread</i> is a thread of execution in a program. The Java
* Virtual Machine allows an application to have multiple threads of
* execution running concurrently.-- JVM允许多个线程并发执行
* <p>
* Every thread has a priority. Threads with higher priority are
* executed in preference to threads with lower priority.--线程有优先级,优先级高的会比低的先要执行 Each thread
* may or may not also be marked as a daemon--线程可能还会有守护(后台)线程. When code running in
* some thread creates a new <code>Thread</code> object, the new
* thread has its priority initially set equal to the priority of the
* creating thread, and is a daemon--初始化的时候,优先级都是平等的 thread if and only if the
* creating thread is a daemon.
* <p>
* When a Java Virtual Machine starts up, there is usually a single
* non-daemon thread (which typically calls the method named
* <code>main</code> of some designated class). The Java Virtual
* Machine continues to execute threads until either of the following
* occurs:--JVM启动时,通常会有一个叫做main线程启动起来,它没有后台(守护线程)的
* <ul>
* <li>The <code>exit</code> method of class <code>Runtime</code> has been
* called and the security manager has permitted the exit operation
* to take place.
* <li>All threads that are not daemon threads have died, either by
* returning from the call to the <code>run</code> method or by
* throwing an exception that propagates beyond the <code>run</code>
* method.
* 线程结束有这两种情况:
* 1.执行退出exit方法
* 2.run方法执行完或者抛出了异常
* </ul>
* <p>
* There are two ways to create a new thread of execution.--两个方法创建新的线程 One is to
* declare a class to be a subclass of <code>Thread</code>. This
* subclass should override the <code>run</code> method of class
* <code>Thread</code>. An instance of the subclass can then be
* allocated and started. For example, a thread that computes primes
* larger than a stated value could be written as follows:
* <hr><blockquote><pre> -- 例子,继承Thread类,重写run方法
* class PrimeThread extends Thread {
* long minPrime;
* PrimeThread(long minPrime) {
* this.minPrime = minPrime;
* }
*
* public void run() {
* // compute primes larger than minPrime
* &nbsp;.&nbsp;.&nbsp;.
* }
* }
* </pre></blockquote><hr>
* <p>
* The following code would then create a thread and start it running:
* <blockquote><pre>
* PrimeThread p = new PrimeThread(143);
* p.start(); -- 启动线程
* </pre></blockquote>
* <p>
* The other way to create a thread is to declare a class that
* implements the <code>Runnable</code> -- 另一个方式实现Runnable接口 interface. That class then
* implements the <code>run</code> method. An instance of the class can
* then be allocated, passed as an argument when creating
* <code>Thread</code>, and started. The same example in this other
* style looks like the following:
* <hr><blockquote><pre>
* class PrimeRun implements Runnable {
* long minPrime;
* PrimeRun(long minPrime) {
* this.minPrime = minPrime;
* }
*
* public void run() { -- 实现Runable接口,重写run方法
* // compute primes larger than minPrime
* &nbsp;.&nbsp;.&nbsp;.
* }
* }
* </pre></blockquote><hr>
* <p>
* The following code would then create a thread and start it running:
* <blockquote><pre>
* PrimeRun p = new PrimeRun(143);
* new Thread(p).start(); -- 启动线程
* </pre></blockquote>
* <p>
* Every thread has a name for identification purposes. More than
* one thread may have the same name. If a name is not specified when
* a thread is created, a new name is generated for it.--每个线程都有自己的名字,在创建的时候如果没有指定,会自动给它一个名字
* <p>
* Unless otherwise noted, passing a {@code null} argument to a constructor
* or method in this class will cause a {@link NullPointerException} to be
* thrown.--不能将参数设置为null
*
* @author unascribed
* @see Runnable
* @see Runtime#exit(int)
* @see #run()
* @see #stop()
* @since JDK1.0
*/

创建多线程

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

继承Thread,重写run方法

  • 创建一个类,继承Thread,重写run方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package com.thread.create;

    /**
    * @author jingLv
    * @date 2020/11/12
    */
    public class MyTread extends Thread {

    @Override
    public void run() {
    for (int i = 0; i < 100; i++) {
    System.out.println(i);
    }
    }
    }
  • 调用测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    package com.thread.create;

    /**
    * @author jingLv
    * @date 2020/11/12
    */
    public class MyThreadDemo {

    public static void main(String[] args) {
    // 创建两个线程对象
    MyTread tread1 = new MyTread();
    MyTread tread2 = new MyTread();

    tread1.start();
    tread2.start();
    }
    }

实现Runnable接口,重写run方法

  • 实现Runnable接口,重写run方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package com.thread.create;

    /**
    * @author jingLv
    * @date 2020/11/12
    */
    public class MyRunnable implements Runnable {

    @Override
    public void run() {
    for (int i = 0; i < 100; i++) {
    System.out.println(i);
    }
    }
    }
  • 调用测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.thread.create;

    /**
    * @author jingLv
    * @date 2020/11/12
    */
    public class MyRunnableDemo {

    public static void main(String[] args) {
    // 创建Runnable类的对象
    MyRunnable myRunnable = new MyRunnable();

    Thread thread1 = new Thread(myRunnable);
    Thread thread2 = new Thread(myRunnable);

    thread1.start();
    thread2.start();
    }
    }

Java实现多线程需要注意的细节

  • run()和start()方法区别
    • run():仅仅是封装被线程执行的代码,直接调用是普通方法
    • start():首先启动了线程,然后再由jvm去调用该线程的run()方法
  • JVM虚拟机的启动是单线程还是多线程?
    • 多线程。不仅仅是启动main线程,还会至少启动垃圾回收线程,回收不用的内存
  • 实现多线程,一般使用Runnable接口
    • 可以避免java中的单继承的限制
    • 应该将并发运行任务和运行机制解耦,因此选择实现Runnable接口这种方式

Thread类解析

设置线程名

Thread.currentThread().getName()查看线程名。

如果没有做什么设置,我们会发现线程的名字是这样:主线程叫做main,其他线程是Thread-x

查看Thread类中线程是如何命名的:

image-20230908102007995

nextThreadNum()的方法是这样实现的:

image-20230908102028305

在查看一下init方法:

image-20230908102055623

  • 实现了Runnable的方式来实现多线程,打印线程名字

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package com.thread.create;

    /**
    * @author jingLv
    * @date 2020/11/12
    */
    public class MyRunnable implements Runnable {

    @Override
    public void run() {
    // 打印出当前线程的名字
    System.out.println(Thread.currentThread().getName());
    }
    }
  • 调用测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package com.thread.create;

    /**
    * @author jingLv
    * @date 2020/11/12
    */
    public class MyRunnableDemo {

    public static void main(String[] args) {
    // 创建Runnable类的对象
    MyRunnable myRunnable = new MyRunnable();
    // 带参构造方法给线程起名字
    Thread thread1 = new Thread(myRunnable, "线程1");
    Thread thread2 = new Thread(myRunnable, "线程2");

    thread1.start();
    thread2.start();

    // 打印当前线程的名字
    System.out.println(Thread.currentThread().getName());
    }
    }
  • 执行结果

    image-20230908102120363

还可以通过setName(String name)的方法来改掉线程的名字。

守护线程

守护线程是为其他线程服务的垃圾回收线程就是守护线程

守护线程有一个特点:

  • 当别的用户线程执行完了,虚拟机就会退出,守护线程也就会被停止掉了。也就是说:守护线程作为一个服务线程,没有服务对象就没有必要继续运行了

使用线程的时候要注意的地方:

  1. 在线程启动前设置为守护线程,方法是setDaemon(boolean on)
  2. 使用守护线程不要访问共享资源(数据库、文件等),因为它可能会在任何时候就挂掉了
  3. 守护线程中产生的新线程也是守护线程

实例

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
package com.thread.create;

/**
* @author jingLv
* @date 2020/11/12
*/
public class MyRunnableDemo {

public static void main(String[] args) {
// 创建Runnable类的对象
MyRunnable myRunnable = new MyRunnable();
// 带参构造方法给线程起名字
Thread thread1 = new Thread(myRunnable, "线程1");
Thread thread2 = new Thread(myRunnable, "线程2");

// 设置守护线程
thread2.setDaemon(true);

thread1.start();
thread2.start();

// 打印当前线程的名字
System.out.println(Thread.currentThread().getName());
}
}

线程1和主线程执⾏完了,我们的守护线程就不执⾏了(电脑性能足够好的可能测不出来)

注意:要在线程启动之前设置守护线程,如果启动了再设置守护线程,会抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Marks this thread as either a {@linkplain #isDaemon daemon} thread
* or a user thread. The Java Virtual Machine exits when the only
* threads running are all daemon threads.
*
* <p> This method must be invoked before the thread is started.
*
* @param on
* if {@code true}, marks this thread as a daemon thread
*
* @throws IllegalThreadStateException
* if this thread is {@linkplain #isAlive alive}
*
* @throws SecurityException
* if {@link #checkAccess} determines that the current
* thread cannot modify this thread
*/
public final void setDaemon(boolean on) {
checkAccess();
if (isAlive()) {
throw new IllegalThreadStateException();
}
daemon = on;
}

优先级线程

线程优先级高仅仅表示线程获取的CPU时间片的几率高,但这不是一个确定的因素!

线程的优先级的高度依赖于操作系统的,Windows和Linux就有所区别(Linux下优先级可能就被忽略了~)

Java提供的优先级默认是5,最低1,最高10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 /**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;

/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;

/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;

优先级的实现

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
/**
* Changes the priority of this thread.
* <p>
* First the <code>checkAccess</code> method of this thread is called
* with no arguments. This may result in throwing a
* <code>SecurityException</code>.
* <p>
* Otherwise, the priority of this thread is set to the smaller of
* the specified <code>newPriority</code> and the maximum permitted
* priority of the thread's thread group.
*
* @param newPriority priority to set this thread to
* @exception IllegalArgumentException If the priority is not in the
* range <code>MIN_PRIORITY</code> to
* <code>MAX_PRIORITY</code>.
* @exception SecurityException if the current thread cannot modify
* this thread.
* @see #getPriority
* @see #checkAccess()
* @see #getThreadGroup()
* @see #MAX_PRIORITY
* @see #MIN_PRIORITY
* @see ThreadGroup#getMaxPriority()
*/
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
// 参数检查,如果存在线程组,那么该线程的优先级不能比组的优先级要高
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}

setPriority0是一个本地(navite)的方法:

1
private native void setPriority0(int newPriority);

线程生命周期

之前介绍线程的线程有三个基本状态:执行、就绪、阻塞

查看如下图,Thread上有很多方法是用来切换线程状态的

image-20230908102416583

sleep方法

调⽤sleep⽅法会进⼊计时等待状态,等时间到了, 进⼊的是就绪状态⽽并⾮是运⾏状态!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds, subject to
* the precision and accuracy of system timers and schedulers.--受精确的时间约束 The thread
* does not lose ownership of any monitors.
*
* @param millis 参数Long值,毫秒
* the length of time to sleep in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static native void sleep(long millis) throws InterruptedException;

加上Sleep后的状态图

image-20230908102209874

yield方法

调用yield方法会先让别的线程执行,但是不确保真正让出

  • 意思:我有空,可以的话,让你们先执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* A hint to the scheduler that the current thread is willing to yield
* its current use of a processor. The scheduler is free to ignore this
* hint.
*
* <p> Yield is a heuristic attempt to improve relative progression
* between threads that would otherwise over-utilise a CPU. Its use
* should be combined with detailed profiling and benchmarking to
* ensure that it actually has the desired effect.
*
* <p> It is rarely appropriate to use this method.--实际上很少用,它不确保一定会让出CPU It may be useful
* for debugging or testing purposes, where it may help to reproduce
* bugs due to race conditions. It may also be useful when designing
* concurrency control constructs such as the ones in the
* {@link java.util.concurrent.locks} package.
*/
public static native void yield();

加上yield后的状态图

image-20230908102456011

join方法

调⽤join⽅法,会等待该线程执⾏完毕后才执⾏别的线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Waits for this thread to die. -- 等待这个线程死亡
*
* <p> An invocation of this method behaves in exactly the same
* way as the invocation
*
* <blockquote>
* {@linkplain #join(long) join}{@code (0)}
* </blockquote>
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final void join() throws InterruptedException {
join(0);
}

查看join的具体实现

img

wait方法是Object上定义的,它是native本地方。

image-20230908102646164

wait方法实际上它也是计时等待(如果带时间参数)的一种!

加上join后的状态图

image-20230908102621410

interrupt方法

线程终端,之前的版本是有stop方法,但被设置过时了。现在已经没有强制线程终止的方法了!

image-20230908102704885

由于stop方法可以让一个线程A终止掉另一个线程B

  • 被终止的线程B会立即释放锁,这可能会让对象处于不一致的状态
  • 线程A也不知道线程B什么时候能够被终⽌掉,万⼀线程B还处理运⾏计算阶段,线程A调⽤stop⽅
    法将线程B终⽌,那就很⽆辜了

总而言之,stop方法太暴力了,不安全,所以被设置过时了。

一般使用的是interrupt来请求终止线程:

  • 要注意的是:interrupt不会真正停止一个线程,它仅仅是给这个线程发了一个信号告诉它,它应该要结束了

  • 也就是说:Java设计者实际上是想线程自己来终止,通过上面的信号,就可以判断处理什么业务了

  • 具体到底中断还是继续运行,应该由被通知的线程自己处理

1
2
3
4
5
6
7
8
9
10
11
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 若未发生中断,就正常执行任务
while (!Thread.currentThread().isInterrupted()) {
// 正常任务代码……
}
// 中断的处理代码……
doSomething();
}
}).start();

再次说明:调⽤interrupt()并不是要真正终⽌掉当前线程,仅仅是设置了⼀个中断标志。这个中断标志可以给我们⽤来判断什么时候该⼲什么活!什么时候中断由我们⾃⼰来决定,这样就可以安全地终⽌线程了

查看源码:

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
44
45
46
47
48
49
50
51
52
53
/**
* Interrupts this thread.--中断当前线程
*
* <p> Unless the current thread is interrupting itself, which is
* always permitted, the {@link #checkAccess() checkAccess} method
* of this thread is invoked, which may cause a {@link
* SecurityException} to be thrown.--只能自己调用中断的方法,不然会抛出安全异常
*
* 【<p> If this thread is blocked in an invocation of the {@link
* Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link
* Object#wait(long, int) wait(long, int)} methods of the {@link Object}
* class, or of the {@link #join()}, {@link #join(long)}, {@link
* #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},
* methods of this class, then its interrupt status will be cleared and it
* will receive an {@link InterruptedException}.
*
* <p> If this thread is blocked in an I/O operation upon an {@link
* java.nio.channels.InterruptibleChannel InterruptibleChannel}
* then the channel will be closed, the thread's interrupt
* status will be set, and the thread will receive a {@link
* java.nio.channels.ClosedByInterruptException}.
*
* <p> If this thread is blocked in a {@link java.nio.channels.Selector}
* then the thread's interrupt status will be set and it will return
* immediately from the selection operation, possibly with a non-zero
* value, just as if the selector's {@link
* java.nio.channels.Selector#wakeup wakeup} method were invoked.】--Java设计者设置中断标志的目的是想由被通知的线程自己处理,而这些方法都阻塞掉了。 被阻塞掉的线程调用中断方法是不合理的(不允许中断已经阻塞的线程)【因为可能会造成中断无效】
*
* <p> If none of the previous conditions hold then this thread's interrupt
* status will be set. </p> -- 上面的三种情况都不发生,才能将对应的标志位置换
*
* <p> Interrupting a thread that is not alive need not have any effect.--中断一个不活动的线程是没有意义的
*
* @throws SecurityException
* if the current thread cannot modify this thread
*
* @revised 6.0
* @spec JSR-51
*/
public void interrupt() {
if (this != Thread.currentThread())
checkAccess(); -- 检查是否有权限

synchronized (blockerLock) {
Interruptible b = blocker; -- 是否阻塞的线程调用(如:sleep)
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this); -- 如果是,抛出异常,将中断标志位改为【false】(一般会在catch中再次修改)
return;
}
}
interrupt0();-- 如果没有,就可以修改到标志位了(没有进入if
}

查看抛出的异常:InterruptedException

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
44
package java.lang;

/**
* Thrown when a thread is waiting, sleeping, or otherwise occupied,
* and the thread is interrupted, either before or during the activity.
* Occasionally a method may wish to test whether the current
* thread has been interrupted, and if so, to immediately throw
* this exception. The following code can be used to achieve
* this effect: -- 当线程正在执行sleep、wait方法时调用线程中断,就会抛出这个异常
* <pre>
* if (Thread.interrupted()) // Clears interrupted status!
* throw new InterruptedException();
* </pre>
*
* @author Frank Yellin
* @see java.lang.Object#wait()
* @see java.lang.Object#wait(long)
* @see java.lang.Object#wait(long, int)
* @see java.lang.Thread#sleep(long)
* @see java.lang.Thread#interrupt()
* @see java.lang.Thread#interrupted()
* @since JDK1.0
*/
public
class InterruptedException extends Exception {
private static final long serialVersionUID = 6700697376100628473L;

/**
* Constructs an <code>InterruptedException</code> with no detail message.
*/
public InterruptedException() {
super();
}

/**
* Constructs an <code>InterruptedException</code> with the
* specified detail message.
*
* @param s the detail message.
*/
public InterruptedException(String s) {
super(s);
}
}

所以说:interrupt方法是不会对线程状态造成影响的,它仅仅设置一个标志位罢了

interrupt线程中断还有另外两个方法(检查该线程是否被中断):

  • 静态方法interrupted() —> 会清除中断标志位

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    /**
    * Tests whether the current thread has been interrupted. The
    * <i>interrupted status</i> of the thread is cleared by this method.--这个方法会清除中断标志位 In
    * other words, if this method were to be called twice in succession, the
    * second call would return false (unless the current thread were
    * interrupted again, after the first call had cleared its interrupted
    * status and before the second call had examined it).
    *
    * <p>A thread interruption ignored because a thread was not alive
    * at the time of the interrupt will be reflected by this method
    * returning false.
    *
    * @return <code>true</code> if the current thread has been interrupted;
    * <code>false</code> otherwise.
    * @see #isInterrupted()
    * @revised 6.0
    */
    public static boolean interrupted() {
    return currentThread().isInterrupted(true);
    }
  • 实例方法isInterrupted() —> 不会清除中断标志位

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * Tests whether this thread has been interrupted. The <i>interrupted
    * status</i> of the thread is unaffected by this method.--这个方法不会影响中断标志位
    *
    * <p>A thread interruption ignored because a thread was not alive
    * at the time of the interrupt will be reflected by this method
    * returning false.
    *
    * @return <code>true</code> if this thread has been interrupted;
    * <code>false</code> otherwise.
    * @see #interrupted()
    * @revised 6.0
    */
    public boolean isInterrupted() {
    return isInterrupted(false);
    }

如果阻塞线程调用了interrupt()方法,那么会抛出异常设置标志位为false,同时该线程会退出阻塞的。

案例说明:

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
44
45
46
47
48
package com.thread.create;

/**
* @author jingLv
* @date 2020/11/18
*/
public class Main {

public static void main(String[] args) {
Main main = new Main();
// 创建线程并启动
Thread thread = new Thread(main.runnable);
System.out.println("This is main");
thread.start();

try {
// 1. 在main线程中睡了3秒钟,因此我们去新的线程中执行了
// 在main线程睡个3秒
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("In main");
e.printStackTrace();
}
// 3.在睡半秒的同事,main的线程的3秒钟过去了,main线程设置了该线程中断
// 设置中断
thread.interrupt();
}

Runnable runnable = () -> {
int i = 0;
try {
while (i < 1000) {
// 2. 每次睡半秒钟才执行
// 睡半秒,再执行
Thread.sleep(500);
System.out.println(i++);
}
} catch (InterruptedException e) {
// 判断该阻塞线程是否还在
System.out.println(Thread.currentThread().isAlive());
// 4. 抛出了异常,该阻塞的线程(睡半秒钟立马停止阻塞,在catch方法还在存活这)线程的中断标志位被修改为了false
// 判断该线程的中断标志位状态
System.out.println(Thread.currentThread().isInterrupted());
System.out.println("In Runnable");
e.printStackTrace();
}
};
}

执行结果:

image-20230908102745261

使用多线程需要注意的问题

使用多线程遇到的问题

1.1线程安全问题

多线程主要是为了提高我们应用程序的使用率。但同时,这会给我们带来很多安全问题!

如果我们在单线程中以”顺序”(串行->独占)的方式执行代码是没有任何问题的。但是到了多线程的环境下(并行),如果没有设计和控制得好,就会给我们带来很多意想不到的状况,也就是线程安全问题,因为在多线程的环境下,线程是交替执行的,一般他们会使用多个线程执行相同的代码。如果在此相同的代码里边有着共享的变量,或者一些组合操作,我们想要的正确结果就很容易出现了问题。

下面看一个例子:

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
package com.thread.create;

/**
* @author jingLv
* @date 2020/11/18
*/
public class Main {

/**
* 买票过程,多个线程卖100张票
*/
public static class TicketDemo implements Runnable {
// 100张票
private int tickets = 100;

@Override
public void run() {
while (true) {
if (tickets > 0) {
try {
// 线程睡半秒
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sale: " + tickets--);
}
}
}
}

public static void main(String[] args) {
TicketDemo ticketDemo = new TicketDemo();
// 启动三个线程出售100张票
Thread thread1 = new Thread(ticketDemo);
Thread thread2 = new Thread(ticketDemo);
Thread thread3 = new Thread(ticketDemo);
thread1.start();
thread2.start();
thread3.start();
}
}

执行结果:

image-20230908103228911

观察结果,发现多个线程卖到同一张票,这就是线程安全问题。正确的情况是,票一共只有100张,三个线程卖出的票只能100张,也就是三个线程卖出的票号是不能有重复的。

如果说:当多个线程访问某个类时候,这个类始终能表现出正确的行为,那么这个类就是线程安全的!

有个原则:能使用JDK提供的线程安全机制,就使用JDK的

1.2性能问题

使用多线程我们的目的就是为了提高应用程序的使用率,但是如果多线程的代码没有设计好的话,那未必会提高效率。反而降低了效率,甚至会造成死锁!

还是以上面的线程安全问题的例子,这个例子线程是不安全的,现在要将该例子优化为线程安全,最简单的方式:使用JDK提供的内置锁synchronized

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
44
45
package com.thread.create;

/**
* @author jingLv
* @date 2020/11/18
*/
public class Main {

/**
* 买票过程,多个线程卖100张票
*/
public static class TicketDemo implements Runnable {
// 100张票
private int tickets = 100;
Object obj = new Object();

@Override
public void run() {
while (true) {
synchronized (obj) {
if (tickets > 0) {
try {
// 线程睡半秒
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " sale: " + tickets--);
}
}
}
}
}

public static void main(String[] args) {
TicketDemo ticketDemo = new TicketDemo();
// 启动三个线程出售100张票
Thread thread1 = new Thread(ticketDemo);
Thread thread2 = new Thread(ticketDemo);
Thread thread3 = new Thread(ticketDemo);
thread1.start();
thread2.start();
thread3.start();
}
}

执行结果:

image-20230908102813457

观察结果,买票的号都是顺序的,没有出现重复的情况了,但是观察执行过程,会发现是执行完一条才会在执行下一条,是同步的过程。

虽然实现了线程安全了,这也会带来很严重的性能问题

  • 每个条执行都得等待上一条执行完成才可以完成对应的操作

这就导致了:我们完成了一个小小的功能,使用了多线程的目的是想要提高效率,但现在没有把握得当,却带来严重的性能问题!

在使用多线程的时候:更严重的时候还有死锁(程序卡主不动了)。

对象的发布与逸出

定义发布和逸出:

  • 发布(publish)使对象能够在当前作用域之外的代码中使用
  • 逸出(escape)当某个不应该发布的对象被发布了

2.1常见的逸出

常见逸出的有下面几种方式:

  • 静态逸出
  • public修改的get方法
  • 方法参数传递
  • 隐式的this
静态逸出

image-20230908102842131

public修饰get方法

image-20230908102903803

方法参数传递

因为把对象传递过去给另外的方法,已经是逸出了~

this逸出

image-20230908102931729

逸出就是本不应该发布对象的地方,把对象发布了。导致我们的数据泄露出去了,这就造成了一个安全隐患

2.2安全发布对象

安全发布对象有几种常见的方式:

  • 在静态域中直接初始化public static Person = new Person()
    • 静态初始化由JVM在类的初始化阶段就执行了,JVM内部存在着同步机制,致使这种方式我们可以安全发布对象
  • 对应的引用保存到到volatile或者AtomicReferance引用中
    • 保证了该对象的引用的可见性和原子性
  • 由final修饰
    • 该对象是不可变,那么线程就一定是安全的,所以是安全发布
  • 由锁来保护
    • 发布和使用的时候都需要加锁,这样才保证能够该对象不会逸出

解决多线程遇到的问题

3.1简述解决线程安全性的办法

使用多线程就一定要保证我们的线程是安全的,这只最重要的地方!

在Java中,一般会有下面这么几种办法来实现线程安全问题:

  • 无状态(没有共享变量)
  • final使该引用变量不可变(如果该对象引用也引用了其他的对象,那么无论是发布或者使用时都需要加锁)
  • 加锁(内置锁,显示Lock锁)
  • 使用JDK提供的类来实现线程安全
    • 原子性(例如:AtomicLong来实现原子性…)
    • 容器(ConcurrentHashMap等等…)
    • ……
  • ……等等

3.2原子性和可见性

3.2.1原子性

在多线程中很多时候都是因为某个操作不是原子性的,使数据混乱出错。如果操作的数据是原子性的,那么就可以很大程度上避免了线程安全问题了!

例如:

  • count++,先读取,后自增,在赋值。这是三个步骤,如果该操作是原子性的,那么就可以说是线程安全了(会没有中间的三步操作,一步到位【原子性】)

原子性就是执行某一操作是不可分割

  • count++操作,就不是一个原子性的操作,它是分成了三个步骤来实现这个操作的
  • JDK中atomic包提供实现原子性操作

image-20230908102953879

也有人将其做成表格来分类:

类型 Integer Long
基本类型 AtomicInteger AtomicLong AtomicBoolean
数组类型 AtomicIntegerArray AtomicLongArray AtomicReferenceArray
属性原子修改器 AtomicIntegerFieldUpdater AtomicLongFieldUpdater AtomicReferenceFieldUpdater
3.2.2可见性

可见性,Java提供了一个关键字:volatile

  • 可以简单认为:volatile是一种轻量级的同步机制

volatile经典总结:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性

将其拆开解释一下:

  • 保证该变量对所有线程的可见性
    • 在多线程的环境下:当这个变量修改时,所有的线程都会知道变量被修改了,也就是所谓的“可见性”
  • 不保证原子性
    • 修改变量(赋值)实质上是在JVM中分了好几步,而在这几步内(从装载变量到修改),它是不安全的

使用了volatile修饰的变量保证了三点

  • 一旦完成写入,任何访问这个字段的线程将会得到最新的值
  • 在写入之前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存
  • volatile可以防止重排序(重排序指的就是:程序执行的时候,CPU、编译器可能会对执行顺序做一些调整,导致执行顺序并不是从上往下的。从而出现了一些意想不到的效果)。而如果声明了volatile,那么CPU、编译器就会知道这个变量是共享的,不会被缓存寄存器或者其他不可见的地方。

一般来说,volatile大多用于标志位上(判断操作),满足下面的条件才应该使用volatile修饰变量:

  • 修改变量时不依赖变量的当前值(因为volatile是不保证原子性的)

  • 该变量不会纳入到不变性条件中(该变量是可变的)

  • 在访问变量的时候不需要加锁(加锁就没必要使用volatile这种轻量级同步机制了)

3.3线程封闭

在多线程的环境下,只要不使用成员变量(不共享数据),那么就不会出现线程安全的问题了。

如下的例子,所有的数据都是方法(栈封闭)上操作的,每个线程都拥有自己的变量,互不干扰!

image-20230908103026751

在方法上操作,只要保证不要在栈(方法)上发布对象(每个变量的作用域仅仅停留在当前的方法上),那么我们的线程就是安全的。

线程封闭的另一个方法:ThreadLocal,使用这个类的API就可以保证每个线程自己独占一个变量。(ThreadLocal会单独介绍……)

3.4不变性

不可变对象线程一定安全。

共享的变量都是可变的,正由于是可变的才会出现线程安全问题。如果该状态是不可变的,那么随便多个线程访问都是没有问题的!

Java提供了final修饰符给我们使用,final的身影我们可能见得比较多,但值得说明的是:

  • final仅仅是不能修改该变量的引用,但是引用里面的数据是可以修改的!

就好像下面的HashMap,用final修饰了。但是它仅仅保证了该对象引用hashMap变量所指向是不可变的,但是hashMap内部的数据是可变的,也就是说:可以add,remove等等操作到集合中~

  • 因此,仅仅只能够说明hashMap是一个不可变的对象引用

    1
    final HashMap<Person> hashMap = new HashMap();

不可变的对象引用在使用的时候,还是需要加锁

  • 或者把Person也设计成一个线程安全的类
  • 因为内部的状态是可变的,不加锁或者Person不是线程安全类,操作都是有危险的!

想要将对象设计成不可变对象,那么要满足下面三个条件:

  • 对象创建后状态就不能修改
  • 对象所有的域都是final修饰的
  • 对象是正确创建的(没有this引用逸出)

String在我们学习的过程中我们就知道它是⼀个不可变对象,但是它没有遵循第⼆点(对象所有的域都是final修饰的),因为JVM在内部做了优化的。但是我们如果是要⾃⼰设计不可变对象,是需要满⾜三个条件的。

image-20230908103051645

3.5线程安全性委托

很多时候我们要实现线程安全未必就需要自己加锁,自己来设计

可以使用JDK给我们提供的对象来完成线程安全的设计:

image-20230908103129601