Java多线程快速入门

Java多线程快速入门

趁着最近课少,复习一下Java多线程相关知识,顺便发一下以前的笔记

1、认识多线程

  • 什么是线程

    线程是指在一个进程中,执行的一个相对独立的、可调度的、可执行的代码片段。线程是操作系统能够运算调度的最小单位,它包含在进程之中,是进程中的实际运作单位,它独立地运行于进程中,并与同一进程内的其他线程共享进程的资源,如内存、文件描述符等。每个线程都有自己的栈、程序计数器和局部变量等,但它们共享进程的静态数据、堆内存和全局变量等。

    PS:可以简单理解线程是线程中的一条执行路径(可以参考流程图)

  • 线程的优缺点

    • 优点
      • 线程可以提高程序的并行性,增加程序的处理能力;

      • 线程创建和切换的开销比进程小,因此更加高效;

      • 线程可以与同一进程内的其他线程共享数据和资源,这样可以避免进程间的数据复制和通信开销。

    • 缺点
      • 同一进程内的线程都共享进程的资源,因此需要进行线程间的同步和互斥,否则容易出现竞争条件和死锁等问题;

      • 线程之间的通信和同步需要额外的开销和复杂度,因此需要仔细规划和设计线程间的通信和同步机制;

      • 由于线程共享进程的地址空间,因此需要避免线程间的访问冲突,否则容易出现数据不一致的问题。

  • 什么是进程

    进程是指在计算机中运行的程序和其相关执行状态的总和。更具体地说,进程包括程序代码、数据、内存中的栈、堆和共享库等资源。每个进程在执行时都有自己的地址空间、内存、堆和栈,以及相应的文件描述符、信号处理程序等。进程是操作系统中最基本的、最重要的资源之一。

    PS:可以简单理解进程就是正在运行的程序

  • 进程的特点

    • 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
    • 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
    • 并发性:任何进程都可以同其他进程一起并发执行
  • 什么是单线程和多线程

    • 单线程:一个进程如果只有一条执行路径,则称为单线程程序
    • 多线程:一个进程如果有多条执行路径,则称为多线程程序
  • 多线程的应用场景

    • 拷贝、迁移大文件,可以单独使用一个线程去拷贝迁移大文件,从而可以空出时间去干其它事情
    • 聊天软件中使用多线程,服务器为每一个客户创建一个线程,处理聊天任务;客户端使用多线程进行界面更新,单独使用一个线程更新界面,一个线程用来接收消息
    • 在购物网站中,为了提高系统性能,单独使用一个线程去获取阻塞队列中的订单消息

    ……

    多线程的主要作用是为了提高系统的性能,充分利用CPU

  • 什么是并行与并发

    • 并行:在同一时刻,有多个指令在多个CPU上同时执行
    • 并发:在同一时刻,有多个指令在同一个CPU上交替执行
  • 什么是生产者和消费者

    • 生产者:负责向共享缓冲区中生产数据
    • 消费者:负责从共享缓冲区中消费数据

2、多线程的实现

2.1 继承Thread类

通过继承Thread类实现多线程

  • 写法一:传统编程方式

    • Step1:编写一个类,继承Thread类,重写Thread类的run方法

      package com.hhxy.thread;
      
      public class MyThread extends Thread {
          /**
           * 在线程开启后,此方法将被自动调用执行
           */
          @Override
          public void run() {
              for (int i = 0; i < 10; i++) {
                  System.out.println(getName() + "被执行了" + i + "次");
              }
          }
      }
      
    • Step2:编写测试类,创建多线程,并运行多线程

      package com.hhxy.test;
      
      import com.hhxy.thread.MyThread;
      
      public class ThreadTest {
          public static void main(String[] args) {
              // 创建线程对象
              MyThread t1 = new MyThread();
              MyThread t2 = new MyThread();
              // 为线程命名
              t1.setName("线程1");
              t2.setName("线程2");
              // 启动线程
              t1.start();
              t2.start();
              // 主线程输出
              for (int i = 0; i < 10; i++) {
                  System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
              }
          }
      }
      

      可以从下图中看出,程序被启动,有三个线程在运行,需要注意的是线程输出是随机的,并不是说谁先调用start方法就会先输出

      image-20230523201327225

  • 写法二:匿名内部类方式

    使用匿名内部类方式就不需要去单独创建一个类类继承Thread类了,而是直接实现Thread

    package com.hhxy.test;
    
    import com.hhxy.thread.MyThread;
    
    public class ThreadTest {
        public static void main(String[] args) {
            // 创建线程对象
            Thread t1 = new Thread(){
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        System.out.println(getName() + "被执行了" + i + "次");
                    }
                }
            };
            Thread t2 = new Thread(()->{
                for (int i = 0; i < 10; i++) {
                    // 注意这里由于使用了匿名内部类的写法,导致这里无法使用this,所以得调用currentThread后去当前线程名
                    System.out.println(Thread.currentThread().getName()+"被执行了" + i + "次");
                }
            });
            // 启动线程
            t1.start();
            t2.start();
         
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
            }
        }
    }
    
    

    image-20230523201930011


