线程、多线程 、线程安全、线程通信、线程池 --java学习笔记

news2024/10/7 19:29:59

目录

什么是线程?

什么是进程?

线程的生命周期

线程的6种状态互相转换

什么是多线程?

如何在程序中创建出多条线程?

多线程的注意事项

多线程的创建方式一:继承Thread类

方式一优缺点:

代码演示:

多线程的创建方式二:实现Runnable接口

方式二的优缺点

代码演示:

线程创建方式二的匿名内部类写法

前两种线程创建方式都存在一个问题

多线程的第三种创建方式:利用Callable接口、FutureTask类实现

FutureTask的API

线程创建方式三的优缺点

代码演示:

Thread提供了很多与线程操作相关的方法

代码演示:

线程安全问题

线程安全问题出现的原因?

取钱案例

代码:

线程同步

方法一、同步代码块

同步锁的注意事项

代码演示:

方法二、同步方法

同步方法底层原理

代码演示:

同步代码块好还是同步方法好?

方法三、Lock锁

Lock的常用方法

代码演示:

线程通信

什么是线程通信?

Object类的等待和唤醒方法:

线程通信的模型

线程池

什么是线程池?

线程池的工作原理

如何得到线程池对象?

ThreadPoolExecutor构造器

新任务拒绝策略:

ExecutorService的常用方法

代码演示:

使用线程池处理Runnable任务

使用线程池处理Callable任务

核心线程数量到底配置为多少好?

 还可以通过Executors创建线程池对象

Executors使用可能存在的陷阱

练习


什么是线程?

  • 线程(Thread)是一个程序内部的一条执行流程
  • 程序中如果只有一条执行流程,那这个程序就是单线程的程序

什么是进程?

  • 正在运行的程序(软件)就是一个独立的进程
  • 线程是属于进程的,一个进程中可以同时运行很多个线程
  • 进程中的多个线程其实是并发和并行执行的

并发进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发

并行:在同一个时刻上,同时有多个线程在被CPU调度执行

多线程是并发和并行同时进行的!

线程的生命周期

  • 也就是线程从生到死的过程中,经历的各种状态及状态转换
  • Java总共定义了6种状态,6种状态都定义在Thread类的内部枚举类中

线程的6种状态互相转换

什么是多线程?

  • 多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)

多线程体现在哪些方面?

比如日常的买票,百度网盘的多个下载任务,都使用到了多线程技术,再例如消息通信、淘宝、京东系统都离不开多线程技术

如何在程序中创建出多条线程?

  • Java是通过java.lang.Thread 类的对象来代表线程的

多线程的注意事项

1、启动线程必须是调用start方法,不是调用run方法

  • 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行
  • 只有调用start方法才是启动一个新的线程执行

2、不要把主线程任务放在启动子线程之前

  • 这样主线程一直是先跑完的,相当于是一个单线程的效果了

多线程的创建方式一:继承Thread类

  • 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
  • 创建MyThread类的对象
  • 调用线程对象的start()方法启动线程(启动后还是执行run方法的)

方式一优缺点:

  • 优点:编码简单
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展

代码演示:

MyThread:

//1、让子类继承Thread线程类
public class MyThread extends Thread{
    //2、必须重写Thread类的run方法
    @Override
    public void run() {
        //描述线程的执行任务
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程输出" + i);
        }
    }
}

test:

//main方法是由一条默认的主线程负责执行
public class threadTest1 {
    public static void main(String[] args) {
        //3、创建MyThread线程类对象代表一个线程
        Thread t = new MyThread();
        //4、启动线程(自动执行run方法)
        t.start();  //此时已是多线程,main线程,t线程

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程main输出" + i);
        }
    }
}

运行结果:


多线程的创建方式二:实现Runnable接口

  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  2. 创建MyRunnable任务对象
  3. 把MyRunnable任务对象交给Thread处理:
  4. 调用线程对象的start()方法启动线程

方式二的优缺点

  • 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强
  • 缺点:需要多一个Runnable对象

代码演示:

MyRunnable:

//1、定义一个任务类,实现Runnable接口
public class MyRunnable implements Runnable{
    //2、重写runnable的run方法
    @Override
    public void run() {
        //线程要执行的任务
        for (int i = 0; i < 5; i++) {
            System.out.println("嘿嘿嘿嘿嘿!!!!!!");
        }
    }
}

test:

package com.zeyu.thread;

public class threadTest2 {
    public static void main(String[] args) {
        //3、创建任务对象
        Runnable target = new MyRunnable();

        //把任务对象交给一个线程对象处理
        new Thread(target).start();

        for (int i = 0; i < 5; i++) {
            System.out.println("耶耶耶耶耶!!!!!!");
        }

    }
}

运行结果:

线程创建方式二的匿名内部类写法

  1. 可以创建Runnable的匿名内部类对象
  2. 再交给Thread线程对象
  3. 再调用线程对象的start()启动线程
public class threadTest2_2 {
    public static void main(String[] args) {
        //1、直接创建Runnable的匿名内部类形式(任务对象)
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("嘻嘻" + i);
                }
            }
        };
        new Thread(runnable).start();

        //简化1
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("呵呵" + i);
                }
            }
        }).start();

        //简化2
        new Thread(() ->{
                for (int i = 0; i < 5; i++)
                    System.out.println("耶耶" + i);
        }).start();

        for (int i = 0; i < 5; i++) {
            System.out.println("哈哈" + i);
        }
    }
}

前两种线程创建方式都存在一个问题

  • 假如线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果

怎么解决这个问题?

  • JDK 5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)
  • 这种方式最大的优点:可以返回线程执行完毕后的结果

多线程的第三种创建方式:利用Callable接口、FutureTask类实现

  1. 创建任务对象
    ——>定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据
    ——>把Callable类型的对象封装成FutureTask(线程任务对象)
  2. 把线程任务对象交给Thread对象
  3. 调用Thread对象的start方法启动线程
  4. 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果

FutureTask的API

线程创建方式三的优缺点

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果
  • 缺点:编码复杂一点

代码演示:

MyCallable:

import java.util.concurrent.Callable;
//1、让这个类实现Callable接口
public class MyCallable implements Callable<String> {
    int n;

    public MyCallable(int n) {
        this.n = n;
    }

    //2、重写call方法
    @Override
    public String call() throws Exception {
        //描述现成的任务,返回线程执行结果
        //求1-n的和
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }

        return "子线程求出了1-" + n + "的和为:" + sum;
    }
}

test:

package com.zeyu.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class threadTest3 {
    public static void main(String[] args) throws Exception {
        //3、创建一个Callable的对象
        Callable<String> call = new MyCallable(100);

        //4、把Callable的对象封装成一个FutureTask对象(任务对象)
        //未来任务对象的作用:
        //1、是一个任务对象,实现类Runnable对象
        //2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕的结果
        FutureTask<String> f1 = new FutureTask<>(call);

        //5、把任务对象交给一个Thread对象
        new Thread(f1).start();

        //6、获取线程执行完毕后返回的结果
        //注意:如果执行到这里上面的线程还没有执行完毕
        //这里的代码会暂停,等上面的线程执行完毕后才会获取结果
        System.out.println(f1.get());
    }
}

运行结果:


Thread提供了很多与线程操作相关的方法

代码演示:

MyThread:

package com.zeyu.thread;
//1、让子类继承Thread线程类
public class MyThread extends Thread{
    public MyThread() {
    }

    public MyThread(String name){
        super(name);
    }

    //2、必须重写Thread类的run方法
    @Override
    public void run() {
        Thread t = Thread.currentThread();  //得到执行它的线程
//        描述线程的执行任务
        for (int i = 0; i < 5; i++) {
            System.out.println(t.getName() + "输出" + i);
        }
    }
}

test:

package com.zeyu.thread;

public class threadTest4 {
    public static void main(String[] args) throws Exception{
        Thread t1 = new MyThread("一号线程");
//        t1.setName("一号线程");
        t1.start();
        t1.join();  //先执行完这个线程再执行别的
        System.out.println(t1.getName());

        Thread t2 = new MyThread("二号线程");
//        t2.setName("二号线程");
        t2.start();
        System.out.println(t2.getName());

        //主线程对象的名字
        //那个线程执行它,它就会得到哪个线程对象
        Thread m = Thread.currentThread();
        m.setName("主线程");
        System.out.println(m.getName());

        for (int i = 0; i < 5; i++) {
            System.out.println(m.getName() + "输出" + i);
        }

        Thread.sleep(5000); //暂停五秒

    }
}

