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

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

可以看到进程实现多处理机环境下的进程调度,分派,切换时, 都需要花费较⼤的时间和空间开销。
引⼊线程主要是为了提⾼系统的执⾏效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理。 使OS具有更好的并发性。
简单来说: 进程实现多处理⾮常耗费CPU的资源,⽽我们引⼊线程是作为调度和分派的基本单位(取代进程的部分基本功能【调度】 )。
线程在哪呢?例如:打开一个音乐播放器,既能播放音乐,点击音乐播放器点击控件还能响应

在同一个进程内可以执行多个任务,而这每一个任务都可以看出是一个线程。则一个进程会有一个或多个线程。
进程和线程
- 进程作为资源分配的基本单位
- 线程作为资源调度的基本单位,是程序的执行单元,执行路径(单线程:一条执行路径,多线程:多条执行路径)。是程序使用CPU的最基本单位。
线程有三个基本状态:执行、就绪、阻塞
线程有五种基本操作:派生、阻塞、激活、调度、结束
线程的属性:
- 轻型实体
- 独立调度和分派的基本单位
- 可并发执行
- 共享进程资源
线程有两个基本类型:
- 用户级线程:管理过程全部由⽤户程序完成, 操作系统内核⼼只对进程进⾏管理。
- 系统级线程(核心级线程):由操作系统内核进⾏管理。操作系统内核给应⽤程序提供相应的系统
调⽤和应⽤程序接⼝API,以使⽤户程序可以创建、执⾏以及撤消线程。

多线程的存在,不是提高程序的执行速度,是为了提供应用程序的使用率,程序的执行其实都是在抢CPU的资源和CPU的执行权。多个进程时在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。
并行与并发
- 并行
- 并⾏性是指同⼀时刻内发⽣两个或多个事件
- 并⾏是在不同实体上的多个事件
- 并发
- 并发性是指同⼀时间间隔内发⽣两个或多个事件
- 并发是在同⼀实体上的多个事件
并⾏是针对进程的, 并发是针对线程的
Java实现多线程
Java实现多线程是使用了Thread这个类,主要看下Thread类的解释(注释)
1 | /** |
创建多线程
- 继承Thread,重写run方法
- 实现Runnable接口,重写run方法
- 实现Callable接口,重写run方法
继承Thread,重写run方法
创建一个类,继承Thread,重写run方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package com.thread.create;
/**
* @author jingLv
* @date 2020/11/12
*/
public class MyTread extends Thread {
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
17package 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
15package com.thread.create;
/**
* @author jingLv
* @date 2020/11/12
*/
public class MyRunnable implements Runnable {
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
19package 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类中线程是如何命名的:

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

在查看一下init方法:

实现了Runnable的方式来实现多线程,打印线程名字
1
2
3
4
5
6
7
8
9
10
11
12
13
14package com.thread.create;
/**
* @author jingLv
* @date 2020/11/12
*/
public class MyRunnable implements Runnable {
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
22package 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());
}
}
执行结果