总结

通过Thread类实现多线程主要有以下几步:

  1. 创建一个类,继承Thread类重写run方法(或者直接使用匿名内部类的方式直接在实现run方法)
  2. 调用无参构造器,创建Thread对象
  3. 调用Thread对象的start方法启动线程

2.2 实现Runnable接口

  • 方式一:传统写法

    • Step1:编写一个类实现Runnable接口,然后重写run方法

      package com.hhxy.runnable;
      
      public class MyRunnable implements Runnable{
      
          /**
           * 线程任务,当Runnable对饮的Thread对象调用start方法,就立刻执行
           */
          @Override
          public void run() {
              for (int i = 0; i < 10; i++) {
                  // 由于没有继承Thread类,所以不能调用getName获取线程名
                  System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
              }
          }
      }
      
    • Step2:编写一个测试类,创建Step1编写的Runnable实现类对象,创建Thread对象,调用Thread的有参构造,将Runnable对象放入Thread构造器中

      package com.hhxy.test;
      
      import com.hhxy.runnable.MyRunnable;
      
      public class RunnableTest {
          public static void main(String[] args) {
              // 创建Runnable对象,表示线程任务
              MyRunnable myRunnable = new MyRunnable();
              // 创建Thread对象
              Thread t1 = new Thread(myRunnable);
              Thread t2 = new Thread(myRunnable);
              // 启动线程
              t1.start();
              t2.start();
      
              for (int i = 0; i < 10; i++) {
                  System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
              }
          }
      }
      

      image-20230523212743523

  • 方式二:匿名内部类写法

    package com.hhxy.test;
    
    import com.hhxy.runnable.MyRunnable;
    
    public class RunnableTest {
        public static void main(String[] args) {
            // 创建Thread对象(直接使用匿名内部类实现Runnable接口)
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
                    }
                }
            });
            Thread t2 = new Thread(()->{
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
                }
            });
    
            // 启动线程
            t1.start();
            t2.start();
    
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
            }
        }
    }
    

    image-20230523212723729


总结

  1. 创建一个类,实现Runnable接口并重写run方法(或者使用匿名内部类的方式直接实现run方法)
  2. 调用有参构造器,创建Thread对象
  3. 调用Thread对象的start方法启动线程

2.3 利用Callable和Futrue接口

  • Step1:创建一个类,实现Callable接口,重写call方法

    package com.hhxy.callable;
    
    import java.util.concurrent.Callable;
    
    public class MyCallable implements Callable<String> {
        /**
         * 线程任务
         * @return 返回线程任务执行后的线程结果
         */
        @Override
        public String call() throws Exception {
            for (int i = 0; i < 10; i++) {
                // 由于没有继承Thread类,所以不能调用getName获取线程名
                System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
            }
            return Thread.currentThread().getName() + "线程执行完毕!";
        }
    }
    

    注意:类的泛型要与call方法的返回值类型保持一致

  • Step2:编写测试类,创建Callable对象,调用有参构造器(参数为Callable对象)创建FutureTask对象,调用有参构造器(参数为FutureTask对象)创建Thread对象,调用Thread对象的start方法

    package com.hhxy.test;
    
    import com.hhxy.callable.MyCallable;
    
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    public class CallableTest {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            // 创建Callable对象
            MyCallable myCallable = new MyCallable();
            // 创建FutureTask对象
            FutureTask ft1 = new FutureTask<>(myCallable);
            FutureTask ft2 = new FutureTask<>(myCallable);
            // 创建Thread对象
            Thread t1 = new Thread(ft1);
            Thread t2 = new Thread(ft2);
    
            // 启动线程
            t1.start();
            t2.start();
    
            // 获取线程任务执行后的结果
            String result1 = ft1.get().toString();
            String result2 = ft2.get().toString();
            System.out.println(result1);
            System.out.println(result2);
    
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
            }
        }
    }
    

    注意:一个Thread对象要对应一个FutureTask对象,如果两个Thread对象共用一个FutureTask对象,获取线程任务的结果会是一致的,并且结果以第一个线程任务的结果为准

    image-20230523215415364


