黑马程序员-Java高级:多线程

时间:2023-02-19 18:29:23

——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-

多线程总结


一、概述
进程:进程是指正在运行中的程序,当打开系统的资源管理器时,会发现很多进程,这些都是在计算机中正在运行中的程序。进程是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。
线程:线程是指进程中的一条执行路径
1.单线程:如果一个进程只有一条执行路径,那么这个程序就会沿着这条执行路径,顺序执行,知道结束,这就是单线程程序。
2.多线程:如果一个进程有多条执行路径,那么程序会在这多条执行路径之间并行执行。
举例:例如迅雷,360等软件都是多线程程序
Java程序的运行原理图:
黑马程序员-Java高级:多线程
注意:Java程序在运行时至少会启动两个线程,一个是main线程,用于执行主方法中的语句,另一个是垃圾回收器线程,用于对垃圾对象的内存进行回收。

二、多线程的实现方案
多线程的实现方案主要有两种:
继承Thread类
实现Runnable接口
1.继承Thread类
Thread类是Java中用于创建并开启线程的类,由于线程的作用在于执行某一段代码,因此要创建一个线程,首先要明确这个线程执行的代码是什么,在查阅API文档对Thread类的描述时,知道run方法用于明确线程要执行的代码,因此创建一个线程的步骤如下:
<1>自定义类MyThread继承Thread类
<2>在MyThread类中重写run()方法
<3>创建线程对象
<4>启动线程

Thread类常用方法介绍:
构造方法:

Thread()
Thread(Runnable target)
Thread(Runnable target, String name)
Thread(String name)

成员方法:

public final String getName() 获取线程名称
static Thread currentThread() 获取当前运行的线程

示例代码:

public class MyThread extends Thread {
@Override
public void run() {
int i = 0;
//为了更好的看到效果,这里写一个死循环
while(true) {
System.out.println("MyThread is running---" + i++);
}
}
}
//测试类
public class ThreadDemo {
public static void main(String[] args) {
//创建MyThread类
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
//启动线程
thread1.start();
thread2.start();
}
}

创建线程的几个注意事项
<1>重写run()方法的意义:因为每一个线程都是为了执行某一段特定代码而存在的,因此run()方法就是线程应该执行的代码。
<2>如何启动线程:启动线程应该调用Thread类中的start()方法,而不是调用run()方法
<3>线程不能多次启动:当试图多次启动一个线程时,会报出运行时异常:
IllegalThreadStateException:不合法的线程状态
<4>注意run和start的区别:run方法代表线程执行的代码,start()方法用于启动一个线程。

线程名称的获取与设置
获取一个线程的名称有以下几种方式:
<1>通过线程对象调用getName()方法
<2>通过调用Thread类的静态方法获取当前正在执行的线程对象,然后通过该对象调用getName()方法
例如:获取主线程的名称:
String name = Thread.currentThread().getName();
设置一个线程的名称有以下几种方式:
<1>创建对象时,通过构造方法初始化线程名称
Thread t = new Thread("threadName");
<2>通过setName()方法手动修改线程名称

2.实现Runnable接口
Runnable接口中定义了用于执行线程代码的run()方法,因此通过实现该接口,再通过Thread类的构造方法,就可以创建线程。
步骤如下:
<1>自定义一个类实现Runnable接口,重写run()方法
<2>创建Runnable接口的具体实例
<3>调用Thread类的构造方法:Thread(Runnable target)Thread(Runnable target, String name),创建一个线程
<4>调用start()方法启动线程
注意:为了简化代码,也可以使用匿名内部类方式。
代码示例:
此处采用匿名内部类方式实现:

Thread myThread = new Thread(new Runnable() {
@Override
public void run() {
......
//线程执行代码
......
}
});
//启动线程
myThread.start();

实现Runnable接口创建线程的好处:
<1>可以避免由于Java单继承特点带来的局限性
<2>将执行代码和数据与线程对象分离,体现了面向对象的思想
<3>可以用多个线程操作同一段代码,实现多线程操作共享资源。

