11.多线程

线程简介

任务,进程,线程,多线程


多任务

边吃饭,边玩手机

边开车,边打电话

边上厕所,边玩手机

  • 看起来是多个任务都在做,但是在同一瞬间,大脑依旧只做了一件事情

多线程

多车道,减少车辆拥堵,提高效率


普通方法调用和多线程

image-20220810150309544


程序,进程,线程

在操作系统中运行的程序就是进程,比如QQ,微信,爱奇艺

一个进程可以有多个线程,比如看视频同时听声音,看弹幕

  • 程序:程序是指令和数据的有序集合,本身没有任何运行的含义,是一个静态的概念
  • 进程:执行程序的一次执行过程,是一个动态的概念。是系统资源分配的单位
  • 线程:一个进程中可以包含多个线程,且至少有一个线程,线程是CPU调度和执行的单位

image-20220810150940109


本章核心概念

  • 线程就是独立的执行路径;
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程
  • main()称之为主线程,为系统的入口,用于执行整个程序;
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统密切相关的,先后顺序是不能人为干预的
  • 对同一份资料操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销,如CPU调度时间,并发控制开销
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

线程创建

Thread、Runnable、Callable


三种创建方式

  1. Thread类:继承Thread类【重点】
  2. Runnable接口:实现Runnable接口【重点】
  3. Callable接口:实现Callable接口(了解)

Thread类

  • 自定义线程类继承Thread类
  • 重写run()方法,编写线程执行体
  • 创建线程对象,调用start()方法启动线程
//创建线程方式1:(1)继承Thread类,(2)重写run()方法,(3)创建一个线程对象,调用start开启线程

                        //(1)继承Thread类
public class TextThread1 extends Thread{
    @Override
    //(2)重写run()方法
    public void run() {
        //run方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("我在看代码"+i);
        }
    }

    public static void main(String[] args) {
        //main方法线程体,主线程

        //(3)创建一个线程对象,调用start开启线程
        TextThread1 textThread1 = new TextThread1();
        textThread1.start();

        for (int i = 0; i < 20; i++) {
            System.out.println("我在学习多线程"+i);
        }
    }
}
案例

使用Thread下载网络图片

//练习Thread,实现多线程同步下载图片
public class TextThread2 extends Thread{
    private String url; //网络图片地址
    private String name;//保存的文件名

    public TextThread2(String url,String name){
        this.url = url;
        this.name = name;
    }

    @Override
    //下载图片线程的执行体
    public void run() {
        WebDowmloader webDowmloader = new WebDowmloader();
        webDowmloader.downloader(url,name);
        System.out.println("下载了文件名为:"+name);
    }

    public static void main(String[] args) {
        TextThread2 t1 = new TextThread2("https://tse1-mm.cn.bing.net/th/id/OIP-C.XJjjP1eQV1uG2xLB7AzxUQHaD7?w=284&h=180&c=7&r=0&o=5&dpr=1.25&pid=1.7","2.jpg");
        TextThread2 t2 = new TextThread2("https://tse3-mm.cn.bing.net/th/id/OIP-C._SWICNF0Q-pPOWSK_-iQzAHaDk?w=349&h=168&c=7&r=0&o=5&dpr=1.25&pid=1.7","3.jpg");
        TextThread2 t3 = new TextThread2("https://tse2-mm.cn.bing.net/th/id/OIP-C.pJlE5S1fBBJRkiP-V2yTQAHaDN?w=312&h=151&c=7&r=0&o=5&dpr=1.25&pid=1.7","4.jpg");
        t1.start();
        t2.start();
        t3.start();
    }
}

//下载器
class WebDowmloader{
    //下载方法
    public void downloader(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO异常,downloader方法出现问题");
        }
    }
}

Runnable接口

  • 定义MyRunnable类实现Runnable接口
  • 实现run()方法,编写线程执行体
  • 创建线程对象,调用start()方法启动线程
