【学习日记2023.4.12】之Thread的实现方式和解决线程安全问题的三种办法以及线程通讯问题和Thread的生命周期

1. 多线程相关概念

  • 并发和并行
    • 并发:在同一时刻,有多个指令在CPU单个核心上 交替 执行
    • 并行:在同一时刻,有多个指令在CPU多个核心上 同时 执行
  • 进程和线程
    • 进程:操作系统中正在运行的一个应用程序。
    • 线程:应用程序中做的事情。比如:360软件中的杀毒,扫描木马,清理垃圾
  • 线程
    • 单线程:程序中如果只有一条执行流程,那这个程序就是单线程的程序。
    • 多线程:程序中如果有多条执行流程,那这个程序就是多线程的程序。
  • 好处和应用场景
    • 好处
      • 提高CPU的利用率,和处理任务的能力
    • 应用场景
      • 桌面应用程序
      • web服务器应用程序
        请添加图片描述

2. 多线程的实现方式

2.1 继承Thread

步骤

  • 定义一个类,继承Thread类
  • 重写run方法
  • 创建对象
  • 调用start()方法,启动线程
/*
    演示Java语言创建多线程方式一:继承Thread类

    注意事项:
        直接调用run方法,不会启动线程
        start():
            1.启动线程
            2.在新启动的线程中,调用run方法
        继承Thread类的优缺点:
            优点:代码书写简单
            缺点:已经继承了一个类,无法再继承其他类,降低了程序的扩展性
 */
public class Demo1 {
    public static void main(String[] args) {
        //3.创建线程对象
        final MyThread myThread = new MyThread();
        //4.调用start方法,启动新的线程
        myThread.start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主方法中的代码执行了" + i);
        }
    }
}

//1.定义类继承Thread类
class MyThread extends Thread{
    //2.重写run方法
    @Override
    public void run() {
        //run方法中的内容,启动线程后,执行的任务
        for (int i = 1; i <= 5; i++) {
            System.out.println("新县城执行了" + i);
        }
    }
}

注意事项和优缺点

  • 注意事项
    • 启动线程必须是调用start方法,而不是直接调用run方法
      • 直接调用run方法,不会启动线程
      • start方法的作用
        • 启动线程
        • 并执行run方法
  • 优点: 代码书写简单
  • 缺点: 自定义类继承Thread,无法继续继承其他类,不利于功能的扩展

内存原理

  • 官网:https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-2.html#jvms-2.5.2
  • 每个Java虚拟机线程都有一个与线程同时创建的专用Java虚拟机栈。
  • 内存图
    请添加图片描述

2.2 实现Runnable接口

步骤

  • 定义任务类,实现Runnable接口
  • 重写run方法
  • 创建任务对象
  • 把任务对象交给一个线程对象处理
public class Demo2 {
    public static void main(String[] args) {
        //3.创建线程对象
        MyRunnable r = new MyRunnable();
        //4.把任务类的对象,交给线程类
        Thread t = new Thread(r);
        //5.通过线程对象(Thread),调用start方法,启动新的线程
        t.start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主方法中的代码执行了" + i);
        }
    }
}

//1.自定义类(任务类)实现Runnable接口
class MyRunnable implements Runnable{
    //2.重写run方法
    @Override
    public void run() {
        //线程启动后要执行的任务
        for (int i = 1; i <= 5; i++) {
            System.out.println("新线程执行了" + i);
        }
    }
}

优缺点

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

2.3 实现Runnable接口(匿名内部类)

public class Demo3 {
 public static void main(String[] args) {
     //匿名内部类的实现方式
     Thread t1 = new Thread(new Runnable() {
         @Override
         public void run() {
             //线程启动后要执行的任务
             for (int i = 1; i <= 5; i++) {
                 System.out.println("新线程一执行了" + i);
             }
         }
     });
     t1.start();

     //lambda表达式  前提:函数式接口(接口中只有一个抽象方法)
     Thread t2 = new Thread(() ->{
         //线程启动后要执行的任务
         for (int i = 1; i <= 5; i++) {
             System.out.println("新线程二执行了" + i);
         }
     });
     t2.start();

     for (int i = 1; i <= 5; i++) {
         System.out.println("主方法中的代码执行了" + i);
     }
 }
}

2.4 实现Callable接口

问题

  • 问题: 假如线程执行完毕后有一些数据需要返回,前两种方式重写的run方法均不能返回结果
  • 解决: JDK5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)
  • 第三种创建方式的优点: 可以返回线程执行完毕后的结果

步骤

  • 创建任务对象
    • 定义一个类实现Callable接口,重写call方法(call方法中的内容,就是线程开启后要执行的任务,并且该方法有返回值)
    • 把Callable类型的对象交给FutureTask
FutureTask构造方法说明
public FutureTask<>(Callable call)把Callable对象交给成FutureTask
  • FutureTask交给交给Thread
  • 调用Thread对象的start方法启动线程
  • 线程执行完毕后,通过FutureTask对象的get方法获取线程任务的执行结果
FutureTask提供的方法说明
public V get() throws Exception获取线程执行call方法返回的结果
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/*
    注意事项及其优缺点:
        get获取线程执行完成结束后的结果
            如果线程没结束,死等(阻塞),所以get方法一定要写在start()之后
        优点:能够获取线程结束后的结果
        缺点:编码复杂
 */
public class Demo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //3.创建Callable实现类对象
        MyCallable c = new MyCallable();
        //4.Callable实现类对象 交给FutureTask
        FutureTask<String> f1 = new FutureTask<>(c);

        //lambda表达式
        FutureTask<String> f2 = new FutureTask(()->{
            for (int i = 1; i <= 50; i++) {
                System.out.println("B向女孩表白的第" + i + "次");
            }
            return "喔同意了!";
        });
        //5.FutureTask交给Thread
        Thread t1 = new Thread(f1);
        Thread t2 = new Thread(f2);
        t1.start();//开启线程
        t2.start();//开启线程
        //6.FutureTask调用get方法获取call方法的返回值
        final String rusult1 = f1.get();//等第一个线程结束后的结果
        final String rusult2 = f2.get();//等第二个线程结束后的结果
        System.out.println("女孩回应A:" + rusult1);
        System.out.println("女孩回应B:" + rusult2);
    }
}

//1.定义类实现Callable接口
//Callable接口中的泛型,和线程结束后得到结果的类型一致
class MyCallable implements Callable{
    //2.重写call方法
    @Override
    public String call() throws Exception {
        //是线程开启后执行的任务,并且带有返回值的
        //模拟向女孩表白
        for (int i = 1; i <= 50; i++) {
            System.out.println("A向女孩表白的第" + i + "次");
        }
        return "其实你是个好人!";
    }
}

注意事项和优缺点

  • 注意事项: FutureTask中的get方法会等待线程运行结束,再获取结果.所以不能写到线程开启start方法之前
  • 优点: 实现接口,可以继续继承其他类、实现其他接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果
  • 缺点: 编码复杂

2.5 三种方式的使用选择

  • 前两种方式,哪种方便用哪种
  • 如果需要获取线程执行后的返回结果,只能用第三种方式

3. Thread常用方法

Thread提供的常用方法说明
public void start()启动线程
public String getName()获取线程的名字,线程名字默认Thread-编号
public void setName(String name)为线程设置名字
public static Thread currentThread()获取当前执行的线程对象
public static void sleep(long time)让当前线程的线程休眠多少毫秒后,再继续执行
public final void join()让调用当前这个方法的线程先执行完
Thread提供的构造方法说明
public Thread(String name)指定线程名字
public Thread(Runnable target)封装Runnable对象成为线程对象
public Thread(Runnable target,String name)封装Runnable对象成为线程对象,并指定线程名字
public class Demo1 {
 public static void main(String[] args) throws ExecutionException, InterruptedException {
     //3.创建Callable实现类对象
     MyCallable c = new MyCallable();
     //4.Callable实现类对象 交给FutureTask
     FutureTask<String> f1 = new FutureTask<>(c);

     //lambda表达式
     FutureTask<String> f2 = new FutureTask(()->{
         for (int i = 1; i <= 50; i++) {
             System.out.println(Thread.currentThread().getName() + "向女孩表白的第" + i + "次");
         }
         return "喔同意了!";
     });
     //5.FutureTask交给Thread
     Thread t1 = new Thread(f1,"男孩A");
     Thread t2 = new Thread(f2);
     t2.setName("男孩B");
     t1.start();//开启线程
     t1.join();//让此线程执行完,再执行其他线程
     t2.start();//开启线程
     //6.FutureTask调用get方法获取call方法的返回值
     final String rusult1 = f1.get();//等第一个线程结束后的结果
     final String rusult2 = f2.get();//等第二个线程结束后的结果
     System.out.println("女孩回应" + t1.getName() + ":" + rusult1);
     System.out.println("女孩回应" + t2.getName() + ":" + rusult2);
 }
}

//1.定义类实现Callable接口
//Callable接口中的泛型,和线程结束后得到结果的类型一致
class MyCallable implements Callable{
 //2.重写call方法
 @Override
 public String call() throws Exception {
     //是线程开启后执行的任务,并且带有返回值的
     //模拟向女孩表白
     for (int i = 1; i <= 50; i++) {
         System.out.println(Thread.currentThread().getName() + "向女孩表白的第" + i + "次");
     }
     return "其实你是个好人!";
 }
}

4. 线程安全问题

4.1 案例

  • 需求: 电影院,一共100张票,三个窗口【同时】售票,用多线程模拟电影院卖票
public class Demo1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread("窗口一");
        t1.start();
        MyThread t2 = new MyThread("窗口二");
        t2.start();
        MyThread t3 = new MyThread("窗口三");
        t3.start();

    }
}

class MyThread extends Thread{
    private static int ticket = 100;//总票数

    public MyThread() {
    }

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

    //如果父类(接口)中定义的方法声明上没有 throws
    //重写的方法就不能使用throws来处理异常,不能使用try…catch
    @Override
    public void run() {
        //线程开启后执行的任务,卖票
        //不断的卖票,死循环
        while (true){
            if (ticket <= 0){
                break;
            }else {
                try {
                    Thread.sleep(100);//模拟出票延迟时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket--;
                System.out.println(getName() + "开始卖票了,还剩:【" + ticket + "】张票");
            }
        }

    }
}

4.2 问题

  • 模拟售票时间,卖票时 线程休眠100毫秒
  • 现象: 出现负数票,重复票
  • 原因: 多个线程操作了共享资源
  • 什么是线程安全问题: 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。

4.3 如何理解共享数据

  • 成员变量,多个线程操作同一个成员变量

5. 解决线程安全问题

5.1 同步代码块

格式

synchronized(锁对象) {
// 操作共享数据的代码
}

作用

  • 底层:线程自动获得锁,释放锁
  • 当有线程进入同步代码块时,锁住(该线程获得锁)
  • 当同步代码块中没有线程执行,锁打开(释放锁)
  • 作用:保证同时只有一个线程在操作共享数据
public class Demo1 {
    public static void main(String[] args) {
        MyThread t1 = new MyThread("窗口1");
        t1.start();
        MyThread t2 = new MyThread("窗口2");
        t2.start();
        MyThread t3 = new MyThread("窗口3");
        t3.start();

    }
}
class MyThread extends Thread{
    private static int ticket = 100;//总票数
    private static Object obj = new Object();//锁对象必须要求是唯一的

    public MyThread() {
    }

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

    //如果父类(接口)中定义的方法声明上没有 throws
    //重写的方法就不能使用throws来处理异常,不能使用try…catch
    @Override
    public void run() {
        //线程开启后执行的任务,卖票
        //不断的卖票,死循环
        while (true){

            synchronized (obj) {
                if (ticket <= 0){
                    break;
                }else {
                    ticket--;
                    System.out.println(getName() + "开始卖票了,还剩:【" + ticket + "】张票");
                }
            }
            try {
                Thread.sleep(100);//模拟出票延迟时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

锁对象唯一

  • 注意: 如果锁对象不唯一,相当于没有锁
private static Object obj = new Object();//锁对象必须要求是唯一的

5.2 同步方法

  • synchronized修饰普通方法:锁对象是this
public synchronized void method(){
        // 操作共享数据的代码
}
  • synchronized修饰静态方法:锁对象是类名.class
public static synchronized void method(){
        // 操作共享数据的代码
}

public class Demo2 {
    public static void main(String[] args) {
        //1.创建Runnable对象
        MyRunnable m = new MyRunnable();//只创建了一个实例对象,故MyRunnable类中成员ticket可不为static
        //2.创建三个线程对象
        Thread t1 = new Thread(m,"12306官网");
        t1.start();
        Thread t2 = new Thread(m,"携程官网");
        t2.start();
        Thread t3 = new Thread(m,"飞猪官网");
        t3.start();

    }
}

class MyRunnable implements Runnable{
    private int ticket = 100;//总票数
//    private int count = 0;
    @Override
    public void run() {
        while (true){
            boolean b = getTicketResult();
            if (b){
                break;
            }
        }
    }

    //如果改方法返回true,表示票卖完了
    //如果改方法返回false,表示票还有剩余
    public synchronized boolean getTicketResult(){
        Thread thread = Thread.currentThread();
        if (ticket == 0){
            return true;
        }else {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticket--;
//            count++;
            System.out.println(thread.getName() + "开始卖票了,还剩" + ticket + "张票");//"已经卖了" + count + "张票,还剩"
            return false;
        }
    }
}

5.3 Lock

  • JDK5提供的锁对象,显示的加锁和释放锁
    • Lock为接口
    • ReentrantLock为具体的实现类
  • 格式
Lock lock = new ReentrantLock();
lock.lock(); // 显式加锁
// 操作共享数据的代码
lock.unlock(); // 显式释放锁
  • 注意:
    • ReentrantLock对象要唯一
    • unlock() 要放到finally代码块中保证一定能释放锁
public class Demo3 {
    public static void main(String[] args) {
        MyThread3 t1 = new MyThread3("携程官网");
        t1.start();
        MyThread3 t2 = new MyThread3("同程官网");
        t2.start();
        MyThread3 t3 = new MyThread3("飞猪官网");
        t3.start();
    }
}

class MyThread3 extends Thread{
    private static int ticket = 100;//总票数
    static Lock lock = new ReentrantLock();//static保证lock锁的唯一性
    private int count = 0;//各个窗口售票张数

    public MyThread3() {
    }

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

    //如果父类(接口)中定义的方法声明上没有 throws
    //重写的方法就不能使用throws来处理异常,不能使用try…catch
    @Override
    public void run() {
        //线程开启后执行的任务,卖票
        //不断的卖票,死循环
        while (true){
            try {
                lock.lock();// 显示加锁,获得锁
                if (ticket == 0){
                    break;
                }else {
                    try {
                        Thread.sleep(10);//模拟出票延迟时间
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket--;
                    count++;
                    System.out.println(getName() + "已经卖了" + count + "张票,还剩:【" + ticket + "】张票");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock(); // 显示的释放锁
            }
        }

    }
}

5.4 线程安全问题小结

请添加图片描述

6. 线程间通讯

6.1 线程间通讯

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

线程通讯的方法(等待,唤醒)

  • 注意: 下列方法要使用当前同步锁对象进行调用。
Object中的方法说明
void wait()让当前线程等待并释放所占锁,直到其他线程调用notify()方法或者notifyAll()方法
void notify()唤醒正在等待的单个线程
void notifyAll()唤醒正在等待的所有线程
public class Demo1 {
    private static Object obj = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() ->{
            synchronized (obj) {
                Thread thread = Thread.currentThread();//获取当前线程对象
                System.out.println(thread.getName() + "开始执行了");
                try {
                    obj.wait();//当前线程在这等待
                    System.out.println(thread.getName() + "结束了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.setName("线程1");
        t1.start();

        Thread t3 = new Thread(() ->{
            synchronized (obj) {
                Thread thread = Thread.currentThread();//获取当前线程对象
                System.out.println(thread.getName() + "开始执行了");
                try {
                    obj.wait();//当前线程在这等待
                    System.out.println(thread.getName() + "结束了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t3.setName("线程3");
        t3.start();

        //
        try {
            Thread.sleep(1000);//睡眠确保线程2在线程1和3启动后才行,防止有没有启动导致无法唤醒睡眠
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread t2 = new Thread(() ->{
            synchronized (obj) {
                Thread thread = Thread.currentThread();//获取当前线程对象
                System.out.println(thread.getName() + "开始执行了");
//                obj.notify();//唤醒任意等待的一个线程
                obj.notifyAll();//唤醒所有线程
                System.out.println(thread.getName() + "结束了");
            }
        });
        t2.setName("线程2");
        t2.start();
    }
}

6.2 线程通讯的常见模型

生产者消费模型

  • 生产者线程负责生产数据
  • 消费者线程负责消费生产者产生的数据

案例

  • 厨师线程(生产者)不断的做包子:
    • 当盘子装不下了,唤醒吃货,自己等待
  • 吃货线程(消费者)不断的吃包子
    • 当盘子空了,唤醒厨师,自己等待
  • 桌子上放有盘子(容器)
public class Desk {
    //盘子容器
    List<String> list = new ArrayList<>();

    //向容器中添加包子
    public synchronized void put() {
        //判断容器是不是满了
        if (list.size() == 10){//满了
            //wait notify notifyAll
            this.notifyAll();//要先唤醒其他线程
            try {
                this.wait();//再等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {//没有满
            Thread thread = Thread.currentThread();
            list.add("包子");
            System.out.println(thread.getName() + "做了一个包子,目前包子数为:" + list.size());
        }
    }

    //从容器中取包子
    public synchronized void gut() {
        //判断容器是否为空
        if (list.size() == 0){//空的
            this.notifyAll();
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {//有包子
            Thread thread = Thread.currentThread();
            list.remove(0);//删除集合中的包子
            System.out.println(thread.getName() + "吃了一个包子,目前包子数为:" + list.size());
        }
    }
}
public class Demo1 {
    public static void main(String[] args) {
        //0.创建桌子的对象
        Desk desk = new Desk();
        //1.创建厨师线程
        Thread chef1 = new Thread(() -> {
            while (true){
                desk.put();
                try {
                    Thread.sleep(400);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        chef1.setName("厨师1");
        chef1.start();

        Thread chef2 = new Thread(() -> {
            while (true){
                desk.put();
                try {
                    Thread.sleep(700);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        chef2.setName("厨师2");
        chef2.start();

        Thread chef3 = new Thread(() -> {
            while (true){
                desk.put();
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        chef3.setName("厨师3");
        chef3.start();

        //2.创建吃货线程
        Thread foodie1 = new Thread(() -> {
            while (true){
                desk.gut();
                try {
                    Thread.sleep(600);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        foodie1.setName("吃货1");
        foodie1.start();

        Thread foodie2 = new Thread(() -> {
            while (true){
                desk.gut();
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        foodie2.setName("吃货2");
        foodie2.start();
    }
}

7. 线程的生命周期

  • 线程的六种状态和相互转换
    请添加图片描述

  • Thread.State 内部枚举类

状态枚举项状态说明阶段
NEW新建线程刚被创建,但是并未启动。
RUNNABLE可运行线程已经调用了start()
BLOCKED阻塞线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态;
WAITING无限等待一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒
TIMED_WAITING计时等待同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态
TERMINATED结束因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
  • 线程对象调用getState()方法可以获取线程的状态
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("线程创建了");
            try {
                Thread.sleep(1000);  // t线程睡1秒钟
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t1.getState()); // 什么状态?  NEW 新建状态
        t1.start();
        System.out.println(t1.getState()); // 什么状态?  RUNNABLE 可运行状态



        // 主线程睡500毫秒
        Thread.sleep(500);
        System.out.println(t1.getState()); // 什么状态?  TIMED_WAITING 计时等待
    }
}

8. 总结

请添加图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值