Java多线程笔记全过程(一)

发布时间 2023-06-19 15:17:30作者: 修炼诗人

一、多线程最基础的基本概念

  一个程序最少需要一个进程,而一个进程最少需要一个线程。

  我们常说的高并发,也并不全是线程级别的并发,在很多开发语言中,比如PHP,很常见的都是进程级别的并发。但是在Java中谈论到的基本都是线程级别的并发。当然了,高并发的来源,与摩尔定律的概念相当,等单个人无法满足任务的需求时,找多个人一起干活。就成为了主流。

  这里介绍几个多线程的概念:

  1.1、进程和线程:

    程序:开发写的代码称之为程序。程序是一组代码,是一组数据和指令集,当然了程序是一个静态的概念。    

    进程:将程序运行起来,称之为进程。进程存在生命周期,随着程序的终止而销毁。进程之间通过TCP/IP端口实现交互。

    线程:线程是CPU调度和执行的最小单位。PS:很多多线程都是模拟出来的,真正的多线程是指多个CPU,也就是多核,如服务器。但是如果CPU只有一个,在同一个时间点内只会执行一个代码,但是由于计算机执行速度极快,导致产生同时执行的错觉。

  1.2、并发、并行、串行

    并行:多个任务同时进行,但是必须具备多核能力,才能实现。

    并发:同一个对象被多个线程同时操作,这一种是假并发,一个CPU可以完成工作。

    串行:一个程序处理完当前进程,按照顺序处理下一个进行,一个接着一个进行。

  1.3、同步和异步:

    同步:当进行方法调用时,必须等到方法调用返回后,才能够执行后续行为。例子:打电话、单线程。

    异步:很像是消息传递,一旦开始,调用方法就会立刻返回,调用可以继续后续行为。例子:发短信、多线程。

  1.4、临界区、死锁、活锁、饥饿:

    临界区:一次被一个线程占用,其他线程必须排队,例子:办公室的打印机。注意:多线程情况下,共享资源必须上锁。

    死锁:在唯一性的条件下,完成任务需要A、B,但是线程1拿着A,线程2拿着B,导致双方一直在等待,形成死循环。

    活锁:经典例子:孔融让梨,虽然线程们都谦让资源,但是大家都没有办法使用资源,导致任务永远完不成。这种情况下,会耗费CPU的时间。

    饥饿:当线程的优先级特别低的情况下,完成线程所需要的资源永远无法获取,会造成饥饿现象。

二、线程的创建以及启动

  • 继承Thread类,并重写run()方法。

  • 实现Runnable接口,并重写run()方法。

  • 通过Callable和Future创建线程

  2.1.1、继承Thread类

1、自定义线程类继承Thread类
2、重写run方法,编写线程执行体
3、创建线程对象,调用start()方法
注意:创建不代表执行,需要抢CPU资源

  2.1.2、代码

public class A extends Thread {
    public A(String name){
        super(name);
    }
    @Override
    public void run(){
        // 线程执行体
        for (int i = 0; i < 10; i++) {
            System.out.println("我是自定义" + Thread.currentThread().getName() + "--" + i);
        }
    }
    public static void main(String[] args){
        // 创建线程对象,准备抢占资源
        A a1 = new A("线程1");
        A a2 = new A("线程2");
        a1.start();
        a2.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("我是主线程--" + i);
        }
    }
}

  2.2.1  实现Runnable接口

 1、自定义线程类实现Runnable接口
 2、重写run方法,编写线程执行体
 3、创建线程对象,调用start()方法
 注意:创建不代表执行,需要抢CPU资源

  2.2.2 代码

public class A implements Runnable  {

    @Override
    public void run(){
        // 线程执行体
        for (int i = 0; i < 10; i++) {
            System.out.println("我是自定义" + Thread.currentThread().getName() + "--" + i);
        }
    }
    public static void main(String[] args){
        // 创建线程对象,准备抢占资源
        // 创建实现类对象
        A myRunnable = new A();
        Thread t1 = new Thread(myRunnable,"线程1");
        Thread t2 = new Thread(myRunnable,"线程2");
        t1.start();
        t2.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("我是主线程--" + i);
        }
    }

}

  2.3.1   实现Callable接口(不常用)

1、实现Callable接口,先要返回值类型
2、重写call()方法,需要抛出异常
3、创建目标对象
4、创建执行服务:ExecutorService ser = Executor.newFixedThreadPool(1);
5、提交执行:Future<Boolean> res = ser.submit(t1);
6、获取结果:boolean r1 = res.get();
7、关闭服务:ser.shutdownNow();

  2.3.2   代码

import java.util.concurrent.*;

// 自定义线程对象,实现Callable接口,重写call()方法
public class A implements Callable<Boolean> {

    @Override
    public Boolean call() throws Exception {
        // 线程执行体
        for (int i = 0; i < 10; i++) {
            System.out.println("我是自定义" + Thread.currentThread().getName() + "--" + i);
        }
        return true;
    }

    public static void main(String[] args) throws ExecutionException,
            InterruptedException {
        // main线程,主线程
        // 创建线程实现类对象
        A thread = new A();
        A thread2 = new A();
        // 创建执行服务,参数是线程池线程数量
        ExecutorService ser = Executors.newFixedThreadPool(2);
        // 提交执行
        Future<Boolean> res = ser.submit(thread);
        Future<Boolean> res2 = ser.submit(thread2);
        // 获取结果
        boolean r1 = res.get();
        boolean r2 = res2.get();
        // 关闭服务
        ser.shutdownNow();
    }
}

三、线程的生命周期

  新建(New)状态: 创建后尚未启动(未调用start()方法)的线程处于这种状态。

  就绪(Ready)状态: 当调用线程对象的start()方法,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行。

  运行(Running)状态: 当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。

  限期等待(Timed Waiting)状态: 处于这种状态的线程也不会被分配CPU执行时间,不过无须等待其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:

  新建无限期等待(Waiting)状态: 处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态。

  阻塞(Blocked)状态: 线程在获取synchronized排他锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。

  死亡(Dead)状态: 线程执行完了或者因异常退出了run()方法,该线程生命周期结束。