多线程
是指从软件或者硬件上实现多个线程并发执行的技术。
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
1. 并发和并行
-
并行:在同一时刻,有多个指令在多个CPU上同时执行
-
并发:在同一时刻,有多个指令在单个CPU上交替执行
假如在边打电话边吃饭,嘴里吃着饭说着话就是并行,说一句话之后吃一口饭就是并发。
2. 进程和线程
-
进程:是正在运行的软件
就是操作系统中正在运行的一个应用程序
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
- 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
- 并发性:任何进程都可以同其他进程一起并发执行
-
线程:是进程中的单个顺序控制流,是一条执行路径(线程是属于进程的,是进程里面做的事)
就是应用程序中所做的事
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序
- 多线程:一个进程如果有多条执行路径,则称为多线程程序
-
注意:
程序是静态的。而进程是程序的动态执行(所有程序必须进入内存中才能执行,而进入内存才能成为进程)
一个程序至少包括一个进程,一个进程至少包括一个线程,具体执行单位是线程
3. 多线程的实现方式
3.1 方式一:继承Thread类
-
Thread介绍
Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。
-
方法介绍
方法名 说明 void run() 在线程开启后,此方法将被调用执行 void start() 使此线程开始执行,Java虚拟机会调用run方法() -
注意:
继承Thread类,要重写run()方法,用来封装被线程执行的代码
如果直接调用run()方法,相当于普通方法的调用,不会有多线程的调用
具体使用是调用start()方法开启线程,然后由JVM调用此线程的run()方法
-
小栗子
//1. 继承Thread类 public class MyThread extends Thread{ //2. 重写run方法 @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("多线程的+"+i); } } } //测试类 public class ThreadDemo01 { public static void main(String[] args) { MyThread t1 = new MyThread(); //开启线程 t1.start(); // MyThread t2 = new MyThread(); // t2.start(); for (int i = 0; i < 100; i++) { System.out.println("main"+i); } //t1线程会和main方法交替执行 } }
3.2 方式二:实现 Runnable 接口
-
Thread 构造方法
方法名 说明 Thread(Runnable target) 分配一个新的Thread对象 Thread(Runnable target, String name) 分配一个新的Thread对象 -
实现步骤
- 定义一个类实现Runnable接口,重写run()方法
- 创建实现类对象
- 创建Thread类对象,
- 把Runnable接口实现类对象作为Thread类的构造方法的参数
- 用Thread类对象调用start()方法,开启线程
-
小栗子
//实现 Runnable接口 public class MyRunnable implements Runnable{ //重写run()方法,也只有run()方法 @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("实现Runnable接口的方式:"+i); } } } //测试类 public class RunnableDemo01 { public static void main(String[] args) { //Runnable接口的实现类对象 MyRunnable mr = new MyRunnable(); //Thread对象,以mr为参数去实例化 Thread t1 = new Thread(mr); // Thread t2 = new Thread(mr); //开启线程,实际执行的run()方法是,mr中重写的run()方法 t1.start(); // t2.start(); //main方法的输出语句,会和t1线程交替执行 for (int i = 0; i < 100; i++) { System.out.println("main:" + i); } } }
3.3 方式三:实现Callable接口 (Future)
-
方法介绍
方法名 说明 V call() 计算结果,如果无法计算结果,则抛出一个异常 FutureTask(Callable callable) 创建一个 FutureTask对象,一旦运行就执行给定的 Callable V get() 如有必要,等待计算完成,然后获取其结果 -
实现步骤
- 定义一个类实现Callable接口,给定的泛型,取决于你call方法要返回什么类型的值,重写call()方法
- 创建其实现类对象
- 创建Future的实现类FutureTask对象,把实现Callable接口的实现类作为参数构造FutureTask对象
- 创建Thread类的对象,把FutureTask对象作为构造方法的参数
- 用Thread类的对象调用start()方法,启动线程
- 再调用get()方法,可以获取线程结束后的结果
-
小栗子
//实现Callable,给定返回值类型 public class MyCallable implements Callable<String> { //重写call方法,该方法有返回值 @Override public String call() throws Exception { for (int i = 0; i < 100; i++) { System.out.println("第"+i+"次向女孩表白!"); } return "成功"; } } //测试类 public class CallableDemo01 { public static void main(String[] args) throws ExecutionException, InterruptedException { //创建MyCallable对象,该对象实现了Callable接口 MyCallable mc = new MyCallable(); //创建FutureTask对象,以mc为构造方法的参数 FutureTask<String> ft = new FutureTask<>(mc); //创建Thread对象,以ft为构造方法的参数 Thread t1 = new Thread(ft); //开启线程 t1.start(); //获取线程执行结束后的结果,就是我们在call()方法里给定的返回值 String s = ft.get(); System.out.println(s); //注意: // 当程序执行到ft.get();的时候,它会死等到前面的线程执行结束 // 获取到结果才会继续执行下面的内容。 // 所以下面的for循环是会在t1.start();开启的线程完全执行结束后才会开始执行 // 如果我们把ft.get();提到t1.start();前面,程序就会陷入死循环。 for (int i = 0; i < 100; i++) { System.out.println("main"+i); } } }
3.4 三种方式的对比
优点 | 缺点 | |
---|---|---|
实现Runnable、Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能直接使用Thread类中的方法 |
继承Thread类 | 编程比较简单,可以直接使用Thread类中的方法 | 可以扩展性较差,不能再继承其他的类 |
3.5 设置和获取线程名称
-
方法介绍
方法名 说明 void setName(String name) 将此线程的名称更改为等于参数name String getName() 返回此线程的名称 static Thread currentThread() 返回对当前正在执行的线程对象的引用 -
注意:
- 线程是有默认名字的:Thread - 编号
- 设置名字可以使用setName()方法,也可以使用有参构造
- 在使用接口实现的方式实现多线程时,我们就不能直接使用Thread类中的getName()方法去获取线程名称,这时我们可以使用currentThread()方法获取线程对象,在调用getName()方法。
-
小栗子
//继承Thread 类 public class MyThread extends Thread{ public MyThread() { } //使用构造方法设置线程名称,需要这个有参构造 public MyThread(String name) { super(name); } //重写run方法 @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(getName()+":"+i); } } } //测试类 public class ThreadDemo02 { public static void main(String[] args) { MyThread mt = new MyThread(); MyThread mt2 = new MyThread("2号线程"); mt.setName("1号线程"); mt.start(); mt2.start(); for (int i = 0; i < 100; i++) { //获取main线程的名称 System.out.println(Thread.currentThread().getName()+i); } } }
3.6 线程休眠
-
方法介绍
方法名 说明 static void sleep(long millis) 使当前正在执行的线程停留(暂停执行)指定的毫秒数 -
注意
在接口实现的方式上,sleep的异常只能try…catch,因为其接口上并没异常的声明,所以只能在实现类上直接处理。
-
小栗子
//实现 Runnable接口 public class MyRunnable implements Runnable{ //重写run()方法,也只有run()方法 @Override public void run() { for (int i = 0; i < 100; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class RunnableDemo02 { public static void main(String[] args) { /*System.out.println("睡觉前"); Thread.sleep(3000);//这边会休息个3秒 System.out.println("睡醒了");*/ //在mr里面实现了sleep() MyRunnable mr = new MyRunnable(); Thread t1 = new Thread(mr); Thread t2 = new Thread(mr); t1.start(); t2.start(); } }
3.7 线程优先级
-
线程调度
- 分时调度模型
- 抢占式调度模型
-
线程优先级具有继承特性
比如A线程启动B线程,则B线程的优先级和A是一样的
-
线程优先级具有随机性
就是说线程优先级高不一定每次都先执行完,只是被执行的效率高
-
优先级相关方法
方法名 说明 final int getPriority() 返回此线程的优先级 final void setPriority(int newPriority) 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10
3.8 后台线程/守护线程
-
相关方法
方法名 说明 void setDaemon(boolean on) 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出
4. 线程的安全问题
-
案例:
某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
public class MyThread implements Runnable{ //总票数100,这是共享的数据 private int tickets = 100; @Override public void run() { while (true){ //票数大于0就卖 if (tickets <=0 ){ System.out.println("已售罄"); break; }else { try { //线程会在这里休眠100毫秒,因为实际买票是需要时间的 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //卖完一张就减一张 tickets--; //然后打印出剩余票数 System.out.println(Thread.currentThread().getName()+"在卖票,还剩"+tickets+"张票"); } } } } public class ThreadTest { public static void main(String[] args) { MyThread mt = new MyThread(); //创建三个Thread类的对象, // 把MyThread对象作为构造方法的参数,并给出对应的窗口名称 Thread t1 = new Thread(mt,"窗口1:"); Thread t2 = new Thread(mt,"窗口2:"); Thread t3 = new Thread(mt,"窗口3:"); t1.start(); t2.start(); t3.start(); } }
-
这个案例会出现的问题:
- 相同的票出现了多次
- 出现了负数的票
-
问题产生的原因
线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现问题
-
4.1 同步代码块 解决数据安全问题
-
当多线程在操作共享数据的时候就会出现数据安全问题(因为java是抢占式调度模型)
-
并且破坏了原子操作(把一块不应该分割的整体,分割了)
-
解决方法:
- 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
- Java提供了同步代码块的方式来解决
-
同步代码块
synchronized(任意对象){ 多条语句操作共享数据的代码 }
synchronized(任意对象):相当于给代码加了锁,而任意对象可以看成是一把锁。
-
注意:
- 同步代码块的锁对象可以是任意对象
- 必须保证对个线程使用的锁对象必须是同一个
- 锁对象的作用是把同步代码块锁住,只允许一个线程在同步代码块中执行
-
好处和弊端
- 好处:解决了多线程的数据安全问题
- 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
-
小栗子
public class MyThread implements Runnable{ //总票数100,这是共享的数据 private int tickets = 100; private static Object object = new Object(); @Override public void run() { while (true){ //加锁,同步代码块中一次只能有一个线程进入 //锁对象必须要唯一,不然锁就没有意义了 synchronized (object) { //票数大于0就卖 if (tickets <=0 ){ System.out.println("已售罄"); break; }else { try { //线程会在这里休眠100毫秒,因为实际买票是需要时间的 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //卖完一张就减一张 tickets--; //然后打印出剩余票数 System.out.println(Thread.currentThread().getName()+"在卖票,还剩"+tickets+"张票"); } } } } } public class ThreadTest { public static void main(String[] args) { MyThread mt = new MyThread(); //创建三个Thread类的对象, // 把MyThread对象作为构造方法的参数,并给出对应的窗口名称 Thread t1 = new Thread(mt,"窗口1:"); Thread t2 = new Thread(mt,"窗口2:"); Thread t3 = new Thread(mt,"窗口3:"); t1.start(); t2.start(); t3.start(); } }
4.2 同步方法
-
格式
修饰符 synchronized 返回值类型 方法名(方法参数) { 方法体; }
-
区别
- 同步代码块可以锁住指定代码,同步方法是锁住方法中所有代码
- 同步代码块可以指定锁对象,同步方法不能指定锁对象
-
同步方法的锁对象是 this
-
同步静态方法
修饰符 static synchronized 返回值类型 方法名(方法参数) { 方法体; }
同步静态方法的锁对象是 类名.class
-
小栗子
public class MyThread03 implements Runnable{ private static int tickets = 100; @Override public void run() { while (true) { // if ("窗口一".equals(Thread.currentThread().getName())) { //窗口一,调用同步方法 // boolean result = synchronizedmethod(); // if (result){ // break; // } // } if ("窗口一".equals(Thread.currentThread().getName())) { //窗口一,调用静态同步方法 boolean result = synchronizedmethod1(); if (result){ break; } } //窗口二,调用同步代码块 if ("窗口二".equals(Thread.currentThread().getName())) { //同步代码块 synchronized (MyThread03.class) { if (tickets <= 0) { System.out.println("已售罄"); break; } else { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } tickets--; System.out.println(Thread.currentThread().getName() + "在卖票,还剩" + tickets + "张票"); } } } } } //同步静态方法 private static synchronized boolean synchronizedmethod1() { if (tickets <= 0) { System.out.println("已售罄"); return true; } else { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } tickets--; System.out.println(Thread.currentThread().getName() + "在卖票,还剩" + tickets + "张票"); return false; } } //同步方法 private synchronized boolean synchronizedmethod() { if (tickets <= 0) { System.out.println("已售罄"); return true; } else { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } tickets--; System.out.println(Thread.currentThread().getName() + "在卖票,还剩" + tickets + "张票"); return false; } } } public class ThreadTest03 { public static void main(String[] args) { MyThread03 mt = new MyThread03(); Thread t1 = new Thread(mt,"窗口一"); Thread t2 = new Thread(mt,"窗口二"); t1.start(); t2.start(); } }
4.3 Lock锁
JDK5之后新提供的锁对象Lock
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
-
ReentrantLock构造方法
方法名 说明 ReentrantLock() 创建一个ReentrantLock的实例 -
加锁解锁的方法
方法名 说明 void lock() 获得锁 void unlock() 释放锁 -
使用方法:
- 在Runable实现类的成员变量创建一个ReentrantLock对象
- 在可能产生线程安全问题的代码前该对象调用lock()方法,获取锁
- 在可能产生线程安全问题的代码后该对象调用unlock()方法,释放锁
4.4 死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
-
死锁是一种僵持状态,多个线程之间互相拥有对方的锁标记而互不相让
-
产生条件:
- 抢占式原则
- (锁的嵌套)互相拥有对方所需的锁标记
- 循环等待
-
解决办法
不要嵌套~
5. 生产者和消费者(等待唤醒机制)
-
概述
所谓生产者消费者问题,实际上主要是包含了两类线程:
一类是生产者线程用于生产数据
一类是消费者线程用于消费数据
为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为
-
Object类的等待和唤醒方法
方法名 说明 void wait() 导致当前线程等待(死等),直到另一个线程调用该对象的 notify()方法或 notifyAll()方法 void notify() 唤醒正在等待对象监视器(锁对象)的单个线程 void notifyAll() 唤醒正在等待对象监视器(锁对象)的所有线程 -
注意:
- 以上三个方法是使用锁对象去调用
- notify()方法是唤醒当前锁对象上等待的随机一个线程
-
小栗子
案例需求
-
桌子类(Desk):定义表示包子数量的变量,定义锁对象变量,定义标记桌子上有无汉堡的变量
-
生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务
1.判断是否有汉堡,决定当前线程是否执行
2.如果有汉堡,就进入等待状态,如果没有汉堡,继续执行,生产汉堡
3.生产汉堡之后,更新桌子上汉堡状态,唤醒消费者消费汉堡
-
消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务
1.判断是否有汉堡,决定当前线程是否执行
2.如果没有汉堡,就进入等待状态,如果有汉堡,就消费汉堡
3.消费汉堡后,更新桌子上汉堡状态,唤醒生产者生产汉堡
-
测试类(Demo):里面有main方法,main方法中的代码步骤如下
创建生产者线程和消费者线程对象
分别开启两个线程
//桌子 public class Desk { //锁对象 private final Object lock = new Object(); //标记 private boolean flag; //数量 private int count; public Desk(boolean flag, int count) { this.flag = flag; this.count = count; } public Desk() { this(false,10); } public Object getLock() { return lock; } public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } public int getCount() { return count; } public void setCount(int count) { this.count = count; } @Override public String toString() { return "Desk{" + "lock=" + lock + ", flag=" + flag + ", count=" + count + '}'; } } //生产者 public class Cooker implements Runnable{ private Desk desk; public Cooker(){} public Cooker(Desk desk) { this.desk = desk; } @Override public void run() { while (true){ synchronized (desk.getLock()){ if (desk.getCount()==0){ break; }else{ if (!desk.isFlag()){ //没有汉堡,做汉堡 System.out.println("厨师在做汉堡"); //更改共享数据为true,表示已经有汉堡了 desk.setFlag(true); //唤醒foodie desk.getLock().notifyAll(); }else { try { //桌上还有汉堡,就不用做,进入等待 desk.getLock().wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } } } //消费者 public class Foodie implements Runnable { private Desk desk; public Foodie(){} public Foodie(Desk desk) { this.desk = desk; } @Override public void run() { while (true){ synchronized (desk.getLock()){ if (desk.getCount() == 0){ //数量为0就退出 break; }else { if (desk.isFlag()){ //有,就吃 System.out.println("吃货在吃汉堡包"); //吃完,更改共享数据为false,表示桌上没有汉堡了 desk.setFlag(false); //将还可以吃汉堡的数量-1 desk.setCount(desk.getCount()-1); //叫醒cooker去做汉堡 desk.getLock().notifyAll(); }else { //没有就等待 try { desk.getLock().wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } } } //测试类 public class Demo { public static void main(String[] args) { //为保证锁唯一,在测试类中创建Desk对象 Desk desk = new Desk(); //将同一个对象传入这两个线程对象中 Cooker c= new Cooker(desk); Foodie f = new Foodie(desk); Thread t1 = new Thread(c); Thread t2 = new Thread(f); t1.start(); t2.start(); } }
-
6. 阻塞队列
-
阻塞队列继承结构
-
常见BlockingQueue
ArrayBlockingQueue: 底层是数组,有界
LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值
-
方法介绍
方法 说明 put(anObject) 将参数放入队列,如果放不进去会阻塞 take() 取出第一个数据,取不到会阻塞 -
小栗子
//生产者 public class Cooker implements Runnable { private ArrayBlockingQueue<String> list; public Cooker(ArrayBlockingQueue<String> list) { this.list = list; } @Override public void run() { try { list.put("汉堡"); System.out.println("厨师放入了一个汉堡"); } catch (InterruptedException e) { e.printStackTrace(); } } } //消费者 public class Foodie implements Runnable { private ArrayBlockingQueue<String> list; public Foodie(ArrayBlockingQueue<String> list) { this.list = list; } @Override public void run() { while (true){ try { String take = list.take(); System.out.println("吃货拿了一个"+take); } catch (InterruptedException e) { e.printStackTrace(); } } } } //测试类 public class Demo { public static void main(String[] args) { ArrayBlockingQueue<String> list = new ArrayBlockingQueue<>(1); Cooker c= new Cooker(list); Foodie f = new Foodie(list); Thread t1 = new Thread(c); Thread t2 = new Thread(f); t1.start(); t2.start(); } }