总结

  1. 创建一个类,实现Callable接口并重写call方法
  2. 创建Callable对象,调用有参构造器(参数为Callable对象)创建FutureTask对象,调用有参构造器(参数为FutureTask对象)创建Thread对象
  3. 调用Thread对象的start方法启动线程
  4. 调用FutureTask对象的get方法,获取线程任务执行后的结果

2.4 三种方式的比较

image-20230523220113375

  • 如果我们想要获取线程任务的执行结果,请使用方式三
  • 如果我们不需要获取线程任务的执行结果,同时对扩展性要求不要,请使用方式一
  • 如果我们不需要获取线程任务的执行结果,同时对扩展性要求较高,请使用方式二

3、Thread类常用API

API介绍

方法名说明
public void run()在线程开启后,run()方法将被自动调用执行
public synchronized void start()开启线程
public final synchronized void setName(String name)为线程命名
public final String getName()获取线程名
public final void setPriority(int newPriority)设置线程的优先级
public final intgetPriority()获取线程的优先级
public final void setDaemon(boolean on)设置为守护线程
public final void join()插入线程/插队线程
public static native void yield();出让线程/礼让线程
public static native Thread currentThread();获取当前线程
public static native void sleep(long millis);让线程休眠指定时间(单位ms)
public final void wait()当前线程等待,直到被其他线程唤醒
public final native void notify();随机唤醒单个线程
public final native void notifyAll();唤醒所有线程
public State getState()获取线程状态

备注

  1. ;结尾的是成员变量,而()结尾的是方法
  2. 优先级值越大,越优先执行。默认是5,最小值是1,最大值是10

3.1 守护线程

  • 守护线程,就是“备胎线程”,当主线程结束后,守护线程会结束(但不是立即结束,而是执行一段时间后结束)

守护线程的应用场景:当我们在进行QQ聊天时,主线程就是QQ程序,而当我们发送文件时,就可以开启一个守护线程,这个守护线程单独用于发送文件,当我们关闭QQ时,QQ这个主线程就结束了,而此时守护线程也没有存在的必要了,所以此时也会随着主线程的结束而结束

public class DaemonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
            }
        });

        // 将t2线程设置为t1的守护线程,t1结束后,t2也会跟着结束(但不是立即结束)
        t2.setDaemon(true);

        t1.start();
        t2.start();

//        for (int i = 0; i < 100; i++) {
//            System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
//        }
    }
}

可以看到,当t1执行完,此时整个程序中只有守护线程,此时JVM就会关闭守护线程(注意,如果我们开启主线程的打印,则t1执行完后,t2守护线程不会结束,因为此时系统中除了守护线程,还存在主线程,并不是只剩守护线程)

image-20230608165926061

3.2 礼让线程

  • 礼让线程,让出当前CPU

在需要多个线程协作、顺序执行的场景中,礼让线程是一种比较常用的线程协作方法,它可以让线程执行的顺序更加合理,提高系统的并发性能。

public class YieldTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
                // 让出当前线程的CPU
                Thread.yield();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
            }
        });

        t1.start();
        t2.start();
    }
}

可以发现当当前系统中同时存在其它线程时,Thread-0只会被执行一次,这就是礼让线程的一个特性:

image-20230608170553424

3.3 插入线程

  • 插入线程,让当前线程等待线程t执行完成后再继续执行

    它的应用场景较少,使用起来也要十分小心,因为很容易发生死锁

    public class JoinTest {
        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(() -> {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
                }
            });
            t.start();
            // 将t设置为插入线程,会阻塞当前线程,直到t执行完才重新执行当前线程
            t.join();
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "被执行了" + i + "次");
            }
        }
    }
    

image-20230608171529176

3.4 线程的生命周期

image-20230608173644133

image-20230609152856122

Java里没有定义运行态,因为当线程运行后直接将当前线程交给了CPU,此时JVM就不需要管这个线程了,所以Java中线程实际的状态只有6个

5、线程安全问题

5.1 synchronized

synchronized是Java中用来实现线程同步的关键字,它可以让多个线程在访问共享资源时,保证同一时刻只有一个线程访问,从而避免线程间的数据竞争和不一致性,实现线程安全。

示例

多线程买票

package com.hhxy.test;

/**
 * @author ghp
 * @date 2023/6/8
 * @title
 * @description
 */
public class ThreadSafeTest {

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

class MyThread extends Thread {

    int ticket = 0;

    @Override
    public void run() {
        while (true) {
            if (ticket < 100) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ticket++;
                System.out.println("正在卖第" + ticket + "张票");
            } else {
                break;
            }
        }
    }
}

当前代码存在,一下问题,每一个线程对于票数量的计算都是独立的,命名只有100张票,但是让三个线程来买,却卖了300张:

image-20230608193557945

同时还会出现超卖问题:

image-20230608194105696

1)代码优化:将ticket使用static修饰,这样多个线程就可以共享一个变量了

    static int ticket = 0;

但是仍然会出现这种情况,只是比例大幅度下降了:

image-20230608193920778

同样仍然会出现超卖问题!

2)代码优化:使用synchronized对同步代码块进行上锁

注意synchronized锁住的对象必须是唯一的

package com.hhxy.test;

public class ThreadSafeTest {

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

class MyThread extends Thread {

    //    int ticket = 0;
    static int ticket = 0;

    @Override
    public void run() {
        while (true) {
            synchronized (ThreadSafeTest.class) {
                if (ticket < 100) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    ticket++;
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                } else {
                    break;
                }
            }
        }
    }
}

温馨提示synchronized不仅可以锁代码块,还可以锁方法。锁方法,不能自己指定,非静态的锁住的是this,静态的是当前类的字节码文件对象

5.2 Lock

Lock是JDK5提供的一种全新的锁对象,位于java.util.concurrent.locks包下,Lock提供了比使用synchronized方法和语句更为广泛的锁操作,通过lock()获取锁,通过unlock()释放锁。Lock是一个接口,不能够直接实例化,一般我们是使用它的实现类ReentrantLock来实例化。

package com.hhxy.test;

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

/**
 * @author ghp
 * @date 2023/6/8
 * @title
 * @description
 */
public class ThreadSafeTest2 {

    public static void main(String[] args) {
        MyThread2 t1 = new MyThread2();
        MyThread2 t2 = new MyThread2();
        MyThread2 t3 = new MyThread2();
        t1.start();
        t2.start();
        t3.start();
    }
}



class MyThread2 extends Thread {