三、线程调度
线程调度是指CPU执行线程的方式,假设一台计算机只有一个CPU,那么CPU在某一时刻只能执行一条指令,那么线程只有在获取CPU执行权限的时候才可以被CPU执行,那么CPU是如何选择何时执行哪个线程的呢?
下面有两种线程调度方式:
<1>分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
<2>抢占式调度模型:给线程定义优先级,让优先级高的线程先获取CPU的执行权,获取较长的执行时间,如果线程的优先级相同,则随机选择线程执行。
注:Java中采用抢占式调度模型
设置和获取优先级的方法(优先级范围:1-10,默认为:5):
public final void setPriority(int newPriority)
public final int getPriority()

四、线程控制
在Thread类中提供了几种方法对线程的执行进行控制,具体如下:
1.线程休眠:
public static void sleep(long millis) throws InterruptedException
注意:该方法抛出一个编译期异常,应该在代码中进行处理
示例代码:

public class ThreadDemo {
public static void main(String[] args) {
Thread myThread = new Thread(new Runnable() {

@Override
public void run() {
try {
//记录线程休眠之前时刻的毫秒值
long beforeSleep = System.currentTimeMillis();
//让线程休眠1000毫秒
Thread.currentThread().sleep(1000);
//记录线程休眠之后时刻的毫秒值
long afterSleep = System.currentTimeMillis();
//打印线程休眠前后的时间差
System.out.println("myThread has slept for " + (afterSleep - beforeSleep) + " millis");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

myThread.start();
}
}

运行结果:
结果1:
myThread has slept for 1002 millis
结果2:
myThread has slept for 1000 millis
结果3:
myThread has slept for 1013 millis

上面这个程序目的在于演示sleep方法的作用,在线程的执行代码中调用sleep()方法使线程休眠1000毫秒,然后通过System类的currentTimeMillis()方法获取休眠前后的时间差,而有趣的是程序的运行结果出现了不同的情况,出现这种结果的原因在于当线程从休眠状态结束时不一定马上获得CPU的执行权,因此休眠前后的时间差会有微小的差异。

2.线程加入
public final void join() throws InterruptedException
等待调用该方法的线程结束,即将调用该方法的线程加入到当前执行的线程,若没有指定等待的时间,则直到调用该方法的线程结束,当前线程继续执行
示例代码:

//自定义线程
public class MyThread extends Thread {

MyThread(String name) {
super(name);
}

@Override
public void run() {
//线程执行代码:控制台打印5次: 当前进程名 + is running
for(int i=0; i<5; i++) {
System.out.println(this.getName() + " is running");
}
}
}
//测试类
public class JoinDemo {
public static void main(String[] args) {
MyThread thread1 = new MyThread("MyThread-1");
MyThread thread2 = new MyThread("MyThread-2");

thread1.start();
// try {
// thread1.join();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
thread2.start();
}
}

测试结果:
join语句部分被注释掉时:
MyThread-2 is running
MyThread-1 is running
MyThread-2 is running
MyThread-1 is running
MyThread-2 is running
MyThread-1 is running
MyThread-2 is running
MyThread-1 is running
MyThread-2 is running
MyThread-1 is running
加入join语句时:
MyThread-1 is running
MyThread-1 is running
MyThread-1 is running
MyThread-1 is running
MyThread-1 is running
MyThread-2 is running
MyThread-2 is running
MyThread-2 is running
MyThread-2 is running
MyThread-2 is running

由测试结果可以看出,不加入join语句时,线程1和线程2随机抢夺CPU执行权,加入join语句时,main线程先将join方法的调用者thread1执行完毕,然后执行thread2。

3.线程礼让
public static void yield()
示意线程调度器当前线程可以暂时放弃CPU的执行权,这时线程调度器可以根据CPU的实际运行情况选择是否将执行权给其他线程,也可以忽略这个示意。
可以想象这样一个情景:
线程A和线程B的run()方法中都调用了yield()
线程A和B都启动了,线程A对JVM说:我不急,让B先来。
线程B也对JVM说:我也不急,让A先来。
JVM说:既然都不急,那就随机来吧。

4.后台线程
public final void setDaemon(boolean on)
又叫守护线程,举一个简单的例子来说明什么是后台线程,假设JVM是我们的计算机,而后台线程就相当于操作系统自带的防火墙,当计算机启动时,防火墙在某一时刻也跟着启动了,而我们并没有感觉到它,它只是默默的在后台运行着,而当我们关闭计算机,后台线程也随着关闭了,我们同样也感觉不到它的关闭。
在Java程序中,当一个线程被更改为后台线程,那么JVM将不在关心它何时结束,当所有非后台线程都执行完毕之后,即便仍然有后台线程在运行,JVM也会正常退出。
代码示例:

public class DeamonThreadDemo {
public static void main(String[] args) {
//定义一个线程
Thread myThread = new Thread(new Runnable() {

@Override
public void run() {
try {
while(true) {
System.out.println("我是一个后台线程");
}
} catch (Exception e) {
System.out.println("我知道这里没有异常");
} finally {
System.out.println("我是后台线程的finally语句,JVM会不会执行我?");
}
}
});
//将myThread设置为后台线程
myThread.setDaemon(true);
myThread.start();
}

上面这个代码中后台线程myThread中的语句一句都没有执行,当main线程退出时,JVM也跟着退出了,此时后台线程还没有来得及运行。

5.中断线程
public void interrupt()
public void stop() 已过时,不建议使用
通过抛出异常的方式中断线程,当线程由于某些原因进入阻塞状态时,通过调用这个方法使线程抛出一个InterruptedException异常,从而终止线程或执行阻塞之后的其他代码。
代码示例:

import java.util.Date;

public class ThreadStop extends Thread {
@Override
public void run() {
System.out.println("开始执行:" + new Date());

// 线程休息10秒钟
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// e.printStackTrace();
System.out.println("线程被终止了");
}

System.out.println("结束执行:" + new Date());
}
}
//测试类
public class ThreadStopDemo {
public static void main(String[] args) {
ThreadStop ts = new ThreadStop();
ts.start();

// 超过三秒线程依然处于sleep()状态,就中断sleep()
try {
Thread.sleep(3000);
// ts.stop();
ts.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

运行结果:
开始执行:Fri Aug 14 23:48:08 CST 2015
线程被终止了
结束执行:Fri Aug 14 23:48:11 CST 2015

使用stop方法的执行结果:
开始执行:Fri Aug 14 23:49:30 CST 2015

从两种执行结果的对比可以知道,stop用于杀死线程,在sleep之后的代码都没有被执行,这显然是不合理的,如果sleep之后有一些重要的执行代码,那么就会使程序产生问题,因此不建议使用。

五、线程安全问题
1.经典案例:卖票问题
需求:某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。
分析:100张电影票为共享数据,3个售票窗口为3个线程,那么程序的关键在于让三个线程共同操作100张票。
首先定义一个卖票类,既然卖票的类要被多个线程操作,这里讲该类实现Runnable接口较为合适。
定义一个卖票类对象,让三个线程操作这个对象中的卖票代码,实现多线程卖票。
代码

public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;

@Override
public void run() {
while (true) {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + "正在出售第"
+ (tickets--) + "张票");
}
}
}
}
//卖票窗口主程序
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}

运行结果:
窗口2正在出售第100张票
窗口3正在出售第99张票
窗口3正在出售第97张票
窗口3正在出售第96张票
窗口3正在出售第95张票
………..
窗口3正在出售第8张票
窗口3正在出售第3张票
窗口3正在出售第2张票
窗口3正在出售第1张票
窗口1正在出售第4张票
窗口2正在出售第5张票

从上面程序的运行结果看,似乎没有什么问题,实现了3个窗口共同卖100张票的功能,但当我们对代码稍加修改,就会发现上面的代码是存在安全隐患的。
修改上面卖票类的代码如下:

@Override
public void run() {
while (true) {
// t1,t2,t3三个线程
// 这一次的tickets = 1;
if (tickets > 0) {
// 为了模拟更真实的场景,我们稍作休息
try {
Thread.sleep(100); //t1进来了并休息,t2进来了并休息,t3进来了并休息,
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第"+ (tickets--) + "张票");
//窗口1正在出售第1张票,tickets=0
//窗口2正在出售第0张票,tickets=-1
//窗口3正在出售第-1张票,tickets=-2
}
}
}

运行结果:
。。。。。。
窗口1正在出售第3张票
窗口3正在出售第2张票
窗口3正在出售第1张票
窗口1正在出售第0张票
窗口2正在出售第0张票

再次运行程序发现,本来只有100张票,最后卖出了两张0号的票,这就出现了线程安全问题,出现这种现象的原因在于线程执行代码时具有延迟性和随机性。

2.出现线程安全问题的前提:
<1>多线程环境
<2>存在共享数据
<3>有多条语句操作共享数据

3.线程安全问题的解决方案:
既然知道出现线程安全问题的前提条件,那么要解决线程安全问题就需要破坏这些前提中的一种,而前两个条件是由实际问题的需求而确定的,一般不能改变,因此就要考虑从第三个条件入手。
解决思路把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。
具体方式:
<1>同步代码块:将操作共享数据的语句放到同步代码块中,并为代码块加上同步锁。
注:同步锁可以是任意对象,但操作相同共享数据的同步锁必须相同,否则线程无法同步。
示例代码:

public class SellTicket implements Runnable {
// 定义100张票
private int tickets = 100;
//创建锁对象
private Object obj = new Object();

@Override
public void run() {
while (true) {
//将操作共享数据的代码加上同步锁对象
synchronized (obj) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(
Thread.currentThread().getName()
+ "正在出售第"
+ (tickets--) + "张票");
}
}
}
}
}
//测试类
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}

运行结果:
。。。。。。
窗口2正在出售第8张票
窗口3正在出售第7张票
窗口1正在出售第6张票
窗口1正在出售第5张票
窗口3正在出售第4张票
窗口2正在出售第3张票
窗口2正在出售第2张票
窗口2正在出售第1张票

经多次运行测试后,再没有出现线程安全问题。

<2>同步方法
将操作共享数据的代码封装成方法,然后将同步关键字synchronized放到方法声明上,就是同步方法
声明格式:

public synchronized void sellTicket() {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "正在出售第" + (tickets--) + "张票 ");
}
}
//在run方法中调用同步方法
public void run() {
while(true) {
sellTicket();
}
}

