第19章 多线程
1. 多线程的概念
多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。
在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。
2.线程的状态和生命周期
操作系统中有“进程”的概念,可以理解为同一时间可以在计算机中同时运行多个程序,比如同时听歌和上网。一个进程中又包含多个同时执行的任务,这被称为“线程”,比如使用微信聊天时可以在发送消息的同时接收消息。
单核CPU在同一时间只能执行一个进程中的一个线程,我们看似这些任务同时执行的原因是CPU将执行时间换成了一块一块的“时间片”,时间片的时长很短,当执行完一段时间片后CPU执行下一次时间片,这些进程在不断的竞争时间片的使用权,所以看似就是同时运行了。
一个线程中的代码在执行过程中大致有如下状态
- 开始状态
- 就绪状态
- 运行状态
- 死亡状态
- 阻塞状态
- 等待状态
其关系和之间的状态切换如下:

3. Java中实现多线程的方式
- 实现java.lang.Runnable接口,并实现它的run方法
public class MyThread1 implements Runnable {
@Override
public void run() {
int count = 0;
while(count<10) {
count++;
System.out.println("Thread1:"+count);
}
}
}
- 继承java.lang.Thread类,并重写它的run方法
Thread类本身实现了Runnable接口
public class MyThread2 extends Thread {
@Override
public void run() {
int count = 0;
while(count<10) {
count++;
System.out.println("Thread2:"+count);
}
}
}
- 启动线程
如果直接通过调用run方法执行程序,相当于顺序执行,不是多线程的方式。
public class Test {
public static void main(String[] args) {
MyThread1 mt1 = new MyThread1(); //runnable
MyThread2 mt2 = new MyThread2(); //thread
//普通的方法调用,不是线程方式
mt1.run();
mt2.run();
}
}
运行结果:

无论多少次运行,结果都是自上而下,先执行mt1的run方法,再执行mt2的run方法
启动多线程方式执行程序必须通过Thread类中的start方法启动
(1)通过继承Thread类实现的线程对象,可以直接调用自身的start方法启动
(2)通过实现Runnable接口实现的线程对象,自身没有start方法,必须通过另一个Thread类对象加载自己后,使用Thread对象提供的start方法启动自己,参考代码如下:
public class Test {
public static void main(String[] args) {
MyThread1 mt1 = new MyThread1(); //runnable
MyThread2 mt2 = new MyThread2(); //thread
Thread t1 = new Thread(mt1); //使用t1对象加载mt1对象
t1.start(); //启动t1即代表启动mt1
mt2.start();
}
}
运行结果

每次执行的结果可能都不一致,因为两个线程对象在竞争CPU的时间片,谁抢到了谁执行,所以可以看做两个对象在“同时运行”
4.线程的结束
可以通过调用Thread类中的stop方法终止线程的执行,但不推荐这样做,因为很可能线程中的代码没有完整执行完毕被粗暴的结束,造成系统不稳定或者数据不完整。
推荐让线程执行完run方法中的所有内容后自然结束
5.线程的阻塞
当遇到阻塞事件时,线程会进入阻塞状态,只有阻塞状态结束后线程才能恢复,但需要回到就绪状态重新竞争。线程进入阻塞状态时不会放弃已经得到的CPU的资源,可以理解为“占着茅坑不拉屎”。
常见的阻塞事件:
- 监听网络连接
- 等待控制台输入
- 等待网络内容输入
- 休眠sleep (Thread.sleep(1000)表示程序休眠1000ms)
6.线程安全 - 车站售票
当多个线程访问同一数据时,由于时间片不能完成run方法的执行,造成数据在读取和写出时产生错乱。
比如车站卖票的程序:假设余票5张,三个窗口(线程)卖票,可能出现窗口一卖票3张,窗口二卖票2张,窗口三卖票2张。单独看每个窗口都是没有问题的,但实际总数上看5张票被卖了7次,数据出了问题。
车票类
public class Ticket {
public int count = 100; //火车票剩余100张
}
窗口类(线程)
public class TicketThread implements Runnable{
private Ticket t;
private String name;
public TicketThread(String name,Ticket t){
this.t = t;
this.name = name;
}
@Override
public void run() {
while(t.count > 0){
sell();
}
}
public void sell(){
if(t.count > 0){
System.out.println("NO."+t.count+"车票被"+name+"售出");
t.count--;
}
}
}
测试类
public class Test {
public static void main(String[] args) {
Ticket t = new Ticket();
TicketThread t1 = new TicketThread("1号窗口", t);
TicketThread t2 = new TicketThread("2号窗口", t);
TicketThread t3 = new TicketThread("3号窗口", t);
Thread tx1 = new Thread(t1);
Thread tx2 = new Thread(t2);
Thread tx3 = new Thread(t3);
tx1.start();
tx2.start();
tx3.start();
}
}
运行结果:

可以明显看到No.100这张票被卖了3次
出现这种情况的主要原因就是第一个线程读取了票号100,没等卖时时间片结束,第2个线程抢到时间片读取票号100...
解决数据问题的主要办法就是建立同步方法或同步块
同步:synchronized,在同步块中声明的资源一次只允许一个线程完整执行完毕后才允许其他线程持有执行。这样保证在第一个线程读取了票号100,没等卖时时间片结束,第2个线程抢到时间片后发现票号资源被一个线程占用,第2个线程阻塞等待直至时间片结束。直到第一个线程将同步块中代码全部执行完毕后,所有线程再次去竞争
同步块会带来线程安全:数据准确,但相应地会降低程序的运行效率。
上述代码的窗口类中加入同步代码块如下:
窗口类(线程)
public class TicketThread implements Runnable{
private Ticket t;
private String name;
public TicketThread(String name,Ticket t){
this.t = t;
this.name = name;
}
@Override
public void run() {
while(t.count > 0){
sell();
}
}
public void sell(){
synchronized (t) {
if(t.count > 0){
System.out.println("NO."+t.count+"车票被"+name+"售出");
t.count--;
}
}
}
}
再次运行测试类中的main方法发现不会出现票号错乱的情况
7.锁和死锁 - 两个面包师
执行同步块A中的代码时,其引用的资源t,对于其他线程是不可用的。这种情况也被称为资源加“锁”。所以在编写程序时可能出现以下情况
- 线程1:同步块中先占用了资源x,需要占用y才能继续执行,执行完毕后同时释放x和y的使用权
- 线程2:同步块中先占用了资源y,需要占用x才能继续执行,执行完毕后同时释放x和y的使用权
两个线程同时需要对方的资源但都执行了一半不能释放自己的资源,此种情况程序无法继续执行,形成了一种僵持的局面,这种情况被称为“死锁”。
示例:面包师制作蛋糕需要材料鸡蛋和面粉,两个面包师一个拿到鸡蛋一个拿到面粉,谁也不能完成面包的制作
资源类
public class Resource {
public String egg = "鸡蛋";
public String flour = "面粉";
}
面包师A
ublic class Cooker1 extends Thread{
private Resource rs;
public Cooker1(Resource rs){
this.rs = rs;
}
@Override
public void run() {
synchronized (rs.egg) {
System.out.println("厨师A获得了鸡蛋");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (rs.flour) {
System.out.println("厨师A获得了面粉");
System.out.println("厨师A制作了蛋糕");
}
}
}
}
面包师B
public class Cooker2 extends Thread {
private Resource rs;
public Cooker2(Resource rs){
this.rs = rs;
}
@Override
public void run() {
synchronized (rs.flour) {
System.out.println("厨师B获得了面粉");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (rs.egg) {
System.out.println("厨师B获得了鸡蛋");
System.out.println("厨师B制作了蛋糕");
}
}
}
}
测试类
public class Test {
public static void main(String[] args) {
Resource rs = new Resource();
Cooker1 c1 = new Cooker1(rs);
Cooker2 c2 = new Cooker2(rs);
c1.start();
c2.start();
}
}
运行结果:程序死锁
避免死锁的方法就是在设计程序资源占用时要合理占用资源,避免多层次同步嵌套占用资源
8.线程的协作 - 生产者与消费者
线程的协作除了互斥方式(即同步synchronized)还包含等待和唤醒。关于等待和唤醒这两个操作的方法是在java.lang.Object类中定义的
- wait() 是当前线程无法获得所需的资源,进入等待状态等待其他线程释放资源执行唤醒后再进行操作。进入等待状态前,线程会释放自己持有的所有资源。
- notify() 唤醒当前等待的线程执行操作
- notfiyAll() 唤醒所有等待的线程竞争执行操作,建议使用这个方法
需要注意的是:由于等待和唤醒操作与资源息息相关,故必须放在同步块或同步方法中执行
示例:生产者和消费者,1个调酒师每次随机生产1-3杯酒,1个客人每次随机喝掉1-3杯酒,吧台最多可以存放10杯酒。当吧台放满10杯酒后,调酒师不再调酒直至吧台不满10杯后再继续调酒。当吧台没有任何酒时,客人不再喝酒直至吧台有酒
酒吧类(测试类)
public class Bar {
public List<String> bar_tab = new ArrayList<String>();
public int max = 10;//吧台的容量
public int count = 0;//目前有酒的数量
//生成随机数的方法 与该问题无关
private static Random random = new Random();
public static int getRandomNumber(){
//返回[1,3]随机整数
return random.nextInt(3) + 1;
}
public static void main(String[] args) {
Bar b = new Bar();
//两个调酒师+两个顾客
Bartender t1 = new Bartender(b);
Bartender t2 = new Bartender(b);
Customer c1 = new Customer(b);
Customer c2 = new Customer(b);
Thread td1 = new Thread(t1);
Thread td2 = new Thread(c1);
Thread td3 = new Thread(t2);
Thread td4 = new Thread(c2);
td1.start();
td2.start();
td3.start();
td4.start();
}
}
调酒师类
public class Bartender implements Runnable{
private Bar bar = new Bar();
public Bartender(Bar bar){
this.bar = bar;
}
public void bartend(){
synchronized (bar) {
while(bar.count == bar.max){
try {
bar.wait();//让出bar的使用权
System.out.println("吧台已满,不能调酒");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
bar.notify();
int num = Bar.getRandomNumber();
if(bar.count + num > bar.max){
//调配出多余的酒
//向吧台上放酒
for(int i = 0; i < bar.max - bar.count; i++){
bar.bar_tab.add("酒");
}
bar.count = bar.max;
System.out.println("调酒师调配了"+(bar.max - bar.count)+"杯酒, 剩余:"+bar.count+"杯酒");
}else{
for(int i = 0; i < num; i++){
bar.bar_tab.add("酒");
}
bar.count += num;
System.out.println("调酒师调配了"+num+"杯酒, 剩余:"+bar.count+"杯酒");
}
}
}
public void run() {
while(true){
bartend();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
顾客类
public class Customer implements Runnable{
private Bar bar = new Bar();
public Customer(Bar bar){
this.bar = bar;
}
public void drink(){
synchronized (bar) {
int num = Bar.getRandomNumber();
while(num > bar.count){
try {
bar.wait();//让出bar的使用权
System.out.println("剩余酒的数量不足,剩余:"+bar.count+"杯, 需求:"+num+"杯");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
bar.notify();
for(int i = 0; i < num; i++){
bar.bar_tab.remove("酒");
}
bar.count -= num;
System.out.println("顾客消费了"+num+"杯酒, 剩余:"+bar.count+"杯酒");
}
}
public void run() {
while(true){
drink();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
这就是一个简单地生产者和消费者的例子