//创建线程方式2:(1)实现runnable接口(2)重写run()方法(3)创建线程对象,调用start()方法启动线程
public class TextRunnable1 implements Runnable{
    @Override
    //(2)重写run()方法
    public void run() {
        //run方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("我在看代码"+i);
        }
    }

    public static void main(String[] args) {
        //main方法线程体,主线程

        //(3)创建一个Runnable接口的实现类对象,调用start开启线程
        TextRunnable1 textRunnable1 = new TextRunnable1();
        //创建线程对象,通过线程对象开启线程,代理
        //Thread thread = new Thread(textRunnable1);
        //thread.start();
        new Thread(textRunnable1).start();

        for (int i = 0; i < 20; i++) {
            System.out.println("我在学习多线程"+i);
        }
    }
}
继承Thread类 实现Runnable接口
子类继承Thread类具备多线程能力 实现接口Runnable具有多线程能力
启动线程:子类对象.start(); 启动线程:传入目标对象+Thread对象.start();
不建议使用:避免OOP单继承局限性 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

案例1:模拟购票
//多个线程同时操作同一个对象
//买火车票的例子

//发现问题:多个线程操作同一个资源的情况下,数据紊乱(使用并发来解决,后面会讲)
public class TextRunnable2 implements Runnable{
    //票数
    private int ticketNums = 10;
    @Override
    public void run() {
        while (true){
            if(ticketNums<=0){
                break;
            }
            //模拟延时
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketNums--+"张票");
        }
    }

    public static void main(String[] args) {
        TextRunnable2 ticket = new TextRunnable2();

        new Thread(ticket,"小明").start();
        new Thread(ticket,"老师").start();
        new Thread(ticket,"黄牛").start();
    }
}

案例2:龟兔赛跑
  1. 首先来个赛道距离,然后要离终点越来越近
  2. 判断比赛是否结束
  3. 打印出胜利者
  4. 龟兔赛跑开始
  5. 故事中是乌龟赢的,兔子需要睡觉,所以我们来模拟兔子睡觉
  6. 最终,乌龟赢得比赛
//模拟龟兔赛跑
public class Race implements Runnable{

    //胜利者
    private static String winner;

