一.线程概述
1.进程:正在执行的程序作为一个进程,进程负责了这个程序的内存空间的划分。
2.疑问1:Windows号称是多任务的操作系统,那么Windows是同时运行多个应用程序吗?
(1) 从宏观的角度:windows确实是在同时运行多个应用程序。
(2) 从微观角度:cpu是做了一个快速切换执行的动作,由于速度太快,所以我们感觉不到在切换而已。
3.线程:线程在一个进程中负责了代码的执行,就是进程中一个执行路径,(一个进程中的代码是由线程去执行的)。
如上图所示,图上的一键优化与垃圾清理在同时运行,在一个进程中同时执行了多个任务。
4.进程与线程的区别:
(1) 进程负责了一个程序的内存空间的分配;
(2) 线程负责了一个程序的执行路径;
5.多线程:在一个进程中有多个线程同时在执行不同的任务。
6.疑问2:既然线程负责了代码的执行,那么我们之前没有学过线程,为什么代码可以执行呢?
因为运行任何一个java程序,jvm在运行的时候都会创建一个main线程去执行main方法中所有代码。
7.疑问3:一个Java应用程序至少有几个线程?
至少有两个线程,一个是主线程负责main方法代码的执行,一个是垃圾回收器线程,负责了回收垃圾。
8.多线程的好处:
(1) 解决了一个进程里面能同时执行多个任务(执行路径)的问题。
(2) 提高了资源的利用率,而不是提高了效率
9.多线程的弊端:
(1) 增加cpu的负担。因为对线程进行管理需要额外的CPU开销,线程的使用会给系统带来上下文切换的额外负担。
(2) 降低了一个进程中线程的执行概率。
(3) 引发了线程安全问题。
(4) 出现了死锁现象。
二.创建线程的方式
1.方式一:继承Thread类。
(1) 步骤:
①自定义一个类继承Thread类;
②重写Thread类的run方法 , 把自定义线程的任务的代码写在run方法中;
③创建Thread的子类对象,并且调用start方法开启线程。
(2) 疑问:重写run方法的目的是什么?
因为每个线程都有自己的任务代码,jvm创建的主线程的任务代码就是main方法中的所有代码, 自定义线程的任务代码就写在run方法中,自定义线程负责了run方法中代码。
(3) 注意: 一个线程一旦开启,那么线程就会执行run方法中的代码,run方法千万不能直接调用,直接调用run方法就相当调用了一个普通的方法而已并没有开启新的线程。
(4) 实例一
public class Demo1 extends Thread {
@Override //把自定义线程的任务代码写在run方法中。
public void run() {
for(int i = 0 ; i < 3 ; i++){
System.out.println("自定义线程:"+i);
}
}
public static void main(String[] args) {
//创建了自定义的线程对象。
Demo1 d = new Demo1();
//调用start方法启动线程
d.start();
for(int i = 0 ; i < 3 ; i++){
System.out.println("main线程:"+i);
}
}
}
运行结果如下图所示:
(5)实例二
①需求:模拟QQ视频与聊天可以同时进行。
②分析:首先需要创建两个线程,视频和聊天同时进行。
③实例:
class TalkThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("聊天中....");
}
}
}
class VideoThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("视频中....");
}
}
}
public class Demo2 {
public static void main(String[] args) {
TalkThread talkThread = new TalkThread();
talkThread.start();
VideoThread videoThread = new VideoThread();
videoThread.start();
}
}
④运行结果:
2.方式二:实现Runnable接口。
(1) 步骤:
①自定义一个类实现Runnable接口;
②重写Runnable接口实现类的run方法,把自定义线程的任务代码定义在run方法上;
③创建Runnable实现类对象;
④创建Thread类的对象,并且把Runnable实现类的对象作为实际参数,传递给Thread类构造方法;
⑤调用Thread对象的start方法开启一个线程。
(2) 疑问1:请问Runnable实现类的对象是线程对象吗?
Runnable实现类的对象并不是一个线程对象,只不过是实现了Runnable接口的对象而已。只有是Thread或者是Thread的子类才是线程对象。因为如果是线程对象,必须得具备一个start()方法可以开启一个线程。
(3) 疑问2:为什么要把Runnable实现类的对象作为实参传递给Thread对象呢?作用是什么?
因为自定义的Runnable实现类中的run方法所属对象是Runnable接口的子类对象,所以要让线程去执行指定对象的run方法。作用就是把Runnable实现类的对象的run方法作为了Thread线程的任务代码去执行了。
(4) 疑问3:如何理解Runnable?
Thread类可以理解为一个工人,而Runnable的实现类的对象就是这个工人的工作(通过构造方法传递),Runnable接口中只有一个run方法,该方法中定义的是会被新线程执行的代码。当我们把Runnable的子类对象传递给Thread的构造方法时,实际上就是让Thread取得run方法,就是给Thread一项任务。
(5) 实例:
public class Demo3 implements Runnable{
@Override
public void run() {
for(int i = 0 ; i < 3 ; i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
public static void main(String[] args) {
//创建Runnable实现类的对象
Demo3 d = new Demo3();
//创建Thread类的对象, 把Runnable实现类对象作为实参传递。
Thread thread = new Thread(d,"张三"); //Thread类使用Target变量记录了d对象,
//调用thread对象的start方法开启线程。
//不能再通过d.start()来开启一个线程,因为start()方法是属于thread类的,而现在是实现了Runnable接口,这个接口只有一个run()方法,这个类又没有定义start()方法。
thread.start();
for(int i = 0 ; i < 3 ; i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
运行结果如下图所示:
三.线程的生命周期状态图
1.CPU的等待资格:CPU在切换过程中,有可能会切换到这里执行你的代码,这时候你就具备等待CPU的资格,就好像学生有等待老师问问题的资格一样,
2.CPU的执行权:CPU目前正在执行你的代码,这时候你的线程就具备了CPU的执行权,如果没有执行到他,那么就只具备等待的资格。
3.线程的生命周期状态图详解
(1) 一个线程一旦被new出来后,这时候处于创建状态,创建状态下的线程什么都不具备,既不具备等待资格,又不具备执行权,这时候只是new了一个线程对象,还没有调用start方法;所以这个线程还没有开启,没有开启的线程是处于创建状态;
(2) 处于创建状态的线程一旦调用了start方法,这个线程马上就进入可运行的状态,可运行状态下的线程是具备cpu的等待资格,不具备cpu的执行权;
(3) 处于可运行状态下的线程一旦抢夺到了cpu的执行权,这时候它就可以进入运行状态,运行状态下的线程具备了cpu的执行权,也具备了cpu的等待资格;
(4) 处于运行状态下的线程完成了任务后,这时候它就进入了死亡状态,死亡状态下的线程既不具备等待资格,又不具备执行资格;
(5) 运行状态下的线程除了可能会完成任务,还有一种可能是被其他线程抢走CPU的执行权,一旦被抢走就会进入可运行状态。所以运行状态和可运行状态是可以互相切换的;
(6) 还有一种状态叫做临时阻塞状态,运行状态下的线程是有可能进入临时阻塞状态的,临时阻塞状态下的线程不具备等待资格。
四.线程常用的方法
1.Thread(String name);
(1) 该方法用于初始化线程的名字。
(2) 线程是有默认的名字的,如下所示:
public class Demo2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 3 ; i++) {
System.out.println(i);
}
}
public static void main(String[] args) {
//创建了一个线程对象
Demo2 d = new Demo2();
System.out.println("线程的名字:"+d.getName());
Demo2 d1 = new Demo2();
System.out.println("线程的名字:"+d1.getName());
}
}
运行结果如下图所示:
(3) 实例
public class Demo3 extends Thread{
public Demo3(String name){
super(name); //调用了Thread类的一个 参数的构造方法。
}
@Override
public void run() {
for (int i = 0; i < 3 ; i++) {
System.out.println(i);
}
}
public static void main(String[] args) {
//创建了一个线程对象
Demo3 d= new Demo3("张三");
System.out.println("线程的名字:"+d.getName());
}
}
运行结果如下图所示:
2.start();
(1) 该方法用于启动一个线程;
(2) 实例:
public class Demo4 extends Thread {
public Demo4(String name){
super(name); //调用了Thread类的一个 参数的构造方法。
}
@Override
public void run() {
for (int i = 0; i < 5 ; i++) {
System.out.println(this.getName()+":"+i);
}
}
public static void main(String[] args) {
//创建了一个线程对象
Demo4 d= new Demo4("张三");
d.start();
}
}
(3) 运行结果:
3.getName();
该方法用于返回线程的名字。
4.setName(String name);
(1) 该方法用于设置线程对象名。
(2) 实例:
//也可以通过setName设置线程的名字
public class Demo5 extends Thread {
public Demo5(String name){
super(name); //调用了Thread类的一个 参数的构造方法。
}
@Override
public void run() {
for (int i = 0; i < 5 ; i++) {
System.out.println(this.getName()+":"+i);
}
}
public static void main(String[] args) {
//创建了一个线程对象
Demo5 d= new Demo5("张三");
d.setName("李四");//设置线程的名字
d.start();//开启线程
}
}
(3) 运行结果:
5.sleep();
(1) 该方法返回线程睡眠指定的毫秒数,是一个静态的方法,哪个线程执行了sleep方法的代码那么就是哪个线程睡眠。
(2) 注意:是哪个线程执行了sleep方法的代码就是哪个线程睡眠,千万不要理解成哪个线程调用了sleep方法那么就是哪个线程睡眠。
(3) 实例一
①实例:
public class Demo6 extends Thread {
public Demo6(String name){
super(name); //调用了Thread类的一个 参数的构造方法。
}
@Override
public void run() {
for (int i = 0; i < 5 ; i++) {
System.out.println(this.getName()+":"+i);
}
}
public static void main(String[] args) throws InterruptedException {
//创建了一个线程对象
Demo6 d= new Demo6("张三");
d.sleep(1000);
d.setName("李四");//设置线程的名字
d.start();//开启线程
}
}
②运行结果:
③疑问:上述代码有两个线程,一个主线程,一个张三的线程,那么当执行到d.sleep(1000);的时候到底是哪一个线程在睡眠?
答:这句代码是由主线程来执行的(因为主方法的所有代码都是由主线程来执行的),所以不管是哪个对象调用sleep方法都是主线程在睡眠,如果把上面的代码移动到重写的run方法中,那么就是张三的线程在睡眠。
(4) 实例二
①实例:
public class Demo7 extends Thread {
public Demo7(String name){
super(name); //调用了Thread类的一个 参数的构造方法。
}
@Override
public void run() {
for (int i = 0; i < 5 ; i++) {
System.out.println(this.getName()+":"+i);
try {
Thread.sleep(1000); //为什么在这里不能抛出异常,只能捕获?? Thread类的run方法没有抛出异常类型,所以子类不能抛出异常类型。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
//创建了一个线程对象
Demo7 d= new Demo7("张三");
d.setName("李四");//设置线程的名字
d.start();//开启线程
}
}
②运行结果:
6.currentThread();
(1) 该方法用于返回当前的线程对象,该方法是一个静态的方法。
(2) 注意:哪个线程执行了currentThread()代码就返回哪个线程的对象。
(3) 实例一:
public class Demo8 extends Thread{
public Demo8(String name){
super(name); //调用了Thread类的一个 参数的构造方法。
}
@Override
public void run() {
for (int i = 0; i < 5 ; i++) {
System.out.println(this.getName()+":"+i);
}
}
public static void main(String[] args) throws InterruptedException {
//创建了一个线程对象
Demo7 d= new Demo7("张三");
d.setName("李四");//设置线程的名字
d.start();//开启线程
Thread mainThread = Thread.currentThread();//获取当前的线程对象,现在的当前线程对象主线程的线程对象,因为这里的代码是主线程执行的,
System.out.println("主线程的名字:"+mainThread.getName());
}
}
(4) 运行结果:
(5) 实例二:
public class Demo9 extends Thread {
public Demo9(String name){
super(name); //调用了Thread类的一个 参数的构造方法。
}
@Override
public void run() {
System.out.println("this:"+this);
System.out.println("当前对象:"+Thread.currentThread());//this和当前对象是同一个对象
}
public static void main(String[] args) throws InterruptedException {
//创建了一个线程对象
Demo9 d= new Demo9("张三");
d.setName("李四");//设置线程的名字
d.start();//开启线程
Thread mainThread = Thread.currentThread();//获取当前的线程对象,现在的当前线程对象主线程的线程对象,因为这里的代码是主线程执行的,
System.out.println("主线程的名字:"+mainThread.getName());
}
}
(6) 运行结果:
7.getPriority();
(1) 该方法用于返回当前线程对象的优先级,默认线程的优先级是5;
(2) 实例:
public class Demo10 extends Thread{
public Demo10(String name){
super(name); //调用了Thread类的一个 参数的构造方法。
}
@Override
public void run() {
for (int i = 0; i < 3 ; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
public static void main(String[] args) {
Demo10 d = new Demo10("张三");
System.out.println("自定义线程的优先级:"+d.getPriority()); //线程的优先级默认是5
System.out.println("主线程的优先级:"+Thread.currentThread().getPriority());
}
}
(3) 运行结果:
8.setPriority(int newPriority);
(1) 该方法用于设置线程的优先级,虽然设置了线程的优先级,但是具体的实现取决于底层的操作系统的实现(最大的优先级是10 ,最小的优先级是1,默认是5)。
(2) 设置线程的优先级。优先级的数字越大,优先级越高,优先级的范围是1~10。
(3) 现在给了张三最大的优先级,张三就一定会先执行完毕吗?
答:不一定,虽然他的优先级别比主线程要高,只不过是他先执行完的概率大点而已。概率大并不代表一定是这样。
(4) 实例:
public class Demo11 extends Thread{
public Demo11(String name){
super(name); //调用了Thread类的一个 参数的构造方法。
}
@Override
public void run() {
for (int i = 0; i < 3 ; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
public static void main(String[] args) {
Demo11 d = new Demo11("张三");
d.start();
d.setPriority(10); //设置线程 的优先级。 优先级的数字越大,优先级越高 , 优先级的范围是1~10
for (int i = 0; i < 3 ; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
(5) 运行结果:
五.线程安全问题
1.实例
(1) 需求:模拟3个窗口同时在售4张票 。
(2) 分析:三个窗口同时在售一辆车的票,也就是需要开启三个线程。
(3) 实例:
class SaleTicket extends Thread {
int num = 4;
public SaleTicket(String name) {
super(name);
}
@Override
public void run() {
while(true){
if(num>0){
System.out.println(Thread.currentThread().getName()+"售出了第"+num+"号票");
num--;
}else{
System.out.println("售罄了..");
break;
}
}
}
}
public class Demo1 {
public static void main(String[] args) {
//创建三个线程对象,模拟三个窗口
SaleTicket thread1 = new SaleTicket("窗口1");
SaleTicket thread2 = new SaleTicket("窗口2");
SaleTicket thread3 = new SaleTicket("窗口3");
//开启线程售票
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果如下图所示:
(4) 问题1:为什么4张票被卖出了12次?
①出现的原因:因为num是非静态的,非静态的成员变量数据是在每个对象中都会维护一份数据的,创建三个线程对象就会有三份数据存在内存中。
②解决方案:把num票数共享出来给三个线程对象使用。使用static修饰。
(5) 问题2: 出现了线程安全问题 ?
线程安全问题的解决方案:sun提供了线程同步机制让我们解决这类问题的。
(6) 改进后的实例:
class SaleTicket implements Runnable{
int num = 4; // 票数,是一个非静态成员变量,每创建一个对象都需要维护一份数据
@Override
public void run() {
while(true){
synchronized ("锁") {
if(num>0){
System.out.println(Thread.currentThread().getName()+"售出了第"+ num+"号票");
num--;
}else{
System.out.println("售罄了..");
break;
}
}
}
}
}
public class Demo4 {
public static void main(String[] args) {
//创建了一个Runnable实现类的对象
SaleTicket saleTicket = new SaleTicket();//现在只创建了一个SaleTicket对象,所以只有一份数据,也就是这份数据共享给三个线程使用,所以num数据不需要共享。
//创建三个线程对象模拟三个窗口
Thread thread1 = new Thread(saleTicket,"窗口1");
Thread thread2 = new Thread(saleTicket,"窗口2");
Thread thread3 = new Thread(saleTicket,"窗口3");
//开启线程售票
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果如下图所示:
2.出现线程安全问题的根本原因:
(1) 存在两个或者两个以上 的线程对象,而且线程之间共享着一个资源;
(2) 有多个语句操作了共享资源。
六.Java线程同步机制
Java提供了两种实现线程同步机制的方式:
1.方式一:同步代码块。
(1) 同步代码块的格式:
synchronized(锁对象){
需要被同步的代码...
}
(2) 同步代码块要注意事项:
① 任意的一个对象都可以做为锁对象。
② 在同步代码块中调用了sleep方法并不会释放锁对象的。
③ 只有真正存在线程安全问题的时候才使用同步代码块,否则会降低效率的。因为每次执行之前都要判断锁对象的状态是开还是关。
④ 多线程操作的锁对象必须 是唯一共享 的。否则无效。如果写成synchronized (new Object())是不能解决线程安全问题的,因为如果Object没有使用static修饰的话,在三个线程中的三个对象中,每一个对象都维护了一个Object对象,线程1进去后只会判断自身的锁对象,线程2自身也戴了一把锁,它判断自己的锁状态是开就也会进去
⑤ 如果是synchronized ("锁")也是可以锁住的,因为双引号引起来的字符串一旦存在了字符串常量池中就不会再创建了,永远都会使用字符串常量池中的锁。
(3) 同步代码块解决线程安全问题
①实例:
class SaleTicket extends Thread{
static int num = 4;//票数 因为非静态的成员变量,非静态的成员变量数据是在每个对象中都会维护一份数据的。因为创建了三个对象,所以应该声明为静态的
static Object o = new Object();
public SaleTicket(String name) {
super(name);
}
@Override
public void run() {
while(true){
//同步代码块
synchronized ("o") {
if(num>0){
System.out.println(Thread.currentThread().getName()+"售出了第"+num+"号票");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
num--;
}else{
System.out.println("售罄了..");
break;
}
}
}
}
}
public class Demo4 {
public static void main(String[] args) {
//创建三个线程对象,模拟三个窗口
SaleTicket thread1 = new SaleTicket("窗口1");
SaleTicket thread2 = new SaleTicket("窗口2");
SaleTicket thread3 = new SaleTicket("窗口3");
//开启线程售票
thread1.start();
thread2.start();
thread3.start();
}
}
②分析:
假设线程2先抢到了CPU的执行权,线程2先进入到同步代码块中,凡是线程到了同步代码块中,都要首先看下锁的状态,目前锁的状态是开着的,就意味着线程2能进去,线程2进去以后,马上会把锁的状态改成关闭,然后执行到输出语句的时候,假设此时线程1抢到了CPU的执行权,线程1执行到同步代码块是,发现锁的状态是关着的,此时线程1就需要在外面待着。线程3抢到CPU的执行权时,锁的状态依然是关着的,所以也需要在外等待。此时CPU的执行权又交给了线程2,一旦线程2执行到了同步代码块结束的位置,锁的状态又会恢复为开的状态,这时候这三个线程又可以同时抢夺CPU的执行权了。
③运行结果:
2.方式二:同步函数。
(1) 同步函数就是使用synchronized修饰一个函数。
(2) 同步函数要注意的事项:
① 如果是一个非静态的同步函数的锁,它的锁对象是this对象,如果是静态的同步函数的锁, 它的锁对象是当前函数所属的类的字节码文件(class对象)。
② 同步函数的锁对象是固定的,不能由你来指定 的。
(3) 实例:
① 需求:一个银行账户有5000块,一对夫妻一个拿着存折,一个拿着卡,开始取钱比赛,每次只能取一千块,要求不准出现线程安全问题。
② 实例:
class BankThread extends Thread{
static int count = 5000;
public BankThread(String name){
super(name);
}
@Override //
public synchronized void run() {
while(true){
synchronized ("锁") {
if(count>0){
System.out.println(Thread.currentThread().getName()+"取走了1000块,还剩余"+(count-1000)+"元");
count= count - 1000;
}else{
System.out.println("取光了...");
break;
}
}
}
}
//静态的函数---->函数所属 的类的字节码文件对象--->BankThread.class 唯一的。
public static synchronized void getMoney(){
}
}
public class Demo1 {
public static void main(String[] args) {
//创建两个线程对象
BankThread thread1 = new BankThread("老公");
BankThread thread2 = new BankThread("老婆");
//调用start方法开启线程取钱
thread1.start();
thread2.start();
}
}
③运行结果:
3.推荐使用:同步代码块。
原因:
(1) 同步代码块的锁对象可以由我们随意指定,方便控制。同步函数的锁对象是固定的,不能由我们来指定。
(2) 同步代码块可以很方便控制需要被同步代码的范围,同步函数必须是整个函数 的所有代码都被同步了。
七.死锁现象
1.java中的同步机制解决了线程安全问题,但是也同时引发死锁现象。
2.注意:死锁现象并不是一定会出现的。
3.死锁现象出现的根本原因:
(1) 存在两个或者两个以上的线程。
(2) 存在两个或者两个以上的共享资源。
4.死锁现象的解决方案:没有方案,只能尽量避免发生而已。
5.实例:
class DeadLock extends Thread{
public DeadLock(String name){
super(name);
}
public void run() {
if("张三".equals(Thread.currentThread().getName())){
synchronized ("遥控器") {
System.out.println("张三拿到了遥控器,准备 去拿电池!!");
synchronized ("电池") {
System.out.println("张三拿到了遥控器与电池了,开着空调爽歪歪的吹着...");
}
}
}else if("李四".equals(Thread.currentThread().getName())){
synchronized ("电池") {
System.out.println("李四拿到了电池,准备去拿遥控器!!");
synchronized ("遥控器") {
System.out.println("李四拿到了遥控器与电池了,开着空调爽歪歪的吹着...");
}
}
}
}
}
public class Demo2 {
public static void main(String[] args) {
DeadLock thread1 = new DeadLock("张三");
DeadLock thread2 = new DeadLock("李四");
//开启线程
thread1.start();
thread2.start();
}
}
(1) 分析:
假设张三先抢到CPU的执行权,当他执行到同步代码块时,拿到遥控器这个锁对象后,会把这个锁关闭,然后就输出"张三拿到遥控器,准备去拿电池",他刚输出这句话以后,李四抢到了CPU的执行权,李四一进来就拿到了电池锁,把电池锁的状态改成了关闭,李四就输出了"李四拿到了电池,准备去拿遥控器",假设这时候李四仍然占据着CPU的执行权,那么它就会去拿遥控器锁,发现遥控器锁是关着的,此时李四就在外面待着;这时张三抢到了CPU的执行权,张三去拿电池时,发现电池锁也是关着的。此时,两个人既进不去又出不了同步代码块,这就出现了张三在等李四的电池,李四在等张三的遥控器,这就引发了相互等待资源的情况,这也就是我们所说的死锁现象。
(2) 运行结果:
由以上运行结果可以知道:死锁现象并不是一定会出现的。
八.线程通讯
1.当一个线程完成了自己的任务时,要通知另外一个线程去完成另外一个任务。最经典的例子是生产者与消费者的例子。
2.线程通讯常用的方法:
(1) wait(): 等待。如果线程执行了wait方法,那么该线程会进入等待的状态,等待状态下的线程必须要被其他线程调用notify方法才能唤醒。一旦执行了wait方法是会释放锁对象的。
(2) notify():唤醒。唤醒线程池等待线程其中的一个。
(3) notifyAll() : 唤醒线程池所有等待线程。
3.wait与notify方法要注意的事项:
(1) wait方法与notify方法是属于Object对象 的。因为锁对象可以是任意的一个对象,如果把这两个方法设计在Thread下,那么就不会是任意的对象了。
(2) wait方法与notify方法必须要在同步代码块或者是同步函数中才能 使用。
因为如果不存在同步代码块或者同步函数,就没有锁这个概念存在,而这两个方法一定要由锁对象调用。
(3) wait方法与notify方法必需要由锁对象调用。因为它是要以锁对象为标识符建立一个线程池的。不同的锁调用wait方法是会建立一个不同的线程池的。
4.生产者和消费者实例
(1) 生产者和消费者说明:
(2) 实例:
//产品类
class Product {
String name; //名字
double price; //价格
boolean flag = false; //产品是否生产完毕的标识,默认情况是没有生产完成。
}
//生产者
class Producer extends Thread {
Product p ; //产品
public Producer(Product p) {
this.p = p ;
}
@Override
public void run() {
int i = 0 ;
while(true){
synchronized (p) {
if(p.flag==false){
if(i%2==0){
p.name = "苹果";
p.price = 6.5;
}else{
p.name="香蕉";
p.price = 2.0;
}
System.out.println("生产者生产出了:"+ p.name+" 价格是:"+ p.price);
p.flag = true;
i++;
p.notifyAll(); //唤醒消费者去消费
}else{
//已经生产 完毕,等待消费者先去消费
try {
p.wait(); //生产者等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
//消费者
class Customer extends Thread{
Product p;
public Customer(Product p) {
this.p = p;
}
@Override
public void run() {
while(true){
synchronized (p) {
if(p.flag==true){ //产品已经生产完毕
System.out.println("消费者消费了"+p.name+" 价格:"+ p.price);
p.flag = false;
p.notifyAll(); // 唤醒生产者去生产
}else{
//产品还没有生产,应该 等待生产者先生产。
try {
p.wait(); //消费者也等待了...
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public class Demo5 {
public static void main(String[] args) {
Product p = new Product(); //产品
//创建生产对象
Producer producer = new Producer(p);
//创建消费者
Customer customer = new Customer(p);
//调用start方法开启线程
producer.start();
customer.start();
}
}
(3) 运行结果:
(4) 生产者与消费者的wait与notify方法说明:
①假设生产者先拿到了CPU的执行权,经过判断后,拿到了锁对象,p.flag==false,继续向下执行,当生产完苹果并输出后p.flag=true,执行到p.notify()时,此时没有线程在等待,没有线程等待时调用notify方法时不会有任何影响,就好像我们去旅游,旅馆的工作人员每天都会叫早,每个房间敲一次门,如果里面有人就会被吵醒,如果没人的话也不会有影响。所以当没有线程等待时,执行notify方法后不会有任何的影响,此时它继续拿到CPU的执行权,当执行判断条件时发现p.flag==true,不满足条件就会执行wait方法,进行等待,这时候JVM就会建立一个线程池,这个线程池是以锁对象为标识符的,这个锁对象就是p,也就是以p为标识符建立一个线程池,线程池就类似一个数组一样,里面存放着线程,生产者执行wait方法后,这个线程就会进入以锁对象为标识符建立的线程池中进行等待,一旦调用了wait方法就会释放锁对象。
②这时候消费者拿到了CPU的执行权,拿到锁后,满足条件就消费,并且把flag改为false,当它执行到notify方法时,就会唤醒以锁对象p为标识符建立的线程池中等待的线程中的其中一个,线程池中只有生产者一个线程,那就会把生产者唤醒。消费者继续拿到CPU的执行权,这时候它继续判断,发现p.flag==false,执行wait方法,进入线程池中,这样就可以实现生产一个消费一个的需求。
③如果消费者一开始就拿到了CPU的执行权,它拿到了锁对象后,发现p.flag==false,还没有生产,这时候它就会进入线程池中等待。这时候把CPU的执行权交给生产者,生产者开始生产。
九.线程的停止
1.停止一个线程 我们一般都会通过一个变量去控制的。
2.如果需要停止一个处于等待状态下的线程,那么我们需要通过变量配合notify方法或者interrupt()来使用。
3.实例:
public class Demo6 extends Thread {
boolean flag = true;
public Demo6(String name){
super(name);
}
@Override
public synchronized void run() {
int i = 0 ;
while(flag){
try {
this.wait(); //张三等待..
} catch (InterruptedException e) {
System.out.println("接收到了异常了....");
}
System.out.println(Thread.currentThread().getName()+":"+i);
i++;
}
}
public static void main(String[] args) {
Demo6 d = new Demo6("张三");
d.setPriority(10);
d.start();
for(int i = 0 ; i<5 ; i++){
System.out.println(Thread.currentThread().getName()+":"+i);
//当主线程的i是3的时候停止张三线程。
if(i==3){
d.flag = false;
d.interrupt(); //把线程的等待状态强制清除,被清除状态的线程会接收到一个InterruptedException。
}
}
}
}
运行结果如下图所示:
十.守护线程
1.守护线程也称为后台线程,在一个进程中如果只剩下 了守护线程,那么守护线程也会死亡。
2.一个线程默认都不是守护线程。
3.实例:
(1) 需求:模拟下载更新包。
(2) 实例:
public class Demo7 extends Thread {
public Demo7(String name){
super(name);
}
@Override
public void run() {
for(int i = 1 ; i<=100 ; i++){
System.out.println("更新包目前下载"+i+"%");
if(i==100){
System.out.println("更新包下载完毕,准备安装..");
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Demo7 d = new Demo7("后台线程");
//d.setDaemon(true); //setDaemon() 设置线程是否为守护线程,true为守护线程, false为非守护线程。
// System.out.println("是守护线程吗?"+ d.isDaemon()); //判断线程是否为守护线程。
d.start();
}
}
(3) 运行结果如下图所示:
注意:默认情况下的线程不是守护线程。
4.守护线程实例
(1) 分析:如果是守护线程,那么当主线程结束时,守护线程也会结束,守护线程会停止下载。
(2) 实例:
public class Demo7 extends Thread {
public Demo7(String name){
super(name);
}
@Override
public void run() {
for(int i = 1 ; i<=100 ; i++){
System.out.println("更新包目前下载"+i+"%");
if(i==100){
System.out.println("更新包下载完毕,准备安装..");
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Demo7 d = new Demo7("后台线程");
d.setDaemon(true); //setDaemon() 设置线程是否为守护线程,true为守护线程, false为非守护线程。
// System.out.println("是守护线程吗?"+ d.isDaemon()); //判断线程是否为守护线程。
d.start();
for(int i = 1 ; i<=3 ; i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
(3) 运行结果:
十一.Join方法
(1) 加入,一个线程如果执行join语句,那么就有新的线程加入,执行该语句的线程必须要让步给新加入的线程先完成任务,然后才能继续执行。
(2) 实例:
//老妈
class Mon extends Thread{
public void run() {
System.out.println("妈妈洗菜");
System.out.println("妈妈切菜");
System.out.println("妈妈准备炒菜,发现没有酱油了..");
//叫儿子去打酱油
Son s= new Son();
s.start();
try {
s.join(); //加入。 一个线程如果执行join语句,那么就有新的线程加入,执行该语句的线程必须要让步给新加入的线程先完成任务,然后才能继续执行。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("妈妈继续炒菜");
System.out.println("全家一起吃饭..");
}
}
class Son extends Thread{
@Override
public void run() {
System.out.println("儿子下楼..");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("儿子一直往前走");
System.out.println("儿子打完酱油了");
System.out.println("上楼,把酱油给老妈");
}
}
public class Demo8 {
public static void main(String[] args) {
Mon m = new Mon();
m.start();
}
}
(3) 运行结果: