概述
- 进程:计算机中的程序关于某数据集合的一次运行活动,是系统进行资源分配和调度的基本单位。
- 线程:就是进程中的一个负责程序执行的控制单元(执行路径)。
- 一个进程中可以有多个执行路径,称之为多线程。
- 一个进程中至少要有一个线程
- 开启多个线程是为了同时运行多部分代码
- 每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务
利弊
- 多线程操作,可以让多个任务并行执行,a任务等待硬盘响应时,让b任务去用cpu,所以,多线程不能提高单任务的运行速度,但是可以提高多个可以并发的任务速度。
- 好处:解决多部分同时运行的问题
- 弊端:线程过多会导致效率的降低
JVM多线程分析
- JVM启动时就启动了多个线程,至少有两个可以分析出来
- 1.执行main函数的线程
- 该线程的任务代码都定义在main函数中
- 2.负责垃圾回收的线程
创建线程
创建方式一
- 继承 Thread 类
- 1、定义一个类继承 Thread 类
- 2、必须覆盖 Thread 类的 run 方法
- 3、直接创建 Thread 的子类对象创建线程
- 4、调用 start 方法开启线程并调用线程的任务 run 方法执行
1 | /* |
- 在上述代码的结尾,调用run和start函数有什么区别?
- 调用start函数是多线程的,调用run函数还是按顺序主线程进行的
线程名称
- 想要展示出当前运行对象线程名称需要使用Thread类的currentThread方法
1 | public class Demo extends Thread{ |
- 运行后可以试着4.5行注释掉,6行解除注释试一下,发现可以自己给线程名称赋值了
图解
- 开启了多线程后,栈内存就和之前不一样了,以前面代码为示例

- 如果在上文倒数第三行加上一句代码“System.out.println(4/0);”
- 观察运行结果,发现main函数已经抛出异常终止了,其余两线程仍能正常进行
线程的状态

创建方式二
- 实现Runnable接口
- 1、定义类实现Runnable接口
- 2、覆盖接口中的run方法,将编程的任务代码封装到run方法中
- 3、通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递
- 4、调用线程对象的start方法开启线程
- 第三步的原因
- 因为线程的任务都封装在Runnable接口子类对象的run方法中,所以要在线程对象创建时就必须明确要运行的任务
1 | public class Demo implements Runnable{ |
方式二细节
方式二优点
- 实现Runnable接口的好处
- 1、将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成了对象
- 2、避免了Java单继承的局限性
- 所以创建线程的第二种方式比较常用
卖票示例
- 在火车站一共有100张票,4个窗口同时卖票,用多线程来描述

- 运行图中左侧代码后发现,并不是想象中的卖100张,而是变成了4*100张,可以选择将ticket类中的num值变成静态变量,但这不是最好的选择。
- 另外还有人说可以不在堆中创建那么多的对象,只创建一个对象,进行4次线程开启,这是不可取的,多次启动一个线程是非法的。
1 | class Ticket implements Runnable{ |
- 通过继承的方式创建没有达到我们的目的,于是就采用上述代码中接口的方式创建
线程安全隐患

- 以车站卖票为示例,如果当车票只剩一张时,t1进入临时堵塞状态,有执行资格不具备执行权,推给t2执行,t2也临时堵塞一下,继续往后推,然后假设t3,t4也是临时堵塞,那他们现在都具备执行资格,没有执行权,然后t1卖出一张票,票数为0,但是t2.3.4都已经进入循环,可以卖票,于是分别卖出了第0.-1.-2张票。这对于铁路局来说是不允许的,有安全隐患。
有人说为什么我运行很多次都没有出现这种错误那,因为这只是有一定可能发生的,为了看起来更清晰,可以人为的方式,使产生安全隐患概率增加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Ticket implements Runnable{
private int num = 100;
public void run(){
while(true){
if(num>0){
try{
Thread.sleep(10);
}catch(InterruptedException){
//解决异常代码
}
System.out.println(num--);
}
}
}
}
}将ticket类改成这样,在运行主类,发现会出现之前所说的安全隐患
线程安全问题产生原因
- 1、多个线程在操作共享数据
- 2、操作共享数据的线程代码有多条
- 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生
解决思路(同步)
- 就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算的,必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算
- 在Java中用同步代码块就可以解决这个问题
1 | 同步代码块格式: |
所以更改代码后变为这样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Ticket implements Runnable{
private int num = 100;
Object obj = new Object();
public void run(){
while(true){
synchronized(obj){
if(num>0){
try{
Thread.sleep(10);
}catch(InterruptedException){
//解决异常代码
}
System.out.println(num--);
}
}
}
}
}这次再去运行一下发现不会有问题
这很像锁,可以比喻成火车上的卫生间,一个人进去后,门锁上,门上有提示“有人”,别人就进不来,只有里面的人出去才可以。
同步的好处:解决了线程的安全问题
- 同步的弊端:相对降低了效率,因为同步外的线程都会判断同步锁
同步前提
同步的前提:同步中必须有多个线程并使用同一个锁
1
2
3
4
5
6
7
8
9
10
11private int num = 100;
public void run(){
Object obj = new Object();
//注意:上面这句代码的位置和之前不同
while(true){
synchronized(obj){
//代码
}
}
}
}运行上面的代码后发现并没有解决安全问题,是因为这样写相当于有四个锁,把二三行代码换位置即可
同步函数
- 需求:两个储户去银行存款,每次100,存三次
1 | public class Bank { |
- “多个线程在操作共享数据(sum),操作共享数据代码有多条(不算trycatch两条)”这两点都符合,所以有线程安全隐患
- 像上面这种情况,可以使用同步代码块,不过还有更简洁的方式就是在函数中加上同步关键字
验证同步函数锁
- 同步函数锁固定是this,同步代码块锁是任意的对象
- 开发中建议使用同步代码块
- 下面是验证时的代码(以双线程卖票为示例)
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
50public class Ticket implements Runnable {
private int num = 100;
boolean flag = true;
public void run() {
if (flag) {
while (true) {
synchronized (this) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "..emmm.." + num--);
}
}
}
}
else {
while (true)
show();
}
}
public synchronized void show() {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "......" + num--);
}
}
}
public class SynFunctionLockDemo {
public static void main(String[] args){
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.flag = false;
t2.start();
}
}
验证静态同步函数锁
- 静态的同步函数使用的锁是 该函数所属的字节码文件对象
- 可以用getClass方法获取,也可以用 当前类名.class 表示
- 验证的代码和上面的大同小异,将this改为this.getClass或者Ticket.class再试试
单例模式涉及的多线程问题
安全隐患分析
- 会出现安全隐患的是懒汉式,也就是延迟加载单例模式
1 | public class Single2 { |
- 仔细想一下,他符合了会出现隐患的两个因素,线程0在进行(s==null)判断后,进入临时堵塞状态,然后线程1进行null的判断,进入临时堵塞状态。这时线程0创建了s对象,返回s,线程1又创建了一个对象,现在就有两个对象,不能保证唯一了,这就出现大问题了。
解决方法
- 加同步代码块解决安全问题,外面再加一次判断是解决效率问题
1 | public class Single2 { |
死锁
- 常见情景之一:同步的嵌套
1 |
- run函数里拿着obj的锁想进this的锁,show函数里拿着this锁想进obj
简单死锁代码
1 | public class MyLock { |
运行上面的代码,观察结果,线程1拿到了B锁,线程2拿到了A锁,他们都想继续,可是互不相让,死锁了
1
2Thread-1 else LockB
Thread-0 if LockA如果试了多次都是没有成功死锁,那就在 if语句 及 else语句 后面各加一句 while(true)
线程间通信
- 多个线程在处理资源,但是任务却不同
- 同步前提:多个线程使用同一个锁
- 接下来用一个例子演示一下
- 需求:有一个资源库存放数据,一条线程在向里面输入,还有一条在向外输出
1 | //资源类 |
- 上面的代码中输入设置的是输入两个人名,需要注意的地方是,输入和输出类中加的锁是一样的,都是r,这个在测试类中,创建in、out对象时,要给他们传一样的参数。否则同步锁无用!
- 但是这样的代码运行后还是不太好,因为输出的都是大片相同的人名,想让他们输入一个就输出一个,就需要使用线程的等待唤醒机制了。
等待唤醒机制
- 涉及方法
- wait():让线程处于冻结状态,被wait的线程会被存储到线程池中
- notify():唤醒线程池中的一个线程(任意)
- nitifyAll():唤醒线程池中的所有线程
这些方法都必须定义在同步中,因为这些方法是用于操作线程状态的方法,必须要明确到底操作的是哪个锁上的线程
观察api文档后,为什么操作线程的方法wait、notify、notifyAll定义在Object类中那?
- 因为这些方法是监视器的方法,监视器其实就是锁,锁可以是任意的对象,任意的对象调用的方式一定定义在Object类中
1 | //资源类 |
- 思路:输入数据之前判断一下是否有数据,如果有数据,控制输入的线程等待,先让控制输出的线程输出,输出数据后,将flag更改为false,意味着资源中数据为空,可以继续输入,然后再将控制输入的线程唤醒,自己(控制输出的线程)等待。这样就可以实现输入一个数据再输出一个数据了。
- 注意:输入输出的锁都是 r,等待唤醒中的线程池都是基于这个锁(也就是 r)
代码优化
上面的代码虽然最后实现了功能,但是封装性差,我们可以把输入,输出的方法定义在资源类中,在输入输出类中调用输入输出的方法
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//资源类
public class Resource {
private String name;
private String sex;
private boolean flag = false;
//输入方法
public synchronized void set(String name,String sex){
if (this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
this.sex = sex;
this.flag = true;
this.notify();
}
//输出方法
public synchronized void out(){
if (!this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("name:" + name + "..." + "sex:" + sex);
this.flag = false;
this.notify();
}
}
//输入类
public class Input implements Runnable {
private Resource r;
public Input(Resource r){
this.r = r;
}
@Override
public void run() {
int x = 0;
while(true) {
if (x == 0) {
r.set("小白","女女女" );
} else {
r.set("black", "malemale");
}
x = (++x) % 2;
}
}
}
//输出类
public class Output implements Runnable {
private Resource r;
public Output(Resource r){
this.r = r;
}
@Override
public void run() {
while (true) {
r.out();
}
}
}对共享资源修改的语句都写在了资源类中,可以给他们加同步锁,但是还是在方法中加上同步关键字看上去更简洁,这样他们加的锁还是同一个锁—> this 。
生产者消费者问题
- 假设生产者生产产品,消费者消费产品,和上面进出资源的问题差不多
问题一
- 这里直接开始4个线程的,两个生产者,两个消费者,运行一下,会发现有错误,比如一个生产者生产完一件产品,消费者连着两次消费这一件产品。这类问题产生的原因是什么那?
- t0、t1是生产者 t2、t3是消费者,让我们按照代码的顺序走一遍。
- t0进行判断,生产产品(1),t0等待,t1进行判断,t1等待。t2进行判断,消费产品(1),t2等待,t3进行判断,t3等待。唤醒t0,t0没有进行判断,生产产品(2),t0等待。然后需要唤醒一个线程,但是这时线程池中有三个在等待,如果唤醒的是t1,t1没有进行判断直接生产产品(3),t1等待,唤醒t2,t2消费了产品(3),如果再唤醒t3,t3没有进行判断,就会再消费产品(3)
- 问题1产生原因:if判断标记,只有一次,会导致不该运行的线程运行了,出现了数据错误的情况
- 解决方法:将资源类中的输入输出方法的 if 判断改为 while 循环,这样就不会唤醒后跳过判断了,不过这样又会产生一个新的问题
问题二
- 问题2:4个线程都处在等待状态,没人去唤醒他们
- 产生原因:notify只能唤醒一个线程,如果本方唤醒本方,无意义,而且while+notify会导致死锁
- 解决办法:将资源类中的输入输出方法的唤醒改为全部唤醒(notifyAll)
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//这里就只贴上资源类的代码了,其他类代码的写法和上面差不多
//还需要有生产者类,消费者类 实现Runnable接口,和Test类
//资源类
public class Resource {
private String name;
private int count = 1;
private boolean flag = false;
public synchronized void set(String name){
while (this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.name = name + count;
count++;
System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
this.flag = true;
this.notifyAll();
}
public synchronized void out(){
while (!this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
this.flag = false;
this.notifyAll();
}
}
问题三
- 解决了第二个问题后,已经不会报错了,但是还有些小弊端,使用notifyAll方法的时候会唤醒所有等待线程,但是如果唤醒了本方等待线程会降低效率,能不能只唤醒对方线程那?
- 解决方法:使用jdk1.5新特性中的Lock Condition,可以创建多个监视器,该问题中,创建两个即可。关于Lock,Condition可以看该文章偏靠后的位置
1 | import java.util.concurrent.locks.Condition; |
locks包
Lock接口
- java.util.concurrent.locks.Lock
- Lock替代了同步代码块或者同步函数,将同步的隐式锁操作变成显式锁操作,同时更为灵活,可以一个锁加上多个监视器。
- 方法:
- lock():获取锁
- unlock():释放锁,通常需要定义在finally代码块中
1 | //同步代码块对于锁的操作是隐式的 |
1 | //同步和锁被封装成了对象,并将操作锁的隐式方式定义到了该对象中 |
- 如果上面代码第六行位置会抛异常的话,一定要将释放锁写在finally里
1
2
3
4
5
6
7
8
9Lock lock = new ReentrantLock();
void show(){
lock.lock();//获取锁
try{
...code...
}finally{
lock.unlock();//释放锁
}
}
Condition接口
- Condition接口出现替代Object中的wait notify notifyAll等方法,将他们单独进行了封装,变成了Condition监视器对象,可以与任意锁进行组合
- 方法:
- await():等待
- signal():唤醒
- signalAll():全部唤醒
1 | //旧版功能演示 |
范例
1 | import java.util.concurrent.locks.Condition; |
- 这是一个处于多线程工作环境下的缓存区,缓存区提供了两个方法,put和take,put是存数据,take是取数据,内部有个缓存队列,具体变量和方法说明见代码,这个缓存区类实现的功能:有多个线程往里面存数据和从里面取数据,其缓存队列(先进先出后进后出)能缓存的最大数值是100,多个线程间是互斥的,当缓存队列中存储的值达到100时,将写线程阻塞,并唤醒读线程,当缓存队列中存储的值为0时,将读线程阻塞,并唤醒写线程,这也是ArrayBlockingQueue的内部实现。
- 下面分析一下代码的执行过程:
- 1、一个写线程执行,调用put方法;
- 2、判断count是否为100,显然没有100;
- 3、继续执行,存入值;
- 4、判断当前写入的索引位置++后,是否和100相等,相等将写入索引值变为0,并将count+1;
- 5、仅唤醒读线程阻塞队列中的一个;
- 6、一个读线程执行,调用take方法;
- 7、……
- 8、仅唤醒写线程阻塞队列中的一个
- 如果不用多个 Condition ,只有一个Condition或者使用的同步代码块的内容时,当存队列中已经存满,这个Lock不知道唤醒的是读线程还是写线程了,如果唤醒的是读线程,皆大欢喜,如果唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了很多时间。
线程终止
wait和sleep区别
- wait可指定也可不指定时间,sleep必须指定时间
- 同步中时,对cpu的执行权和锁的处理不同
- wait:释放执行权,释放锁
- sleep:释放执行权,不释放锁
终止线程方式
- 停止线程:
- stop方法:不推荐使用
- run方法结束
- 怎么控制线程中的任务结束那?
- 任务中都会有循环结构,只要控制住循环就可以结束任务,控制循环通常就用定义标记来完成
1 | //通过在主函数中调用setFlag方法,即可终止线程 |
但是使用这种方法也有一定的弊端,如果线程处于冻结状态无法读取标记,程序就不能停下来了
1
2
3
4
5
6
7
8
9
10
11//这样的程序无法终止
public synchronized void run(){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
while(flag){
System.out.println(Thread.currentThread().getName()+" run");
}
}可以使用interrupt()方法将线程从冻结状态强制恢复到运行状态中来,让线程具备cpu的执行资格,但是强制动作会发生InterruptedException异常,记得要处理
1 | public class StopThread implements Runnable{ |
守护线程
- setDaemon可以将线程变为守护线程(后台线程)
- 后台线程在运行时与前台线程无差别,前台线程终止时必须手动终止,如果前台线程都终止了,后台线程自动终止
线程小方法
join
- join方法也会抛出中断异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14//有线程t0、t1
t0.start();
t0.join();
t1.start();
System.out.println(" ");
//运行结果一定是t0线程运行完,
//运行main线程,再运行t1线程
//情况2
t0.start();
t1.start();
t0.join();
System.out.println(" ");
//运行结果:t0,t1互相抢占cpu,main等t0执行完再执行
线程优先值
- 可以给线程分配不同优先级的值,会让他们在执行中被执行几率增加
1 | //t0为线程名 |
两道多线程面试题
- 判断下面的程序是否能成功运行,如果不能,报错在哪一行
1 | public class Test implements Runnable{ |
- 判断下面程序,运行结果是什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class ThreadTest {
public static void main(String[] main){
new Thread(new Runnable() {
public void run() {
System.out.println("我是任务里面的 ");
}
}){
public void run() {
System.out.println("我是子类里面的 ");
}
}.start();
}
}
//现在的运行结果是“我是子类里面的”
//如果注释掉8.9.10行,结果是“我是任务里面的”