一 简述
- 是指从软件或者硬件上实现多个线程并发执行的技术。
- 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
- 多线程技术就是同时执行多个应用程序,多线程技术需要硬件的支持
二 概念
- 并行:在同一时刻,有多个指令在多个CPU上同时执行
- 并发:在同一时刻,有多个指令在单个CPU上交替执行
- 进程:是正在运行的软件
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
- 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
- 并发性:任何进程都可以同其他进程一起并发执行
- 线程:是进程中的单个顺序控制流,是一条执行路径(线程就是进程里面做的事)
- 单线程:一个进程如果只有一条执行路径,则称之为单线程程序
- 多线程:一个进程如果有多条执行路径,则称之为多线程程序
三 实现方法
- 继承Thread类
- 定义一个类MyThread继承Thread类
- 在MyThread中重写run()方法
- 创建MyThread对象
- 启动线程 start()方法
public class Test { public static void main(String[] args) { //创建线程对象 MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); //启动线程 t1.start(); t2.start(); } } class MyThread extends Thread{ //run中代码,为线程在开启后执行的代码 public void run(){ for (int i = 0; i < 5; i++) { System.out.println("线程开启了" + i); } } }
- 运行结果:俩个线程之间的运行顺序由CPU决定,不存在固定顺序
- 为什么要重写run()方法?因为run()方法是用来封装被线程执行的代码
- run()与start()方法的区别?run()方法:封装线程执行的代码,直接调用,相当于普通方法的调用,并没有开启线程。 start()方法:启动线程,然后由JVM调用此线程的run()方法。
- 实现Runnable接口
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable中重写run()方法
- 创建MyRunnable对象
- 创建Thread类对象,把MyRunnable对象作为构造方法的参数
- 启动线程
public class Test { public static void main(String[] args) { //创建线程对象,通过实现Runnable接口的对象作为参数 Thread t1 = new Thread(new MyRunnable()); Thread t2 = new Thread(new MyRunnable()); //启动线程 t1.start(); t2.start(); } } class MyRunnable implements Runnable{ //run中代码,为线程在开启后执行的代码 public void run(){ for (int i = 0; i < 5; i++) { System.out.println("线程开启了" + i); } } }
- Callable与FutureTask
- 定义一个类MyCallable实现Callable<>接口,注意:Callable接口存在泛型表达式,其泛型定义的类为 call()方法中的返回值类型
- 在MyCallable类中重写call()方法(与run()方法类似)但是call方法存在线程结束的返回值
- 创建MyCallable类的对象
- 使用FutureTask<>类创建对象,参数为MyCallable类的对象,泛型类与Callable相同。在FutureTask中存在get()方法,用来接收线程结束的返回值。注意:在一段代码开始运行后:先使用main线程调用main方法,如果使用get那么需要对应线程结束运行后才能得到结果,否则将死等结果,造成代码无法停止
- 创建Thread对象,把FutureTask<>类对象作为构造方法参数
- 启动线程
- 注意:在使用是需要抛出异常
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class Test { public static void main(String[] args) throws Exception { //创建MyCallable类对象 MyCallable mc = new MyCallable(); //通过MyCallable类对象创建FutureTask类对象 FutureTask<String> ft1 = new FutureTask<>(mc); FutureTask<String> ft2 = new FutureTask<>(mc); //通过FutureTask对象创建Thread类对象 Thread t1 = new Thread(ft1); Thread t2 = new Thread(ft2); //启动线程 t1.start(); t2.start(); //输出线程结束的结果 System.out.println(ft1.get()); System.out.println(ft2.get()); } } class MyCallable implements Callable<String> { //call中代码,为线程在开启后执行的代码,与run方法不同,其存在返回值 public String call() throws Exception { for (int i = 0; i < 5; i++) { System.out.println("线程开启了" + i); } return "MyCallable类"; } }
- 三种方法的比较:其中只有实现Callable接口时,在结束线程时有放回值
优点 |
缺点 | |
实现Runnable 或Callable接口 |
扩展性强,实现该接口的同时 还可以继承其他类 |
编程相对复杂,不能直接 使用Thread类中的方法 |
继承Thread类 |
编程比较简单,可以直接使用 Thread类中的方法 |
可扩展性较差, 不能继承其他类 |
四 Thread类常用方法
1 String getName(); 返回线程的名字 注:线程有默认的名字,格式:Thread-编号(没有设置名字的线程,启动时编号由0开始,逐步加一) 2 void setName(String name); 将此线程的名称更改为参数name 注:通过构造方法也可以设置线程名字,但如果使用继承与Thread类的子类时, 需要在子类中重写带参构造的方法,因为子类的构造方法需要写入super关键字调用父类的构造方法。 而不会默认继承 3 public static Thread currentThread(); 返回对当前正在执行的线程对象的引用 注:使用场景在继承接口时,其没有继承Thread类则不能使用getName()方法, 可以先使用currentThread获得对象,再使用getName() :Thread.currentThread().getName() 4 public static void sleep(long time); 让线程休眠指定的时间,单位为毫秒 注:其为类方法,单个对象调用只会使得该类休眠 在继承Thread类和实现接口的方法中,使用sleep()方法,必须使用try-catch解决异常,不能使用throw 5 public final void setDaemon(boolean on); 设置为守护线程 注:当普通线程结束时,守护线程也会随之结束
五 多线程的实现
- 线程调度
- 多线程的并发运行:计算机中的CPU,在任意时刻只能执行一条机器指令。每个线程只有获得CPU的使用权才能执行代码。各个线程轮流获得CPU的使用权,分别执行各自的任务
- 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些
- Java使用的是抢占式调度模型
1 public final void setPriority(int newPriority); 设置线程的优先级 2 public final int getPriority(); 获取线程的优先级 注:优先级 1 - 10;默认值为 5 优先级越高,只是抢夺到CPU的执行权的机率更高,不是绝对的
- 线程生命周期:
六 线程的安全问题
- 案例:“卖票”,需求:一共100张票,有3个卖票窗口,模拟卖票系统
- 错误思路,使用继承Thread有局限性,因为:需要创建3次对象,相当于3个参数对象没有达到预期效果:代码如下:
public class Test { public static void main(String[] args) throws Exception { Ticket t1 = new Ticket(); Ticket t2 = new Ticket(); Ticket t3 = new Ticket(); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } //创建Ticket类,实现多线程 class Ticket extends Thread{ //票数 private int ticket = 100; public void run(){ while (true){ if (ticket == 0){ System.out.println("票买完了"); break; } else { ticket--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩下" + ticket + "张票"); } } } }
- 效果图:从图中可以看出,实际上三个窗口并没有关联到一起,各卖各的,相当于一共卖了300张票
- 解决方法:使用Runable接口,使得创建的3个线程,可以共用一个参数或者将共享对象使用继承Thread类设置为static类型:private static int ticket
public class Test { public static void main(String[] args) throws Exception { //创建实现Runnable接口的对象 Ticket ticket = new Ticket(); //使该对象作为唯一参数,创建Thread对象,保证线程共用一个参数 Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); Thread t3 = new Thread(ticket); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } //创建Ticket类,实现多线程 class Ticket implements Runnable{ //票数 private int ticket = 100; public void run(){ while (true){ if (ticket == 0){ System.out.println("票买完了"); break; } else { ticket--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩下" + ticket + "张票"); } } }
- 效果图:虽然打印的次序与顺序不一,但是三者相关联,总票数为100,但同时也存在着出现重复票的问题,在下面代码中通过阻塞方法sleep进行分析
- 若在程序中加入时间延时,在出票时,添加100毫秒延时,使用sleep方法,try-catch解决异常
public class Test { public static void main(String[] args) throws Exception { //创建实现Runnable接口的对象 Ticket ticket = new Ticket(); //使该对象作为唯一参数,创建Thread对象,保证线程共用一个参数 Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); Thread t3 = new Thread(ticket); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } //创建Ticket类,实现多线程 class Ticket implements Runnable{ //票数 private int ticket = 100; public void run(){ while (true){ if (ticket <= 0){ System.out.println("票买完了"); break; } else { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } ticket--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩下" + ticket + "张票"); } } } }
- 结果演示:出现很多相同票,并且出现负票
- 结果分析:在代码运行期间,任何时候3个线程都可能出现CPU的抢占使用,并不是一个线程对象在一次代码完整结束后,才会进行下一个代码。这样就会导致出现,同时对票数 private int ticket 的操作,导致运行结果问题的产生。延迟100毫秒,使得问题可以被放大化。
- 问题解决
- 问题分析:多线程同时操作共享数据导致
- 解决思路:使多线程不能同时对共享数据操作,将程序锁起来,即使获得了CPU使用权,若已有线程进行操作,则该线程仍不能使用代码
- 实现方式:将操作共享数据的多条代码锁起来,让任意时刻只能有一个线程执行。Java中提供了同步代码块的方式来解决
- 同步代码块:锁多条语句的代码块,可以使用同步代码块实现:
synchronized(任意对象/锁对象){ 多条语句操作共享的数据代码 }
- 该代码锁:默认情况下是打开的 ,但只要有一个线程进去执行代码了,锁就会关闭,当线程执行完,锁才会自动打开
- 同步/锁的好处:解决了多线程的数据安全问题
- 同步/锁的弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
- 代码演示:
public class Test { public static void main(String[] args) throws Exception { //创建实现Runnable接口的对象 Ticket ticket = new Ticket(); //使该对象作为唯一参数,创建Thread对象,保证线程共用一个参数 Thread t1 = new Thread(ticket); Thread t2 = new Thread(ticket); Thread t3 = new Thread(ticket); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } //创建Ticket类,实现多线程 class Ticket implements Runnable { //票数 private Integer ticket = 100; //创建对象作为锁 private Object obj = new Object(); public void run() { while (true) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj) { //锁对象 if (ticket <= 0) { System.out.println("票买完了"); break; } else { ticket--; System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩下" + ticket + "张票"); } } } } }
- 效果图:可以看出,没有出现同一票俩次购买的问题,同时打印顺序也与票数相对应
- 注意事项:只将操作到共享数据的代码放入 synchronized同步块中,(sleep代码放在synchronized()方法外,因为sleep若在synchronized中使用,则该线程休眠时,不会自动释放锁,导致其他线程无法操作)同时synchronized()的参数为一个锁对象,要确保多个线程使用的是同一把锁(即同使用同一个对象作为参数)
- 若使用的是继承Thread类的方法实现多线程时,一定要确保共享数据,以及锁全是静态数据,才能保证在new一个新对象时,不会创建新的共享数据与锁
//静态票数 private static Integer ticket = 100; //静态接口 private static Object obj = new Object();
- 这是同时new三个对象
Ticket t1 = new Ticket(); Ticket t2 = new Ticket(); Ticket t3 = new Ticket();
- 使用的仍然时共享数据与锁,可以达到多线程要求
- 同步方法:
- 格式
修饰符 synchronized 返回值类型 方法名(参数列表){方法体}
- 与同步代码块区别:
- 同步代码块可以锁住指定的代码块,同步方法则是锁住所有的代码
- 同步代码块可以指定锁的对象(参数),同步方法不能指定锁对象
- 同步方法的锁是什么?以该类的对象作为锁相当于代码块 synchronized (this)
- 特殊:使用静态同步方法时,锁对象为:类名.class,当前类字节码文件对象
- Lock锁
- 因为使用synchronized方法的锁是自动加锁和释放锁,为了获得更广泛的锁功能java提供了Lock锁
1 void lock(); 获得锁 2 void unlock(); 释放锁
- Lock是接口不能被实例化,可采用它的实现类ReentrantLock来创建对象
- 代码演示:
synchronized(任意对象/锁对象){ 多条语句操作共享的数据代码 } 转化为: //创建ReentrantLock对象 ReentrantLock lock = new ReentrantLock(); //上锁 lock.lock(); 多条语句操作共享的数据代码 //释放锁 lock.unlock();
- 为了防止代码中间报错,而没有释放锁,可将unlock()放入,finally中
ReentrantLock lock = new ReentrantLock(); public void run() { while (true) { //解决sleep使用try - catch - finally try { Thread.sleep(1000); lock.lock(); 多条语句操作共享的数据代码 } catch (Exception e) { e.printStackTrace(); } finally { //在finally中释放锁,确保锁的释放 lock.unlock(); } } } }
- 死锁:
- 概念:由于俩个或多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往CPU执行
- 建议:不要写锁的嵌套,防止死锁发生