    @Override
    public void run() {
        for (int i = 0; i <= 1000; i++) {

            //模拟兔子休息,每10步休息一次
            if(Thread.currentThread().getName().equals("兔子") && i%100==0){
                try {
                    Thread.sleep(3);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            //判断比赛是否结束
            boolean flag = gameOver(i);
            //如果比赛结束了,就停止程序
            if (flag){
                break;
            }

            System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
        }
    }

    //判断比赛是否结束
    private boolean gameOver(int steps){
        //判断是否有胜利者
        if(winner!=null){    //已经存在胜利者
            return true;
        }{
            if (steps>=1000){
                winner = Thread.currentThread().getName();
                System.out.println("winner is"+winner);
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();

        new Thread(race,"兔子").start();
        new Thread(race,"乌龟").start();
    }
}

Cllable接口(仅了解)

  • 实现Callable接口,需要返回值类型
  • 重写call方法,需要抛出异常
  • 创建目标对象
  • 创建执行服务 ExecutorService ser = Executors.newFixedThreadPood(1);
  • 提交执行 Future<Boolean> result = ser.submit(t1);
  • 获取结果 boolean r1 = result.get();
  • 关闭服务 ser.shutdownNow();

静态代理

  • 真实对象(You)和代理对象(WeddingCompany)都要实现同一个接口(Marry)
  • 代理对象要代理真实角色:把参数(You)传进去

好处:

  • 代理对象可以做很多真实对象做不了的事情
  • 真实对象可以专注做自己的事情
public class StaticProxy {
    public static void main(String[] args) {

        You you = new You();//你要结婚

        new Thread( ()-> System.out.println("我爱你") ).start();

        new WeddingCompany(new You()).HappyMarry();//把你交给婚庆公司,公司帮你准备结婚
    }
}

interface Marry{
    //人间四大喜事:久旱逢甘露、他乡遇故知、洞房花烛夜、金榜题名时
    void HappyMarry();
}

//真实角色
class You implements Marry{
    @Override
    public void HappyMarry() {
        System.out.println("新婚快乐");
    }
}

//代理角色
class WeddingCompany implements Marry{

    private Marry target;//代理谁-->真实目标角色(传过来的You)

    WeddingCompany(Marry target) {
        this.target = target;
    }

    @Override
    public void HappyMarry() {
        before();
        this.target.HappyMarry();//真实对象
        after();
    }

    private void before() {
        System.out.println("结婚之前:布置现场");
    }

    private void after() {
        System.out.println("结婚之后:收尾款");
    }
}

lamda表达式

为什么使用lamda表达式

  • 避免匿名内部类定义过多
  • 让代码看起来简洁
  • 去掉没意义的代码,只留下核心的逻辑

理解Functional Interface(函数式接口),是学习lamda表达式的关键所在

函数式接口的定义:

  • 任何接口,如果只包含唯一一个抽象方法,那么他就是一个函数式接口
public interface Runnable{
	public abstract void run();
}
  • 对于函数式接口,我们可以通过lamda表达式来创建该接口的对象

推导过程:逐步简化

//推导Lambda表达式
public class TestLambda1 {
    //3.静态内部类
    static class Like2 implements Ilike{
        @Override
        public void lambda() {
            System.out.println("i like lambda2");
        }
    }

    public static void main(String[] args) {
        Ilike like = new Like();
        like.lambda();

        like = new Like2();
        like.lambda();

        //4.局部内部类
        class Like3 implements Ilike{
            @Override
            public void lambda() {
                System.out.println("i like lambda3");
            }
        }

        like = new Like3();
        like.lambda();

        //5.匿名内部类,没有类的名称,必须借助接口和父类
        like = new Ilike() {
            @Override
            public void lambda() {
                System.out.println("i like lambda4");
            }
        };
        like.lambda();

        //6.lambda简化
        like = ()->{
            System.out.println("i like lambda5");
        };
        like.lambda();
    }
}

//1.定义一个函数式接口
interface Ilike{
    void lambda();
}

//2.实现类
class Like implements Ilike{
    @Override
    public void lambda() {
        System.out.println("i like lambda");
    }
}

推导过程2:带参数

public class TestLambda2 {
    static class Love implements Ilove{
        @Override
        public void love(int a) {
            System.out.println("I love you-->"+a);
        }
    }

    public static void main(String[] args) {
        Ilove love1 = new Love();
        love1.love(1);

        Ilove love2 = new Love();
        love2.love(2);

        class Love implements Ilove{
            @Override
            public void love(int a) {
                System.out.println("I love you-->"+a);
            }
        }

        Ilove love3 = new TestLambda2.Love();
        love3.love(3);

        Ilove love4 = new Ilove() {
            @Override
            public void love(int a) {
                System.out.println("I love you-->"+a);
            }
        };
        love4.love(4);

        Ilove love5 = (int a) -> {
            System.out.println("I love you-->"+a);
        };
        love5.love(5);

        //简化1:去掉参数类型(多个参数也可以去掉参数类型,要去掉就都去掉)
        Ilove love6 = (a) -> {
            System.out.println("I love you-->"+a);
        };
        love6.love(6);

        //简化2:简化括号
        Ilove love7 = a->{
            System.out.println("I love you-->"+a);
        };
        love7.love(7);

        //简化3:去掉花括号::只能有一行代码的情况下,才能去掉花括号
        Ilove love8 = a -> System.out.println("I love you-->"+a);
        love8.love(8);

    }
}

interface Ilove{
    void love(int a);
}

class Love implements Ilove{
    @Override
    public void love(int a) {
        System.out.println("I love you-->"+a);
    }
}

线程状态

image-20220811165532627


线程方法

方法 说明
setPriority(int newPriority) 更改线程的优先级
static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠
void join() 等待该线程终止
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
void interrupt() xxxxxxxxxx67 1public class TestLambda2 {2    static class Love implements Ilove{3        @Override4        public void love(int a) {5            System.out.println("I love you-->"+a);6       }7   }8​9    public static void main(String[] args) {10        Ilove love1 = new Love();11        love1.love(1);12​13        Ilove love2 = new Love();14        love2.love(2);15​16        class Love implements Ilove{17            @Override18            public void love(int a) {19                System.out.println("I love you-->"+a);20           }21       }22​23        Ilove love3 = new TestLambda2.Love();24        love3.love(3);25​26        Ilove love4 = new Ilove() {27            @Override28            public void love(int a) {29                System.out.println("I love you-->"+a);30           }31       };32        love4.love(4);33​34        Ilove love5 = (int a) -> {35            System.out.println("I love you-->"+a);36       };37        love5.love(5);38​39        //简化1:去掉参数类型(多个参数也可以去掉参数类型,要去掉就都去掉)40        Ilove love6 = (a) -> {41            System.out.println("I love you-->"+a);42       };43        love6.love(6);44​45        //简化2:简化括号46        Ilove love7 = a->{47            System.out.println("I love you-->"+a);48       };49        love7.love(7);50​51        //简化3:去掉花括号::只能有一行代码的情况下,才能去掉花括号52        Ilove love8 = a -> System.out.println("I love you-->"+a);53        love8.love(8);54​55   }56}57​58interface Ilove{59    void love(int a);60}61​62class Love implements Ilove{63    @Override64    public void love(int a) {65        System.out.println("I love you-->"+a);66   }67}java
boolean isAlive() 测试线程是否处于活动状态

线程停止

  • 不推荐使用JDK提供的stop()\destroy()方法
  • 推荐线程自己停止下来
  • 建议使用一个标志位进行终止变量,当flag=false,则终止线程
//测试停止
//1.建议线程正常停止-->利用次数,不建议死循环
//2.建议使用标志位-->设置一个标志位
//3.不要使用stop或者destory等
public class TestStop implements Runnable{
    //1.设置一个标志位
    private boolean flag = true;
    @Override
    public void run() {
        int i = 0;
        while (flag){
            System.out.println("run...Thread--------------------"+i++);
        }
    }

    //2.设置一个公开的方法停止线程,转换标志位
    public void stop(){
        this.flag = false;
    }

    public static void main(String[] args) {
        TestStop testStop = new TestStop();

        new Thread(testStop).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main"+i);
            if(i==900){
                //调用stop方法,切换标志位,终止线程
                testStop.stop();
                System.out.println("线程该停止了");
            }
        }
    }
}

线程休眠

  • sleep(时间) 指定当前线程阻塞的毫秒数(1000=1s)
  • sleep存在异常InterruptedException
  • sleep时间到达后线程进入就绪状态
  • sleep可以模拟网络延迟,倒计时等
  • 每个对象都有一个锁,sleep不会释放锁

案例:模拟倒计时

//模拟倒计时
public class TestSleep2 {

    public static void main(String[] args) {
        //打印当前时间
        Date startTime = new Date(System.currentTimeMillis());

        while (true){
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
                startTime = new Date(System.currentTimeMillis());//更新时间
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
//        try {
//            tenDown();
//        } catch (Exception e) {
//            throw new RuntimeException(e);
//        }
    }

    public static void tenDown() throws Exception{
        int num = 10;

        while (true){
            Thread.sleep(1000);
            System.out.println(num--);
            if (num<=0){
                break;
            }
        }
    }
}

线程礼让

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让cpu重新调度,礼让不一定成功!看cpu心情

通俗解释:你本来是在座位上的,有一个人来了,你站起来,重新和他竞争座位,而不是你把座位让给他

//线程礼让:礼让不一定成功
public class TestYield {
    public static void main(String[] args) {
        MyYield myYield = new MyYield();

        new Thread(myYield,"a").start();
        new Thread(myYield,"b").start();
    }
}

class MyYield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始执行");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"线程停止执行");
    }
}

Join线程强制执行

  • Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
  • 可以想象成插队
//Join线程强制执行
public class TestJoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("VIP来喽!!!!!!"+i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //启动线程
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();

        //主线程
        for (int i = 0; i < 500; i++) {
            if (i == 200){
                thread.join();//插队
            }
            System.out.println("main"+i);
        }
    }
}

线程状态观测

Thread.State

线程可以处于以下状态之一:

  • NEW 尚未启动的线程处于此状态
  • RUNNABE 在JAVA虚拟机中执行的线程处于此状态
  • BLOCKED 被阻塞等待监视器锁定你的线程处于此状态
  • WAITING 正在等待另一个线程执行特定动作的线程处于此状态
  • TIMED_WAITING 正在等待另一个线程执行动作到达指定等待时间的线程处于此状态
  • TERMINATED 已退出的线程处于此状态
//观测线程状态
public class TestState {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("///////");
        });

        //观察状态
        Thread.State state = thread.getState();
        System.out.println(state);   //NEW

        //观察启动后
        thread.start();  //启动线程
        state = thread.getState();
        System.out.println(state);  //Run

        while (state != Thread.State.TERMINATED){   //只要线程不终止,就一直输出状态
            Thread.sleep(100);
            state = thread.getState();  //更新线程状态【while循环里的变量,一定要记得更新】
            System.out.println(state);  //输出状态,因为启动后会有5秒睡眠,所以要TIMED_WAITING
        }
    }
}