    //    int ticket = 0;
    static int ticket = 0;

    Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                if (ticket < 100) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    ticket++;
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                } else {
                    break;
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

可以看到又出现了多个窗口卖同一张票的情况:

image-20230608202258730

出现这个问题的原因很简单,因为Lock对象没有加static,导致每创建一个MyThread2对象,都会新建一个Lock对象,所以我们需要使用static修饰Lock对象

    static Lock lock = new ReentrantLock();

image-20230608202509443

6、等待唤醒机制

等待唤醒机制是Java中常见的线程同步机制之一,它通过Object类的wait()和notify()/notifyAll()方法实现线程间的通信,实现“生产者-消费者”模型等多线程编程场景。

示例

示例一:

这里将利用wait()notify()/notifyAll()方法实现等待唤醒机制

image-20230609145427471

  • 桌子:用来放面条,同时记录食客消费面条的数量,以及桌子上面条的数量

    public class Desk {
        // 消费者最大能消费的食物数量
        public static int count = 10;
        // 桌子上食物的数量 0-桌子上没有食物 1-桌子上有食物
        public static int foodFlag = 0;
        // 锁对象,用于上锁
        public static final Object lock = new Object();
    }
    
  • 生产者:生产面条

    public class Cook extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (Desk.lock) {
                    // 判断美食家是否还能吃下
                    if (Desk.count == 0) {
                        // 美食家已经吃饱了
                        break;
                    } else {
                        // 美食家还能吃,判断桌子上是否有食物
                        if (Desk.foodFlag == 0) {
                            // 桌子上没有食物,厨师做面条,然后唤醒正在等待的美食家
                            Desk.foodFlag++;
                            System.out.println("厨师做了" + Desk.foodFlag + "碗面条");
                            Desk.lock.notifyAll();
                        } else {
                            // 桌子上有食物,厨师等待
                            try {
                                System.out.println("厨师等待……");
                                Desk.lock.wait();
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                }
            }
        }
    }
    
  • 消费者:消费面条

    public class Foodie extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (Desk.lock) {
                    // 判断美食家是否吃饱
                    if (Desk.count == 0) {
                        // 美食家已经吃饱了
                        break;
                    } else {
                        // 美食家还没有吃饱,判断桌子上是否有食物
                        if (Desk.foodFlag == 1) {
                            // 桌子上有食物
                            Desk.count--;
                            Desk.foodFlag--;
                            System.out.println("美食家还能吃" + Desk.count + "碗面");
                            // 唤醒美食家,让他继续做面
                            Desk.lock.notifyAll();
                        } else {
                            // 桌子上没有食物
                            try {
                                System.out.println("美食家等待……");
                                Desk.lock.wait();
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                }
            }
        }
    }
    
  • 测试类:

    public class Main {
        public static void main(String[] args) {
            Cook cook = new Cook();
            Foodie foodie = new Foodie();
            cook.start();
            foodie.start();
        }
    }
    

    image-20230609145109448

示例二:

这里将使用阻塞队列来实现等待唤醒机制

备注:阻塞队列(Blocking Queue)是Java中的一种线程安全的队列,它支持在队列为空时阻塞获取元素,或者在队列已满时阻塞插入元素,可以很好地用于实现生产者-消费者模型等多线程编程场景。

  • 桌子:

    public class Desk {
        // 消费者最大能消费的食物数量
        public static int count = 10;
    }
    
    
  • 生产者:

    public class Cook extends Thread {
        ArrayBlockingQueue<String> queue;
    
        public Cook(ArrayBlockingQueue queue) {
            this.queue = queue;
        }
    
        @Override
        public void run() {
            while (true) {
                // 判断美食家是否还能吃下
                if (Desk.count == 0) {
                    // 美食家已经吃饱了
                    break;
                } else {
                    // 美食家还能吃,判断桌子上是否有食物
                    if (queue.isEmpty()) {
                        // 桌子上没有食物,厨师做面条
                        try {
                            queue.put("面条");
                            System.out.println("厨师做了1碗面条");
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        System.out.println("厨师等待……");
                    }
                }
            }
        }
    }
    
  • 消费者:

    public class Foodie extends Thread {
        ArrayBlockingQueue<String> queue;
    
        public Foodie(ArrayBlockingQueue queue) {
            this.queue = queue;
        }
    
        @Override
        public void run() {
            while (true) {
                // 判断美食家是否吃饱
                if (Desk.count == 0) {
                    // 美食家已经吃饱了
                    break;
                } else {
                    // 美食家还没有吃饱,判断桌子上是否有食物
                    if (queue.isEmpty()) {
                        // 桌子上没有食物了,美食家等待
                        System.out.println("美食家等待……");
                    } else {
                        // 桌子上有食物
                        try{
                            Desk.count--;
                            String food = queue.take();
                            System.out.println("美食家吃了1碗面条,美食家还能吃" + Desk.count + "碗" + food);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
    
            }
        }
    }
    
  • 测试类:

    public class Main {
        public static void main(String[] args) {
            ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
            Cook cook = new Cook(queue);
            Foodie foodie = new Foodie(queue);
            cook.start();
            foodie.start();
        }
    }
    

    image-20230609152220481

    打印出现重复,是由于打印语句在锁的外面,阻塞队列内部是使用了Lock锁,最终的实际效果是和示例一一致的,只是打印语句会发生错乱,并不影响最终效果

7、综合案例

7.1 售票

需求:一共有100张电影票,可以在两个窗口领取,假设每次领取的时间为100毫秒,请用多线程模拟卖票过程并打印剩余电影票的数量

  • 测试类:

    public class Main {
    
        public static void main(String[] args) {
            // synchronized实现
            Thread t1 = new MyThread();
            Thread t2 = new MyThread();
            // lock实现
    //        Thread t1 = new MyThread2();
    //        Thread t2 = new MyThread2();
            t1.start();
            t2.start();
        }
    }
    
    
  • 线程类:

    1)synchronized实现:

    public class MyThread extends Thread {
        public static int ticket = 100;
    
        @Override
        public void run() {
            while (true) {
                if (ticket==0){
                    break;
                }else {
                    synchronized (MyThread.class) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        if (ticket > 0) {
                            ticket--;
                            System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩" + ticket);
                        }
                    }
                }
            }
        }
    }
    

    2)lock实现:

    public class MyThread2 extends Thread {
        public static int ticket = 100;
        public static final Lock lock = new ReentrantLock();
    
        @Override
        public void run() {
            while (true) {
                if (ticket == 0) {
                    break;
                } else {
                    lock.lock();
                    try {
                        Thread.sleep(100);
                        ticket--;
                        System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩" + ticket);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    } finally {
                        lock.unlock();
                    }
                }
            }
        }
    }
    

7.2 赠送礼物

需求:有100份礼品,两人同时发送,当剩下的礼品小于10份的时候则不再送出。利用多线程模拟该过程并将线程的名字和礼物的剩余数量打印出来。

  • 测试类:和7.1一样,略

  • 线程类:

    public class MyThread extends Thread {
        public static int count = 100;
    
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (MyThread.class) {
                    if (count < 10) {
                        break;
                    } else {
                        count--;
                        System.out.println(Thread.currentThread().getName() + "送出一个礼物,当前礼物还剩" + count);
                    }
                }
            }
        }
    }
    

    备注:这里有一个小疑惑,synchronized必须要把if-else全部锁住才能成功,如果和7.1一样,只锁else代码,会导致多多送一个礼物,线程1送出第90个礼物后,线程2还会送出第91个礼物,全部锁住就不会发生这样的事情。

    自我解惑:其实出现这个问题的原因,是由于当线程1进入else中,还没有执行count–操作,此时线程2也进入了else,但此时锁被线程1拿到了,线程2在else中等待,这就导致线程1执行完count–后释放锁,线程2接着又拿到锁执行count–,这就导致,线程1送出第90个礼物后,线程2还会送出第91个礼物,全部锁住就不会发生这样的事情。