还可以通过setName(String name)的方法来改掉线程的名字。
守护线程
守护线程是为其他线程服务的,垃圾回收线程就是守护线程
守护线程有一个特点:
- 当别的用户线程执行完了,虚拟机就会退出,守护线程也就会被停止掉了。也就是说:守护线程作为一个服务线程,没有服务对象就没有必要继续运行了。
使用线程的时候要注意的地方:
- 在线程启动前设置为守护线程,方法是
setDaemon(boolean on) - 使用守护线程不要访问共享资源(数据库、文件等),因为它可能会在任何时候就挂掉了
- 守护线程中产生的新线程也是守护线程
实例
1 | package com.thread.create; |
线程1和主线程执⾏完了,我们的守护线程就不执⾏了(电脑性能足够好的可能测不出来)
注意:要在线程启动之前设置守护线程,如果启动了再设置守护线程,会抛出异常
1 | /** |
优先级线程
线程优先级高仅仅表示线程获取的CPU时间片的几率高,但这不是一个确定的因素!
线程的优先级的高度依赖于操作系统的,Windows和Linux就有所区别(Linux下优先级可能就被忽略了~)
Java提供的优先级默认是5,最低1,最高10
1 | /** |
优先级的实现
1 | /** |
setPriority0是一个本地(navite)的方法:
1 | private native void setPriority0(int newPriority); |
线程生命周期
之前介绍线程的线程有三个基本状态:执行、就绪、阻塞
查看如下图,Thread上有很多方法是用来切换线程状态的

sleep方法
调⽤sleep⽅法会进⼊计时等待状态,等时间到了, 进⼊的是就绪状态⽽并⾮是运⾏状态!
1 | /** |
加上Sleep后的状态图

yield方法
调用yield方法会先让别的线程执行,但是不确保真正让出
- 意思:我有空,可以的话,让你们先执行
1 | /** |
加上yield后的状态图

join方法
调⽤join⽅法,会等待该线程执⾏完毕后才执⾏别的线程
1 | /** |
查看join的具体实现

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

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

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

由于stop方法可以让一个线程A终止掉另一个线程B。
- 被终止的线程B会立即释放锁,这可能会让对象处于不一致的状态
- 线程A也不知道线程B什么时候能够被终⽌掉,万⼀线程B还处理运⾏计算阶段,线程A调⽤stop⽅
法将线程B终⽌,那就很⽆辜了
总而言之,stop方法太暴力了,不安全,所以被设置过时了。
一般使用的是interrupt来请求终止线程:
要注意的是:interrupt不会真正停止一个线程,它仅仅是给这个线程发了一个信号告诉它,它应该要结束了
也就是说:Java设计者实际上是想线程自己来终止,通过上面的信号,就可以判断处理什么业务了
具体到底中断还是继续运行,应该由被通知的线程自己处理
1 | Thread thread = new Thread(new Runnable() { |
再次说明:调⽤interrupt()并不是要真正终⽌掉当前线程,仅仅是设置了⼀个中断标志。这个中断标志可以给我们⽤来判断什么时候该⼲什么活!什么时候中断由我们⾃⼰来决定,这样就可以安全地终⽌线程了!
查看源码:
1 | /** |
查看抛出的异常:InterruptedException
1 | package java.lang; |
所以说: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 | package com.thread.create; |
执行结果:

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

观察结果,发现多个线程卖到同一张票,这就是线程安全问题。正确的情况是,票一共只有100张,三个线程卖出的票只能100张,也就是三个线程卖出的票号是不能有重复的。
如果说:当多个线程访问某个类时候,这个类始终能表现出正确的行为,那么这个类就是线程安全的!
有个原则:能使用JDK提供的线程安全机制,就使用JDK的。
1.2性能问题
使用多线程我们的目的就是为了提高应用程序的使用率,但是如果多线程的代码没有设计好的话,那未必会提高效率。反而降低了效率,甚至会造成死锁!
还是以上面的线程安全问题的例子,这个例子线程是不安全的,现在要将该例子优化为线程安全,最简单的方式:使用JDK提供的内置锁synchronized
1 | package com.thread.create; |
执行结果:

观察结果,买票的号都是顺序的,没有出现重复的情况了,但是观察执行过程,会发现是执行完一条才会在执行下一条,是同步的过程。
虽然实现了线程安全了,这也会带来很严重的性能问题:
- 每个条执行都得等待上一条执行完成才可以完成对应的操作
这就导致了:我们完成了一个小小的功能,使用了多线程的目的是想要提高效率,但现在没有把握得当,却带来严重的性能问题!
在使用多线程的时候:更严重的时候还有死锁(程序卡主不动了)。
对象的发布与逸出
定义发布和逸出:
- 发布(publish)使对象能够在当前作用域之外的代码中使用
- 逸出(escape)当某个不应该发布的对象被发布了
2.1常见的逸出
常见逸出的有下面几种方式:
- 静态逸出
- public修改的get方法
- 方法参数传递
- 隐式的this
静态逸出

public修饰get方法

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

逸出就是本不应该发布对象的地方,把对象发布了。导致我们的数据泄露出去了,这就造成了一个安全隐患!
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包提供实现原子性操作

也有人将其做成表格来分类:
| 类型 | 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线程封闭
在多线程的环境下,只要不使用成员变量(不共享数据),那么就不会出现线程安全的问题了。
如下的例子,所有的数据都是方法(栈封闭)上操作的,每个线程都拥有自己的变量,互不干扰!

在方法上操作,只要保证不要在栈(方法)上发布对象(每个变量的作用域仅仅停留在当前的方法上),那么我们的线程就是安全的。
线程封闭的另一个方法: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在内部做了优化的。但是我们如果是要⾃⼰设计不可变对象,是需要满⾜三个条件的。

3.5线程安全性委托
很多时候我们要实现线程安全未必就需要自己加锁,自己来设计。
可以使用JDK给我们提供的对象来完成线程安全的设计:
