返回顶部
首页 > 资讯 > 后端开发 > JAVA >从零开始学习 Java:简单易懂的入门指南之线程同步(三十五)
  • 664
分享到

从零开始学习 Java:简单易懂的入门指南之线程同步(三十五)

学习javapython开发语言 2023-10-18 13:10:32 664人浏览 八月长安
摘要

线程同步 1.线程同步1.1卖票【应用】1.2卖票案例的问题1.3同步代码块解决数据安全问题【应用】1.4同步方法解决数据安全问题【应用】1.5Lock锁【应用】1.6死锁 2.生产者消

线程同步

1.线程同步

1.1卖票【应用】

  • 案例需求

    某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

  • 实现步骤

    • 定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;

    • 在SellTicket类中重写run()方法实现卖票,代码步骤如下

    • 判断票数大于0,就卖票,并告知是哪个窗口卖的

    • 卖了票之后,总票数要减1

    • 票卖没了,线程停止

    • 定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下

    • 创建SellTicket类的对象

    • 创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称

    • 启动线程

  • 代码实现

    public class SellTicket implements Runnable {    private int tickets = 100;    //在SellTicket类中重写run()方法实现卖票,代码步骤如下    @Override    public void run() {        while (true) {            if(ticket <= 0){                    //卖完了                    break;                }else{                    try {                        Thread.sleep(100);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    ticket--;                    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");                }        }    }}public class SellTicketDemo {    public static void main(String[] args) {        //创建SellTicket类的对象        SellTicket st = new SellTicket();        //创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称        Thread t1 = new Thread(st,"窗口1");        Thread t2 = new Thread(st,"窗口2");        Thread t3 = new Thread(st,"窗口3");        //启动线程        t1.start();        t2.start();        t3.start();    }}

1.2卖票案例的问题

  • 卖票出现了问题

    • 相同的票出现了多次

    • 出现了负数的票

  • 问题产生原因

    线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现问题

1.3同步代码块解决数据安全问题【应用】

  • 安全问题出现的条件

    • 多线程环境

    • 有共享数据

    • 有多条语句操作共享数据

  • 如何解决多线程安全问题呢?

    • 基本思想:让程序没有安全问题的环境
  • 怎么实现呢?

    • 把多条语句操作共享数据的代码给起来,让任意时刻只能有一个线程执行即可

    • Java提供了同步代码块的方式来解决

  • 同步代码块格式:

    synchronized(任意对象) { 多条语句操作共享数据的代码 }

    synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁

  • 同步的好处和弊端

    • 好处:解决了多线程的数据安全问题

    • 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

  • 代码演示

    public class SellTicket implements Runnable {    private int tickets = 100;    private Object obj = new Object();    @Override    public void run() {        while (true) {            synchronized (obj) { // 对可能有安全问题的代码加锁,多个线程必须使用同一把锁                //t1进来后,就会把这段代码给锁起来                if (tickets > 0) {                    try {                        Thread.sleep(100);                        //t1休息100毫秒                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    //窗口1正在出售第100张票                    System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");                    tickets--; //tickets = 99;                }            }            //t1出来了,这段代码的锁就被释放了        }    }}public class SellTicketDemo {    public static void main(String[] args) {        SellTicket st = new SellTicket();        Thread t1 = new Thread(st, "窗口1");        Thread t2 = new Thread(st, "窗口2");        Thread t3 = new Thread(st, "窗口3");        t1.start();        t2.start();        t3.start();    }}

1.4同步方法解决数据安全问题【应用】

  • 同步方法的格式

    同步方法:就是把synchronized关键字加到方法上

    修饰符 synchronized 返回值类型 方法名(方法参数) { 方法体;}

    同步方法的锁对象是什么呢?

    ​ this

  • 静态同步方法

    同步静态方法:就是把synchronized关键字加到静态方法上

    修饰符 static synchronized 返回值类型 方法名(方法参数) { 方法体;}

    同步静态方法的锁对象是什么呢?

    ​ 类名.class

  • 代码演示

    public class MyRunnable implements Runnable {    private static int ticketCount = 100;    @Override    public void run() {        while(true){            if("窗口一".equals(Thread.currentThread().getName())){                //同步方法                boolean result = synchronizedMthod();                if(result){                    break;                }            }            if("窗口二".equals(Thread.currentThread().getName())){                //同步代码块                synchronized (MyRunnable.class){                    if(ticketCount == 0){                       break;                    }else{                        try {Thread.sleep(10);                        } catch (InterruptedException e) {e.printStackTrace();                        }                        ticketCount--;                        System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");                    }                }            }        }    }    private static synchronized boolean synchronizedMthod() {        if(ticketCount == 0){            return true;        }else{            try {                Thread.sleep(10);            } catch (InterruptedException e) {                e.printStackTrace();            }            ticketCount--;            System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");            return false;        }    }}
  public class Demo {      public static void main(String[] args) {          MyRunnable mr = new MyRunnable();          Thread t1 = new Thread(mr);          Thread t2 = new Thread(mr);              t1.setName("窗口一");          t2.setName("窗口二");              t1.start();          t2.start();      }  }

1.5Lock锁【应用】

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,jdk5以后提供了一个新的锁对象Lock

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化

  • ReentrantLock构造方法

    方法名说明
    ReentrantLock()创建一个ReentrantLock的实例
  • 加锁解锁方法

    方法名说明
    void lock()获得锁
    void unlock()释放锁
  • 代码演示

    public class Ticket implements Runnable {    //票的数量    private int ticket = 100;    private Object obj = new Object();    private ReentrantLock lock = new ReentrantLock();    @Override    public void run() {        while (true) {            //synchronized (obj){//多个线程必须使用同一把锁.            try {                lock.lock();                if (ticket <= 0) {                    //卖完了                    break;                } else {                    Thread.sleep(100);                    ticket--;                    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");                }            } catch (InterruptedException e) {                e.printStackTrace();            } finally {                lock.unlock();            }            // }        }    }}public class Demo {    public static void main(String[] args) {        Ticket ticket = new Ticket();        Thread t1 = new Thread(ticket);        Thread t2 = new Thread(ticket);        Thread t3 = new Thread(ticket);        t1.setName("窗口一");        t2.setName("窗口二");        t3.setName("窗口三");        t1.start();        t2.start();        t3.start();    }}

1.6死锁

  • 概述

    线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行

  • 什么情况下会产生死锁

    1. 资源有限
    2. 同步嵌套
  • 代码演示

    public class Demo {    public static void main(String[] args) {        Object objA = new Object();        Object objB = new Object();        new Thread(()->{            while(true){                synchronized (objA){                    //线程一                    synchronized (objB){                        System.out.println("小康同学正在走路");                    }                }            }        }).start();        new Thread(()->{            while(true){                synchronized (objB){                    //线程二                    synchronized (objA){                        System.out.println("小薇同学正在走路");                    }                }            }        }).start();    }}

2.生产者消费者

2.1生产者和消费者模式概述【应用】

  • 概述

    生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。

    所谓生产者消费者问题,实际上主要是包含了两类线程:

    ​ 一类是生产者线程用于生产数据

    ​ 一类是消费者线程用于消费数据

    为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

    生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为

    消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

  • Object类的等待和唤醒方法

    方法名说明
    void wait()导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
    void notify()唤醒正在等待对象监视器的单个线程
    void notifyAll()唤醒正在等待对象监视器的所有线程

2.2生产者和消费者案例【应用】

  • 案例需求

    • 桌子类(Desk):定义表示包子数量的变量,定义锁对象变量,定义标记桌子上有无包子的变量

    • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务

      判断是否有包子,决定当前线程是否执行

      如果有包子,就进入等待状态,如果没有包子,继续执行,生产包子

      生产包子之后,更新桌子上包子状态,唤醒消费者消费包子

    • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务

      判断是否有包子,决定当前线程是否执行

      如果没有包子,就进入等待状态,如果有包子,就消费包子

      消费包子后,更新桌子上包子状态,唤醒生产者生产包子

    • 测试类(Demo):里面有main方法,main方法中的代码步骤如下

      创建生产者线程和消费者线程对象

      分别开启两个线程

  • 代码实现

    public class Desk {    //定义一个标记    //true 就表示桌子上有汉堡包的,此时允许吃货执行    //false 就表示桌子上没有汉堡包的,此时允许厨师执行    public static boolean flag = false;    //汉堡包的总数量    public static int count = 10;    //锁对象    public static final Object lock = new Object();}public class Cooker extends Thread {//    生产者步骤://            1,判断桌子上是否有汉堡包//    如果有就等待,如果没有才生产。//            2,把汉堡包放在桌子上。//            3,叫醒等待的消费者开吃。    @Override    public void run() {        while(true){            synchronized (Desk.lock){                if(Desk.count == 0){                    break;                }else{                    if(!Desk.flag){                        //生产                        System.out.println("厨师正在生产汉堡包");                        Desk.flag = true;                        Desk.lock.notifyAll();                    }else{                        try {Desk.lock.wait();                        } catch (InterruptedException e) {e.printStackTrace();                        }                    }                }            }        }    }}public class Foodie extends Thread {    @Override    public void run() {//        1,判断桌子上是否有汉堡包。//        2,如果没有就等待。//        3,如果有就开吃//        4,吃完之后,桌子上的汉堡包就没有了//                叫醒等待的生产者继续生产//        汉堡包的总数量减一        //套路:            //1. while(true)死循环            //2. synchronized 锁,锁对象要唯一            //3. 判断,共享数据是否结束. 结束            //4. 判断,共享数据是否结束. 没有结束        while(true){            synchronized (Desk.lock){                if(Desk.count == 0){                    break;                }else{                    if(Desk.flag){                        //有                        System.out.println("吃货在吃汉堡包");                        Desk.flag = false;                        Desk.lock.notifyAll();                        Desk.count--;                    }else{                        //没有就等待                        //使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.                        try {Desk.lock.wait();                        } catch (InterruptedException e) {e.printStackTrace();                        }                    }                }            }        }    }}public class Demo {    public static void main(String[] args) {                        Foodie f = new Foodie();        Cooker c = new Cooker();        f.start();        c.start();    }}

2.3生产者和消费者案例优化【应用】

  • 需求

    • 将Desk类中的变量,采用面向对象的方式封装起来
    • 生产者和消费者类中构造方法接收Desk类对象,之后在run方法中进行使用
    • 创建生产者和消费者线程对象,构造方法中传入Desk类对象
    • 开启两个线程
  • 代码实现

    public class Desk {    //定义一个标记    //true 就表示桌子上有汉堡包的,此时允许吃货执行    //false 就表示桌子上没有汉堡包的,此时允许厨师执行    //public static boolean flag = false;    private boolean flag;    //汉堡包的总数量    //public static int count = 10;    //以后我们在使用这种必须有默认值的变量   // private int count = 10;    private int count;    //锁对象    //public static final Object lock = new Object();    private final Object lock = new Object();    public Desk() {        this(false,10); // 在空参内部调用带参,对成员变量进行赋值,之后就可以直接使用成员变量了    }    public Desk(boolean flag, int count) {        this.flag = flag;        this.count = count;    }    public boolean isFlag() {        return flag;    }    public void setFlag(boolean flag) {        this.flag = flag;    }    public int getCount() {        return count;    }    public void setCount(int count) {        this.count = count;    }    public Object getLock() {        return lock;    }    @Override    public String toString() {        return "Desk{" +                "flag=" + flag +                ", count=" + count +                ", lock=" + lock +                '}';    }}public class Cooker extends Thread {    private Desk desk;    public Cooker(Desk desk) {        this.desk = desk;    }//    生产者步骤://            1,判断桌子上是否有汉堡包//    如果有就等待,如果没有才生产。//            2,把汉堡包放在桌子上。//            3,叫醒等待的消费者开吃。    @Override    public void run() {        while(true){            synchronized (desk.getLock()){                if(desk.getCount() == 0){                    break;                }else{                    //System.out.println("验证一下是否执行了");                    if(!desk.isFlag()){                        //生产                        System.out.println("厨师正在生产汉堡包");                        desk.setFlag(true);                        desk.getLock().notifyAll();                    }else{                        try {desk.getLock().wait();                        } catch (InterruptedException e) {e.printStackTrace();                        }                    }                }            }        }    }}public class Foodie extends Thread {    private Desk desk;    public Foodie(Desk desk) {        this.desk = desk;    }    @Override    public void run() {//        1,判断桌子上是否有汉堡包。//        2,如果没有就等待。//        3,如果有就开吃//        4,吃完之后,桌子上的汉堡包就没有了//                叫醒等待的生产者继续生产//        汉堡包的总数量减一        //套路:            //1. while(true)死循环            //2. synchronized 锁,锁对象要唯一            //3. 判断,共享数据是否结束. 结束            //4. 判断,共享数据是否结束. 没有结束        while(true){            synchronized (desk.getLock()){                if(desk.getCount() == 0){                    break;                }else{                    //System.out.println("验证一下是否执行了");                    if(desk.isFlag()){                        //有                        System.out.println("吃货在吃汉堡包");                        desk.setFlag(false);                        desk.getLock().notifyAll();                        desk.setCount(desk.getCount() - 1);                    }else{                        //没有就等待                        //使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.                        try {desk.getLock().wait();                        } catch (InterruptedException e) {e.printStackTrace();                        }                    }                }            }        }    }}public class Demo {    public static void main(String[] args) {                        Desk desk = new Desk();        Foodie f = new Foodie(desk);        Cooker c = new Cooker(desk);        f.start();        c.start();    }}

2.4阻塞队列基本使用

  • 阻塞队列继承结构

在这里插入图片描述

  • 常见BlockingQueue:

    ArrayBlockingQueue: 底层是数组,有界

    LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值

  • BlockingQueue的核心方法:

    put(anObject): 将参数放入队列,如果放不进去会阻塞

    take(): 取出第一个数据,取不到会阻塞

  • 代码示例

    public class Demo02 {    public static void main(String[] args) throws Exception {        // 创建阻塞队列的对象,容量为 1        ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);        // 存储元素        arrayBlockingQueue.put("汉堡包");        // 取元素        System.out.println(arrayBlockingQueue.take());        System.out.println(arrayBlockingQueue.take()); // 取不到会阻塞        System.out.println("程序结束了");    }}

2.5阻塞队列实现等待唤醒机制

  • 案例需求

    • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务

      构造方法中接收一个阻塞队列对象

      在run方法中循环向阻塞队列中添加包子

      打印添加结果

    • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务

      构造方法中接收一个阻塞队列对象

      在run方法中循环获取阻塞队列中的包子

      打印获取结果

    • 测试类(Demo):里面有main方法,main方法中的代码步骤如下

      创建阻塞队列对象

      创建生产者线程和消费者线程对象,构造方法中传入阻塞队列对象

      分别开启两个线程

  • 代码实现

    public class Cooker extends Thread {    private ArrayBlockingQueue<String> bd;    public Cooker(ArrayBlockingQueue<String> bd) {        this.bd = bd;    }//    生产者步骤://            1,判断桌子上是否有汉堡包//    如果有就等待,如果没有才生产。//            2,把汉堡包放在桌子上。//            3,叫醒等待的消费者开吃。    @Override    public void run() {        while (true) {            try {                bd.put("汉堡包");                System.out.println("厨师放入一个汉堡包");            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}public class Foodie extends Thread {    private ArrayBlockingQueue<String> bd;    public Foodie(ArrayBlockingQueue<String> bd) {        this.bd = bd;    }    @Override    public void run() {//        1,判断桌子上是否有汉堡包。//        2,如果没有就等待。//        3,如果有就开吃//        4,吃完之后,桌子上的汉堡包就没有了//                叫醒等待的生产者继续生产//        汉堡包的总数量减一        //套路:        //1. while(true)死循环        //2. synchronized 锁,锁对象要唯一        //3. 判断,共享数据是否结束. 结束        //4. 判断,共享数据是否结束. 没有结束        while (true) {            try {                String take = bd.take();                System.out.println("吃货将" + take + "拿出来吃了");            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}public class Demo {    public static void main(String[] args) {        ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1);        Foodie f = new Foodie(bd);        Cooker c = new Cooker(bd);        f.start();        c.start();    }}

后记
👉👉💕💕美好的一天,到此结束,下次继续努力!欲知后续,请看下回分解,写作不易,感谢大家的支持!! 🌹🌹🌹

来源地址:https://blog.csdn.net/m0_59230408/article/details/132341399

--结束END--

本文标题: 从零开始学习 Java:简单易懂的入门指南之线程同步(三十五)

本文链接: https://lsjlt.com/news/431299.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作