7.3 打印数字

需求:同时开启两个线程,共同获取1-100之间的所有数字,输出所有的奇数。

  • 测试类:和7.1一样,略

  • 线程类:

    package com.hhxy.demo05;
    
    /**
     * @author ghp
     * @date 2023/6/9
     * @title
     * @description
     */
    public class MyThread extends Thread {
        public static int n = 0;
    
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (MyThread.class) {
                    if (n == 100) {
                        break;
                    } else {
                        n++;
                        if (n % 2 != 0) {
                            System.out.println(Thread.currentThread().getName() + "找到一个奇数" + n);
                        }
                    }
                }
            }
        }
    }
    

7.4 抢红包

需求:抢红包也用到了多线程。

​ 假设:100块,分成了3个包,现在有5个人去抢。

​ 其中,红包是共享数据。

​ 5个人是5条线程。

  • 测试类:略

  • 线程类:

    package com.hhxy.demo06;
    
    import java.util.Random;
    
    public class MyThread extends Thread {
        // 红包的金额
        public static double money = 100;
        // 红包的个数
        public static int count = 3;
        // 红包的最小值
        public static final double MIN = 0.01;
    
        @Override
        public void run() {
            synchronized (MyThread.class) {
                double price = 0;
                if (count == 1) {
                    // 只剩一个红包了,剩下的钱都是这个红包
                    count--;
                    price = money;
                    money -= price;
                } else {
                    if (count > 1) {
                        count--;
                        Random random = new Random();
                        // 红包的金额范围是 0.01~(money-(count-1)*0.01)
                        double t = random.nextInt(1001 - count);
                        price = t / 100;
                        if (price == 0) {
                            price = 0.01;
                        }
                        money -= price;
                    }
                }
                System.out.println(this.getName() + "抢一个" + price + "元的红包,红包金额还剩" + money + ",红包数量还剩" + count);
            }
        }
    }
    

    image-20230609171824954

7.5 抽奖箱

需求:有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 {10,5,20,50,100,200,500,800,2,80,300,700};

创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”

  • 测试类:

        public static void main(String[] args) {
            Thread t1 = new MyThread();
            Thread t2 = new MyThread();
            t1.setName("抽奖箱一");
            t2.setName("抽奖箱二");
            t1.start();
            t2.start();
        }
    

    image-20230609173612338

  • 线程类:

    package com.hhxy.demo07;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    /**
     * @author ghp
     * @date 2023/6/9
     * @title
     * @description
     */
    public class MyThread extends Thread {
    
        public static List<Integer> list = new ArrayList<Integer>() {{
            add(10);
            add(5);
            add(20);
            add(50);
            add(100);
            add(200);
            add(500);
            add(800);
            add(2);
            add(80);
            add(300);
            add(700);
        }};
    
    
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (MyThread.class) {
                    if (list.size() == 0) {
                        break;
                    } else {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        Collections.shuffle(list);
                        Integer res = list.remove(0);
                        System.out.println(this.getName() + "抽到了" + res + "元,抽奖箱中剩余" + list.size());
                    }
                }
            }
        }
    }
    

7.6 多线程统计并求最大值

需求:

​ 在上一题基础上继续完成如下需求:

​ 每次抽的过程中,不打印,抽完时一次性打印(随机)

​ 在此次抽奖过程中,抽奖箱1总共产生了6个奖项。

