一、多线程概述
1.1 程序、进程、线程
- 程序是为完成特定任务、用某种语言编写的一组指令的集合。即指一 段静态的代码,静态对象。
- 进程是正在运行着的程序,是资源分配的单位
- 线程就进程中运行着的函数,是一个程序内部的一条执行路径。线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开 销小
1.2 为什么使用多线程
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统CPU的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和 修改
1.3 何时使用多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写 操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
二、多线程的创建与使用
2.1 继承 Thread 类(JDK 5 以前)
步骤
1、定义子类继承 Thread 类
2、子类中重写 Thread 类中的 run 方法
3、创建 Thread 子类对象
4、调用线程对象的 start 方法:启动线程,运行 run 方法
注意:如果手动调用 run 方法,就是调用了类里面的普通方法,并不是多线程
代码实现:
class MyThread1 extends Thread { //1
private static int ticketNum = 100; //这里必须使用静态变量才能共享数据
@Override
public void run() { //2
while(ticketNum > 0) {
System.out.println(getName() + ": " + ticketNum);
ticketNum--;
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1(); //3
MyThread1 t2 = new MyThread1();
MyThread1 t3 = new MyThread1();
t1.setName("窗口一");
t2.setName("窗口三");
t3.setName("窗口二");
t1.start(); //4
t2.start();
t3.start();
}
}
2.2 实现 Runnable 接口 (JDK 5 以前)
步骤:
1、定义子类,实现 Runnable 接口
2、子类重写 Runnable 中的 run 方法
3、通过 Thread 类含参(参数为Runnable 接口的子类对象)构造器创建线程对象
4、调用 Thread 类的 start 方法:开启线程,调用 Runnable 子类接口的 run 方法
代码实现:
方法一:定义类实现 Runnable 接口
class MyThread2 implements Runnable { //1
private int ticketNum = 100;
@Override
public void run() { //2
while(ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + ": " + ticketNum);
ticketNum--;
}
}
}
public class ThreadTest2 {
public static void main(String[] args) {
MyThread2 r = new MyThread2(); //3
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
Thread t3 = new Thread(r);
t1.setName("窗口一");
t2.setName("窗口三");
t3.setName("窗口二");
t1.start(); //4
t2.start();
t3.start();
}
}
方法二:lambda 表达式
public class ThreadTest2 {
public static void main(String[] args) {
final int[] ticketNum = {100};
//1 创建 Runnable 接口的匿名实现类对象
//2 实现 run 方法
Runnable myThread3 = () -> {
while(ticketNum[0] > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + ticketNum[0]);
ticketNum[0]--;
}
};
//3
Thread t1 = new Thread(myThread3);
Thread t2 = new Thread(myThread3);
Thread t3 = new Thread(myThread3);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
//4
t1.start();
t2.start();
t3.start();
}
}
两种方式如何选用
一般使用实现 Runnable 接口的方式,原因有一下两点
1、实现接口避免了单继承的局限性
2、多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份数据(如果用及成 Thread 类的方式,就必须使用静态变量)
2.3 实现 Callable 接口 (JDK 5 新增)
步骤
1、创建子类实现 Callable 接口
2、子类重写 Callable 接口的 call 方法
3、创建 FutureTask 的对象 fTask
4、通过 fTask 创建 Thread 对象
5、调用 Thread 对象的 start 方法:启动线程,调用线程的 run 方法
代码实现:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
//1
class MyThread3 implements Callable<Integer> {
private int ticket = 100;
@Override
public Integer call() throws Exception { //2
while(ticket > 0) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + ": " + ticket);
ticket--;
}
return ticket;
}
}
public class ThreadTest3 {
public static void main(String[] args) {
//3
MyThread3 myThread3 = new MyThread3();
FutureTask<Integer> futureTask = new FutureTask<>(myThread3);
FutureTask<Integer> futureTask2 = new FutureTask<>(myThread3);
FutureTask<Integer> futureTask3 = new FutureTask<>(myThread3);
//4
Thread t1 = new Thread(futureTask, "窗口一");
Thread t2 = new Thread(futureTask2, "窗口二");
Thread t3 = new Thread(futureTask3, "窗口三");
//5
t1.start();
t2.start();
t3.start();
}
}
与 Runnable 相比,Callable 功能更强大:
1、call 方法可以有返回值
2、call 方法可以抛出异常
3、Callable 支持泛型
2.4 使用线程池(JDK 5 新增)
2.4.1 背景
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能的影响很大。所以解决办法就是提前创建好多个线程,放入线程池中,使用时直接获取,用完还回去,这样就避免了频繁创建和销毁。
2.4.2 使用线程池的好处
1、提高响应速度(减少了创建新线程的时间)
2、降低资源小号啊(重复利用线程池中的线程,不需要每次都创建
3、便于线程管理(可以设置线程池的大小,最大的线程数,线程没有任务时最多保持多长时间后会中止等)
2.4.3 实现
步骤:
1、提供指定线程数量的线程池
2、执行指定线程的操作, 需要提供实现 Runnable 接口或 Callable 接口的实现类对象
3、关闭连接池
代码实现:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyThread4_1 implements Runnable {
int sum = 0;
@Override
public void run() {
for(int i = 0; i <= 100; i++) {
if(i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class MyThread4_2 implements Runnable {
@Override
public void run() {
for(int i = 0; i <= 100; i++) {
if(i % 2 == 1) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadTest4 {
public static void main(String[] args) {
//1 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//2 执行指定线程的操作, 需要提供实现 Runnable 接口或 Callable 接口的实现类对象
service.execute(new MyThread4_1());
service.execute(new MyThread4_2());
//3 关闭连接池
service.shutdown();
}
}
三、线程的生命周期
3.1 生命周期中的五种状态
- 新建:当一个 Thread 类或其子类的对象被声明创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被 start() 后,将进入线程队列等待 CPU 时间片,此时它已经具备了运行的条件,只是没有分配到资源。
- 运行:当就绪的线程被调度并获得 CPU 资源时,便进入运行状态,run() 方法定义了线程的操作和功能。
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制地中止或出现异常导致结束。
3.2 线程状态转换
四、线程的同步
4.1 问题的提出
当多个线程操作共享数据时,一个线程对多条语句只执行了一部分,还没有 执行完,另一个线程参与进来执行。导致共享数据的错误。第二部分中使用的火车买票的例子就会出现线程安全问题。
4.2 解决办法
对于执行共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
具体实现方式就是加锁,访问共享数据的代码必须使用同一把锁(锁可以是任何对象),因此,对于实现 Runnable 的方式可以使用静态或者非静态的的对象,对于继承 Thread 的方式必须使用静态对象,一般来说,实现 Runnable 的方式使用 this 充当锁,继承 Thread 的方式使用 类名.class 充当锁
4.2.1 使用 Sychronized 方法
1、同步代码块
synchronized (对象){ // 需要被同步的代码; }
例:
实现 Runnable 接口:
class MyThread1 implements Runnable {
private int ticketNum = 100;
@Override
public void run() {
while(true) {
synchronized (this) { //使用 this 来充当锁
if(ticketNum > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + ticketNum);
ticketNum--;
}
if(ticketNum == 0) break;
}
}
}
}
继承 Therad 类:
class MyThread2 extends Thread {
private static int ticketNum = 100;
@Override
public void run() {
while(true) {
synchronized (MyThread2.class) { //使用 MyThread2.class 充当锁
if(ticketNum > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + ticketNum);
ticketNum--;
}
if(ticketNum == 0) break;
}
}
}
}
2、同步方法
实现 Runnable 接口:
class MyThread3 implements Runnable {
private int ticketNum = 100;
@Override
public void run() {
while(true) {
sendTicket();
if(ticketNum == 0) break;
}
}
private synchronized void sendTicket() {
if(ticketNum > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + ticketNum);
ticketNum--;
}
}
}
继承 Thread 类
class MyThread4 extends Thread {
private static int ticketNum = 100;
@Override
public void run() {
while(true) {
sendTicket();
if(ticketNum == 0) break;
}
}
public static synchronized void sendTicket() { //注意:一定要是 static 的
if(ticketNum > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + ticketNum);
ticketNum--;
}
}
}
4.2.2 使用 Lock 方法(JDK 5 新增)
class MyThread5 implements Runnable {
private final ReentrantLock lock = new ReentrantLock();
private int ticketNum = 100;
@Override
public void run() {
while(true) {
lock.lock();
try { // 这里使用 try-finally 是因为防止到最后 break 之后,lock 不会解锁
if(ticketNum > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + ticketNum);
ticketNum--;
}
if(ticketNum == 0) break;
} finally {
lock.unlock();
}
}
}
}
4.2.3 解决单例设计模式中懒汉式线程不安全的问题
使用 syncronized 实现
public class SingleClass {
private static SingleClass singleClass;
private SingleClass() {
}
public static SingleClass getInstance() {
if(singleClass == null) { //在这里加一步判断可以提高效率,因为当 singleClass 已经被实例化之后线程就不用阻塞了
synchronized (SingleClass.class) {
if(singleClass == null) {
singleClass = new SingleClass();
}
}
}
return singleClass;
}
}
使用 Lock 实现
public class SingleClass {
private static SingleClass singleClass;
private static Lock lock = new ReentrantLock();
private SingleClass() {
}
public static SingleClass getInstance() {
if(singleClass == null) { //在这里加一步判断可以提高效率,因为当 singleClass 已经被实例化之后线程就不用阻塞了
lock.lock();
if(singleClass == null) {
singleClass = new SingleClass();
}
lock.unlock();
}
return singleClass;
}
}
4.2.4 synchronized 与 Lock 对比
- Lock 是显示锁(手动开启和关闭锁),syncronzied 是隐式锁,出了作用域自动释放;
- Lock 只有代码块锁,syncronized 有代码块锁和方法锁;
- 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(Lock 有很多子类)。
4.3 死锁
什么是死锁:死锁就是两个运行着的线程都在等对方释放资源,所以这两个线程都不会结束。
死锁简单示例:
五、线程的通信
5.1 为什么要线程通信
- 多个线程并发执行时,在默认情况下 CPU 是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且希望它们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
- 如果没有使用线程通信来操作同一份数据的话,直接使用线程同步就可以实现,但是会很大程度上造成多线程之间对同一变量的争夺,而且无规律。
5.2 什么是线程通信
**线程通信:**就是多个线程在操作同一份数据时,避免对同一共享变量的争夺。
5.3 线程通信机制
- wait():当前线程挂起并放弃 CPU 和 同步资源并等待,识别的线程可访问并修改共享变量,而当前线程排队等候其他线程调用 notify() 或 notifyAll() 方法唤醒,唤醒后等待重新获得对监控器的所有权才能继续进行。
- notify():唤醒正在排队等候同步资源的线程中优先级最高者结束等待。
- notifyAll():唤醒正在排队等候同步资源的所有线程结束等待。
注意事项:这三个方法只有在 syncronized 方法或 syncronized 代码块中才能使用。
5.3 示例
使用两个线程交替打印 1—100,线程 1, 2 交替打印。
class MyThread1 implements Runnable {
private int i = 1;
@Override
public void run() {
while(true) {
synchronized (this) {
notify();
if(i <= 100) {
System.out.println(Thread.currentThread().getName() + ": " + i);
i++;
} else {
break;
}
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
5.3 线程通信举例——生产者消费者问题
class Product implements Runnable{
private Clerk clerk;
public Product(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() { //生产者线程
while(true) {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + "开始生产产品");
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.addProduct();
}
}
}
}
class Consumer implements Runnable{
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() { //消费者线程
while(true) {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + "开始消费产品");
try {
Thread.sleep((long) (Math.random() * 100));
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}
}
class Clerk {
private int products;
public Clerk() {
this.products = 0;
}
public synchronized void addProduct() {
if(products >= 20) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
products++;
System.out.println(Thread.currentThread().getName() + "生产了第 " + products + " 个产品");
notify();
}
}
public synchronized void consumeProduct() {
if(products <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + "消费了第 " + products + " 个产品");
products--;
notifyAll();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Product product1 = new Product(clerk);
Product product2 = new Product(clerk);
Consumer consumer1 = new Consumer(clerk);
Thread p1 = new Thread(product1);
Thread p2 = new Thread(product2);
p1.setName("生产者1");
p2.setName("生产者2");
Thread c1 = new Thread(consumer1);
c1.setName("消费者1");
p1.start();
p2.start();
c1.start();
}
}
5.4 wait() 和 sleep() 的区别
- 任何类都有 wait() 方法,因为它是在 Object 中定义的,而 sleep() 是在 Thread 类中定义的;
- wait() 必须结合 syncronized 使用,而 sleep() 可以在任何地方使用;
- wait() 会放弃对象锁(从 5.3 可以看出),而 sleep() 不会放弃对象锁。