多线程
基本概念
- 程序和进程的概念
- 程序 - 数据结构 + 算法,主要指存放在硬盘上的可执行文件(对于Windows操作系统来说,可执行文件说白了就是exe)。 一旦双击程序把它跑起来,它就不在硬盘上了,而是跑到内存中去了。
- 进程 - 主要指运行在内存中的可执行文件。可以理解为运行起来的程序。任务管理器中运行了很多很多的进程,其实也就是我们开了很多很多任务。 为什么要执行多进程?同时可以干很多活。
- 目前主流的操作系统都支持多进程,为了让操作系统同时可以执行多个任务(目的,也就是核心理念,也就是为了让当前计算机同时开很多个软件),但进程是重量级的, 也就是新建一个进程会消耗CPU和内存空间等系统资源(进程达到一定数目之后就会发现CPU和内存不够分配了,就会出现电脑卡死的现象),因此进程的数量比较局限。
- 线程的概念(解决了进程的所出现的局限性)
- **多线程的概念:**为了解决上述问题就提出线程的概念,线程就是进程内部的程序流,也就是说操作系统内部支持多进程的,而每个进程的内部又是支持多线程的,线程是轻量的,新建线程会共享所在进程的系统资 源,因此目前主流的开发都是采用多线程(既节省资源,又同时可以让它执行多个任务)。 多线程的实例:比如说一个安全软件是一个进程,这个进程中既可以开垃圾清理,同时又可以开病毒扫描和漏洞修复功能。这就叫一个进程的内部又开了好多的线程,这也就验证了确实在进程的内部可以开很多线程。
- 好处:依然可以同时开多个任务,并且线程是轻量的,它不像进程,只要运行一个进程就要消耗CPU,申请内存。而线程会共享所在进程的资源,也就是说对资源的消耗比较小。
- 多线程的执行原理:多线程是采用时间片轮转法(CPU快速的在多个线程之间进行切换,每个线程都分配了一个很小的时间片迅速切换,只要切换速度足够快,就感觉像这些线程在同时执行,这些任务在同时执行,我们把这种策略称为时间片轮转法)来保证多个线程的并发执行,所谓并发就是指宏观并行微观串行的机制(通过一个时间段来看,多个线程同时都在执行,通过一个时间点来看,只是在一个线程一个线程的执行)。
- 很久之前的电脑是单核的,也就是说只有一个CPU。多进程也好,多线程也罢,都是说同时要干很多活,单核只有一个CPU很难同时做多个活(就好比给你一个人派了很多任务让你一个人干,这样也很难)。现在的4核、6核、8核可以理解为4个CPU、6个CPU、8个CPU,每一个CPU分一个任务,一个线程的话,确实可以多个线程同时执行,此时再迅速切换,多线程的感觉就更好了。
- 总结:为什么要采用多线程?无论是多线程还是多进程,都是为了让我们的操作系统,我们的计算机同时执行多个任务。在以后的开发中,一旦让我们的代码,我们的模块同时干多个活的话,我们就要采用多线程或者多进程的机制。再Java中就是采用多线程。
线程的创建(重中之重)
-
Thread类的概念
-
java.lang.Thread类代表线程(创建线程得使用这个类),任何线程对象都是Thread类(子类)的实例。
-
Thread类是线程的模板,封装了复杂的线程开启等操作,封装了操作系统的差异性。
-
public class Thread extends Object implements Runnable线程是程序中执行的线程。 Java虚拟机允许应用程序同时运行多个执行线程。 // 每个线程都有优先权。 具有较高优先级的线程优先于具有较低优先级的线程执行。 每个线程可能也可能不会被标记为守护进程。 当在某个线程中运行的代码创建一个新的Thread对象时,新线程的优先级最初设置为 等于 创建线程的优先级,并且 当且仅当创建线程是守护进程时才是守护进程线程。 当Java虚拟机启动时,通常会有一个非守护进程线程(通常调用某个指定类的名为main的方法,也就是主线程)。 Java虚拟机继续执行线程,直到发生以下任一情况: 已调用类Runtime的exit方法,并且安全管理器已允许执行退出操作。 通过调用run方法返回或抛出超出run方法传播的异常,所有非守护程序线程的线程都已死亡。 有两种方法可以创建新的执行线程。 一种是将类声明为Thread的子类。 此子类应覆盖类Thread的run方法。 然后可以分配和启动子类的实例。 例如,计算大于规定值的素数的线程可以写成如下: -------------------------------------------------------------------------------- class PrimeThread extends Thread { long minPrime; PrimeThread(long minPrime) { this.minPrime = minPrime; } public void run() { // compute primes larger than minPrime . . . } } -------------------------------------------------------------------------------- 然后,以下代码将创建一个线程并开始运行: PrimeThread p = new PrimeThread(143); p.start(); 创建线程的另一种方法是声明一个实现Runnable接口的类。 该类然后实现run方法。 然后可以分配类的实例,在创建Thread时作为参数传递,然后启动。 此其他样式中的相同示例如下所示: -------------------------------------------------------------------------------- class PrimeRun implements Runnable { long minPrime; PrimeRun(long minPrime) { this.minPrime = minPrime; } public void run() { // compute primes larger than minPrime . . . } } -------------------------------------------------------------------------------- 然后,以下代码将创建一个线程并开始运行: PrimeRun p = new PrimeRun(143); new Thread(p).start(); // 每个线程都有一个用于识别目的的名称。 多个线程可能具有相同的名称。 如果在创建线程时未指定名称,则会为其生成新名称。 除非另有说明,否则将null参数传递给null中的构造函数或方法将导致抛出NullPointerException 。
-
-
创建方式
-
自定义类继承Thread类并重写run方法(因为Thread类中的run方法啥也不干,只有重写此方法之后才能给该方法赋予一定的功能),然后创建该类的对象调用start方法。 优点:代码量更少。缺点:为了让该类能表达线程的概念就继承了Thread类,在项目后续的更新迭代中我们发现:该类还有一个真正的父类,这个类就没法写了,因为继承的特点:一个类只能有一个父类但是可以有多个子类。因为Java语言只支持单继承。缺点也就是不利于后继代码的更新和维护。
-
案例题目:线程的创建和启动方式一(实际上也就是对Thread类中run方法的重写测试)。说明了为什么要调用start()方法,因为调用start()方法才表示又启动了一个线程,变成了多线程同时执行;而run()方法就是普通成员方法的调用而已。
package com.lagou.module05.task03; public class SubThreadRun extends Thread { @Override public void run() { // 此处没必要使用super.run();调用父类中的run方法,因为没有必要 // 打印1 ~ 20之间的所有整数 for (int i = 1; i <= 20; i++) { System.out.println("run方法中:i = " + i); // 1 2 ... 20 } } }
package com.lagou.module05.task03; public class SubThreadRunTest { public static void main(String[] args) { // 1.声明Thread类型的引用指向子类类型的对象 Thread t1 = new SubThreadRun(); // 多态 // 2.调用run方法测试,本质上就是相当于对 普通成员方法 的调用,因此 执行流程就是run方法的代码执行完毕后才能继续向下执行 //t1.run(); 多态调用run就是调用子类重写之后的run()方法 // 用于启动线程,Java虚拟机会 自动 调用该线程类中的run方法 // 相当于又启动了一个线程,加上执行main方法的线程(主线程)是两个线程 // 启动了线程之后,新启动的线程去调用Thread子类中的run方法,主线程继续往下走,因此也就交错执行了,这也就是交错执行的原因 // 执行结果是交错的结果,也就验证了两个线程同时执行,两个任务同时执行,这也就是多线程的意义 // 为什么要开多线程?目的是为了同时执行多个任务,此处同时在执行两个任务,此处达到效果也就正好是多线程的由来。 t1.start(); // 打印1 ~ 20之间的所有整数 // 让run方法和main方法同时输出1到20之间的所有整数 for (int i = 1; i <= 20; i++) { System.out.println("-----------------main方法中:i = " + i); // 1 2 ... 20 } } }
-
自定义类实现Runnable接口并重写run方法(Thread类继承自Object类,同时也实现了Runnable接口,Runnable接口中有一个方法叫做run()这也是Thread类中的run()方法的由来——实现了Runnable接口之后重写的,故而我们也可以越过Thread类直接去找Runnable接口),创建该类的对象作为实参来构造Thread类型的对象,然后使用Thread类型的对象调用start方法(因为start()方法是Thread类中的方法,Thread类中的方法也就需要Thread类的引用对象去调用)。方式二也就是直接实现Runnable接口,越过Thread类。我们自定义类虽然实现了Runnable接口,但是和Thread类之间并没有实际关系,所以它不能调start(),所以只能创建Thread类的对象,拿着Thread类的对象自己去调Thread类中的start方法。这样就合情合理了。
-
**案例题目:**线程的创建和启动方式二及源码分析。正确的接口实现类的命名规范:接口 + Impl。
package com.lagou.module05.task03; public class SubRunnableRun implements Runnable { @Override public void run() { // 打印1 ~ 20之间的所有整数 for (int i = 1; i <= 20; i++) { System.out.println("run方法中:i = " + i); // 1 2 ... 20 } } }
package com.lagou.module05.task03; public class SubRunnableRunTest { public static void main(String[] args) { // 1.创建自定义类型的对象,也就是实现Runnable接口类的对象 SubRunnableRun srr = new SubRunnableRun(); // 2.使用该对象作为实参构造Thread类型的对象 // 由源码可知:经过构造方法的调用之后,Thread类中的成员变量target的数值为srr。 Thread t1 = new Thread(srr); // 接口类型的参数作为形参的方法传参方式一:传入一个接口类型的实现类类型对象。 // 3.使用Thread类型的对象调用start方法 // 若使用Runnable引用构造了线程对象,调用该方法(run)时最终调用接口中的版本,也就是接口指向的引用的版本(实现类或者是匿名内部类) // 下方是去run方法的源码中找而不是start方法中的源码找,因为我们说的很清楚了Java虚拟机去调用run方法而不是start方法去调用 // 由run方法的源码可知:if (target != null) { // target.run(); // } // 此时target的数值不为空这个条件成立,执行target.run()的代码,也就是srr.run()的代码,因为构造方法中给成员变量target赋值为srr t1.start(); //srr.start(); Error 为什么创建了Runnable接口实现类的对象之后还要创建Thread类型的对象再去调用start方法呢?因为Runnable接口没有start方法,start方法属于Thread类 // 打印1 ~ 20之间的所有整数 // 为了体验交错的感觉:两个线程同时执行才有交错的感觉 for (int i = 1; i <= 20; i++) { System.out.println("-----------------main方法中:i = " + i); // 1 2 ... 20 } } }
-
-
相关的方法
方法声明 功能介绍 Thread() 使用无参的方式构造对象 Thread(String name) 根据参数指定的名称来构造对象,给与线程一个名字,否则名字是Thread-0、Thread-1等等。 Thread(Runnable target) 根据参数指定的引用来构造对象,其中Runnable是个接口类型 Thread(Runnable target, String name) 根据 参数指定引用 和名称来构造对象 void run() 若使用Runnable引用构造了线程对象,调用该方法时最终调用接口中的版本
若没有使用Runnable引用构造线程对象,调用该方法时则啥也不做
使用不同版本构造器创建的线程对象去调用run方法的效果不同。void start() 用于启动线程,Java虚拟机会自动调用该线程的run方法
是Java虚拟机调用run(),并不是线程源码中start()去调用run()方法。 -
案例题目:通过源码追踪证明由无参构造方法构造Thread类型的对象时,run方法确实啥也不干。
package com.lagou.module05.task03; public class ThreadTest { public static void main(String[] args) { // 1.使用无参方式构造Thread类型的对象 /** * public Thread() { * this(null, null, "Thread-" + nextThreadNum(), 0); 调用本类中的其它构造方法 * } * * 第一个参数是group,第二个参数是target,第三个参数是name,第四个参数是stackSize * * public Thread(ThreadGroup group, Runnable target, String name, * long stackSize) { * this(group, target, name, stackSize, null, true); * } * * 此时target就是null * * private Thread(ThreadGroup g, Runnable target, String name, * long stackSize, AccessControlContext acc, * boolean inheritThreadLocals) { * ... * this.target = target; 当成员变量与形参变量同名时,为了明确认定是成员变量,就加一个this.,此处也就是给成员变量赋值 * ... * } * 此处target也就是null */ // 由源码可知:Thread类中的成员变量target的数值为null。 Thread t1 = new Thread(); // 2.调用run方法进行测试 // 从打印的结果上来看,使用无参构造创建的对象调用run方法确实啥也不干 // 由源码可知:由于成员变量target的数值为null,因此条件if (target != null)不成立,跳过{}中的代码不执行 // 而run方法中除了上述代码再无代码,因此证明run方法确实啥也不干,也就是if() {}后没有别的代码了可以证明 /** * @Override * public void run() { * if (target != null) { 此处target一定是Thread类中的成员变量target * target.run(); * } * } */ t1.run(); // 3.打印一句话 System.out.println("我想看看你到底是否真的啥也不干!"); } }
-
执行流程
- 执行main方法的线程叫做主线程,执行run方法的线程叫做新线程/子线程。
- main方法是程序的入口(目前学过的所有Java代码都是从main方法入口),对于start方法之前的代码来说,由主线程执行一次(代码从上到下依次执行),当start方法调用成功后线程的个数由1个变成了2个,新启动的线程去执行run方法的代码,主线程继续向下执行,两个线程各自独立运行互不影响(交错打印)。
- 当run方法执行完毕后子线程结束,当main方法执行完毕后主线程结束。
- 两个线程执行没有明确的先后执行次序,由**操作系统调度算法**来决定。实际上说到底也就是前面所说的时间片轮转法,它们都是需要去抢时间片的,还得需要系统去调度。
-
方式的比较
- 继承Thread类的方式代码简单,但是若该类继承Thread类后则无法继承其它类(继承的特点),而实现 Runnable接口的方式代码复杂,但不影响该类继承其它类以及实现其它接口,因此以后的开发中推荐使用第二种方式。(没有特殊要求的话使用第二种方法)
-
匿名内部类的方式
-
**案例题目:**使用匿名内部类的方式来创建和启动线程。将我们之前所学的所有语法功底体现出来了。
package com.lagou.module05.task03; public class ThreadNoNameTest { public static void main(String[] args) { // 匿名内部类的语法格式:父类/接口类型 引用变量名 = new 父类/接口类型() { 方法的重写 }; // 1.使用继承加匿名内部类的方式创建并启动线程 /*Thread t1 = new Thread() { 此处变量名的价值无非就是为了调用start方法 @Override public void run() { System.out.println("张三说:在吗?"); } }; t1.start();*/ // 代码的优化 new Thread() { @Override public void run() { System.out.println("张三说:在吗?"); } }.start(); // 2.使用实现接口加匿名内部类的方式创建并启动线程 /*Runnable ra = new Runnable() { @Override public void run() { System.out.println("李四说:不在。"); } }; Thread t2 = new Thread(ra); t2.start();*/ // 代码优化 /*new Thread(new Runnable() { @Override public void run() { System.out.println("李四说:不在。"); } }).start();*/ // Java8开始支持lambda表达式: (形参列表)->{方法体;} /*Runnable ra = ()-> System.out.println("李四说:不在。"); new Thread(ra).start();*/ new Thread(()-> System.out.println("李四说:不在。")).start(); } }
-
线程的生命周期(熟悉)
- 新建状态 - 使用new关键字创建之后进入的状态,此时线程并没有开始执行。
- 就绪状态 - 调用start方法后进入的状态,此时线程还是没有开始执行。
- 运行状态 - 使用线程调度器调用该线程后进入的状态,此时线程开始执行,当线程的时间片执行完毕后任务没有完成时回到就绪状态,排队等待下一次调度。 时间片轮转法
- 消亡(消失死亡)状态 - 当线程的任务执行完成后进入的状态,此时线程已经终止。
- 阻塞状态 - 当线程执行的过程中发生了阻塞事件进入的状态,如:sleep方法(休眠,一旦调用了sleep方法之后,线程就不能再继续往下执行了,此时线程就开始呼呼大睡了)。 阻塞状态解除后进入就绪状态。CPU不会等着我们睡醒再执行,一旦我们进入阻塞状态之后,CPU就会调用别的线程让别的线程先执行。等阻塞结束之后,我们就进入就绪状态重新排队而不是立刻执行。因为别的线程已经在执行了,就算我们现在阻塞解除了也不能立刻马上拿到CPU的执行权。
- CPU如果看着我们呼呼大睡,CPU就不能被高效地利用,那么有些线程执行的性能就太低了,分配的时间片太短了,执行速度就慢了。
- 特别注意:线程一旦进入消亡状态之后就不可以再次启动,否则会发生IllegalThreadStateException异常异常。
线程的编号和名称(熟悉)
方法声明 | 功能介绍 |
---|---|
long getId() | 获取调用对象所表示线程的编号,相当于人的身份证号,是为了唯一标识该线程的 |
String getName() | 获取调用对象所表示线程的名称,相当于人的名字,名字可以重复,编号(身份证号)不可重复 |
void setName(String name) | 设置/修改线程的名称为参数指定的数值,没有setId(),因为ID是唯一标识,我们也没见过派出所可以随便修改身份证号 |
static Thread currentThread() | 获取当前正在执行线程的引用,类名.调用,返回一个Thread类型的引用 |
-
案例题目
自定义类继承Thread类并重写run方法,在run方法中先打印当前线程的编号和名称,然后将线程的名称修改为"zhangfei"后再次打印编号和名称。 要求在main方法中也要打印主线程的编号和名称。
继承Thread类的方式完成上述案例题题目
package com.lagou.module05.task03; public class ThreadIdNameTest extends Thread { public ThreadIdNameTest(String name) { super(name); // 表示调用父类的构造方法 当前线程类没有成员变量name } @Override public void run() { System.out.println("子线程的编号是:" + getId() + ",名称是:" + getName()); // 14 Thread-0(默认名称) guanyu // 修改名称为"zhangfei" setName("zhangfei"); System.out.println("修改后子线程的编号是:" + getId() + ",名称是:" + getName()); // 14 zhangfei } public static void main(String[] args) { ThreadIdNameTest tint = new ThreadIdNameTest("guanyu"); // 要使用有参构造方法必须重写构造方法 tint.start(); // tint.start(); // 编译ok,运行发生java.lang.IllegalThreadStateException异常 // 获取当前正在执行线程的引用,当前正在执行的线程是主线程,也就是获取主线程的引用 Thread t1 = Thread.currentThread(); System.out.println("主线程的编号是:" + t1.getId() + ", 名称是:" + t1.getName()); // 主线程的编号是:1, 名称是:main } }
实现Runnable接口的方法实现上述案例题目
package com.lagou.module05.task03; public class RunnableIdNameTest implements Runnable { @Override public void run() { // 此处使用的依旧是面向对象的那点东西 // 获取当前正在执行线程的引用,也就是子线程的引用 Thread t1 = Thread.currentThread(); // 此处不能直接调用,因为Runnable接口与Thread类之间没有任何联系 // 没有继承关系就先得到这个对象的引用,再通过引用.去调用就行了 System.out.println("子线程的编号是:" + t1.getId() + ", 名称是:" + t1.getName()); // 14 guanyu t1.setName("zhangfei"); System.out.println("修改后子线程的编号是:" + t1.getId() + ", 名称是:" + t1.getName()); // 14 zhangfei } public static void main(String[] args) { RunnableIdNameTest rint = new RunnableIdNameTest(); //Thread t2 = new Thread(rint); Thread t2 = new Thread(rint, "guanyu"); t2.start(); // 获取当前正在执行线程的引用,当前正在执行的线程是主线程,也就是获取主线程的引用 Thread t1 = Thread.currentThread(); System.out.println("主线程的编号是:" + t1.getId() + ", 名称是:" + t1.getName()); } }
常用的方法(重点)
方法声明 | 功能介绍 |
---|---|
static void yield() | 礼让,当前线程让出处理器/CPU(离开Running状态),使当前线程进入Runnable 状态等待。也就是我本来已经抢到时间片了,但是我调用该方法之后我就让出CPU,就告诉CPU说:哥们,我不急,你先去执行别的线程吧,然后我重新去排队。 该方法运行起来效果不明显,看不出来此处就不作测试了。 |
static void sleep(times) | 使当前线程从 Running 放弃处理器进入Block状态, 休眠times毫秒, 再返 回到Runnable。如果其他线程打断当前线程的Block(sleep), 就会发生 InterruptedException。 |
int getPriority() | 获取线程的优先级 |
void setPriority(int newPriority) | 修改线程的优先级。优先级越高的线程不一定先执行,但该线程获取到时间片的机会会更多 一些。 |
void join() | 等待该线程终止(该线程就是指引用调用对象,也就是谁调用join方法我就等待,这是一直等,我指的是主线程,也就是把子线程启动后和主线程的交错运行变成了顺序执行而已,与其它子线程无关,其它子线程与该子线程依旧是交错执行) |
void join(long millis) | 等待参数指定的毫秒数(等待参数指定的毫秒数,过去了还没结束就不等了) |
boolean isDaemon() | 用于判断是否为守护线程 |
void setDaemon(boolean on) | 用于设置线程为守护线程 |
- 测试sleep方法
package com.lagou.module05.task03;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
public class ThreadSleepTest extends Thread {
// 声明一个布尔类型的变量作为循环是否执行的条件
private boolean flag = true;
// 子类中重写的方法不能抛出更大的异常
@Override
public void run() {
// 每隔一秒获取一次系统时间并打印(无线循环),模拟时钟的效果
while (flag) { // 也可以使用for(;;)
// 获取当前系统时间并调整格式打印
// LocalDateTime.now();
Date d1 = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(d1));
// 睡眠1秒钟
try {
// 此处异常只能捕获不能抛出,因为子类重写的方法中不能抛出更大的异常
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThreadSleepTest tst = new ThreadSleepTest();
tst.start();
// 主线程等待5秒后结束子线程
System.out.println("主线程开始等待...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 停止子线程 过时 不建议使用(但是还是可以使用)
//tst.stop();
tst.flag = false;
// 可以使用flag的方法让线程停下来,像之前的张三说完,李四说用的也是flag
// 还有五子棋游戏中的黑手下完白手下。
System.out.println("主线程等待结束!");
}
}
-
线程优先级的管理:
- 优先级顾名思义就是优先执行的级别。不仅可以获取还可以修改线程的优先级。
变量和类型 字段 描述 static int MAX_PRIORITY 线程可以拥有的最大优先级。 10 static int MIN_PRIORITY 线程可以拥有的最低优先级。 1 static int NORM_PRIORITY 分配给线程的默认优先级。 5 最大优先级是10,最小优先级是1,一般的正常的优先级是5 优先级较高的线程不一定先执行,但是优先级越高的线程获取到CPU时间片的机会会更多一些
package com.lagou.module05.task03; public class ThreadPriorityTest extends Thread { @Override public void run() { //System.out.println("子线程的优先级是:" + getPriority()); // 5 10 优先级越高的线程不一定先执行。 // for (int i = 0; i < 20; i++) { System.out.println("子线程中:i = " + i); } } public static void main(String[] args) { ThreadPriorityTest tpt = new ThreadPriorityTest(); // 设置子线程的优先级 tpt.setPriority(Thread.MAX_PRIORITY); tpt.start(); Thread t1 = Thread.currentThread(); //System.out.println("主线程的优先级是:" + t1.getPriority()); // 5 普通的优先级 for (int i = 0; i < 20; i++) { System.out.println("--主线程中:i = " + i); } } }
-
测试两个join方法:主线程创建并启动子线程,如果自线程中要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。如果主线程想等待子线程执行完成之后再结束,比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到 join() 方法
package com.lagou.module05.task03; public class ThreadJoinTest extends Thread { @Override public void run() { // 模拟倒数10个数的效果 System.out.println("新年倒计时开始..."); for (int i = 10; i > 0; i--) { // 不能连着喊,肯定要1秒钟喊一次 System.out.println(i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("新年快乐!"); } public static void main(String[] args) { ThreadJoinTest tjt = new ThreadJoinTest(); tjt.start(); // 主线程开始等待 System.out.println("主线程开始等待..."); try { // 也就是说我们要等待谁就让谁去调用join方法 // 表示当前正在执行的线程对象等待调用线程对象,也就是主线程等待子线程终止 // 这个方法也就是让其它线程等该线程结束再执行 //tjt.join(); 你只要不结束,我就一直等,相当于等到海枯石烂 // 该方法也就是让其它线程等待该线程5秒之后再执行 tjt.join(5000); // 最多等待5秒,5秒该线程还没结束的话我就不等你了,就say goodbye // 主线程虽然不走了,但是我子线程该怎么执行还是怎么执行 // sleep是谁调用谁等待,而join方法是谁调用谁被等待,相当于其他线程在监视子线程,我看你结束不结束 } catch (InterruptedException e) { e.printStackTrace(); } //System.out.println("终于等到你,还好没放弃!"); System.out.println("可惜不是你,陪我到最后!"); } }
-
守护线程:守护线程有点像保镖的意思,守护线程是为了用来守护其他线程的,为其它线程提供服务的。下方案例是测试守护线程具体有什么作用。
package com.lagou.module05.task03; public class ThreadDaemonTest extends Thread { @Override public void run() { //System.out.println(isDaemon()? "该线程是守护线程": "该线程不是守护线程"); // 默认不是守护线程 // 当子线程不是守护线程时,虽然主线程先结束了,但是子线程依然会继续执行,直到打印完毕所有数据为止 // 当子线程是守护线程时,当主线程结束后,则子线程随之结束 // 也就是当主线程结束后,子线程发现:哎,主线程结束了,我也要结束,所以子线程的数据并没有打印完毕 // 至于主线程结束后,子线程还多打了很多数据可以这样来理解:踩刹车也是需要时间的,很少一脚把刹车踩到底。所以就是子线程发现主线程结束之后也想结束,就逮住这个时间多打印了几条数据。这是子线程设置为守护线程和不设置为守护线程的一个区别。 // 守护线程的价值 // 在以后的开发中如果希望主线程结束之后,该线程也随之结束,就把该线程也设置为守护线程。 // 在以后开发中如果希望主线程结束之后,该线程不会随之结束,就把该线程设置为非守护线程。 for (int i = 0; i < 50; i++) { System.out.println("子线程中:i = " + i); } } public static void main(String[] args) { ThreadDaemonTest tdt = new ThreadDaemonTest(); // setDaemon方法 必须 在线程启动之前设置子线程为守护线程,否则不好使 /** * 否则会报如下异常 * Exception in thread "main" java.lang.IllegalThreadStateException * at java.base/java.lang.Thread.setDaemon(Thread.java:1410) * at com.lagou.module05.task03.ThreadDaemonTest.main(ThreadDaemonTest.java:19) */ tdt.setDaemon(true); tdt.start(); // tdt.setDaemon(true); for (int i = 0; i < 20; i++) { System.out.println("-------主线程中:i = " + i); } } }
-
案例题目
编程创建两个线程,线程一负责打印1 ~ 100之间的所有奇数,其中线程二负责打印1 ~ 100之间的 所有偶数。
在main方法启动上述两个线程同时执行,主线程等待两个线程终止。
之前我们只启动一个线程,此处启动两个线程,而且干的活还不一样,这个可以和之前使用匿名内部类的方式;如果不用匿名内部类也无非就是写两个线程类。
当多个线程执行不同的代码时实际上只要写不同的线程类重写不同的run方法
继承方式
package com.lagou.module05.task03; public class SubThread1 extends Thread { @Override public void run() { // 打印1 ~ 100之间的所有奇数 for (int i = 1; i <= 100; i += 2) { System.out.println("子线程一中: i = " + i); } } }
package com.lagou.module05.task03; public class SubThread2 extends Thread { @Override public void run() { // 打印1 ~ 100之间的所有偶数 for (int i = 2; i <= 100; i += 2) { System.out.println("------子线程二中: i = " + i); } } }
package com.lagou.module05.task03; public class SubThreadTest { public static void main(String[] args) { SubThread1 st1 = new SubThread1(); SubThread2 st2 = new SubThread2(); st1.start(); st2.start(); System.out.println("主线程开始等待..."); try { // 下方子线程调用join,只是让主线程等待当前两个子线程结束而已,如果只有一个子线程调用join方法,那么另外一个子线程不会等待它结束之后再执行,而是和它 交替执行 ,即:在主线程的方法体中不管有无调用join,或者是否同时都调用了join方法,子线程都是交替执行的。而主线程会等待,当然,都没有调用join方法的时候主线程也是与它们交替执行。 // 大致可以理解为,在谁的执行方法的方法体中调用了join方法,谁就得等;而谁调用了sleep方法,谁就得等。 // 我们一般都是在main方法中调用join方法,故而都是主线程等待其它线程先执行完再执行。 st1.join(); st2.join(); // Thread.currentThread().join(); 主线程如果也调用join方法,在main方法中join方法之后的代码都不会执行 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("主线程等待结束!"); } }
实现Runnable接口方法
package com.lagou.module05.task03; public class SubRunnable1 implements Runnable { @Override public void run() { // 打印1 ~ 100之间的所有奇数 for (int i = 1; i <= 100; i += 2) { System.out.println("子线程一中: i = " + i); } } }
package com.lagou.module05.task03; public class SubRunnable2 implements Runnable { @Override public void run() { // 打印1 ~ 100之间的所有偶数 for (int i = 2; i <= 100; i += 2) { System.out.println("------子线程二中: i = " + i); } } }
package com.lagou.module05.task03; public class SubRunnableTest { public static void main(String[] args) { SubRunnable1 sr1 = new SubRunnable1(); SubRunnable2 sr2 = new SubRunnable2(); Thread t1 = new Thread(sr1); Thread t2 = new Thread(sr2); t1.start(); t2.start(); System.out.println("主线程开始等待..."); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("主线程等待结束!"); } }
使用匿名内部类的方式就自己练一下即可
线程同步机制(重点)
-
基本概念
-
线程同步的原因:当多个线程同时访问同一种共享资源时(比如:同时对一个变量赋值),可能会造成数据的覆盖等不一致性问题(也就是多个线程同时抢占共享资源),此时就需要对线程之间进行通信和协调(比如:线程A先执行,执行完之后线程B再执行),这种协调通信的机制就叫做线程的同步机制。
-
多个线程并发读写同一个临界资源时会发生线程并发安全问题。
-
异步操作:多线程并发的操作,各自独立运行。
-
同步操作:多线程串行的操作,先后执行的顺序。
-
通俗易懂一点就是:线程同步将原本多个线程之间的并发操作锁住变成了线程的串行操作。本来一起去访问共享资源,现在协调一下,让它们一个一个的去访问。这就叫线程同步机制。
-
案例题目:证明两个账户(线程)再同时访问一个共享资源的时候肯会造成数据的覆盖等不一致性问题。下方以两个人分别拿着银行卡与对应的存折进行取款操作为例。
package com.lagou.module05.task03; import java.util.concurrent.locks.ReentrantLock; public class AccountRunnableTest implements Runnable { private int balance; // 用于描述账户的余额 // set中进行合理值判断、有参构造器中调用set方法对成员变量进行赋值此处就不写了 public AccountRunnableTest() { } public AccountRunnableTest(int balance) { this.balance = balance; } public int getBalance() { return balance; } public void setBalance(int balance) { this.balance = balance; } @Override public void run() { // 1.模拟从后台查询账户余额的过程,下面这行代码相当于从后台查出账户余额赋给一个临时变量temp int temp = getBalance(); // temp = 1000 temp = 1000 // 2.模拟取款200元的过程 if (temp >= 200) { System.out.println("正在出钞,请稍后..."); temp -= 200; // temp = 800 temp = 800 try { // 模拟5秒数钱出钞的过程 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("请取走您的钞票!"); } else { System.out.println("余额不足,请核对您的账户余额!"); } // 3.模拟将最新的账户余额写入到后台,下行代码就是将temp的值写入更新后台数据库 setBalance(temp); // balance = 800 balance = 800 } public static void main(String[] args) { AccountRunnableTest account = new AccountRunnableTest(1000); // 开户的时候给一个账户余额 // 模拟两人同时使用银行卡以及与之对应的存折进行取款操作,然后发生了数据不一致性问题 // 此处相当于开辟了两条线程 Thread t1 = new Thread(account); Thread t2 = new Thread(account); t1.start(); // t1一启动,Java虚拟机会自动调用该线程的run方法,而t1是拿着Runnable引用去构造的,此处调start最终调的run实际上是接口引用所指向的类中的run方法,此处不多解释,上方追溯果源码 t2.start(); // 几乎在t1执行run方法的同时,t2也启动了,t2也过来取一下账户余额(此时仍是1000,因为t1在呼呼大睡了——调用了sleep方法,之后的更新操作没有执行完),线程2判断大于200,也执行了取款操作,t1中的temp此时是800,t2中的temp此时也是800,t2此时也开始呼呼大睡了——执行到sleep方法。由于线程一是先睡的,同样的时间,线程一先睡,线程二后睡,那么,线程一先起来。然后线程一给balance的值设为800。此时,线程二也睡醒了,也给balance赋值为了800。最终,我们的balance实际上就赋值为了800。 System.out.println("主线程开始等待..."); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } // 经过线程一线程二共同的操作之后,balance最终变成了800 System.out.println("最终的账户余额为:" + account.getBalance()); // 800 800 } }
-
-
解决方案
-
由程序结果可知:当两个线程同时对同一个账户进行取款时,导致最终的账户余额不合理。有几率发生两个线程同时取款200元之后,原本有1000元的账户还剩下800元。
-
引发原因(根本原因):线程一执行取款时还没来得及将取款后的余额写入更新到后台,线程二就已经开始取款,拿到的是取款操作没有结束之前的账户余额,所以导致之后的账户余额不合理了。 也就是线程一还没有取款结束将最终的余额写进去,线程二就开始取款了。
-
解决方案:让线程一执行完毕取款操作后,再让线程二执行即可,将线程的并发操作改为串行操作。
-
经验分享:在以后的开发尽量减少串行操作的范围,从而提高效率。
-
简单代码优化
package com.lagou.module05.task03; import java.util.concurrent.locks.ReentrantLock; public class AccountRunnableTest implements Runnable { private int balance; // 用于描述账户的余额 // set中进行合理值判断、有参构造器中调用set方法对成员变量进行赋值此处就不写了 public AccountRunnableTest() { } public AccountRunnableTest(int balance) { this.balance = balance; } public int getBalance() { return balance; } public void setBalance(int balance) { this.balance = balance; } @Override public void run() { // 1.模拟从后台查询账户余额的过程,下面这行代码相当于从后台查出账户余额赋给一个临时变量temp int temp = getBalance(); // temp = 1000 temp = 1000 // 2.模拟取款200元的过程 if (temp >= 200) { System.out.println("正在出钞,请稍后..."); temp -= 200; // temp = 800 temp = 800 try { // 模拟5秒数钱出钞的过程 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("请取走您的钞票!"); } else { System.out.println("余额不足,请核对您的账户余额!"); } // 3.模拟将最新的账户余额写入到后台,下行代码就是将temp的值写入更新后台数据库 setBalance(temp); // balance = 800 balance = 800 } public static void main(String[] args) { AccountRunnableTest account = new AccountRunnableTest(1000); // 开户的时候给一个账户余额 // 模拟两人同时使用银行卡以及与之对应的存折进行取款操作,然后发生了数据不一致性问题 // 此处相当于开辟了两条线程 Thread t1 = new Thread(account); Thread t2 = new Thread(account); t1.start(); // t2.start(); System.out.println("主线程开始等待..."); try { t1.join(); // 虽然这样可以解决这个问题,但是开发中不会这么干。原因是:多线程的意义在于让多个线程同时干活,说白了就是为了同时执行多个任务,这也就是我们创建多线程的意义。 // 此处创建完两个线程之后,线程一先启动执行,执行完之后;线程二再启动执行,这样做多线程就没有了意义。这就像我们拥有同一个账户的两个人中一个出门,另一个就不能出门了,直到出门的那个回来了,剩下的这个才能够出门 // 下面这种方式还不如不创建多线程,直接让主线程把两个run方法一调就行了,还创建什么线程? t2.start(); // 也就是等待线程一取款操作结束后再启动线程二,线程二再取款,这样也就可以避免这个错误的发生了。 t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } // 经过线程一线程二共同的操作之后,balance最终变成了800 System.out.println("最终的账户余额为:" + account.getBalance()); // 800 800 } } // 解决方法:不要站在我家门口不让我出门,而是站在银行大门前,为了不让我们同时取款,不允许我们二者同时进,相对好一点。至少可以同时出门了,线程一去干这个,我可能有点别的其它的事。这样我们就互不影响,直到我们二者同时来到银行门口的时候不允许我们二者同时进,防止我们一起取款。总比把我挡在家门口要好很多。
-
-
实现方式
- 在Java语言中使用synchronized关键字(就理解为现实生活中的一把锁即可)来实现同步/对象锁机制从而保证线程执行的原子性(线程执行的这段代码已经是最小单位了,要么就不执行,要么就给我全部执行完,中间不允许给我打断,因为已经是最小单位了,不允许再分割了),具体方式如下:
1、使用 同步代码块 的方式实现 部分代码的锁定(愿意锁哪段代码就把锁定的代码放到大括号里面去),格式如下:
synchronized(类类型的引用) { // 只要是一个类类型的引用变量就行,无论是什么类型都无所谓。这是Java中强大的地方,所有对象都支持锁,都可以当做锁来用。专业术语——同步监视器
// 同步监视器必须是同一个类类型的同一个引用,否则锁不住
// 加锁/加同步代码块的方式:ctr + alt + T
编写所有需要锁定的代码;
}
2、使用 同步方法 的方式实现 所有代码 的锁定。
直接使用 synchronized 关键字来 修饰整个方法 即可
该方式 等价于:
synchronized(this) { 整个方法体的代码 }
synchronized 关键字的作用就是相当于一把锁
把一段代码锁起来,让这个线程要么不执行,要么执行就给我执行完,在执行的过程中不允许打断。
-
案例题目:实现Runnable接口并使用同步代码块优化上述案例。
package com.lagou.module05.task03; import java.util.concurrent.locks.ReentrantLock; public class AccountRunnableTest implements Runnable { private int balance; // 用于描述账户的余额 private Demo dm = new Demo(); // set中进行合理值判断、有参构造器中调用set方法对成员变量进行赋值此处就不写了 public AccountRunnableTest() { } public AccountRunnableTest(int balance) { this.balance = balance; } public int getBalance() { return balance; } public void setBalance(int balance) { this.balance = balance; } @Override public void run() { // 下一行代码验证了两个线程同时启动和先终止完一个线程再启动另一个线程的区别 System.out.println("线程" + Thread.currentThread().getName() + "已启动..."); synchronized (dm) { // ok 这一行代码就相当于是把下面代码全部加了一把锁锁起来了 // 锁起来的好处就是:不允许两个线程同时执行代码块中的代码,也就是两个线程可以同时启动 //synchronized (new Demo()) { // 锁不住 要求必须是同一个对象 相当于每一个线程都有一把锁,而非总共只有一把锁,每一个都加把锁,最先进去的就出不来了 // 引用变量的好处:new 完对象就一个对象,所有的线程都共用这一个对象,相当于所有线程共用一把锁,就把这锁住了 // 1.模拟从后台查询账户余额的过程,下面这行代码相当于从后台查出账户余额赋给一个临时变量temp int temp = getBalance(); // temp = 1000 temp = 1000 // 2.模拟取款200元的过程 if (temp >= 200) { System.out.println("正在出钞,请稍后..."); temp -= 200; // temp = 800 temp = 800 try { // 模拟5秒数钱出钞的过程 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("请取走您的钞票!"); } else { System.out.println("余额不足,请核对您的账户余额!"); } // 3.模拟将最新的账户余额写入到后台,下行代码就是将temp的值写入更新后台数据库 setBalance(temp); // balance = 800 balance = 800 } } public static void main(String[] args) { AccountRunnableTest account = new AccountRunnableTest(1000); // 开户的时候给一个账户余额 Thread t1 = new Thread(account); Thread t2 = new Thread(account); //Thread t2 = new Thread(account2); t1.start(); t2.start(); System.out.println("主线程开始等待..."); try { t1.join(); //t2.start(); // 也就是等待线程一取款操作结束后再启动线程二 t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("最终的账户余额为:" + account.getBalance()); // 600 800 } } // 这是告诉大家一个.java文件中可以有多个类,只不过public修饰的公共类只能有一个,并且类名要与文件名相同 class Demo{}
-
案例题目:用继承的方式使用同步代码块也能实现同步的操作。
package com.lagou.module05.task03; public class AccountThreadTest extends Thread { private int balance; // 用于描述账户的余额 // Demo类在此处可以用是因为我们创建这个类时没有加访问修饰符是默认权限,故而在本包中都可以使用 private static Demo dm = new Demo(); // 隶属于类层级,所有对象共享同一个,否则每个线程对象都有一个dm,用的不是同一个dm,意味着就锁不住了 // 但是现在多个线程意味着是多个账户,其实也合理,但是此处是在讨论线程同步的问题,只关心它锁不锁得住 public AccountThreadTest() { } public AccountThreadTest(int balance) { this.balance = balance; } public int getBalance() { return balance; } public void setBalance(int balance) { this.balance = balance; } @Override public void run() { System.out.println("线程" + Thread.currentThread().getName() + "已启动..."); synchronized (dm) { // ok //synchronized (new Demo()) { // 锁不住 要求必须是同一个对象 // 1.模拟从后台查询账户余额的过程 int temp = getBalance(); // temp = 1000 temp = 1000 // 2.模拟取款200元的过程 if (temp >= 200) { System.out.println("正在出钞,请稍后..."); temp -= 200; // temp = 800 temp = 800 try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("请取走您的钞票!"); } else { System.out.println("余额不足,请核对您的账户余额!"); } // 3.模拟将最新的账户余额写入到后台 setBalance(temp); // balance = 800 balance = 800 } test(); } public static void main(String[] args) { // 此处开了两个线程相当于是开了两个户,而不是像上面的代码一样只开了一个户 AccountThreadTest att1 = new AccountThreadTest(1000); att1.start(); AccountThreadTest att2 = new AccountThreadTest(1000); att2.start(); System.out.println("主线程开始等待..."); try { att1.join(); //t2.start(); // 也就是等待线程一取款操作结束后再启动线程二 att2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("最终的账户余额为:" + att1.getBalance()); // 800 } }
-
案例题目:使用同步代码块的方式来实现所有代码的锁定(使用实现Runnable接口的方式)
package com.lagou.module05.task03; import java.util.concurrent.locks.ReentrantLock; public class AccountRunnableTest implements Runnable { private int balance; // 用于描述账户的余额 private Demo dm = new Demo(); private ReentrantLock lock = new ReentrantLock(); // 准备了一把锁 // set中进行合理值判断、有参构造器中调用set方法对成员变量进行赋值此处就不写了 public AccountRunnableTest() { } public AccountRunnableTest(int balance) { this.balance = balance; } public int getBalance() { return balance; } public void setBalance(int balance) { this.balance = balance; } @Override public /*synchronized*/ void run() {// 使用同步方法的方式相当于是把所有的代码锁起来了 // 由源码可知:最终是account对象来调用run方法,因此当前正在调用的对象就是account,也就是说this就是account synchronized (this) { // ok 同步方法的方式等价于这个 当前正在调用run方法的对象是account,因为run方法的源码是使用target.run()(target != null 时),故而此处的this是account // 故而this是否锁得住只要看两个线程对象的account是否是同一个即可,而synchronized(this)能锁住的话,synchronized同步方法也能锁住,也就证明了在此处二者是等价的 // 下一行代码验证了两个线程同时启动和先终止完一个线程再启动另一个线程的区别 System.out.println("线程" + Thread.currentThread().getName() + "已启动..."); //synchronized (dm) { // ok 这一行代码就相当于是把下面代码全部加了一把锁锁起来了 // 锁起来的好处就是:不允许两个线程同时执行代码块中的代码,也就是两个线程可以同时启动 //synchronized (new Demo()) { // 锁不住 要求必须是同一个对象 相当于每一个线程都有一把锁,而非总共只有一把锁,每一个都加把锁,最先进去的就出不来了 // 引用变量的好处:new 完对象就一个对象,所有的线程都共用这一个对象,相当于所有线程共用一把锁,就把这锁住了 // 1.模拟从后台查询账户余额的过程,下面这行代码相当于从后台查出账户余额赋给一个临时变量temp int temp = getBalance(); // temp = 1000 temp = 1000 // 2.模拟取款200元的过程 if (temp >= 200) { System.out.println("正在出钞,请稍后..."); temp -= 200; // temp = 800 temp = 800 try { // 模拟5秒数钱出钞的过程 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("请取走您的钞票!"); } else { System.out.println("余额不足,请核对您的账户余额!"); } // 3.模拟将最新的账户余额写入到后台,下行代码就是将temp的值写入更新后台数据库 setBalance(temp); // balance = 800 balance = 800 //} } public static void main(String[] args) { // 此处是创建一个账户使用两个线程 AccountRunnableTest account = new AccountRunnableTest(1000); // 开户的时候给一个账户余额 //AccountRunnableTest account2 = new AccountRunnableTest(1000); Thread t1 = new Thread(account); Thread t2 = new Thread(account); //Thread t2 = new Thread(account2); // 此处两个线程传入两个对象,this不相同,synchronized(this)无法锁住,synchronized同步方法此时也无法锁住。由此证明一点:synchronized修饰方法和synchronized(this)确实是等价的关系。 t1.start(); t2.start(); System.out.println("主线程开始等待..."); try { t1.join(); //t2.start(); // 也就是等待线程一取款操作结束后再启动线程二 t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("最终的账户余额为:" + account.getBalance()); // 600 800 } } // 这是告诉大家一个.java文件中可以有多个类,只不过public修饰的公共类只能有一个,并且类名要与文件名相同 class Demo{}
-
总结:以后开发中到底使用同步方法,还是使用同步代码块。取决于我们到底是锁定部分代码还是锁定所有代码。当然同步代码块中也可以使用sychronized(this),这样就不用去创建Demo这个类,也不用去创建Demo这个类的对象了,也很省心。
-
案例题目:使用同步代码块的方式来实现所有代码的锁定(使用继承Thread类的方式)。继承Thread类实现同步方法的应用。
package com.lagou.module05.task03; public class AccountThreadTest extends Thread { private int balance; // 用于描述账户的余额 // Demo类在此处可以用是因为我们创建这个类时没有加访问修饰符是默认权限,故而在本包中都可以使用 private static Demo dm = new Demo(); // 隶属于类层级,所有对象共享同一个,否则每个线程对象都有一个dm,用的不是同一个dm,意味着就锁不住了 // 但是现在多个线程意味着是多个账户,其实也合理,但是此处是在讨论线程同步的问题,只关心它锁不锁得住 public AccountThreadTest() { } public AccountThreadTest(int balance) { this.balance = balance; } public int getBalance() { return balance; } public void setBalance(int balance) { this.balance = balance; } @Override // 此处不能加static关键字,因为这个方法是对父类方法的重写,父类中的方法没有加static关键字,故而此处也不能加static关键字 public /*static*/ /*synchronized*/ void run() { // 此处使用synchronized修饰方法锁不住,因为两个this不一样 // 此处是两个不同的对象来调用run方法 // run方法不能加static关键字,那么我们可以把run方法的功能提取出来放到一个static修饰的方法里面。 // 此处只要加写好的test方法即可,这种方法成功锁住了 test(); } // 静态方法也可以使用synchronized关键字修饰,也就是锁定 // 现在我们就把它提炼出来成为一个静态方法了,此时方法上可以加synchronized关键字 public /*synchronized*/ static void test() { synchronized (AccountThreadTest.class) { // 该类型对应的Class对象,由于类型是固定的,因此Class对象也是唯一的,因此可以实现同步 System.out.println("线程" + Thread.currentThread().getName() + "已启动..."); //synchronized (dm) { // ok //synchronized (new Demo()) { // 锁不住 要求必须是同一个对象 // 1.模拟从后台查询账户余额的过程 // 此处不能调用getBalance()方法的原因:不能在静态上下文中访问非静态的成员。 int temp = 1000; //getBalance(); // temp = 1000 temp = 1000 // 2.模拟取款200元的过程 if (temp >= 200) { System.out.println("正在出钞,请稍后..."); temp -= 200; // temp = 800 temp = 800 try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("请取走您的钞票!"); } else { System.out.println("余额不足,请核对您的账户余额!"); } // 我们此处只是验证这个方法到底锁不锁得住而已,故而我们可以暂时忽略掉get\set方法不是静态方法的问题。 // 3.模拟将最新的账户余额写入到后台 //setBalance(temp); // balance = 800 balance = 800 } } public static void main(String[] args) { AccountThreadTest att1 = new AccountThreadTest(1000); att1.start(); AccountThreadTest att2 = new AccountThreadTest(1000); att2.start(); // 此处是new了两个不同的线程对象,二者都start调用run方法,此时由于是两个对象,run方法中的this也就不相同了。因此synchronized放在run方法上也就锁不住了 // 因为synchronized修饰方法等价于synchronized(this) // 解决方法:和之前使用对象锁不住一样,之所以锁不住是因为每new一个对象都有一个独有的dm Demo对象,所以我们加了static关键字。现在要让方法能锁住,此处也得加static关键字 System.out.println("主线程开始等待..."); try { att1.join(); //t2.start(); // 也就是等待线程一取款操作结束后再启动线程二 att2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("最终的账户余额为:" + att1.getBalance()); // 800 } }
-
**总结:**实现同步方法的应用的时候,如果只是在run方法上添加synchronized关键字的话不足以锁住。因为创建并使用的是两个对象,故而两个对象调用start方法来调用run方法中的this是不一致的。总而言之就是一句话,在实现Runnable接口的时候要使用同步方法时可以是非静态方法,此时sychronized修饰方法等价于synchronized(this);在继承Thread类时使用同步方法必须是静态方法,否则锁不住,此时synchronized修饰方法等价于synchronized(类名.class)。
-
静态方法的锁定
- 当我们对一个静态方法加锁,如:
- public synchronized static void xxx(){….}
- 等价于synchronized(类名.class){ … }
- 那么该方法锁的对象是类对象。每个类都有唯一的一个类对象。获取类对象的方式:类名.class。
- 静态方法与非静态方法同时使用了synchronized后它们之间是非互斥关系的。
- 原因在于:静态方法锁的是类对象而非静态方法锁的是当前方法所属对象。
- 当我们对一个静态方法加锁,如:
-
注意事项
- 使用synchronized保证线程同步应当注意:
- 多个需要同步的线程在访问同步块时,看到的应该是同一个锁对象引用。 同一个对象才能锁住,不同对象锁不住的。
- 在以后开发中使用同步块时应当尽量减少同步范围以提高并发的执行效率。锁定的代码越少越好,因为锁定的越多,串行的代码、串行的时间就会越长。锁定的代码越少,并发执行的代码就会越多,效率也就越高。
- 使用synchronized保证线程同步应当注意:
-
线程安全类和不安全类
- StringBuffer类是线程安全的类,但StringBuilder类不是线程安全的类。
- Vector类 和 Hashtable类是线程安全的类,但ArrayList类和HashMap类不是线程安全的类。
- Collections.synchronizedList() 和 Collections.synchronizedMap()等方法实现安全。
- 以StringBuffer和StringBuilder为例,查一下源码发现:StringBuffer几乎每一个方法上都加了synchronized关键字,这样线程就是安全的,一个进去了执行该方法,其它线程就要在外边等着。StringBuilder线程不安全是因为类中没有一个方法是加了synchronized关键字的。不考虑多线程、只有一个线程进行访问的时候使用StringBuilder,因为它效率高。如果是多个线程访问,要考虑到线程安全的问题的时候就使用StringBuffer。
- 如果不考虑多线程,我们使用ArrayList和HashMap;如果考虑线程安全问题,我们也不使用Vector和HashTable,因为它们已经过时了,就使用Collections.synchronizedList() 和 Collections.synchronizedMap()这两个工具方法将非线程安全的这两个类转换为线程安全的进一步使用。由此一来Vector和HashTable就彻底的淘汰了。
- Collections.synchronizedList 和 Collections.synchronizedMap中的所有方法和ArrayList和HashMap中的方法都是一样的,且方法上都加了synchronized关键字
-
死锁的概念
-
线程一执行的代码:
public void run(){ synchronized(a){ //持有对象锁a,等待对象锁b synchronized(b){ 编写锁定的代码; } } }
-
线程二执行的代码:
public void run(){ synchronized(b){ //持有对象锁b,等待对象锁a synchronized(a){ 编写锁定的代码; } } }
上述两个线程同时启动会引发死锁现象,也就是锁死了。我拿着a等待b,你拿着b等待a,我告诉你:你赶紧把b锁给我吧,你不给我我里面的代码执行不完,我的a锁也不能自动释放。然后你告诉我:不好意思,你不把a锁给我,我这里面的代码执行不完我的锁b一时半会也不能自动释放。这样一来就锁死了。
-
注意:
在以后的开发中尽量减少同步的资源(锁定的代码越少越好),减少同步代码块的嵌套结构的使用(就是不要在同步代码块里边再去使用同步代码块)!这也是synchronized补充的几个问题。
-
-
使用Lock(锁)实现线程同步(实现同步的第二种方式)
-
基本概念
- 从Java5开始提供了更强大的线程同步机制—使用显式定义的同步锁对象来实现。
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。作用:实现线程同步,实现共享资源的锁定。
- 该接口的主要实现类是ReentrantLock类,该类拥有与synchronized相同的并发性,在以后的线程 安全控制中,经常使用ReentrantLock类显式加锁和释放锁。
-
常用的方法
方法声明 功能介绍 ReentrantLock() 使用无参方式构造对象 void lock() 获取锁(加锁) void unlock() 释放锁(解锁) -
初步使用Lock锁
package com.lagou.module05.task03; import java.util.concurrent.locks.ReentrantLock; public class AccountRunnableTest implements Runnable { private int balance; // 用于描述账户的余额 private ReentrantLock lock = new ReentrantLock(); // 准备了一把锁 // set中进行合理值判断、有参构造器中调用set方法对成员变量进行赋值此处就不写了 public AccountRunnableTest() { } public AccountRunnableTest(int balance) { this.balance = balance; } public int getBalance() { return balance; } public void setBalance(int balance) { this.balance = balance; } @Override public void run() { // 开始加锁 lock.lock(); // 好比上厕所,进厕所之后直接把门反锁了 System.out.println("线程" + Thread.currentThread().getName() + "已启动..."); // 1.模拟从后台查询账户余额的过程,下面这行代码相当于从后台查出账户余额赋给一个临时变量temp int temp = getBalance(); // temp = 1000 temp = 1000 // 2.模拟取款200元的过程 if (temp >= 200) { System.out.println("正在出钞,请稍后..."); temp -= 200; // temp = 800 temp = 800 try { // 模拟5秒数钱出钞的过程 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("请取走您的钞票!"); } else { System.out.println("余额不足,请核对您的账户余额!"); } // 3.模拟将最新的账户余额写入到后台,下行代码就是将temp的值写入更新后台数据库 setBalance(temp); // balance = 800 balance = 800 lock.unlock(); // 实现解锁 快速把事办完,打开门出来。别人就可以进来上厕所了。 // synchronized关键字相当于是自动释放锁,而使用Lock是手动释放。 } public static void main(String[] args) { AccountRunnableTest account = new AccountRunnableTest(1000); // 开户的时候给一个账户余额 Thread t1 = new Thread(account); Thread t2 = new Thread(account); t1.start(); t2.start(); System.out.println("主线程开始等待..."); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("最终的账户余额为:" + account.getBalance()); // 600 800 } }
-
与synchronized方式的比较
- Lock是显式锁,需要手动实现开启和关闭操作,而synchronized是隐式锁,执行完锁定代码后自动释放。
- Lock只有同步代码块方式的锁,而synchronized有同步代码块方式和同步方法两种锁。
- 使用Lock锁方式时,Java虚拟机将花费较少的时间来调度线程(因为我们手动地去控制了锁定和释放),因此性能更好。
-
-
Object类常用的方法
方法声明 功能介绍 void wait() 用于使得线程进入等待(阻塞)状态,直到其它线程调用notify()或notifyAll()方法 void wait(long timeout) 用于进入等待状态,直到其它线程调用方法或参数指定的毫秒数已经过去为止 void notify() 用于唤醒等待的单个线程 void notifyAll() 用于唤醒等待的所有线程 上述方法是用来实现线程通信的,即:线程和线程之间进行交互的。比如说:线程A进入wait()状态了,我们要线程B去把它唤醒。
案例题目:启动两个线程,让二者去打印1~100之间的整数。要求:线程一和线程二之间来回切换着打印,你一下我一下的打印。
package com.lagou.module05.task03; // 测试线程通信 // 要启动两个线程进行通信,就得访问同一个共享资源,使用同一个对象,所以此处就使用实现Runnable接口的方法进行测试 public class ThreadCommunicateTest implements Runnable { private int cnt = 1; // 让两个线程交错的打印一个变量 @Override public void run() { while (true) { synchronized (this) { // 为了让线程同步,不发生取款问题,我这边还没取完,你那边就开始取 // 把它锁起来,让任意一个时刻只有一个线程能执行里面的代码 // 不能把无限循环锁起来,否则它会一直占着锁,让第二个线程无法抢占资源,让后线程一直接将代码执行完了 // 每当有一个线程进来后先大喊一声,调用notify方法,把另外一个线程叫醒 // 已经进入sychronized中,不怕其它线程抢占资源 // 如果有很多线程就使用notifyAll()方法 notify(); // 不管条件是否成立,只要进来就喊。 if (cnt <= 100) { // 只要cnt的值不到100就一直打印 // notify();如果在这里调用唤醒方法,最后还会发生阻塞,程序不会结束,因为这里是先判断条件再唤醒,如果打印完100了,条件就不满足了,就无法再唤醒了,就有一个线程直接阻塞在这里了。 System.out.println("线程" + Thread.currentThread().getName() + "中:cnt = " + cnt); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } cnt++; // 当前线程打印完毕一个整数后,为了防止继续打印下一个数据,则调用wait方法 try { // 让自己先睡一下,不要抢占资源 wait(); // 当前线程进入阻塞状态,自动释放对象锁,必须在锁定的代码中调用 } catch (InterruptedException e) { e.printStackTrace(); } } else { break; } } } } public static void main(String[] args) { ThreadCommunicateTest tct = new ThreadCommunicateTest(); Thread t1 = new Thread(tct); t1.start(); Thread t2 = new Thread(tct); t2.start(); } } 无论是wait()方法还是notify()方法都必须在锁定的内部使用。因为wait()方法阻塞之后会自动释放对象锁,如果不在同步代码块中还释放啥?无处释放,就有异常了。
-
生产者和消费者模型
-
生产者和消费者模型问题是线程这一块非常著名的问题,而且是非常有难度的一个问题。 生产者看作是一个线程,消费者看作是一个线程,仓库实际上就是这两个线程共同访问的共享资源。任意时候访问仓库的线程有且只有一个,这样就要把仓库锁起来。共享的就意味着生产者拥有的仓库,消费者也要拥有,即有且只拥有一个仓库。
-
案例题目:模拟生产者消费者模型实现过程
package com.lagou.module05.task03; /** * 编程实现仓库类 */ public class StoreHouse { private int cnt = 0; // 用于记录产品的数量:达到10个就满了,0个就空了 // 生产者生产产品 public synchronized void produceProduct() { notify(); if (cnt < 10) { // 只要产品小于10就可以接着生产 System.out.println("线程" + Thread.currentThread().getName() + "正在生产第" + (cnt+1) + "个产品..."); cnt++; } else { try { wait(); // wait()只能在锁定的代码中使用,故而把方法加上synchronized关键字 } catch (InterruptedException e) { e.printStackTrace(); } } } // 消费者消费产品 public synchronized void consumerProduct() { notify(); if (cnt > 0) { // 只要产品大于0个就继续消费 System.out.println("线程" + Thread.currentThread().getName() + "消费第" + cnt + "个产品"); cnt--; } else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }
package com.lagou.module05.task03; /** * 编程实现生产者线程,不断地生产产品 */ public class ProduceThread extends Thread { // 声明一个仓库类型的引用作为成员变量,是为了能调用调用仓库类中的生产方法 合成复用原则 private StoreHouse storeHouse; // 为了确保两个线程共用同一个仓库,这才是共享同一个资源 // 生产者消费者两个线程必须共用同一个线程对象,所以这里我们仓库对象不能再这里自己new,通过构造方法传进来即可 public ProduceThread(StoreHouse storeHouse) { this.storeHouse = storeHouse; } @Override public void run() { // 不断地去做一件事情就while(true)无限循环呗 while (true) { storeHouse.produceProduct(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
package com.lagou.module05.task03; /** * 编程实现消费者线程,不断地消费产品 */ public class ConsumerThread extends Thread { // 声明一个仓库类型的引用作为成员变量,是为了能调用调用仓库类中的生产方法 合成复用原则 private StoreHouse storeHouse; // 为了确保两个线程共用同一个仓库 public ConsumerThread(StoreHouse storeHouse) { this.storeHouse = storeHouse; } @Override public void run() { while (true) { storeHouse.consumerProduct(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
package com.lagou.module05.task03; public class StoreHouseTest { public static void main(String[] args) { // 创建仓库类的对象 StoreHouse storeHouse = new StoreHouse(); // 创建线程类对象并启动 ProduceThread t1 = new ProduceThread(storeHouse); ConsumerThread t2 = new ConsumerThread(storeHouse); t1.start(); t2.start(); } }
-
线程池(熟悉)
-
实现Callable接口(创建线程的第三种方式)
-
从Java5开始新增加创建线程的第三种方式为实现java.util.concurrent.Callable接口。
-
如果希望执行线程的任务之后有一个返回结果就使用实现Callable接口的方式
-
常用的方法如下:
方法声明 功能介绍 V call() 计算结果并返回,V是泛型
-
-
FutureTask类
-
java.util.concurrent.FutureTask类用于描述可取消的异步计算,该类提供了Future接口的基本实 现,包括启动和取消计算、查询计算是否完成以及检索计算结果的方法,也可以用于获取方法调用后的返回结果。 也就是说可以通过这个类得到Callable接口中call方法的返回值结果。FutureTask类实际上是Runnable接口间接的实现类,故而我们可以通过它创建线程对象。
-
常用的方法如下:
方法声明 功能介绍 FutureTask(Callable callable) 根据参数指定的引用来创建一个未来任务 V get() 获取call方法计算的结果 -
案例题目:编程实现线程的第三种创建方式(通过实现Callable接口 + Futuretask类)
package com.lagou.module05.task03; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class ThreadCallableTest implements Callable { @Override // 此处返回值为Object是因为Callable接口是泛型的但是此处我们没有给泛型,和之前讲集合一样 public Object call() throws Exception { // 计算1 ~ 10000之间的累加和并打印返回 int sum = 0; for (int i = 1; i <= 10000; i++) { sum +=i; } System.out.println("计算的累加和是:" + sum); // 50005000 return sum; // 自动装箱技术,int自动装箱为Integer,Integer类型是Object的子类类型,当然可以直接返回出去 // 也就是多态的第三种使用场合 } public static void main(String[] args) { ThreadCallableTest tct = new ThreadCallableTest(); FutureTask ft = new FutureTask(tct); Thread t1 = new Thread(ft); t1.start(); Object obj = null; try { obj = ft.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println("线程处理方法的返回值是:" + obj); // 50005000 } }
-
-
线程池的由来
- 在服务器编程模型(可能涉及到大量客户端连接服务器的场景)的原理,每一个客户端连接用一个单独的线程为之服务,当与客户端的会话结束时,线程也就结束了,即每来一个客户端连接,服务器端就要创建一个新线程。 前面说了自动装箱池、常量池。
- 如果访问服务器的客户端很多,那么服务器要不断地创建和销毁线程,这将严重影响服务器的性能。
-
概念和原理
- 线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中。 避免了大量线程的创建和销毁,也就提高了服务器的性能。
- 在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务 后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
- 由于线程池里的线程很多,意味着我们可以同时给线程池下发很多的任务由线程池再去分配给单独的空闲的线程去执行就可以了。
-
相关类和方法
-
从Java5开始提供了线程池的相关类和接口:java.util.concurrent.Executors类和 java.util.concurrent.ExecutorService接口。
-
其中Executors是个工具类和线程池的工厂类(是个工具类,专门创建并返回线程池的),可以创建并返回不同类型的线程池,常用方法如下:(调用下面方法之后我们就得到了一个线程池对象了,池子里面也已经有线程了,就可以往里面提交任务,给线程布置任务了)
方法声明 功能介绍 static ExecutorService newCachedThreadPool() 创建一个可根据需要创建新线程的线程池,必要时创建新线程,空闲线程会保留60秒 static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程 池 static ExecutorService newSingleThreadExecutor() 创建一个只有一个线程的线程池 -
其中ExecutorService接口是真正的线程池接口,主要实现类是ThreadPoolExecutor,常用方法 如下:
方法声明 功能介绍 void execute(Runnable command) 执行任务和命令,通常用于执行Runnable,给线程布置的是run类型的任务 Future submit(Callable task) 执行任务和命令,通常用于执行,给线程布置的是call类型的任务 Callable void shutdown() 启动有序关闭,等池子把所有任务做完,就使用shutdown方法关闭线程池。 -
线程池的简单使用:线程池怎么用:无非就是我们派任务,我们只需要把线程要执行的任务代码交给它线程池自动去管理完成相应的功能。当然在我们的开发中,等我们学了框架之后我们就不需要再写这样的代码,框架里面把线程封装的会更好,包括线程池的应用。很多时候也把这种方式认为是创建线程的第四种方式,问到的时候我们一定要答得出来。
package com.lagou.module05.task03; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolTest { public static void main(String[] args) { // 1.创建一个线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); // 2.向线程池中布置任务 executorService.submit(new ThreadCallableTest()); // 3.关闭线程池 executorService.shutdown(); } }
-
-
-
线程池的7个核心参数以及它们的含义
-
线程池种如果核心线程都被占用了,此时有新的请求过来会发生什么?那拒绝策略有哪些?如果我们用了无界队列,那么他有什么坏处呢?
-
String类是被final修饰的,你了解final的原理吗?
-
GC了解多少呢?
-
什么对象会进入到老年代?
-
CMS和G1的区别
-
如何进行JVM调优?
总结
主线程执行main方法,子线程执行run方法。