百木园-与人分享,
就是让自己快乐。

java多线程基础学习

目录

  • 一、多线程概述
    • 1.1、进程和线程的概念
      • 1.1.1、进程
      • 1.1.2、线程
    • 1.2、线程创建的方法
      • 1.2.1、继承Thread类
      • 1.2.2、重写Runnable接口
      • 1.2.3、重写callable接口
    • 1.3、线程的状态机
      • 1.3.1、线程的状态
      • 1.3.2、线程的状态机
  • 二、多线程实现
    • 2.1、继承Thread类
      • 2.1.1、Thread的基本用法
      • 2.1.2、多线程下载网图
    • 2.2、实现Runnable接口
      • 2.2.1、Runnable基本用法
      • 2.2.2、模拟龟兔赛跑
    • 2.3、实现Callable接口
    • 2.4、并发问题
    • 2.5、补充Lambda表达式
      • 2.5.1、Lambda的简介
      • 2.5.2、Lambda表达式用法引入
      • 2.5.3、Lambda深入理解
  • 三、线程状态转换
    • 3.1、线程停止
    • 3.2、线程休眠
    • 3.3、线程礼让
    • 3.4、线程加入
    • 3.5、线程状态检测
    • 3.6、线程优先级
    • 3.7、守护线程
  • 四、线程同步
    • 4.1、线程同步的概念
    • 4.2、Java线程同步方法
      • 4.2.1、同步方法或者同步块
        • ①、synchronized修饰方法
        • ②、synchronized同步块
      • 4.2.2、Lock锁
        • ①、一般写法
        • ②、案例说明
  • 五、线程通信
    • 2.1、线程通信的概念
      • 2.1.1、为什么需要线程通信
      • 2.1.2、线程的通信的方式
    • 2.2、管程法
    • 2.3、信号量法
    • 2.4、线程池法

一、多线程概述

1.1、进程和线程的概念

1.1.1、进程

  1. 进程是执行程序的一次执行过程,是一个动态的过程,是一个活动的实体,是系统资源分配的单位
  2. 一个应用程序的运行就可以被看做是一个进程

1.1.2、线程

  1. 线程,是运行中的实际的任务执行者,一般的,一个进程中包含了多个可以同时运行的线程
  2. 线程就是独立的执行路径,是cpu调度和执行的单位,是序执行流中最小执行单位,是进程中实际运行单位
  3. 多线程的调度由CPU决定,无法人为干预
  4. 在java中,一定存在着两个线程,一个是main方法,即主线程,一个是gc线程,用于jvm的垃圾回收

1.2、线程创建的方法

1.2.1、继承Thread类

1.2.2、重写Runnable接口

1.2.3、重写callable接口

1.3、线程的状态机

1.3.1、线程的状态

java中线程共有五种状态

  • 新建状态
  • 就绪状态
  • 阻塞状态
  • 运行状态
  • 死亡状态

1.3.2、线程的状态机

  1. 通用线程状态机

    image-20220728105431770

  2. 对应java中的实现手段

    image-20220728105514149

二、多线程实现

2.1、继承Thread类

用法上述内容讲到,直接上代码

2.1.1、Thread的基本用法

  1. 代码实现

    package com.kuang.class1;
    
    /**
     * 1、继承Thread类来实现多线程的demo
     */
    public class TestThreadDemo extends Thread {
        // 继承Thread类
        @Override
        public void run() {
            // 重写run方法
            for (int i = 0; i < 20; i++) {
                System.out.println(\"子线程--\" + i);
            }
        }
    
        public static void main(String[] args) {
    
            // 创建子线程对象,并调用start方法开启子线程
            new TestThreadDemo().start();
    
            // 这是主线程代码
            for (int i = 0; i < 20; i++) {
                System.out.println(\"主线程--\" + i);
            }
        }
    }
    
  2. 效果展示

    可以看见子线程和主线程交替执行,但同一时间只能由一个执行,即宏观并发,微观交替

2.1.2、多线程下载网图

  • 需要用到common-io包
  • 实现WebDownLoader类,里面实现一个DownLoader()方法,主要用到FileUtils.copyURLToFile()方法,将URL资源转为图片,即实现下载
  • 主类继承Thread类,run()方法调用DownLoader()来实现多线程下载
  • main方法创建对象,调用start()方法开启线程
  1. 代码实现

    package com.kuang.class1;
    
    import org.apache.commons.io.FileUtils;
    
    import java.io.File;
    import java.io.IOException;
    import java.net.URL;
    
    /**
     * 使用多线程下载网络图片的demo
     *      需要用到common-io.jar
     */
    public class TestThreadToDownload extends Thread {
    
        private final String url;
        private final String name;
    
        public TestThreadToDownload(String url, String name) {
            this.url = url;
            this.name = name;
        }
    
        @Override
        public void run() {
            // 调用下载器实现多线程下载
            WebDownLoader downLoader = new WebDownLoader();
            downLoader.DownLoader(url,name);
            System.out.println(\"下载了文件名为 :\" + name);
        }
    
        public static void main(String[] args) {
            // 创建多线程对象
            TestThreadToDownload t1 = new TestThreadToDownload(\"https://img1.baidu.com/it/u=3726701668,178087506&fm=253&fmt=auto&app=138&f=JPEG?w=353&h=499\",\"./src/com/kuang/class1/image/1.jpg\");
            TestThreadToDownload t2 = new TestThreadToDownload(\"https://img1.baidu.com/it/u=3726701668,178087506&fm=253&fmt=auto&app=138&f=JPEG?w=353&h=450\",\"./src/com/kuang/class1/image/2.jpg\");
            TestThreadToDownload t3 = new TestThreadToDownload(\"https://img1.baidu.com/it/u=3726701668,178087506&fm=253&fmt=auto&app=138&f=JPEG?w=353&h=500\",\"./src/com/kuang/class1/image/3.jpg\");
    
            // 开启线程
            t1.start();
            t2.start();
            t3.start();
        }
    }
    
    
    /**
     * 下载器实现
     */
    class WebDownLoader {
    
        public void DownLoader(String url, String name) {
            try{
                // 主要用到该方法,将传入的URL下载成本地文件
                FileUtils.copyURLToFile(new URL(url),new File(name));
            }
            catch(IOException e){
                e.printStackTrace();
                System.out.println(\"IO异常,DownLoader下载出问题\");
            }
    
        }
    }
    
  2. 效果展示

    输出了下载的文件名

    image-20220728113936353

    对应路径存在下载的图片文件,则下载成功

    image-20220728114011249

2.2、实现Runnable接口

2.2.1、Runnable基本用法

  • 实现Runnable接口并且必须重写其中的run()方法
  • Runnable创建多线程使用静态代理模式,首先必须创建实现了该接口的类的对象
  • 然后将对象传入Thread的构造器,创建一个Thread对象
  • 最后通过Thread类的对象调用start()方法开启线程

Runnable接口实现多线程比较特殊,但最终都是通过Thread类来运行的;推荐使用Runnable接口而非Thread类,因为java只能单继承,但可以实现多个接口

  1. 代码实现

    package com.kuang.class1;
    
    /**
     * 使用Runnable接口来实现多线程
     */
    public class TestRunnableDemo implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                System.out.println(\"子线程--\" +i);
            }
        }
    
        public static void main(String[] args) {
            // 静态代理模式
            // 先创建实现了Runnable接口的对象
            // 再将这个对象传入Thread的构造器,创建一个Thread类对象
            // 最后调用start方法开启
            new Thread(new TestRunnableDemo()).start();
    
            // 主线程代码
            for (int i = 0; i < 20; i++) {
                System.out.println(\"主线程--\" +i);
            }
        }
    }
    
  2. 效果展示

    主线程和子线程交替运行

    image-20220728115935628

2.2.2、模拟龟兔赛跑

  • 设置一个静态类变量winner模拟胜出者
  • 设置一个裁判方法,如果已经存在胜出者,则线程停止;如果步数大于等于100,将该线程名赋给winner,否则线程继续循环
  1. 代码实现

    package com.kuang.class1;
    
    /**
     * 多线程实现龟兔赛跑demo,
     */
    public class TestRaceDemo implements Runnable {
    
        public static String winner;   // 获胜者变量
    
        @Override
        public void run() {
            for (int i = 1; i <= 100; i++) {
    
                boolean flag = gameOver(i);
                if (flag) {
                    break;
                }
    
                System.out.println(Thread.currentThread().getName() + \"--->跑了\" + i + \"步\");
    
                // 兔子在中间开始休息
                if (i % 45 == 0 && Thread.currentThread().getName().equals(\"兔子\")) {
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
    
                // 模拟乌龟跑的慢
                if(Thread.currentThread().getName().equals(\"乌龟\")) {
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
    
        }
        // 裁判方法
        public boolean gameOver(int steps) {
            if(winner != null) {
                return true;
            }
    
            if (steps >= 100) {
                winner = Thread.currentThread().getName();
                System.out.println(\"胜出者是\" + winner);
                return true;
            }
    
            return false;
        }
    
        public static void main(String[] args) {
            TestRaceDemo raceDemo = new TestRaceDemo();
    
            new Thread(raceDemo,\"兔子\").start();
            new Thread(raceDemo,\"乌龟\").start();
        }
    }
    
  2. 效果展示

    兔子中途休息

    image-20220728191310830

    乌龟奋起直追,最后获胜

    image-20220728191336942

2.3、实现Callable接口

  • 实现Callable接口,并且重写call()方法,该方法具有返回值,返回值类型同implements定义Callable<[返回值类型]>时的类型

主方法开启多线程由两种方法:

方法1:分为创建服务、提交执行、获取结果、关闭服务四个固定步骤

  • 第0步,先创建实现了Callable接口的类的对象
  • 通过ExecutorService service = Executors.newFixedThreadPool(<线程池容量>)创建一个自定义线程数的线程池,并开启服务
  • Future<[call方法返回类型]> r1 = service.submit(<第0步对象>)将线程提交执行
  • r1.get()获取线程执行完毕后的返回值
  • service.shutdownNow()关闭服务

方法2:通过Thread代理模式开启线程

  • 先创建实现了Callable接口的类的对象
  • FutureTask<String> task = new FutureTask<>(call)创建FutureTask对象
  • new Thread(task,\"小明\").start()开启线程

FutureTask泛型间接实现了Runnable接口,相当于转换了一下

  1. 代码实现

    package com.kuang.class1;
    
    import java.util.concurrent.*;
    
    /**
     * 实现callable接口来创建多线程
     */
    public class TestCallableDemo implements Callable<String> {
    
        boolean flag = false;
        @Override
        // 此处返回值与上方泛型一致
        // 该方法相当于run方法,但带有返回值
        public String call() {
            if(!flag) {
                for (int i = 1; i <= 10; i++) {
                    if (i == 10) {
                        flag = true;
                    }
                    System.out.println(Thread.currentThread().getName() + \"正在干饭-->\" + i);
                }
            }
    
            return Thread.currentThread().getName() + \"干完饭啦!\";
        }
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            // 创建目标对象
            TestCallableDemo call = new TestCallableDemo();
    
            // 创建服务,这里创建了一个容纳三个线程的线程池
            ExecutorService service = Executors.newFixedThreadPool(3);
    
            // 提交执行
            Future<String> r1 = service.submit(call);
            Future<String> r2 = service.submit(call);
            Future<String> r3 = service.submit(call);
    
            System.out.println(r1.get());
            System.out.println(r2.get());
            System.out.println(r3.get());
    
            service.shutdownNow();
    
    
            // 第二种实现多线程的方法
            /*FutureTask<String> task = new FutureTask<>(call);
            new Thread(task,\"小明\").start();*/
    
        }
    }
    
  2. 效果展示

    image-20220728125452611

2.4、并发问题

在不控制并发的前提下实现买票系统,多线程进行买票,观察票的情况

  • 创建票数的变量
  • 实现Runnable接口,在run()方法中模拟买票,每执行一次(买一张票),票就减少一张
  • 主线程创建多个子线程来模拟并发
  1. 代码实现

    package com.kuang.class1;
    
    /**
     * 多线程买票,不控制并发
     */
    public class TestBuyTicketDemo implements Runnable {
        private static int ticketNum = 10;  // 票的数量
    
        @Override
        public void run() {
    
            while (ticketNum > 0) {
    
                // 模拟延时
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
    
                // 买票程序
                System.out.println(Thread.currentThread().getName() + \"拿到了第\" + ticketNum-- + \"票\");
            }
        }
    
        public static void main(String[] args) {
            TestBuyTicketDemo t = new TestBuyTicketDemo();
    
            // 多线程买票
            new Thread(t,\"小明\").start();
            new Thread(t,\"张三\").start();
            new Thread(t,\"黄牛党\").start();
        }
    }
    
  2. 效果展示

    多次执行可能发现,不同人可能买到同一张票

  3. **问题发现 **

    多个人买到了同一张票,导致数据不唯一

2.5、补充Lambda表达式

2.5.1、Lambda的简介

  1. 为什么要用
    • 避免匿名内部类定义过多,可以让代码简洁紧凑,留下核心的逻辑
    • Lambda表达式能够让程序员的编程更加高效
  2. 何时使用?
    • 前提:必须存在一个函数式接口(即接口中只定义了一个抽象方法),这样我们就可以使用Lambda表达式来创建该接口的对象
  3. 语法格式
    • (parameters) -> expression[表达式]
    • [(parameters) -> statements[语句]
    • [(parameters) ->{ statements; }

2.5.2、Lambda表达式用法引入

将接口的实现类、静态内部类、局部内部类、匿名内部类、Lambda式进行对比,可以发现Lambda表达式异常简洁

  1. 代码实现

    package com.kuang.class2;
    
    /**
     * lambda表达式的测试类
     * 该表达式使用的前提是,有有函数型接口
     */
    
    // 定义函数式接口:只存在一个抽象方法的接口
    interface ILike {
        void lambda();
    }
    public class TestLambda1 {
    
        // 静态内部类实现接口
        static class Like2 implements ILike {
            @Override
            public void lambda() {
                System.out.println(\"静态内部类的lambda方法\");
            }
        }
    
        public static void main(String[] args) {
    
            // 定义局部内部类
            class Like3 implements ILike {
                @Override
                public void lambda() {
                    System.out.println(\"局部内部类的lambda方法\");
                }
            }
    
            // 定义匿名内部类
            ILike like4 = new ILike() {
                @Override
                public void lambda() {
                    System.out.println(\"匿名内部类的lambda方法\");
                }
            };
    
            // lambda表达式写法
            ILike like5 = () -> System.out.println(\"真正的lambda表达式\");
    
            new Like1().lambda();
            new Like2().lambda();
            new Like3().lambda();
            like4.lambda();
            like5.lambda();
        }
    }
    
    // 定义接口实现类,传统用法
    class Like1 implements ILike {
    
        @Override
        public void lambda() {
            System.out.println(\"接口实现类的lambda方法\");
        }
    }
    
  2. 效果展示

    定义接口对象like5,使用Lambda表达式,只关心核心逻辑的写法;使用时,直接通过该对象调用接口中的方法即可

2.5.3、Lambda深入理解

上面式函数式接口中的抽象方法无参数、无返回值,这里定义有参数及返回值的抽象方法;

详细请看Java Lambda 表达式 | 菜鸟教程 (runoob.com)

  1. 代码实现

    package com.kuang.class2;
    
    /**
     * Lambda表达式测试2
     *      接口方法有参数及返回值
     */
    public class TestLambda2 {
        public static void main(String[] args) {
    
            // 这里使用lambda表达式,实现了接口中的抽象方法,将两数运算具体化,这里定义加法运算
            MathOperation add = Integer::sum;
            // 减法运算
            MathOperation sub = (int a, int b) -> a - b;
            // 乘法运算
            MathOperation multi = (a,b) -> a * b;
    
            System.out.println(add.operation(1,2));
            System.out.println(sub.operation(3,5));
            System.out.println(multi.operation(8,7));
    
        }
    
        // 定义函数式接口,
        interface MathOperation {
            // 将两数运算抽象出来
            int operation(int a, int b);
        }
    }
    
  2. 效果展示

    image-20220729123416874

三、线程状态转换

线程的五大状态在概述中已经讲到,这里主要讲解让线程状态转换的方法

3.1、线程停止

  • 不推荐使用jdk中的内置方法,如stop(),因为这是使线程强制停止的方法,可能会导致多线程的一些问题
  • 推荐自定义标志位,并加上逻辑判断,让线程在某一条件下自动停止运行
  1. 代码实现

    package com.kuang.class3;
    
    /**
     * 线程状态之:线程停止
     *      建议使用标志位加条件判断,让线程自行停止
     *      不建议使用jdk的内置方法,如stop()
     */
    public class TestThreadStop implements Runnable {
    
        @Override
        public void run() {
            int i = 0;
            while (i != 15) {       //   设置标志位,让线程主动停止
                System.out.println(\"子线程---run\" + i++);
            }
    
            System.out.println(\"子线程停止!!!\");
        }
    
        public static void main(String[] args) {
    
    
            new Thread(new TestThreadStop()).start();
    
            for (int i = 0; i < 20; i++) {
                System.out.println(\"main---run\" + i);
            }
        }
    }
    
  2. 效果展示

    当子线程循环变量i为15时,子线程自动跳出循环,结束运行

    image-20220729134434869

3.2、线程休眠

  • 主要用到Sleep()方法让线程从运行状态转变为阻塞状态
  • 每一个对象都有锁,Sleep()不会释放锁
  1. 代码实现

    实现循环显示当前时间的案例,一秒钟循环一次

    package com.kuang.class3;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    /**
     * 线程休眠Sleep
     */
    public class TestThreadSleep {
        public static void main(String[] args) {
            Date date = new Date(System.currentTimeMillis());   // 获得当前时间
    
            while(true) {
    
                try {
                    Thread.sleep(1000);   // 一秒休眠一次
                    System.out.println(new SimpleDateFormat(\"HH:mm:ss\").format(date));
                    date = new Date(System.currentTimeMillis());  // 更新时间
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
  2. 效果展示

    image-20220729140256730

3.3、线程礼让

主要用到yield()方法,在多线程情况下,让正处在运行态的线程转换为就绪态,重新竞争CPU调度(但是礼让不一定成功,再次调度还是看CPU心情)

  1. 代码实现

    package com.kuang.class3;
    
    /**
     * 线程礼让的demo
     */
    public class TestYield implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                if (i % 2 == 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    Thread.yield();    // 线程礼让
                }
                    System.out.println(Thread.currentThread().getName() + \"线程执行\" + i + \"次\");
            }
    
            System.out.println(Thread.currentThread().getName() + \"线程运行结束\");
        }
    
        public static void main(String[] args) {
            TestYield yield = new TestYield();
    
            new Thread(yield,\"a\").start();
            new Thread(yield,\"b\").start();
        }
    }
    
  2. 效果展示

    可以看出ab线程交替执行

    image-20220729141731806

3.4、线程加入

主要用到jion()方法,线程加入相当于该线程插队,插队后该线程强制执行到结束,其他线程在此期间阻塞

  1. 代码实现

    package com.kuang.class3;
    
    /**
     * 线程加入的案例,线程加入相当于让这个线程强制执行完,其他线程在此期间是阻塞的
     */
    public class TestThreadJoin implements Runnable{
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(\"VIP线程\" + i);
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            TestThreadJoin join = new TestThreadJoin();
    
            Thread thread = new Thread(join);
            thread.start();
    
            for (int i = 0; i < 10; i++) {
                if(i == 5) {
                    thread.join();   //  线程插队,并且强制执行
                }
                System.out.println(\"main线程\" + i);
            }
        }
    }
    
  2. 效果展示

    主线程执行五次后,子线程加入,并且执行完毕

    image-20220729142808361

3.5、线程状态检测

用到Thread.State中的几个类变量,分别对应线程不同时期的状态

  1. 代码实现

    package com.kuang.class3;
    
    /**
     * 观测线程状态
     */
    public class TestThreadState {
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() -> {
                for (int i = 0; i < 5; i++) {
    
                    if (i % 2 == 0) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    System.out.println(\"线程运行中。。。\");
                }
    
                System.out.println(\"/////////\");
            });
    
            Thread.State state = thread.getState();
            System.out.println(state);   // NEW状态
    
            thread.start();
            state = thread.getState();
            System.out.println(state);
    
            while(state != Thread.State.TERMINATED) {
    
                state = thread.getState();
                System.out.println(state);
    
                Thread.sleep(500);  // 每0.5s检测一次状态
            }
        }
    }
    
  2. 效果展示

    image-20220729150119404

3.6、线程优先级

  • 线程的优先级从低到高分为了10个等级,分别对应数字1到10
  • 一条线程可以使用setPriority()方法自定义设置优先级,可以使用getPriority()查看优先级
  • 高优先级并非优先调度,只是被调度的概率提高了,具体调度还是得看CPU
  1. 代码实现

    package com.kuang.class3;
    
    public class TestThreadPriority implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + \"的优先级是\" + Thread.currentThread().getPriority());
        }
    
        public static void main(String[] args) {
            TestThreadPriority test = new TestThreadPriority();
    
            Thread t1 = new Thread(test,\"t1\");
            Thread t2 = new Thread(test,\"t2\");
            Thread t3 = new Thread(test,\"t3\");
            Thread t4 = new Thread(test,\"t4\");
            Thread t5 = new Thread(test,\"t5\");
    
            // 线程自定义优先级
            t1.setPriority(Thread.MAX_PRIORITY);
            t2.setPriority(Thread.MIN_PRIORITY);
            t3.setPriority(8);
            t4.setPriority(3);
            t5.setPriority(Thread.NORM_PRIORITY);    //  默认优先级
    
            t1.start();
            t2.start();
            t3.start();
            t4.start();
            t5.start();
        }
    }
    
  2. 效果展示

    可见高优先级并非优先调度

    image-20220729154906159

3.7、守护线程

  • java提供了两种线程:守护线程用户线程
  • 守护线程,是指在程序运行时 在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分
  • 用户线程可以理解为被守护线程,JVM会等待所有用户线程,当用户线程执行完毕后,只剩守护线程存在,这时JVM会关闭,而不会等待守护线程再执行完毕,会杀死所有的守护线程(因为守护线程守护着用户线程,当用户线程执行完毕后,守护线程就无事可做了,当然没必要再往下继续执行了)
  • 守护线程的优先级一般较低,用户可以使用setDaemon()方法主动设置线程为守护线程
  1. 代码实现

    package com.kuang.class3;
    
    /**
     * 主要测试守护线程的设置
     */
    public class TestDaemon implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+\"线程启动\");
    
            // 这里延时是为了阻塞守护线程,让用户线程先执行完毕
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 这句话永远不会输出,因为被守护线程关闭后,jvm就关闭了,jvm不会等守护线程结束
            System.out.println(Thread.currentThread().getName()+\"线程关闭\");
        }
    
        public static void main(String[] args){
            System.out.println(\"main线程启动\");
    
            TestDaemon test = new TestDaemon();
            Thread thread = new Thread(test,\"A\");
    
    
            thread.setDaemon(true);
            thread.start();
            System.out.println(\"main线程关闭\");
        }
    }
    
  2. 效果展示

    可以发现JVM不会等待守护线程执行完毕,在用户线程执行完后会立即杀死守护线程

    image-20220729162843155

四、线程同步

​ 第二节讲到不控制线程并发的一些问题,比如买票是抢到同一张票(数据不唯一),严重的可能会导致死锁现象,即线程的永久等待;本节针对控制多线程并发的一系列问题,提出相应的解决方法。

4.1、线程同步的概念

  1. 线程不同步的后果

    ​ 当多线程在操作共享资源时,如果不控制线程的并发,就可能会导致共享区数据不唯一,或者线程间的死锁现象;尤其针对高并发环境下,比如12306买票,或者微信抢红包;如果不控制并发,很可能会出现比如有人在群里发了一个一百块红包,一百人同时开抢,可能这一百人都会抢到一百块钱,给平台造成损失。

  2. 何时需要线程同步

    ​ 当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象(共享资源)就是其中一种

    线程同步的底层涉及到一些算法问题,深入理解的话就需要学习《操作系统》

4.2、Java线程同步方法

4.2.1、同步方法或者同步块

主要学习synchronized关键字的使用

①、synchronized修饰方法

​ 使方法转变为同步方法,相当于给方法加锁;当方法中有线程正在操作,该方法就会被加锁,其他线程就必须排队;直到该线程操作完毕后主动解锁,其他线程再竞争进入该方法;之后依旧进行加锁和解锁操作。

  1. 代码实现

    重现抢票程序,观察使用synchronized修饰前和修饰后,输出结果的变化

    package com.kuang.class4.synchronizedDemo;
    
    /**
     * 不安全的买票案例,体验同步修饰符的用法
     */
    public class UnsafeBuyTicket {
        public static void main(String[] args) {
            BuyTicket station = new BuyTicket();
            new Thread(station,\"小明\").start();
            new Thread(station,\"黄牛党\").start();
        }
    }
    class BuyTicket implements Runnable {
        private int ticketNum = 10;     // 票的数量
        boolean flag = true;
        @Override
        public void run() {
            while(flag) {
                try {
                    buy();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        // 此处将synchronized去掉再加上,观察输出变化
        private synchronized void buy() throws InterruptedException {
            if (ticketNum <= 0) {
                flag = false;
                return;
            }
            System.out.println(Thread.currentThread().getName() + \"拿到了第\" + ticketNum-- + \"张票\");
            Thread.sleep(100);
        }
    }
    

    buy()方法加锁,观察变化

  2. 前后对比

    为加锁前:可能会出现两个人拿到同一张票的情况

    image-20220730194725442

    加锁后:一张票只能被一个人拿,线程安全

    image-20220730194813066

②、synchronized同步块

  • 用法

    synchronized(boj) {}
    

    此处obj称为同步监视器,obj可以是任何对象,但一般选用共享区资源作为监视器,例如上述买票程序中的票的数量ticketNum就是共享区资源

  1. 代码实现

    多线程操作不安全的泛型集合ArrayList,观察加锁前后变化

    package com.kuang.class4.synchronizedDemo;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 线程不安全的泛型集合,体验同步块的用法
     */
    public class UnsafeList {
    
        public static void main(String[] args) {
            List<String> list = new ArrayList<>();
                for (int i = 0; i < 100000; i++) {
                    new Thread(() ->{
                        synchronized (list) {
                            list.add(Thread.currentThread().getName());}
                    }).start();
                }
                
                // 这里的sleep是为了让主线程休眠,否则主线程可能会提前输出list的size
                try {
                    Thread.sleep(3000);
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(list.size());
        }
    }
    

    多线程给list添加1000000条数据,最后输出list的大小

  2. 前后对比

    未使用同步块之前,缺少数据

    使用同步块后,list的大小准确

4.2.2、Lock锁

取自JUC包下的锁机制,只能对代码块进行加锁,但效率比synchronized更高

①、一般写法

private final ReentrantLock lock = new ReentrantLock();   //  定义锁

public void run() {
    try {
        lock.lock();
        // 这里写需要同步的代码块
    }
    finally {
        lock.unlock();   //  如同步代码块有异常,则将解锁写在此处
        // 但一般都在此处解锁
    }
}

②、案例说明

继续以买票案例为例

  1. 代码实现

    package com.kuang.class4.lockDemo;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * 锁机制的测试案例
     */
    public class TestLock {
        public static void main(String[] args) {
            BuyTicket2 ticket2 = new BuyTicket2();
    
            new Thread(ticket2,\"小明\").start();
            new Thread(ticket2,\"小红\").start();
            new Thread(ticket2,\"黄牛党\").start();
        }
    }
    
    class BuyTicket2 implements Runnable {
    
        private int ticketNum = 10;
        boolean flag = true;
    
        private final ReentrantLock lock = new ReentrantLock();  // 定义可重入锁
        
        @Override
        public void run() {
            while(flag) {
                try {
                    buy();
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        private  void buy() throws InterruptedException {
    
            try {
                lock.lock();   //  在临界区加锁
                if (ticketNum <= 0) {
                    flag = false;
                    return;
                }
                System.out.println(Thread.currentThread().getName() + \"拿到了第\" + ticketNum-- + \"张票\");
            }
            finally {
                lock.unlock();   // 退出临界区后解锁
            }
            Thread.sleep(1000);   //  本线程阻塞,其他线程开始竞争
        }
    }
    
  2. 效果展示

    线程安全,无死锁和不安全数据

    image-20220802121311424

五、线程通信

2.1、线程通信的概念

2.1.1、为什么需要线程通信

线程是操作系统调度的最小单位,有自己的栈空间,可以按照既定的代码逐步的执行,但是如果每个线程间都孤立的运行,那就会造资源浪费。所以在现实中,我们需要这些线程间可以按照指定的规则共同完成一件任务,所以这些线程之间就需要互相协调,这个过程被称为线程的通信

因此线程通信可以概括为:当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺,并且互相协调,完成同一件任务

2.1.2、线程的通信的方式

  1. 共享内存
  2. 管程
  3. 信号量
  4. 管道
  5. 其他

2.2、管程法

实现生产者、消费者模型,该模型大致定义如下:

  • 存在缓冲区,生产者在缓冲区放入东西,消费者从缓冲区取出东西进行消费
  • 生产者,当缓冲区不满时,生产者就生产东西放入缓冲区;当缓冲区满时,生产者就停止生产进入阻塞,并通知消费者取出东西
  • 消费者,当缓冲区存在东西时,就不断取出;当缓冲区为空时,消费者停止消费进入阻塞,并通知生产了生产东西
  1. 代码实现

    package com.kuang.class5;
    
    /**
     * 测试生产者和消费者的案例
     */
    
    public class TestProducerAndCustomer {
        public static void main(String[] args) {
            SynContainer container = new SynContainer();
    
            new Producer(container).start();   // 启动生产者线程
            new Customer(container).start();   // 启动消费者线程
        }
    }
    
    
    // 生产者类
    class Producer extends Thread {
        SynContainer container;
    
        public Producer(SynContainer container) {
            this.container = container;
        }
    
        @Override
        public void run() {
            for (int i = 1; i < 101; i++) {
                container.push(new Goods(i));   //  给缓冲区放入东西
                System.out.println(\"生产了第\" + i + \"个商品\");
            }
        }
    }
    
    class Customer extends Thread {
        SynContainer container;
    
        public Customer(SynContainer container) {
            this.container = container;
        }
    
        @Override
        public void run() {
            for (int i = 1; i < 101; i++) {
                System.out.println(\"消费了第\" + container.pop().id + \"个商品\");
                // 从缓冲区取出东西
            }
        }
    }
    
    //  商品类
    class Goods {
        int id;   // 产品编号
    
        public Goods(int id) {
            this.id = id;
        }
    }
    
    // 缓冲区类
    class SynContainer {
        Goods[] goods = new Goods[10];   // 定义一个容量为10的缓冲区
        int count = 0;  // 缓冲区商品计数器
    
        // 放入缓冲区
        public synchronized void push(Goods g) {
            if (count == goods.length) {
                // 缓冲区满了,生产者休息
                try {
                    this.wait();
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            goods[count] = g;
            count++;
            // 通知消费者消费
            this.notifyAll();
        }
    
        // 从缓冲区取出
        public synchronized Goods pop() {
            if (count == 0) {
                try {
                    // 缓冲区为空,消费者阻塞
                    this.wait();
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            count--;
            // 通知生产者生产
            this.notifyAll();
            return goods[count];
        }
    }
    
  2. 效果展示

    ![https://img2022.cnblogs.com/blog/2875618/202209/2875618-20220912215157733-2044630329.png)

2.3、信号量法

是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。

  1. 代码实现

    代码实现生产者消费者模型的吃水果案例,妈妈往盘子中放水果,我从盘子中拿水果吃,盘子只有一个;当盘子中有水果,就不能再放了,并通知我吃水果;当盘子中没了水果,我就不能吃,并通知妈妈放水果。

    package com.kuang.class5;
    
    /**
     * 吃水果的生产者消费者模型
     *      妈妈往盘子放水果,我从盘子中取水果吃
     */
    public class TestEatFruit {
    
        public static void main(String[] args) {
            Plate plate = new Plate();
    
            new Me(plate).start();
            new Mom(plate).start();
        }
    }
    
    class Mom extends Thread {
        Plate plate;
    
        public Mom(Plate plate) {
            this.plate = plate;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                if (i % 2 == 0) {
                    plate.put(\"苹果\");
                }
                else if (i % 5 == 0) {
                    plate.put(\"香蕉\");
                }
                else {
                    plate.put(\"橘子\");
                }
            }
        }
    }
    
    class Me extends Thread {
        Plate plate;
    
        public Me(Plate plate) {
            this.plate = plate;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                plate.eat();
            }
        }
    }
    
    class Plate {
        String fruitName;   // 水果名
        boolean mutex = true;   // 定义互斥信号量
    
        public synchronized void put(String name) {
            if (!mutex) {
                try {
                    this.wait();   //  当盘子中有水果就必须阻塞
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(\"妈咪给了一个\" + name);
    
            this.notifyAll();   //  放了一个水果,通知我吃水果
            this.fruitName = name;
            this.mutex = !mutex;
        }
    
        public synchronized void eat() {
            if (mutex) {
                try {
                    this.wait();   //  当盘子中没有水果,就阻塞
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                System.out.println(\"我吃掉了一个\" + fruitName);
    
                this.notifyAll();   //  吃掉一个水果,通知放水果
                this.mutex = !mutex;
            }
        }
    }
    
  2. 效果展示

    两两一组

2.4、线程池法

在高并发情况下,经常要进行线程的创建与销毁,对性能影响很大;线程池法的思路就是提前创建好多个线程,放入线程池中,使用时直接获取,使用完后放入池中,可以必变重复的创建和销毁操作,提高服务器效率(可以类比共享单车,提前投放一批单车,使用时直接扫码用,使用完后放回单车点)

思路

  • 多线程实现类实现Runnable接口
  • 主类使用ExecutorService创建线程池
  • 使用execute(Runnable obj)执行任务,该方法属于上面接口,没有返回值
  • 最后使用shutdown(),关闭连接池
  1. 代码实现

    package com.kuang.class5;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /**
     * 使用线程池方法实现线程通信
     */
    public class TestThreadPool {
        public static void main(String[] args) {
            // 创建线程池服务,参数为线程池大小
            // 该部分内容属于JUC编程,Executors也属于JUC包
            ExecutorService service = Executors.newFixedThreadPool(10);
    
            service.execute(new MyThread());
            service.execute(new MyThread());
            service.execute(new MyThread());
            service.execute(new MyThread());
            service.shutdown();
        }
    }
    
    class MyThread implements Runnable {
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    }
    
  2. 效果展示


来源:https://www.cnblogs.com/70ny/p/16687435.html
本站部分图文来源于网络,如有侵权请联系删除。

未经允许不得转载:百木园 » java多线程基础学习

相关推荐

  • 暂无文章