JavaLearn# (13)多线程:线程生命周期线程控制线程同步线程通信线程池ForkJoin框架

Posted LRcoding

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaLearn# (13)多线程:线程生命周期线程控制线程同步线程通信线程池ForkJoin框架相关的知识,希望对你有一定的参考价值。

1. 进程和线程

  • 程序:一段静态的代码,是应用程序执行的蓝本

  • 进程:指一种正在运行的程序,有自己的地址空间

    • 动态性(正在运行的程序)
    • 并发性(同时运行)
    • 独立性(QQ 和 微信互不干扰)
  • 并发和并行的区别

    • 并行(parallel):多个 CPU 同时执行多个任务,宏观和微观来看,都是同时执行
    • 并发(concurrency):一个 CPU 同时执行多个任务(采用时间片轮转,A执行一段时间,B再执行一段时间)
  • 线程

    • 进程内部的一个执行单元,它是程序中一个单一的顺序控制流程,又被称为轻量级进程(lightweight process)
    • 如果在一个进程中,同时运行了多个线程,用来完成不同的工作,则被称为多线程
    • 线程特点
      • 轻量级进程
      • 独立调度的基本单位
      • 共享进程资源
      • 并发执行

  • 线程和进程的区别

区别进程线程
根本区别作为资源分配的单位(例:申请30M内存)调度和执行的单位(从30M中划分资源)
开销大:每个进程有自己独立的代码和数据空间小:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器
所处环境操作系统中能同时运行多个任务同一应用程序中有多个顺序流同时执行
分配内存每个进程分配不同的内存区域除了CPU,不会为线程分配资源,使用的都是进程的,线程组只能共享资源
包含关系没有线程的进程可以被看作单线程的,如果一个进程内拥有多个线程,那么就是多个线程同时完成线程是进程的一部分

2. 线程的定义方式

2.1 继承 Thread 类

  • Thread 类常用方法
    • run():线程要执行的任务
    • start():启动线程
    • getName():获取线程名称
    • getPriority():获取优先级
    • Thread.currentThread():得到当前线程
  • 启动 main 方法,自动创建 main 线程

创建一个线程类:(extends Thread

public class TreadDemo extends Thread{
    /**
     * 线程体:线程要执行的任务
     */
    @Override
    public void run() {
        while (true) {
            System.out.println("[A] 线程执行了" + this.getName() + "  " + this.getPriority());
        }
    }
}

创建线程对象、启动线程

// 创建线程对象
Tread treadDemo = new TreadDemo();
treadDemo.setName("设置线程名称"); // 设置线程的名称
treadDemo.setPriority(Thread.MAX_PRIORITY); // 设置线程的优先级


// 启动线程
treadDemo.start();
// treadDemo.run();   此处为普通的方法调用,直接调用 run() 方法

2.2 实现 Runnable 接口

  • 两种方式的比较:
    • 继承 Thread 类:编程简单,但是单继承,无法继承其他类
    • 实现 Runnable 接口:编程稍繁琐,可以再继承其他类,实现其他接口,操作的为同一个任务便于多个线程共享同一个资源

定义一个线程类:(implements Runnable

public class RunnableDemo implements Runnable{
    @Override
    public void run() {
        while (true) {
            System.out.println("[A] 线程执行了" + Thread.currentThread().getName() + "  "
                    + Thread.currentThread().getPriority());
        }
    }
}

创建线程对象、启动线程

// 创建线程对象
Runnable runnable = new RunnableDemo();
Thread thread = new Thread(runnable); // 使用 runnable

// 启动线程
thread.setName("[A] 线程");
thread.setPriority(Thread.NORM_PRIORITY);
thread.start();

// 创建另一个线程对象
Thread thread1 = new Thread(runnable); // 使用同一个 runnable,操作的为同一个任务对象
thread1.setName("[A] 线程 2");

=====》 还可以直接使用匿名内部类的方式

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        while (true) {
            System.out.println("[A] 线程执行了" + Thread.currentThread().getName() + "  "
                               + Thread.currentThread().getPriority());
        }
    }
};

// 创建一个线程
Thread thread = new Thread(runnable); // 使用 runnable
thread.setName("[A] 线程");
thread.setPriority(Thread.NORM_PRIORITY);
thread.start();

// 创建另一个线程
Thread thread1 = new Thread(runnable); // 使用同一个 runnable,操作的为同一个对象
thread1.setName("[A] 线程 2");

2.3 实现 Callable 接口

JDK 1.5后推出

  • 可以有返回值,支持泛型的返回值
  • 可以抛出检查异常
  • 需要借助 FutureTask,获取返回结果等

定义一个线程类:(implements Callable

public class CallableDemo implements Callable<Integer> {
    /**
     * 有返回值,并且可以抛出异常
     * @return
     * @throws Exception
     */
    @Override
    public Integer call() throws Exception {
        if (false) {
            throw new Exception();
        }
        return new Random().nextInt(10);
    }
}

创建线程对象、启动线程

// 创建线程
Callable<Integer> callable = new CallableDemo();
// 需要使用 FutureTask 进行操作
FutureTask<Integer> task = new FutureTask(callable);
// 最终放入的是 FutureTask 对象
Thread thread = new Thread(task);

// 启动线程
thread.start();

// 获取返回值
Integer i = task.get();  // 得不到返回值,就一直等待!!!!!!!!!!
task.get(3, TimeUnit.SECONDS); // 就等待 3 秒,获取不到就报错
System.out.println(i);

3. 线程的生命周期

  • 新生状态:
    • new 创建一个线程对象后,该对象就进入了新生状态
    • 在此时的线程,拥有自己的内存空间,通过 start() 方法进入就绪状态
  • 就绪状态:
    • 此时线程具备了运行条件,但还没分配到CPU
    • 当系统选定一个等待执行的线程后,它就进入了执行状态,称为“CPU调度”
  • 运行状态
    • 执行自己的 run 方法中的代码,直到等待某资源而阻塞,或完成任务而死亡
    • 如果在给定时间片内,没执行结束,就会被系统给换下来回到阻塞状态
  • 阻塞状态
    • 处于运行状态的线程,在某些情况下(执行了sleep方法、等待I/O设备等)将让出CPU 并暂停自己的运行,进入阻塞状态
    • 只有当睡眠时间结束,或资源已获取到,才能进入就绪状态中等待,被系统选中后,从停止的位置继续运行
  • 死亡状态
    • 正常运行的线程,完成了所有的工作
    • 线程被强制性的终止,如通过 stop 方法来终止一个线程【不推荐使用】
    • 线程抛出未捕获的异常

4. 线程控制

可以对线程的生命周期进行干预

基础线程类

public class MyThread extends Thread {
    @Override
    public void run() {
        this.setPriority(6);
        for (int i = 0; i < 100; i++) {
            System.out.println("线程开始执行了:" + this.getName() + "  " + this.getPriority());
        }
    }
}

4.1 join()

阻塞其他线程,等待该线程执行完之后,再执行其他线程

for (int i = 0; i < 100; i++) {
    if (i == 50) {
        Thread thread = new MyThread();
        thread.setName("自定义线程类");
        thread.start();

        // 阻塞主线程,当 自定义线程 执行完,再执行主线程,【在 start() 方法之后】
        thread.join();
    }
    System.out.println("主线程:" + Thread.currentThread().getName());
}

4.2 sleep()

让出 CPU,自身进入阻塞状态

Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.start();

for (int i = 0; i < 100; i++) {
    try {
        Thread.sleep(1000); // 主线程休眠 1 秒,进入【阻塞状态】,时间到后再次进入【就绪状态】
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("主线程:" + Thread.currentThread().getName());
}

4.3 yield()

礼让 CPU,执行一会儿,然后退回到就绪状态,继续争抢CPU(可能会立即抢到,继续执行)

Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.start();

for (int i = 0; i < 100; i++) {
    Thread.yield(); // 主线程进行礼让,执行一会儿后,让出CPU,进入【就绪状态】
    System.out.println("主线程:" + Thread.currentThread().getName());
}

4.4 setDaemon()

守护线程(寄生线程),启动它的线程执行完了,那么它也要停止执行

Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.setDaemon(true); // 启动它的线程执行结束之后,它也停止执行,【在 start() 方法之前】
thread.start();

for (int i = 0; i < 100; i++) {
    System.out.println("主线程:" + Thread.currentThread().getName());
}

4.5 interrupt()

并不是结束了线程,而是修改了线程的状态,需要线程类进行判断isInterrupted()

Thread thread = new MyThread();
thread.setName("自定义线程类");
thread.start();

for (int i = 0; i < 100; i++) {
    System.out.println("主线程:" + Thread.currentThread().getName());
}

thread.interrupt();  // 修改线程的状态,告诉它该结束运行了

线程类修改为:

public class MyThread extends Thread {
    @Override
    public void run() {
        while (this.isInterrupted()) { // 进行判断,是否该结束了
            System.out.println("线程开始执行了:" + this.getName() + "  " + this.getPriority());
        }
    }
}

5. 线程同步

5.1 问题的提出

  • 场景:

    • 多个用户同时操作一个银行账户。每次取款 400 元,取款前先检查余额是否足够,如果不够,放弃取款

      例如 2 个人同时取 400,卡内还有 600 元

  • 分析:

    • 使用多线程解决
    • 开发一个取款线程类,每个用户对应一个线程对象
    • 多个线程共享同一个银行账户,使用 Runnable 方式
  • 思路

    • 创建银行账户类 Account
    • 创建取款线程 AccountRunnable
    • 创建测试类,进行测试

Account 类

public class Account {
    private int balance = 600;

    /**
     * 取款
     * @param money
     */
    public void withDraw(int money) {
        this.balance -= money;
    }

    /**
     * 查看余额
     * @return
     */
    public int getBalance() {
        return balance;
    }
}

AccountRunnable 类

public class AccountRunnable implements Runnable {
    private Account account = new Account();

    /**
     * 取款的步骤
     */
    @Override
    public void run() {
        // 在这个流程中,需要保证【有一个人进入了,另一个人就无法进入】
        // ======================== 取款开始 ===================================
        if (account.getBalance() >= 400) {
            try {
                Thread.sleep(10); // 需要特别注意的地方
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.withDraw(400);
            System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + account.getBalance());
        } else {
            System.out.println(Thread.currentThread().getName() + " 取款失败,余额:" + account.getBalance());
        }
        // ======================== 取款结束 ===================================
    }
}

TestAccount 类

public static void main(String[] args) {
    // 创建 Runnable 对象
    Runnable runnable = new AccountRunnable();
    
    Thread user1 = new Thread(runnable, "张三");
    user1.start();

    Thread user2 = new Thread(runnable, "张三妻子");
    user2.start();
}

可能出现的结果:

5.2 同步代码块

针对上面的问题,第一种解决方案:使用同步代码块

synchronized (account) { // 使用共享资源作为锁
    
    // 在开始取款之前,先【加上一把锁】,保证【有一个人进入了,另一个人就无法进入】
    // ======================== 取款开始 ===================================
    if (account.getBalance() >= 400) {
        try {
            Thread.sleep(10); // 需要特别注意的地方
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.withDraw(400);
        System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + account.getBalance());
    } else {
        System.out.println(Thread.currentThread().getName() + " 取款失败,余额:" + account.getBalance());
    }
    // ======================== 取款结束 ===================================
}

5.2.1 同步监视器(锁子)

synchronized (同步监视器) { 同步代码块 }

  • 必须是引用数据类型,不能是基本数据类型
  • 在同步代码块中,可以改变它的值,但是不能改变其引用建议使用 final 修饰同步监视器
  • 一般使用共享资源作同步监视器即可
  • 也可创建一个专门的同步监视器,没有任何业务意义
  • 尽量不要使用 String 和包装类(Integer)作同步监视器

5.2.2 执行过程

  • 第一个线程,来到同步代码块,发现同步监视器为 open 状态,需要改为 close,然后执行其中的代码
  • 第一个线程,在执行过程中,发生了线程切换(阻塞),第一个线程就失去了 cpu,但是没有开锁 open
  • 第二个线程,获取了cpu,来到同步代码块,发现同步监视器为 close 状态,无法执行其中的代码,也进入阻塞状态
  • 第一个线程,再次获取 cpu,接着执行后续的代码;执行完成后,开锁 open
  • 第二个线程,也再次获取到 cpu,来到同步代码块,发现为 open 状态,重复第一个线程的处理过程(加锁)

注意:同步代码块中,可以发生线程切换,但是后续的线程,由于无法开锁,所以无法执行同步代码块

5.2.3 分析

  • 加上锁之后,安全了,但是效率降低,而且可能出现死锁

  • 多个代码块使用了同一个锁A,锁住一个代码块的同时,也把其他地方使用这把锁A的代码块锁住了

    导致其他线程无法访问这些代码块

    但是没有锁住使用其他锁B的代码块,其他线程可以执行这些代码块

5.3 同步方法

提取需要加锁的代码块,形成一个方法,给方法加锁(不能给 run() 方法加锁

public synchronized void withDraw() { // 【synchronized】 写在方法上
    if (account.getBalance() >= 400) {
        try {
            Thread.sleep(10); // 需要特别注意的地方
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.withDraw(400);
        System.out.println(Thread.currentThread().getName() + " 取款成功,余额:" + account.getBalance());
    } else 以上是关于JavaLearn# (13)多线程:线程生命周期线程控制线程同步线程通信线程池ForkJoin框架的主要内容,如果未能解决你的问题,请参考以下文章

13.3 线程的生命周期

多线程的生命周期以及多线程的使用场景

Java多线程与并发——线程生命周期和线程池

Java多线程并发02——线程的生命周期与常用方法

Java 多线程系列2——多线程的生命周期及生产消费者模型

iOS多线程全套:线程生命周期,多线程的四种解决方案,线程安全问题,GCD的使用,NSOperation的使用