注意:成员方法的同步锁是this,即调用该方法的当前对象;
静态方法的同步锁是该静态方法所属类的字节码文件对象(反射知识)

使用哪一种方式解决同步问题?
同步代码块和同步方法都能解决线程同步问题,如果锁对象是this,就可以考虑使用同步方法。否则能使用同步代码块的尽量使用同步代码块。

<3>.JDK5以后可以使用Lock锁解决同步问题:
调用void lock()
需要同步的代码
调用void unlock()

六、线程间通信
1.死锁问题
死锁问题是指多个线程在执行过程中由于争夺资源而产生的相互等待的现象。
线程死锁示例:

public class DieLockThread implements Runnable{

private boolean flag;
//定义两个不同的锁A,B
private Object lockA = new Object();
private Object lockB = new Object();

@Override
public void run() {
while(true){
//通过标志位让线程每次进入不同的语句块
if(flag) {
//线程请求获取A锁
synchronized (lockA) {
System.out.println("I have lockA.");
//线程在获取A锁之后,请求获取B锁,但仍占有A锁
synchronized (lockB) {
System.out.println("I have lockB.");
}
}
flag = false;
}else {
//线程请求获取B锁
synchronized (lockB) {
System.out.println("I have lockB.");
//线程在获取B锁之后,请求获取A锁,但仍占有B锁
synchronized (lockA) {
System.out.println("I have lockA.");
}
}
flag = true;
}
}
}
}
//测试类
public class DieLockDemo {
public static void main(String[] args) {
DieLockThread dieLockThread = new DieLockThread();
Thread thread1 = new Thread(dieLockThread, "A-thread");
Thread thread2 = new Thread(dieLockThread, "B-thread");

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

测试代码的运行结果:
I have lockB.
I have lockA.
I have lockA.
I have lockB.

运行的结果是程序卡在了这个状态,永远不会停止,这就是线程的死锁。
产生这个结果的原因是一个线程获取A锁后等待获取B锁,另一个线程获取B锁后等待获取A锁,因此两个线程陷入无限等待获取对方占有的锁的状态,程序永远无法结束。

2.线程间通信-生产者消费者问题
Java提供了线程之间的等待唤醒机制,来进行线程间的通信,这个机制可以解决线程的死锁问题。
<1>生产者消费者问题模型:
需求:现在有一份共享资源,两个线程,一个线程用于生产共享资源,另一线程用于消费共享资源,只有当生产者线程生产了资源时,消费者线程才可以对资源进行消费,而当没有资源时,消费者线程必须等待生产者线程生产资源。
分析:上面的问题是线程间通信的典型问题,如何实现生产者线程和消费者线程之间按规则操作资源呢?
通过Object类中的wait()和notify()方法

代码:
生产者代码:

import java.util.ArrayList;
public class Producer implements Runnable {
//定义一个资源
private ArrayList<Object> resources;
//通过构造方法从外界获取资源对象
public Producer(ArrayList<Object> resources) {
super();
this.resources = resources;
}

@Override
public void run() {
while(true) {
//用共享资源当做线程同步锁
synchronized(resources) {
//如果没有资源:集合中没有元素
if(resources.size() == 0) {
//生产资源:向集合中添加元素
resources.add("element-" + resources.size());
//打印提示:新元素已经添加
System.out.println("New element has been produced.");
//唤醒消费者线程消费已经生产的资源
resources.notify();
}else{
//如果有资源,等待消费者消费
try {
resources.wait();
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
}
}
}

消费者代码:

import java.util.ArrayList;
public class Consumer implements Runnable {
// 定义一个资源变量
private ArrayList<Object> resources;

// 通过构造方法从外界获取资源对象
public Consumer(ArrayList<Object> resources) {
super();
this.resources = resources;
}

@Override
public void run() {
// 用共享资源当做线程同步锁
synchronized (resources) {

while (true) {
// 如果有资源:集合中有元素
if (resources.size() != 0) {
// 消费资源
Object element = resources.remove(resources.size() - 1);
System.out.println(element + "has been consumed.");
// 唤醒生产者线程
resources.notify();
} else {
// 没有可消费的资源,等待生产者生产
try {
resources.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}

测试类代码

import java.util.ArrayList;
public class ThreadDemo {
public static void main(String[] args) {
// 创建资源对象
ArrayList<Object> resources = new ArrayList<Object>();
// 创建生产者消费者对象
Producer p = new Producer(resources);
Consumer c = new Consumer(resources);
// 创建生产者消费者线程
Thread produce = new Thread(p, "producer");
Thread consume = new Thread(c, "consumer");
// 启动线程
produce.start();
consume.start();
}
}

运行结果:
…….
New element has been produced.
element-0 has been consumed.
New element has been produced.
element-0 has been consumed.
New element has been produced.
element-0 has been consumed.
New element has been produced.
element-0 has been consumed.
……

上面的代码示例通过wait()和notify()方法,让生产者和消费者线程之间进行了通信,当生产者生产资源时,消费者等待,当消费者消费资源时,生产者等待。

六、线程的状态转换图
下面的这个图总结了线程在运行过程中可能会进入的几种状态:
黑马程序员-Java高级:多线程

在API文档中也定义了线程的6种状态:
<1>NEW 新建状态,线程已经创建,但尚未启动
<2>RUNNABLE 在JVM中执行的线程
<3>BLOCKED 同步阻塞状态,等待获取同步锁
<4>WAITING 等待阻塞状态,等待被某些事件唤醒
<5>TIMED_WAITING 有限等待,如果在指定时间内没有被唤醒那么不再等待
<6>TERMINATED 终止状态,线程退出。