​ 分别为:10,20,100,500,2,300最高奖项为300元,总计额为932元

​ 在此次抽奖过程中,抽奖箱2总共产生了6个奖项。

​ 分别为:5,50,200,800,80,700最高奖项为800元,总计额为1835元

通过创建共享变量实现:

public class MyThread extends Thread {

    public static List<Integer> list = new ArrayList<Integer>() {{
        add(10);
        add(5);
        add(20);
        add(50);
        add(100);
        add(200);
        add(500);
        add(800);
        add(2);
        add(80);
        add(300);
        add(700);
    }};

    public static List<Integer> list1 = new ArrayList<>();
    public static List<Integer> list2 = new ArrayList<>();


    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (MyThread.class) {
                if (list.size() == 0) {
                    if ("抽奖箱一".equals(this.getName())){
                        System.out.println(list1);
                    }else {
                        System.out.println(list2);
                    }
                    break;
                } else {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    Collections.shuffle(list);
                    Integer res = list.remove(0);
                    if ("抽奖箱一".equals(this.getName())){
                        list1.add(res);
                    }else{
                        list2.add(res);
                    }
                    System.out.println(this.getName() + "抽到了" + res + "元,抽奖箱中剩余" + list.size());
                }
            }
        }
    }
}

通过创建局部变量实现:

public class MyThread2 extends Thread {

    public static List<Integer> list = new ArrayList<Integer>() {{
        add(10);
        add(5);
        add(20);
        add(50);
        add(100);
        add(200);
        add(500);
        add(800);
        add(2);
        add(80);
        add(300);
        add(700);
    }};


    @Override
    public void run() {
        List<Integer> currentList = new ArrayList<>();
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (MyThread.class) {
                if (list.size() == 0) {
                    System.out.println(this.getName() + currentList);
                    break;
                } else {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    Collections.shuffle(list);
                    Integer res = list.remove(0);
                    currentList.add(res);
                    System.out.println(this.getName() + "抽到了" + res + "元,抽奖箱中剩余" + list.size());
                }
            }
        }
    }
}

7.7 多线程之间的比较

需求:在上一题基础上继续完成如下需求,比较两个线程的最大值

线程类:

package com.hhxy.demo08;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;

/**
 * @author ghp
 * @date 2023/6/9
 * @title
 * @description
 */
public class MyCallable implements Callable<Integer> {
    public static List<Integer> list = new ArrayList<Integer>() {{
        add(10);
        add(5);
        add(20);
        add(50);
        add(100);
        add(200);
        add(500);
        add(800);
        add(2);
        add(80);
        add(300);
        add(700);
    }};

    @Override
    public Integer call() throws Exception {
        List<Integer> currentList = new ArrayList<>();
        while (true) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (MyThread.class) {
                if (list.size() == 0) {
                    System.out.println(Thread.currentThread().getName() + currentList);
                    break;
                } else {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    Collections.shuffle(list);
                    Integer res = list.remove(0);
                    currentList.add(res);
                    System.out.println(Thread.currentThread().getName() + "抽到了" + res + "元,抽奖箱中剩余" + list.size());
                }
            }
        }
        // 获取当前线程抽取到的最大值
        int max = 0;
        if (currentList.size()!=0){
            max = Collections.max(currentList);
        }
        return max;
    }
}

测试类:

package com.hhxy.demo08;


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

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // Callable
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> f1 = new FutureTask<>(myCallable);
        FutureTask<Integer> f2 = new FutureTask<>(myCallable);
        Thread t1 = new Thread(f1);
        Thread t2 = new Thread(f2);

        t1.setName("抽奖箱一");
        t2.setName("抽奖箱二");
        t1.start();
        t2.start();
        System.out.println("抽奖箱一的最大值" + f1.get());
        System.out.println("抽奖箱二的最大值" + f2.get());
    }
}