线程优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行(更像一个概率,概率大的更有可能被优先执行)
  • 线程的优先级用数字表示,范围从1~10
    • Thread.MIN_PRIORITY = 1
    • Thread.MAX_PRIORITY = 10
    • Thread.NORM_PRIORITY = 10
  • 使用一下方式改变或获取优先级
    • getPriority().setPriority(int xx)
//测试线程的优先级
public class TestPriority {
    public static void main(String[] args) {
        //主线程默认优先级,改不了
        System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());

        MyPriority myPriority = new MyPriority();

        Thread t1 = new Thread(myPriority);
        Thread t2 = new Thread(myPriority);
        Thread t3 = new Thread(myPriority);
        Thread t4 = new Thread(myPriority);
        Thread t5 = new Thread(myPriority);
        Thread t6 = new Thread(myPriority);

        //先设置优先级,再启动
        t2.setPriority(1);
        t3.setPriority(4);
        t4.setPriority(Thread.MAX_PRIORITY);
        t5.setPriority(8);
        t6.setPriority(7);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();
    }
}
class MyPriority implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
    }
}

守护线程

  • 线程分为用户线程守护线程

  • 虚拟机必须确保用户线程执行完毕

  • 虚拟机不用等待守护线程执行完毕

    • 如后台记录操作日志,监控内存,垃圾回收