运行结果:


线程安全问题

  • 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题

线程安全问题出现的原因?

  • 存在多个线程在同时执行
  • 同时访问一个共享资源
  • 存在修改该共享资源

取钱案例

需求:
小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万
分析:

  1. 需要提供一个账户类,接着创建一个账户对象代表2个人的共享账户
  2. 需要定义一个线程类(用于创建两个线程,分别代表小明和小红)
  3. 创建2个线程,传入同一个账户对象给2个线程处理
  4. 启动2个线程,同时去同一个账户对象中取钱10万

代码:

账户类:

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

public class Account {
    private double money;   //余额

    public static void test(){  //静态方法建议用类名.class作为锁
        synchronized (Account.class) {
            System.out.println();
        }
    }
            //同步方法,内部隐含了一个锁(this)
    public void drawMoney(double money){    //模拟取钱
        String name = Thread.currentThread().getName();

            if(this.money >= money){
                System.out.println(name + "取钱" + money + "元成功!");
                this.money -= money;
                System.out.println(name + "取钱后,账户余额还剩" + this.money + "元");
            }else{
                System.out.println(name + "取钱失败,余额不足!");
            }

    }

    public Account() {
    }

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

    public double getMoney() {
        return money;
    }

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

取钱线程类:

package com.zeyu.threadsafe;

public class DrawThread extends Thread{
    private Account acc;

    public DrawThread(Account acc, String name){
        super(name);
        this.acc = acc;
    }

    @Override
    public void run() {
        acc.drawMoney(100000);
    }
}

test:

package com.zeyu.threadsafe;

public class threadSafeTest1 {
    public static void main(String[] args) {
        //创建一个账对象,代表俩个人的共同账户
        Account account = new Account(100000);

        //创建俩个线程代表小红和小明,再去同一个账户取钱10w
        new DrawThread(account,"小红").start();
        new DrawThread(account,"小明").start();
    }
}

运行结果:

可以看到,此时就出现了线程安全问题,俩个人同时去银行取钱,且都判定余额足够,于是俩个人都取了10w出去,结果余额直接剩-10w,这显然是不行的,那么该如何解决这种线程安全问题?


线程同步

  • 解决线程安全问题的方案

线程同步的思想

  • 让多个线程实现先后依次访问共享资源,这样就解决了安全问题

方法一、同步代码块

  • 作用:把访问共享资源的核心代码给上锁,以此保证线程安全
  • 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行

同步锁的注意事项

  • 对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug

锁对象可以随便选择一个唯一的对象吗?

  • 不能,会影响其他无关线程的执行

锁对象的使用规范

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

代码演示:

同样是刚才的取钱案例,将核心代码块锁住,便可解决线程安全问题

 synchronized (this) {     //同步代码块
            if(this.money >= money){
                System.out.println(name + "取钱" + money + "元成功!");
                this.money -= money;
                System.out.println(name + "取钱后,账户余额还剩" + this.money + "元");
            }else{
                System.out.println(name + "取钱失败,余额不足!");
            }
        }

运行结果:


方法二、同步方法

  • 作用:把访问共享资源的核心方法给上锁,以此保证线程安全
  • 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行

同步方法底层原理

  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码
  • 如果方法是实例方法:同步方法默认用this作为的锁对象
  • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象

代码演示:

直接将方法锁住:

       //同步方法,内部隐含了一个锁(this)
public synchronized void drawMoney(double money){    //模拟取钱
        String name = Thread.currentThread().getName();

            if(this.money >= money){
                System.out.println(name + "取钱" + money + "元成功!");
                this.money -= money;
                System.out.println(name + "取钱后,账户余额还剩" + this.money + "元");
            }else{
                System.out.println(name + "取钱失败,余额不足!");
            }
    }

运行结果:

同步代码块好还是同步方法好?

  • 范围上:同步代码块锁的范围更小,同步方法锁的范围更大
  • 可读性:同步方法更好

方法三、Lock锁

  • LocK锁是IDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大
  • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象

Lock的常用方法

代码演示:

在类里面定义一个Lock锁对象,然后在核心代码块执行前用锁对象上锁,结束后再解锁,为了避免核心代码块出现错误导致无法解锁,可以用try-catch-finall 方式

定义Lock锁对象