8、线程池

  • 什么是线程池

    线程池是一种多线程处理方式,它可以有效地管理和调度多个线程的执行。在使用线程池的情况下,可以避免因为创建大量线程而导致系统性能下降、内存消耗过大等问题。线程池中的线程都是已经创建好的线程对象,并保存在线程池中,每个线程可以执行多个任务,任务执行完毕后并不会立刻销毁线程,而是会保留在池中等待下次执行。

  • 为什么需要线程池

    在多线程编程中,往往需要创建大量的线程来执行任务。但是,直接创建线程会导致以下问题:

    1. 系统资源浪费:对于一些线程生命周期很短的任务(比如执行完一段代码后就会结束的任务),频繁地创建、销毁线程会消耗大量的系统资源,并且增加了系统开销。

    2. 系统性能下降:当同时需要执行大量的任务时,不加限制地创建线程可能会导致系统性能下降、运行速度变慢,因为线程的创建和销毁开销非常大。

    3. 系统不稳定:在高并发情况下,线程过多时会导致系统崩溃、运行不稳定。

    线程池的作用就是解决以上问题。它可以避免频繁地创建、销毁线程,可以提前准备好一定数量的线程,让线程复用,从而降低创建和销毁线程的开销,同时还可以严格地限制线程的数量和执行时间,实现对线程的调度和管理。

    使用线程池的好处:

    1. 提高系统效率:通过线程的复用和调度,可以充分利用系统资源,提高系统效率。

    2. 提高程序响应速度:线程池中的线程可以随时响应任务,从而提高程序的响应速度。

    3. 避免系统由于线程过多而不稳定:由于可以控制线程的数量,线程池可以避免系统出现由于线程过多而导致的不稳定状态,提高系统的可靠性。

    总而言之,线程池在多线程编程中是一种非常重要的工具,可以避免系统性能问题和不稳定问题,提高系统效率和可靠性。

8.1 自定义线程

  • 如何创建线程池

    package com.hhxy.demo09;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Demo01 {
        public static void main(String[] args) throws InterruptedException {
            // 创建线程池
            ExecutorService pool = Executors.newCachedThreadPool();
            // 提交任务
            pool.submit(new MyRunnable());
            // main线程休眠1s,这样的目的是为了让Thread-0尽快执行完任务,之后就都会是Thread-0执行
    //        Thread.sleep(1000);
            pool.submit(new MyRunnable()); // 不光可以
    //        Thread.sleep(1000);
            pool.submit(new MyRunnable());
    //        Thread.sleep(1000);
            // 销毁线程池(线程池一般不销毁)
            pool.shutdown();
        }
    }
    
  • 线程池相关概念

    • 先提交的任务不一定限制性

    • 当核心线程真在忙,且线程池等待队列中的任务已满时,会创建临时线程

    • 线程池能最大处理的任务数:核心线程数量+等待队列的长度+临时线程的数量,超过这个长度的任务会拒绝服务

      拒绝策略:

      • AbortPolicy:丢弃并抛出异常RejectedExecutionException异常(默认策略)
      • DiscardPolicy:丢弃任务,但不抛出异常(不推荐)
      • DiscardOldstPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中
      • CallerRunsPolicy:调用任务的run()方法绕过线程池直接执行
package com.hhxy.demo10;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) {
        /*
        参数一:核心线程数量 >=0
        参数二:最大线程数 >=核心线程数量
        参数三:空闲线程最大存活时间 >=0
        参数四:时间单位 
        参数五:任务队列 !=null
        参数六:创建线程工厂 !=null
        参数七:任务的拒绝策略 !=null
         */
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3, //  核心线程数量,不能小于0
                6, // 最大线程数量,不能小于核心线程数量,临时线程数量=最大线程数量-核心线程数量
                60, // 时间值
                TimeUnit.SECONDS, // 时间单位
                new ArrayBlockingQueue<>(3), // 阻塞队列长度
                Executors.defaultThreadFactory(), // 获取线程的方式
                new ThreadPoolExecutor.AbortPolicy() // 任务的拒绝策略
        );
    }
}

8.2 线程池最大并行数

  • CPU密集型运算:最大并行数+1

    第一种方式:

    image-20230614161653363

    从这里可以看出,我笔记本的最大并行数是16

    第二种方式:

            int count = Runtime.getRuntime().availableProcessors();
            System.out.println("当前电脑最大逻辑处理数:"+ count); // 16
    
  • I/O密集型运算: 最大并行数 ∗ 期望 C P U 利用率 ∗ 总时间 ( C P U 计算时间 + 等待时间 ) C P U 计算时间 最大并行数*期望CPU利用率*\frac{总时间(CPU计算时间+等待时间)}{CPU计算时间} 最大并行数期望CPU利用率CPU计算时间总时间(CPU计算时间+等待时间)

    比如:从本地文件中,读取两个数据(耗时1秒速),并进行相加(耗时1秒钟)

    则此时计算式为: 16 ∗ 100 % ∗ ( 2 s ) / 1 s = 16 16 *100\%*(2s)/1s = 16 16100%(2s)/1s=16,所以此时线程池的最大数量为16

    CPU的等待时间可以使用 thread dump 进行计算

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知识汲取者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值