//测试守护线程
public class TestDaemon {
    public static void main(String[] args) {
        God god = new God();
        You you = new You();

        Thread thread = new Thread(god);    //这个位置经常把god忘记放进Thread()
        thread.setDaemon(true);   //默认为false,表示用户线程,手动设置true表示守护线程

        thread.start(); //守护线程启动

        new Thread(you).start();    //用户线程启动
    }
}

//守护者
class God implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("我在保护!!");
        }
    }
}

//用户
class You implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 36500; i++) {
            System.out.println("开心活着");
        }
        System.out.println("----------------GG----------------");
    }
}

案例:取钱业务

需求:二人共同账户,余额10w,模拟二人同时取钱

分析

1.需要提供一个账户类,创建一个账户对象代表2人共享账户

2.需要定义一个线程类,线程类可以处理账户对象

3.创建两个线程对象,传入同一个账户对象

4.启动2个线程,去同一个账户对象中取钱10w

逻辑:

image-20220928165800659

结果展示:

image-20220928165834965

账户类

package 案例_银行取钱;

public class Account {
    private String cardid;
    private double money;//账户余额

    public Account(){
    }
    
    public Account(String cardid, double money) {
        this.cardid = cardid;
        this.money = money;
    }

    public String getCardid() {
        return cardid;
    }