    private final Lock lk = new ReentrantLock();  //锁对象

上锁解锁:

  try {   //即使锁住的代码块出现了bug,也会解锁
            lk.lock();  //上锁
            if(this.money >= money){
                System.out.println(name + "取钱" + money + "元成功!");
                this.money -= money;
                System.out.println(name + "取钱后,账户余额还剩" + this.money + "元");
            }else{
                System.out.println(name + "取钱失败,余额不足!");
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lk.unlock();    //解锁
        }

运行结果:

线程通信

什么是线程通信?

  • 当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。

线程通信的常见模型(生产者与消费者模型)

  • 生产者线程负责生产数据
  • 消费者线程负责消费生产者生产的数据,
  • 注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产!

Object类的等待和唤醒方法:

注意:上述方法应该使用当前同步锁对象进行调用!

线程通信的模型

3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃

四种情况:

1、抢到桌子的是厨师,发现没有包子,做包子,做完之后唤醒他人,自己沉睡

2、抢到桌子的是厨师,发现有包子,唤醒他人,自己沉睡

3、抢到桌子的是吃货,发现有包子,吃包子,吃完之后唤醒他人,自己沉睡

4、抢到桌子的是吃货,发现没有包子,唤醒他人,自己盛水

所以最后必然会是生产者生产——>消费者消费的流程

代码演示:

Desk类:

import java.util.ArrayList;

public class Desk {
    ArrayList<String> list = new ArrayList<>();
    //因为有多个线程访问,所以肯定要给方法上锁,先保证线程安全,再进行线程通信
    //因为使用的是同一个锁(this,也就是调用方法的对象,都是同一个),所以一把锁可以拦住所有厨师和吃货,桌子上只能有一个人
    public synchronized void put() {
        String name = Thread.currentThread().getName();
        try {   //判断是否有包子
            if(list.size() == 0){
                list.add(name + "做的肉包子");
                System.out.println(name + "做了一个肉包子!");
                //花俩秒做包子
                Thread.sleep(2000);

                //唤醒别人,等待自己(先唤醒,再等待!)
                this.notify();
                this.wait();
            }else {
                //有包子,不做了
                //唤醒别人,等待自己
                this.notify();
                this.wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public synchronized void get() {
        String name = Thread.currentThread().getName();

        try {
            if(list.size() == 1){
                //有包子,吃了
                System.out.println(name + "吃了" + list.remove(0));
                //吃包子花费一秒
                Thread.sleep(1000);

                //唤醒别人,等待自己
                this.notify();
                this.wait();
            }else{
                //没有包子
                //唤醒别人,等待自己
                this.notify();
                this.wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

test:

package com.zeyu.thread_communication;

public class thread_communicationTest1 {
    public static void main(String[] args) {
        Desk desk = new Desk();

        new Thread(() -> {
            while (true) {
                desk.put();
            }
        },"厨师1").start();

        new Thread(() -> {
            while (true) {
                desk.put();
            }
        },"厨师2").start();

        new Thread(() -> {
            while (true) {
                desk.put();
            }
        },"厨师3").start();

        new Thread(() -> {
            while (true) {
                desk.get();
            }
        },"吃货1").start();

        new Thread(() -> {
            while (true) {
                desk.get();
            }
        },"吃货2").start();
    }
}

运行结果:

线程池

什么是线程池?

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

不使用线程池的问题

        用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能

线程池的工作原理

复用线程,任务列表每有一个任务进来,工作线程就接手处理,处理完手上的任务再从任务列表找新任务

谁代表线程池?

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

如何得到线程池对象?

  • 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
  • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象

ThreadPoolExecutor构造器

参数一corePoolSize:指定线程池的核心线程的数量      正式工:3
参数二maximumPoolSize:指定线程池的最大线程数量      最大员工数:5  决定  临时工:2
参数三keepAliveTime:指定临时线程的存活时间      临时工空闲多久被开除
参数四unit:指定临时线程存活的时间单位(秒、分、时、天)
参数五workOueue:指定线程池的任务队列     客人排队的地方
参数六threadFactory:指定线程池的线程工厂      负责招聘员工的(hr)
参数七handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理(任务拒绝策略)

新任务拒绝策略:

ExecutorService的常用方法

代码演示:

使用线程池处理Runnable任务
import java.util.concurrent.*;

public class thread_poolTest1 {
    public static void main(String[] args) {
        //创建线程池
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 10, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        Runnable target = new MyRunnable();
        pool.execute(target);   //自动创建新线程处理任务
        pool.execute(target);   //自动创建新线程处理任务
        pool.execute(target);   //自动创建新线程处理任务
        pool.execute(target);   //复用前面的核心线程
        pool.execute(target);   //复用前面的核心线程

        pool.shutdown();    //等着线程池的任务全部结束之后再关闭线程池
//        pool.shutdownNow(); //立即关闭线程池不管任务有没有全部结束
    }
}

运行结果:

可以看到,线程一和线程三都处理了俩个任务,这就是线程的复用,而临时线程只有在核心线程都在工作且任务列表堆满的时候才会创建出来工作

使用线程池处理Callable任务
import java.util.concurrent.*;

public class thread_poolTest2 {
    public static void main(String[] args) throws Exception {
        //创建线程池对象
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 10, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
        //使用线程池处理Callable任务
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));
        Future<String> f4 = pool.submit(new MyCallable(400));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());

        pool.shutdown();
    }
}

MyCallable:

import java.util.concurrent.Callable;

//1、让这个类实现Callable接口
public class MyCallable implements Callable<String> {
    int n;

    public MyCallable(int n) {
        this.n = n;
    }

    //2、重写call方法
    @Override
    public String call() throws Exception {
        //描述现成的任务,返回线程执行结果
        //求1-n的和
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }

        return Thread.currentThread().getName() + "线程求出了1-" + n + "的和为:" + sum;
    }
}

运行结果:

这里也复用了线程2

核心线程数量到底配置为多少好?

  • 计算密集型任务: 核心线程数量 = CPU的核数 + 1
  • IO密集型任务,核心线程数 = CPU核数 * 2

 cpu核数 = ctrl + alt + delete --> 任务管理器 --> 性能 --> cpu

 还可以通过Executors创建线程池对象

例如:

//通过executors创建线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(3);  //核心线程数

Executors是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象

注意 :这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象

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

练习

目标:有100份礼品小红,小明两人同时发送,当剩下的礼品小于10份的时候则不再送出,利用多线程模拟该过程并将线程的名称打印出来。并最后在控制台分别打印小红,小明各自送出多少分礼物

代码演示:

GIft类:

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

public class Gift {
    private int gift;   //礼物数
    private final Lock lock = new ReentrantLock();  //Lock锁
    private int xh = 0; //记录小红送出多少
    private int xm = 0; //记录小明送出多少

    public void giveGifts(){
        Thread t = Thread.currentThread();
        try {
            lock.lock();    //上锁,避免线程安全问题
            if(this.gift >= 10){
                System.out.println(t.getName() + "送出了1份礼品,目前还剩" + -- this.gift + "份礼品...");
                if(t.getName().equals("小红")) xh++;
                else xm++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();  //解锁
        }
    }

    public Gift() {
    }

    public Gift(int gift) {
        this.gift = gift;
    }

    public int getGift() {
        return gift;
    }

    public void setGift(int gift) {
        this.gift = gift;
    }

    public int getXh() {
        return xh;
    }

    public void setXh(int xh) {
        this.xh = xh;
    }

    public int getXm() {
        return xm;
    }

    public void setXm(int xm) {
        this.xm = xm;
    }
}

MyThread类:

import java.util.concurrent.atomic.AtomicInteger;

public class MyThread extends Thread{
    Gift gift = new Gift();

    public MyThread(Gift gift, String name){
        super(name);
        this.gift = gift;
    }

    @Override
    public void run() {
        while (gift.getGift() >= 10) {  //如果礼物数大于等于10就接着送
            gift.giveGifts();
        }
    }
}

Test类:

public class giveGifts {
    public static void main(String[] args) throws Exception {
        // 目标:有100份礼品小红,小明两人同时发送,当剩下的礼品小于10份的时候则不再送出,
        // 利用多线程模拟该过程并将线程的名称打印出来。并最后在控制台分别打印小红,小明各自送出多少分礼物。
        Gift gift = new Gift(100);  //礼物初始化

        MyThread xh = new MyThread(gift, "小红"); //创建小红线程
        MyThread xm = new MyThread(gift, "小明"); //创建小明线程
        xh.start(); //小红开始送礼
        xm.start(); //小明开始送礼

        xh.join();  //小红和小明送完之后再总结送出多少
        xm.join();

        System.out.println("小红送出了" + gift.getXh() + "份");
        System.out.println("小明送出了" + gift.getXm() + "份");
        System.out.println("最终还剩" + gift.getGift() + "份礼品");
    }
}

运行结果:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1574697.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

n3.队列

1.队列 和堆栈一样&#xff0c;队列也属于受限制的线性表。 和堆栈不同的是&#xff0c;堆栈只能在一端进行出栈和入栈 &#xff08;先进后出&#xff09;&#xff0c;而队列只能在尾部插入&#xff0c;在头部删除&#xff08;先进先出&#xff09;**。 队列的操作 ——入…

8个免费视频素材网站,让你永久告别资源付费!

视频剪辑&#xff0c;需要用到各种类型的视频素材&#xff0c;一定要收藏好这8个网站&#xff0c;免费下载&#xff0c;让你永久告别资源付费&#xff0c;白嫖真爽。 1、菜鸟图库 https://www.sucai999.com/video.html?vNTYwNDUx 菜鸟图库虽然是个设计素材网站&#xff0c;但…

【编译原理】手工打造语法分析器

重点&#xff1a; 语法分析的原理递归下降算法&#xff08;Recursive Descent Parsing&#xff09;上下文无关文法&#xff08;Context-free Grammar&#xff0c;CFG&#xff09; 关键点&#xff1a; 左递归问题深度遍历求值 - 后续遍历 上一篇「词法分析器」将字符串拆分为…

idea的后端环境配置

首先&#xff0c;在你刚打开idea时红色箭头所指的是你进行配置的地方&#xff0c;接下来我把具体步骤说一下 1&#xff0c;直接点击箭头所指的地方就会出现如图界面&#xff0c;然后点击Tomcat server,使其展开点击第一个 第二步取消勾选&#xff0c;第三步选择bin的上一级然后…

14届蓝桥杯省赛 C/C++ B组 T8 整数删除(双向链表,堆)

瞬间定位一个数的左边或者右边&#xff0c;需要用到双向链表。 在过程中不断维护最小值&#xff0c;需要用到堆。 所以定义一个pair类型优先队列&#xff0c;每次取出堆顶进行删除&#xff0c;并且同时让删除元素的左右元素加上其值。 同时需要注意&#xff0c;在删除元素之后…

Spring Cloud Hoxton.SR7 Supported Boot Version: 2.3.2.RELEASE

1、地址 Spring Cloudhttps://docs.spring.io/spring-cloud/docs/Hoxton.SR7/reference/html/ 2、 截图

探索K-近邻算法(KNN):原理、实践应用与文本分类实战

第一部分&#xff1a;引言与背景 KNN算法在机器学习领域的重要性及其地位 KNN算法作为机器学习中的基石之一&#xff0c;由于其概念直观、易于理解并且不需要复杂的模型训练过程&#xff0c;被广泛应用于多种场景。它在监督学习中占据着特殊的位置&#xff0c;尤其适用于实时…

Oracle数据库启动顺序

Oracle数据库启动顺序 启动数据库 首先使用oracle用户登录Linux&#xff0c;用lsnrctl status查看监听状态 1、&#xff1a;进入sqlplus $ sqlplus /nolog SQL> 2&#xff1a;使用sysdab角色登录sqlplus SQL> conn /as sysdba 3&#xff1a;启动数据库 SQL> startup …

宝藏推荐|GitHub登顶项目之中文排行榜!开源资料,天花板级别!

&#x1f680;近年来&#xff0c;开源的力量愈发显现其重要性&#xff0c; 不仅推动了技术的发展&#xff0c;更改变了世界的面貌。 马斯克&#xff0c;这位科技界的巨星也多次强调开源的重要性&#xff0c; 他深知只有共享知识&#xff0c;才能推动人类文明的进步&#x1f…

Atcode搜索

D - Medicines on Grid (atcoder.jp) 这是一道搜索题目&#xff0c;我们使用bfs来做&#xff0c;因为本题目没让你求最小路径&#xff0c;使用dfs也可以&#xff0c;这里使用bfs。 本题目核心思想如下&#xff1a; 小高要从起点到终点&#xff0c;要求其在能量大于等于0时到…

搜索与图论——拓扑排序

有向图的拓扑排序就是图的宽度优先遍历的一个应用 有向无环图一定存在拓扑序列&#xff08;有向无环图又被称为拓扑图&#xff09;&#xff0c;有向有环图一定不存在拓扑序列。无向图没有拓扑序列。 拓扑序列&#xff1a;将一个图排成拓扑序后&#xff0c;所有的边都是从前指…

k8s1.28-helm安装kafka-Raft集群

参考文档 [Raft Kafka on k8s 部署实战操作 - 掘金 (juejin.cn)](https://juejin.cn/post/7349437605857411083?fromsearch-suggest)部署 Raft Kafka&#xff08;Kafka 3.3.1 及以上版本引入的 KRaft 模式&#xff09;在 Kubernetes (k8s) 上&#xff0c;可以简化 Kafka 集群…

分类预测 | Matlab实现GWO-LSSVM灰狼算法优化最小二乘支持向量机数据分类预测

分类预测 | Matlab实现GWO-LSSVM灰狼算法优化最小二乘支持向量机数据分类预测 目录 分类预测 | Matlab实现GWO-LSSVM灰狼算法优化最小二乘支持向量机数据分类预测分类效果基本介绍程序设计参考资料 分类效果 基本介绍 1.Matlab实现GWO-LSSVM灰狼算法优化最小二乘支持向量机数据…

一文彻底搞懂JAVA 异常分类及处理

文章目录 1. 概念2. 异常分类3. 异常的处理方式4. throw 和 throws 的区别 1. 概念 如果某个方法不能按照正常的途径完成任务&#xff0c;就可以通过另一种路径退出方法。在这种情况下会抛出一个封装了错误信息的对象。此时&#xff0c;这个方法会立刻退出同时不返回任何值。另…

蓝桥杯第十四届C++C组

三国游戏 题目描述 小蓝正在玩一款游戏。游戏中魏蜀吴三个国家各自拥有一定数量的士兵X, Y, Z (一开始可以认为都为 0 )。游戏有 n 个可能会发生的事件&#xff0c;每个事件之间相互独立且最多只会发生一次&#xff0c;当第 i 个事件发生时会分别让 X, Y, Z 增加Ai , Bi ,Ci …

【1】初识 Python

【1】初识 Python 1、编程语言(1) 语言(2) 编程语言(3) 如何利用编程语言与计算机交流(4) 常见的编程语言(5) 语法 2、Python 简介(1) 什么是 Python(2) Python 能做什么(3) Python 的由来(4) Python的特点① 语法精简② 生态好&#xff0c;开发效率高③ Python开发初体验&…

全国计算机等级考试三级Linux应用与开发技术考试-习题汇总

https://blog.csdn.net/qq_42025798/article/details/119155696 3.第1章-计算机体系结构与操作系统-练习题-简答题 https://blog.csdn.net/qq_42025798/article/details/119186151 4.第1章-计算机体系结构与操作系统-练习题-填空题 https://blog.csdn.net/qq_42025798/article/…

风险模型总结

系统性风险 系统性风险&#xff08;Systematic Risk&#xff09;微观层面的定义由夏普&#xff08;William Sharpe&#xff09;在资本资产定价模型&#xff08;CAPM&#xff09;中首次提出&#xff0c;即资本市场中存在的不能通过分散投资予以消除的风险 模型也会带来风险 详…

xhadmin多应用Saas框架和FastAdmin有什么区别?

xhadmin是什么&#xff1f; xhadmin 是一套基于最新技术的研发的多应用 Saas 框架&#xff0c;支持在线升级和安装模块及模板&#xff0c;拥有良好的开发框架、成熟稳定的技术解决方案、提供丰富的扩展功能。为开发者赋能&#xff0c;助力企业发展、国家富强&#xff0c;致力于…

mac老版本如何升级到最新版本

mac老版本如何升级到最新版本 老macbook升级新版本&#xff08;Big sur、Monterey&#xff09; 首先介绍我的电脑的机型及情况&#xff1a; 2015年初的MacBook Air 处理器是1.6Hz 双核Interl Core i5 内存4G 老版本只能升到10.13 想要升到最高版本的原因&#xff1a;想要注册…