JAVA学习(进阶班)|Java学习第十章(二)

视频链接:https://www.bilibili.com/video/BV1Rx411876f?p=1
视频范围P797 - P809

目录描述

  • 1.死锁概念
  • 2.开发中如何解决线程安全问题
  • 3.守护线程
    • 3.1 概述
    • 3.2 实现
  • 4.定时器
    • 4.1 概述
    • 4.2 实现
  • 5.实现线程的第三种方式 Callable
  • 6.wait和notify方法
    • 6.1 基础概念
    • 6.2 生产者和消费者模式
    • 6.3 实现生产者和消费者模式
    • 6.4 编程练习题

1.死锁概念 死锁代码需要会写,一般面试官也要求你会写,只有会写,才会在以后的开发中注意这个事情,因为死锁很难调试
死锁示意图:
JAVA学习(进阶班)|Java学习第十章(二)
文章图片

package deadlock; public class DeadLock { public static void main(String[] args) { Object o1 = new Object(); Object o2 = new Object(); //t1和t2两个线程共享o1,o2 Thread t1 = new MyThread1(o1,o2); Thread t2 = new MyThread2(o1,o2); t1.start(); t2.start(); } }class MyThread1 extends Thread{ Object o1; Object o2; public MyThread1(Object o1, Object o2) { this.o1 = o1; this.o2 = o2; }@Override public void run() { synchronized (o1){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o2){} }} }class MyThread2 extends Thread{ Object o1; Object o2; public MyThread2(Object o1, Object o2) { this.o1 = o1; this.o2 = o2; }@Override public void run() { synchronized (o2){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o1){} } } }

运行结果:
JAVA学习(进阶班)|Java学习第十章(二)
文章图片

2.开发中如何解决线程安全问题
  • 第一种方案:尽量使用局部变量代替“实例变量和静态变量”
  • 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了(一个线程对应1个对象,100个线程对应100个对象,对象不共享了,就没有数据安全问题了)
  • 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized【线程同步机制】
注意:不要一上来就选择线程同步synchronized,因为synchronized会让程序的执行效率降低,系统的用户吞吐量降低,用户体验差,在不得已的情况下再选择线程同步机制
3.守护线程 3.1 概述 Java语言中线程分为两大类:用户线程和守护线程【后台线程,例如:垃圾回收器】
守护线程特点:
  1. 一般守护线程是一个四循环
  2. 所有的用户线程只要结束,守护线程自动结束
注意:主线程main方法是一个用户线程
问题:守护线程用在哪?
答:每天零点的时候系统数据自动备份【这个需要使用到定时器,并且可以将定时器设置为守护线程,一直在那里看着,每到零点的时候就备份一次,所有的用户线程如果结束了,守护线程自动退出,没有必要进行数据备份了】
3.2 实现 主要代码: t.setDaemon(true);
package thread; public class ThreadTest14 { public static void main(String[] args) { Thread t = new BakDataThread(); t.setName("备份数据的线程"); //启动线程之前,将线程设置为守护线程 t.setDaemon(true); t.start(); //主线程:主线程是用户线程 for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "---->" + i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }class BakDataThread extends Thread{ @Override public void run() { int i = 0; //即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止 while (true){ System.out.println(Thread.currentThread().getName() + "---->" + (++i)); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }

4.定时器 4.1 概述 定时器作用:间隔特定的时间,执行特定的程序【在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的】
例如:每周要进行银行账户的总账操作;每天要进行数据的备份操作
在java中实现定时器方式:
  1. 使用sleep方法,睡眠,设置睡眠时间,每到这个时间点醒来,执行任务,这种方式是最原始的定时器(比较low)
  2. 在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用,不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持定时任务的
  3. 在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架,这个框架只要进行简单的配置,就可以完成定时器的任务
4.2 实现 使用定时器指定定时任务
package thread; import javax.xml.crypto.Data; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class TimerTest { public static void main(String[] args) throws Exception{ //创建定时器对象 Timer timer = new Timer(); //Timer timer = new Timer(true); //指定定时任务 //timer.schedule(定时任务,第一次执行时间,间隔多久执行一次); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date firstTime= sdf.parse("2022-04-04 11:00:00"); timer.schedule(new LogTimerTask(),firstTime,1000 * 10); //采样匿名内部类的形式也可以 /* timer.schedule(new TimerTask(){ @Override public void run() {} },firstTime,1000 * 10); */ } }//编写一个定时任务类 //假设是一个记录日志的定时任务 class LogTimerTask extends TimerTask{@Override public void run() { //编写你需要的执行的任务就行了 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String strtTime= sdf.format(new Date()); System.out.println(strtTime + ":成功完成了一次数据备份!"); } }

运行结果:
JAVA学习(进阶班)|Java学习第十章(二)
文章图片

5.实现线程的第三种方式 Callable 实现Callable接口(JDK8新特性)
特点:这种方式实现的线程可以获取线程的返回值,之前讲解的那两种方式是无法获取线程返回值的,因为run方法返回void
优点:可以获取到线程的执行结果
缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低
package thread; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; //JUC包下的,属于java的并发包,老JDK中没有这个包,新特性public class ThreadTest15 { public static void main(String[] args) throws Exception{//第一步:创建一个”未来任务类“对象 //参数非常重要,需要给一个Callable接口实现类对象 FutureTask task = new FutureTask(new Callable() { @Override public Object call() throws Exception {//call()方法就相当于run方法,只不过这个有返回值 //线程执行一个任务,执行之后可能会有一个执行结果 //模拟执行 System.out.println("call method begin!"); Thread.sleep(1000 * 10); System.out.println("call method end!"); int a = 100; int b = 200; return a + b; //自动装箱(300结果变成Integer) } }); //创建线程对象 Thread t = new Thread(task); //启动线程 t.start(); //这里是main方法,这是在主线程中 //在主线程中,怎么获取t线程的返回结果 //get()方法的执行会导致”当前线程阻塞“ Object obj = task.get(); System.out.println("线程执行结果:" + obj); //main方法这里的程序要想执行必须等待get()方法的结束 //而get()方法可能需要很久,因为get()方法是为了拿另一个线程的执行结果 //另一个线程执行是需要时间的 System.out.println("hello world!"); } }

6.wait和notify方法 6.1 基础概念
  1. wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,因为这两个方式是Object类中自带的
  2. wait()方法作用:让正在o对象上获得的线程进入等待状态,无期限等待,直到被唤醒为止
Object o = new Object(); o.wait(); //该方法的调用,会让“当前线程(正在o对象上活动的线程)”进入等待状态

  1. notify()方法作用:唤醒正在o对象上等待的线程
Object o = new Object(); o.notify();

注意:还有一个notifyAll()方法:唤醒o对象上处于等待的所有线程
JAVA学习(进阶班)|Java学习第十章(二)
文章图片

6.2 生产者和消费者模式 JAVA学习(进阶班)|Java学习第十章(二)
文章图片

6.3 实现生产者和消费者模式
  1. 使用wait和notify方法实现生产者和消费者模式
  2. 生产者和消费者模式:生产线程负责生产,消费线程负责消费,生产线程和消费线程要达到均衡,这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法
  3. wait方法和notify方法建立在线程同步的基础之上,因为多线程要同时操作一个仓库,有线程安全问题
  4. wait方法作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放掉t线程之前占有的o对象的锁
  5. notify方法作用:notify()让正在o对象上等待的线程唤醒,只是通知,不会释放掉o对象之前占有的锁
模拟:仓库使用List集合,List集合中假设只能存储1个元素,1个元素就表示仓库满了,如果List集合中元素个数是0,就表示仓库空了,保证List集合中永远都是最多存储1个元素,必须做到这种效果:生成1个消费1个
package thread; import java.util.ArrayList; import java.util.List; public class ThreadTest16 { public static void main(String[] args) { //创建1个仓库对象,共享的 List list = new ArrayList(); //创建两个线程对象 //生产者线程 Thread t1 = new Thread(new Producer(list)); //消费者线程 Thread t2 = new Thread(new Consumer(list)); t1.setName("生产者线程"); t2.setName("消费者线程"); t1.start(); t2.start(); } }//生产线程 class Producer implements Runnable{ //仓库 private List list; public Producer(List list) { this.list = list; }@Override public void run() { //一直生产(使用死循环来模拟一直生产) while (true){ //给仓库对象list加锁 synchronized (list){ if (list.size() > 0){//大于0,说明仓库中已经有1个元素了 //当前线程进入等待状态,并且释放list集合的锁 try { //当前线程进入等待状态,并且释放Producer之前占有的list集合的锁 list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //程序能够执行到这里说明仓库是空的,可以生产 Object obj = new Object(); list.add(obj); System.out.println(Thread.currentThread().getName() + "-->" + obj); //唤醒消费者消费 list.notifyAll(); }}} }//消费线程 class Consumer implements Runnable{ //仓库 private List list; public Consumer(List list) { this.list = list; }@Override public void run() { //一直消费 while (true){ synchronized (list){ if (list.size() == 0){ try { //仓库已经空了 //消费者线程等待,释放掉list集合的锁 list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //程序能够执行到此处说明仓库中有数据,进行消费 Object obj = list.remove(0); System.out.println(Thread.currentThread().getName() + "-->" + obj); //唤醒生产者生产 list.notifyAll(); }}} }

运行结果:
JAVA学习(进阶班)|Java学习第十章(二)
文章图片

6.4 编程练习题 题目:
使用生产者和消费者模式实现,交替输出:
假设只有两个线程,输出以下结果:
t1–>1
t2–>2
t1–>3
t2–>4
t1–>5
t2–>6

要求:必须交替,并且t1线程负责输出奇数。t2线程负责输出偶数。
两个线程共享一个数字,每个线程执行时都要对这个数字进行:++
代码实现:
package thread; public class exam01 { public static void main(String[] args) {//创建共享数字对象 Num num = new Num(); //创建两条线程 //奇数线程 Thread t1 = new Thread(new OddNum(num)); //偶数线程 Thread t2 = new Thread(new EvenNum(num)); //修改线程名称 t1.setName("t1"); t2.setName("t2"); //启动线程 t1.start(); t2.start(); } }//共享数字对象 class Num{ int i = 1; }//偶数线程 class EvenNum implements Runnable{ //共享数字 private Num num; public EvenNum(Num num) { this.num = num; }@Override public void run() { //死循环i++ while(true){ synchronized (num){ if (num.i % 2 == 1){ //如果num是奇数,偶数线程进入等待状态 try { num.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //程序进行到这说明是偶数,输出,并对数字进行++操作 System.out.println(Thread.currentThread().getName() + "--->" + (num.i++)); //一秒输出一次 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }//唤醒奇数线程(?) num.notify(); }}} }//奇数线程 class OddNum implements Runnable{ private Num num; public OddNum(Num num) { this.num = num; }@Override public void run() { while (true){ synchronized (num){ if (num.i % 2 == 0){ try { num.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //执行到这说明是奇数,输出,并对数字进行++操作 System.out.println(Thread.currentThread().getName() +"--->" + (num.i++)); //一秒输出一次 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //唤醒偶数线程(?) num.notify(); } } } }

运行结果:
【JAVA学习(进阶班)|Java学习第十章(二)】JAVA学习(进阶班)|Java学习第十章(二)
文章图片

    推荐阅读