    public void setCardid(String cardid) {
        this.cardid = cardid;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }

    public void drawMoney(double money) {
        //0.先获取是谁来取钱
        String name = Thread.currentThread().getName();
        //1.判断账户余额是否够
        if(this.money >= money){
            //2.取钱
            System.out.println(name + "来取钱成功,吐出:"+money);
            //3.更新余额
            this.money -= money;
            System.out.println(name + "取钱后剩余:"+this.money);
        }else{
            //4.余额不足
            System.out.println(name + "来取钱,余额不足!");
        }
    }
}

线程类

package 案例_银行取钱;

public class ThreadDemo {
    public static void main(String[] args) {
        //1.定义线程类,创建一个共享的账户对象
        Account acc = new Account("ICBC-111",100000);

        //2.创建两个线程对象,代表两个人同时进来
        new DrawThread(acc,"xiaoming").start();
        new DrawThread(acc,"xiaohong").start();
    }
}

取钱的线程对象

package 案例_银行取钱;

//取钱的线程类

public class DrawThread extends Thread{
    //接受处理的账户对象
    private Account acc;
    public DrawThread(Account acc,String name){
        super(name);
        this.acc = acc;
    }
    public void run(){
        //取钱的
        acc.drawMoney(100000);
    }
}

线程同步【重点】

使用场景:多个线程操作同一个资源(并发),为了解决线程安全问题

形成条件:队列+锁

锁机制synchronized:当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。存在以下问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

方法一:同步代码块

作用:把出现线程安全问题的核心代码上锁

原理:每次只能一个线程进入,执行完毕后自动解锁

synchronized(同步锁对象){
	操作共享资源的代码(核心代码)
}

快捷键:ctrl+alt+T:9

任意唯一对象的缺点:影响其他无关线程的执行,会影响其他取钱的人(一个锁锁住了千家万户)

锁对象的规范要求:使用共享资源作为锁对象(账户)

  • 对于实例方法,建议使用this作为锁对象
  • 对于静态方法建议使用字节码(类名.class)作为锁对象

加锁:

public void drawMoney(double money) {
    //0.先获取是谁来取钱
    String name = Thread.currentThread().getName();
    // 同步代码块
    //synchronized ("锁") {
    synchronized (this) {      //this = acc
        //1.判断账户余额是否够
        if(this.money >= money){
            //2.取钱
            System.out.println(name + "来取钱成功,吐出:"+money);
            //3.更新余额
            this.money -= money;
            System.out.println(name + "取钱后剩余:"+this.money);
        }else{
            //4.余额不足
            System.out.println(name + "来取钱,余额不足!");
        }
    }
}

结果展示:

image-20220928171121429

方法二:同步方法

作用:把出现线程安全问题的核心方法上锁

格式:

修饰符 synchronized 返回值类型 方法名称(形参列表){
	操作共享资源的代码
}

加锁:

public synchronized void drawMoney(double money) {
    //0.先获取是谁来取钱
    String name = Thread.currentThread().getName();
    //1.判断账户余额是否够
    if(this.money >= money){
        //2.取钱
        System.out.println(name + "来取钱成功,吐出:"+money);
        //3.更新余额
        this.money -= money;
        System.out.println(name + "取钱后剩余:"+this.money);
    }else{
        //4.余额不足
        System.out.println(name + "来取钱,余额不足!");
    }
}

结果展示:

image-20220928180808978

方法三:Lock锁

  • 为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便
  • Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
  • Lock是接口不能直接实例化,这里采用他的实现类ReentrantLock来构建Lock锁对象
方法名称 说明
public ReentrantLock() 获得Lock锁的实现类对象

Lock的API

方法名称 说明
void lock() 获得锁
void unlock() 释放锁
package 线程同步_锁_Lock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Account {
    private String cardid;
    private double money;//账户余额
    //final修饰后:锁对象是唯一不可替换的
    private final Lock lock = new ReentrantLock();

    public Account(){
    }
    public void drawMoney(double money) {
        //0.先获取是谁来取钱
        String name = Thread.currentThread().getName();
        //1.判断账户余额是否够
        lock.lock();//上锁
        try {
            if(this.money >= money){
                //2.取钱
                System.out.println(name + "来取钱成功,吐出:"+money);
                //3.更新余额
                this.money -= money;
                System.out.println(name + "取钱后剩余:"+this.money);
            }else{
                //4.余额不足
                System.out.println(name + "来取钱,余额不足!");
            }
        } finally {
            lock.unlock();//解锁
        }   //加try finall:即使出了BUG,也会解锁
    }
    public Account(String cardid, double money) {
        this.cardid = cardid;
        this.money = money;
    }

    public String getCardid() {
        return cardid;
    }

    public void setCardid(String cardid) {
        this.cardid = cardid;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }
}

线程通信

什么是线程通信、如何实现?

  • 所谓线程通信就是线程间相互发送数据,线程通常通过共享一个数据的方式实现
  • 线程间会根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做

线程通信常见模型

  • 生产者与消费者模型:生产者负责产生数据,消费者线程负责消费数据
  • 要求:生产者线程生产完数据后,唤醒消费者,然后等待自己;消费者消费完该数据后,唤醒生产者,然后等待自己

线程通信的前提

  • 线程通信通常是在多个线程操作同一个共享资源的时候需要进行通信,且要保证线程安全

Object类的等待和唤醒方法:

方法名称 说明
void wait() 让当前线程等待并释放所占锁,知道另一个线程调用notify()或notifyAll()方法
void notify() 唤醒正在等待的单个线程
void notifyAll() 唤醒正在等待的所有线程
  • 上述方法应该使用当前同步锁对象进行调用

流程:

image-20220928203840351

创建线程对象:

package 线程通信;

public class ThreadDemo {
    public static void main(String[] args) {
        //目标:了解线程通信的流程
        //使用三个人存钱,两个人取钱 模拟线程通信思想(一存一取)
        //1.创建账户对象,代表5个人共同操作的账户
        Account acc = new Account("ICBC-112",0);
        
        //2.创建2个取钱线程
        new DrawThread(acc,"小明").start();
        new DrawThread(acc,"小红").start();
        
        //3.创建3个存钱线程
        new DepositThread(acc,"qindie").start();
        new DepositThread(acc,"gandie").start();
        new DepositThread(acc,"yuefudie").start();
    }
}

存钱的线程类:

package 线程通信;
//取钱的线程类
public class DepositThread extends Thread{
    //接受处理的账户对象
    private Account acc;
    public DepositThread(Account acc, String name){
        super(name);
        this.acc = acc;
    }
    public void run(){
        //存钱的
        while (true) {
            acc.deposit(100000);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

取钱的线程类:

package 线程通信;
//取钱的线程类
public class DrawThread extends Thread{
    //接受处理的账户对象
    private Account acc;
    public DrawThread(Account acc, String name){
        super(name);
        this.acc = acc;
    }
    public void run(){
        //取钱的
        while (true) {
            acc.drawmoney(100000);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

公共账户具体操作:

package 线程通信;

public class Account {
    private String cardid;
    private double money;//账户余额

    public Account(){
    }

    public Account(String cardid, double money) {
        this.cardid = cardid;
        this.money = money;
    }
    //三人来存钱
    public synchronized void deposit(double money) {
        try {
            String name = Thread.currentThread().getName();
            if (this.money ==0 ){
                //没钱了
                this.money += money;
                System.out.println(name + "存钱" + money + "成功!存钱后余额:" + this.money);
                //有钱了:唤醒别人,等待自己
                this.notifyAll(); //唤醒所有线程
                this.wait();//锁对象,让当前线程等待
            }else {
                //有钱,不存钱
                this.notifyAll(); //唤醒所有线程
                this.wait();//锁对象,让当前线程等待
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    //两人来取钱
    public synchronized void drawmoney(double money) {
        try {
            String name = Thread.currentThread().getName();
            if(this.money >= money){    //钱够
                this.money -= money;
                System.out.println(name + "来取钱" + money + "成功!余额是:" + this.money);
                //没钱了
                this.notifyAll(); //唤醒所有线程
                this.wait();//锁对象,让当前线程等待
            }else {     //钱不够,唤醒别人,等待自己
                this.notifyAll(); //唤醒所有线程
                this.wait();//锁对象,让当前线程等待
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    public String getCardid() {
        return cardid;
    }
    public void setCardid(String cardid) {
        this.cardid = cardid;
    }
    public double getMoney() {
        return money;
    }
    public void setMoney(double money) {
        this.money = money;
    }
}

线程池概述

什么是线程池?

  • 线程池就是一个可以复用线程的技术

不使用线程池的问题

  • 如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能

线程池实现的API、参数说明

谁代表线程池?

  • JDK5.0起提供了代表线程池的接口:ExecutorService

如何得到线程池对象

  • 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象

image-20220928204914300

  • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象

方式一:ThreadPoolExecutor

ThreadPoolExecutor构造器的参数说明

image-20220929154050662

临时线程什么时候创建?

  • 新任务提交时发现核心线程(正式工)都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程

什么时候开始会拒绝任务?

  • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝

线程池处理Runnable任务

ThreadPoolExecutor创建线程池对象实例

ExecutorService pools = new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS,new ArrayBlockingQueue<>(6),Executor.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy())

ExecutorService的常用方法

方法名称 说明
void execute(Runnable command) 执行任务/命令,没有返回值,一般用来执行R
Future submit(Callable task) 执行任务,返回未来任务对象获取线程结果,一般拿来执行Callable任务
void shutdown() 等任务执行完毕后关闭线程池
List shutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

方式二:使用Executors的工具类构建线程池

image-20220929200942203

注:Executors的底层也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的

使用Executors可能存在的陷阱

  • 大型并发系统环境中使用Executors如果不注意可能出现系统风险

image-20220929201842459

定时器

  • 定时器是一种控制任务延时调用,或者周期调用的技术
  • 作用:闹钟,定时邮件发送

定时器的实现方式

  • 方式一:Timer
  • 方式二:ScheduledExecutorService

Timer定时器

构造器 说明
public Timer() 创建Timer定时器对象

Timer定时器的特点和存在的问题

  • Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入
  • 可能因为其中的某个任务的异常使Timer线程死掉,从而影响后任务执行
package 定时器;
//目标:Timer定时器的使用和了解
import java.util.Timer;
import java.util.TimerTask;
 
public class TimerDemo1 {
    public static void main(String[] args) {
        //1.创建Timer定时器
        Timer timer = new Timer(); //定时器本身就是一个单线程
        //2.调用方法,处理定时任务
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行一次");
            }
        }, 3000,2000);
    }
}

ScheduledExecutorService定时器

  • ScheduledExecutorService是jdk1.5中引入了并发包,弥补了Timer的缺陷,ScheduledExecutorService内部为线程池
Executors的方法 说明
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 得到线程池对象
ScheduledExecutorService的方法 说明
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDalay, long period, TimeUnit unit) 周期调度方法
package 定时器;

//目标:ScheduledExecutorService定时器

import java.util.Date;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class TimerDemo2 {
    public static void main(String[] args) {
        //1.创建ScheduledExecutorService线程池,做定时器
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
        //2.开启定时任务
        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行A" + new Date());
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },0 , 2 , TimeUnit.SECONDS);

        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行B" + new Date());
                System.out.println(10/0);
            }
        },0 , 2 , TimeUnit.SECONDS);

        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行C" + new Date());
            }
        },0 , 2 , TimeUnit.SECONDS);
    }
}
posted @ 2022-10-11 22:18  啦啦米老鼠  阅读(27)  评论(0)    收藏  举报