Java学习第十章(二)

视频链接:https://www.bilibili.com/video/BV1Rx411876f?p=1

视频范围P797 - P809

1.死锁概念

死锁代码需要会写,一般面试官也要求你会写,只有会写,才会在以后的开发中注意这个事情,因为死锁很难调试

死锁示意图
在这里插入图片描述

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){

            }
        }
    }
}

运行结果

在这里插入图片描述

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.定时器 Timer

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 + ":成功完成了一次数据备份!");
    }
}

运行结果

在这里插入图片描述

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对象上处于等待的所有线程
在这里插入图片描述

6.2 生产者和消费者模式

在这里插入图片描述

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();
            }

        }

    }
}

运行结果

在这里插入图片描述

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();
            }
        }
    }
}

运行结果

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值