详解 Java 多线程(笔记)

本文详细介绍了Java中多线程的实现方式,包括继承Thread类、实现Runnable接口和Callable接口。此外,还讲解了线程状态、线程操作(如线程停止、休眠、礼让、中断等)、线程同步(同步代码块、同步方法、Lock和死锁)以及线程协作。最后,讨论了线程池的概念和Java提供的ExecutorService、Executors工具类,以及如何自定义线程池。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是进程和线程?

几乎所有的操作系统都支持进程的概念,所有运行中的任务通常对应一个进程 (Process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。

  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命 周期和各种不同的状态,这些概念在程序中都是不具备的。

  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。

大部分操作系统都支持多进程并发运行,现代的操作系统几乎都支持同时运行多个任 务。

多线程则扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程 (Thread)也被称作轻量级进程(Lightweight Process),线程是进程的执行单元。 就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了。对于绝大多数的应用程序来说,通常仅要求有一个主线程,但也可以在该进程内创建多条顺序执行流,这些顺序执行流就是线程,每个线程也是互相独立的。 线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。 线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资 源,它与父进程的其他线程共享该进程所拥有的全部资源。因为多个线程共享父进程 里的全部资源,因此编程更加方便;但必须更加小心,因为需要确保线程不会妨碍同 一进程里的其他线程线程可以完成一定的任务,可以与其他线程共享父进程中的共享 变量及部分环境,相互之间协同来完成进程所要完成的任务。

简单来说:

  • 进程:进程是系统进行资源分配和调度的基本单位,可以将进程理解为一个正在执行的程序。

  • 线程:线程是程序执行的最小单位,一个进程可由一个或多个线程组成

总结:操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。

Java 线程实现

Java 提供了三种实现线程的方式:

  • 继承 Thread 类创建线程类

  • 实现 Runnable 接口创建线程类

  • 实现 Callable 接口,重写call方法

一、继承 Thread 类创建线程类

实现步骤:

  1. 定义类继承Thread类,并重写Thread类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run()方法称为线程执行体。

  2. 创建Thread子类的实例,即创建了线程对象。

  3. 调用线程对象的start()方法来启动该线程。

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        // 调用start方法方可启动线程
        // 不能调用run()方法,run方法只是thread的一个普通方法,还是在主线程里执行。
        mt1.start();
        mt2.start();

        System.out.println("---------------------------------");
    }
}
/**
 * 继承Thread类创建线程类 继承Thread类,重写run方法
 */
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("线程ID:" + getId());
        System.out.println("线程名称:" + getName());

        for (int i = 0; i < 50; i++) {
            System.out.println("线程ID:" + getId() + ";线程名称:" + getName());
        }
    }
}

运行结果:

 从程序可以看出,现在的两个线程对象是交错运行的,哪个线程对象抢到了 CPU 资源,哪个线程就可以运行,所以程序每次的运行结果肯定是不一样的,在线程启动虽然调用的是 start() 方法,但实际上调用的却是 run() 方法定义的主体。

不建议使用:避免 OOP 单继承局限性

二、实现Runnable接口创建线程类

实现步骤:

  1. 定义类实现Runnable接口,并重写Runnable接口的run()方法,该run()方法的 方法体就代表了线程需要完成的任务

  2. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对 象,该Thread对象才是真正的线程对象

  3. 调用该Thread对象的start()方法来启动该线程

public class RunnableDemo {
    public static void main(String[] args) {
        //创建Runnable实现类的实例
        MyRunnable mr = new MyRunnable();

        //创建Thread类,并把Runnable实现类的实例作为参数,用来开启线程(代理)
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        //开启线程
        t1.start();
        t2.start();
    }
}
class MyRunnable implements Runnable{

    @Override
    public void run() {
        // 实现Runnable接口的,无法直接使用getId(),getName()等方法
        // 需要使用Thread.currentThread() 来获取到当前对象才行
        System.out.println("线程ID:" + Thread.currentThread().getId());
        System.out.println("线程名称:" + Thread.currentThread().getName());

        for (int i = 0; i < 50; i++) {
            System.out.println("线程ID:" + Thread.currentThread().getId() + ";线程名称:" + Thread.currentThread().getName());
        }
    }
}

 

运行结果:

 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

通过 Thread 类和 Runable 接口都可以实现多线程,那么两者有哪些联系和区别呢?

Thread 类的定义

public class Thread extends Object implements Runnable{}

从 Thread 类的定义可以清楚的发现,Thread 类也是 Runnable 接口的子类,但在Thread类中并没有完全实现 Runnable 接口中的 run() 方法。在 Thread 类中的 run() 方法调用的是 Runnable 接口中的 run() 方法,也就是说此方法是由 Runnable 子类完成的,所以如果要通过继承 Thread 类实现多线程,则必须覆写 run()。

实际上 Thread 类和 Runnable 接口之间在使用上也是有区别的,如果一个类继承 Thread类,则不适合于多个线程共享资源,而实现了 Runnable 接口,就可以方便的实现资源的共享。

三、实现 Callable 接口

实现步骤:

  • 实现 Callable 接口,需要返回值类型

  • 重写 call 方法,需要抛出异常

  • 创建目标对象

  • 创建执行服务

  • 提交执行,获取结果

  • 关闭服务

public class CallableDemo {
    //实现 Callable 接口,需要返回值类型
    public static class MyCallableClass implements Callable<String>{
        //标志位
        private int flag = 0;

        public MyCallableClass(int flag) {
            this.flag = flag;
        }

        //重写 call 方法,需要抛出异常
        @Override
        public String call() throws Exception {
            if (this.flag == 0){
                // 如果flag的值为0,则立即返回
                return "flag = 0";
            }
            if (this.flag == 1){
                // 如果flag的值为1,做一个无限循环
                try{
                    while (true){
                        System.out.println("looping...");
                        Thread.sleep(2000);
                    }
                }catch (InterruptedException e){
                    System.out.println("Interrupted");
                }
                return "false";
            }else {
                // falg不为0或者1,则抛出异常
                throw new Exception("Bad flag value!");
            }
        }
    }

    public static void main(String[] args) {
        //创建目标对象
        MyCallableClass task1 = new MyCallableClass(0);
        MyCallableClass task2 = new MyCallableClass(1);
        MyCallableClass task3 = new MyCallableClass(2);

        //创建一个执行任务的服务
        ExecutorService es = Executors.newFixedThreadPool(3);// nThreads 池中的线程数
        try {
            // 提交并执行任务,任务启动时返回了一个 Future对象
            // 如果想得到任务执行的结果或者是异常可对这个Future对象进行操作
            Future<String> future1 = es.submit(task1);
            // 获得第一个任务的结果,如果调用get方法,当前线程会等待任务执行完毕后才往下执行
            System.out.println("task1: " + future1.get());

            Future<String> future2 = es.submit(task2);
            // 等待5秒后,再停止第二个任务。
            Thread.sleep(5000);
            //如果执行此任务的线程应该被中断;否则,允许进行中的任务完成
            System.out.println("task2 cancel: " + future2.cancel(true));

            // 获取第三个任务的输出,因为执行第三个任务会引起异常
            // 所以下面的语句将引起异常的抛出
            Future<String> future3 = es.submit(task3);
            System.out.println("task3: " + future3.get());


        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
            //关闭服务
            es.shutdownNow();
        }
    }
}

运行结果:

Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。

Callable和Runnable有几点不同:

  • Callable规定的方法是call(),而Runnable规定的方法是run()

  • Callable的任务执行后可返回值,而Runnable的任务是不能返回值的

  • call()方法可抛出异常,而run()方法是不能抛出异常的

  • 运行Callable任务可拿到一个Future对象

Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。

通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。

Java 线程状态

线程的五种状态:

  1. 新建状态(New):线程对象实例化后就进入了新建状态。

  2. 就绪状态(Runnable):线程对象实例化后,其他线程调用了该对象的 start()方法,虚拟机便会启动该线程,处于就绪状态的线程随时可能被调度执行。

  3. 运行状态(Running):线程获得了时间片,开始执行。只能从就绪状态进入运行状态

  4. 阻塞状态(Blocked):线程因为某个原因暂停执行,并让出CPU的使用权后便进入了阻塞状态。

    • 等待阻塞:调用运行线程的wait()方法,虚拟机会把该线程放入等待池

    • 同步阻塞:运行线程获取对象的同步锁时,该锁已被其他线程获得,虚拟机会把该线程放入锁定池

    • 其他线程:调用运行线程的sleep()方法或join()方法,或线程发出I/O请求 时,进入阻塞状态

  5. 结束状态(Dead):线程正常执行完或异常退出时,进入了结束状态

Java 程序每次运行至少启动两个线程,每当使用 Java 命令执行一个类时,实际上都会启动一个 JVM,每一个JVM实际上就是在操作系统中启动一个线程,Java 本身具备了垃圾的收集机制。所以在 Java 运行时至少会启动两个线程,一个是 main 线程,另外一个是垃圾收集线程。

线程操作

Thread 类的常用方法

返回类型构造器/方法说明
 Thread()分配新的 Thread 对象
 Thread(Runnable target)分配新的 Thread 对象。
 Thread(Runnable target, String name)分配新的 Thread 对象。
static ThreadcurrentThread()返回对当前正在执行的线程对象的引用
static voidsleep(long millis)在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。
static voidsleep(long millis, int nanos)在指定的毫秒数加指定的纳秒数内让当前正在执行的线 程休眠(暂停执行)
static voidyield()暂停当前正在执行的线程对象,并执行其他线程
voidstart()使该线程开始执行;Java 虚拟机调用该线程的 run 方 法。
longgetId()返回该线程的标识符
StringgetName()返回该线程的名称
intgetPriority()返回线程的优先级
booleanisAlive()测试线程是否处于活动状态
voidjoin()等待该线程终止。
voidjoin(long millis)等待该线程终止的时间最长为 millis 毫秒
voidjoin(long millis, int nanos)等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒
voidsetName(String name)改变线程名称,使之与参数 name 相同
voidsetPriority(int newPriority)更改线程的优先级。参数范围[1,10]
voidinterrupt()中断运行中的线程
voidsetDaemon(boolean on)即使 Java 线程结束了,后台线程依然会继续执行

线程停止

建议线程正常停止 ---> 利用次数,不建议死循环

建议使用标志位 ---> 设置一个标志位

不要使用 stop 或者 destroy 等过时或者 JDK 不建议使用的方法

public class ThreadDemo implements Runnable {
    //设置一个标识位
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (flag){
            System.out.println("run...Thread" + i++);
        }
    }

    //设置一个公开的方法停止线程,转换标识位
    public void stop(){
        this.flag = false;
    }

    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();
        for (int i = 0; i < 100; i++) {
            System.out.println("i=" + i);
            if (i == 99){
                //调用 stop 方法切换标识位,让线程停止
                td.stop();
                System.out.println("Thread stop");
            }
        }
    }
}

线程休眠

在程序中允许一个线程进行暂时的休眠,sleep(时间) 指定当前线程阻塞的毫秒数,存在 InterruptedException 异常,当时间达到后该线程就会进入就绪状态,sleep 不会释放锁

public class ThreadDemo implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            try {
                Thread.sleep(500);//休眠
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行,i = " + i);
        }
    }


    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        new Thread(td,"线程").start();
    }
}

线程礼让

让当前正在执行的线程暂停,但不阻塞,将线程从运行状态转为就绪状态,让CPU重新调度,不一定礼让成功

public class ThreadDemo implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(500);//休眠
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行,i = " + i);
            if (i == 2){
                System.out.print("线程礼让:");
                Thread.yield();//线程礼让
            }
        }
    }


    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        new Thread(td,"线程A").start();
        new Thread(td,"线程B").start();
    }
}

运行结果:

线程强制执行

合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞

public class ThreadDemo implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(Thread.currentThread().getName() + "运行,i = " + i);
        }
    }


    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        Thread t = new Thread(td,"线程");
        t.start();
        for (int i = 0; i < 50; i++) {
            if (i == 10){
                try {
                    t.join();//线程强制执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("main 线程运行 ---> " + i);
        }
    }
}

线程中断

当一个线程运行时,另外一个线程可以直接通过interrupt()方法中断其运行状态

public class ThreadDemo implements Runnable {

    @Override
    public void run() {
        System.out.println("1、进入 run() 方法");
        try {
            Thread.sleep(5000);//线程休眠5秒
            System.out.println("2、已经完成了休眠");
        } catch (InterruptedException e) {
            System.out.println("3、休眠被终止");
            return ; // 返回调用处
        }
        System.out.println("4、run()方法正常结束");
    }


    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        Thread t = new Thread(td,"线程");
        t.start();
        try {
            Thread.sleep(2000);//休眠 2 秒
        } catch (InterruptedException e) {
            System.out.println("3、休眠被终止") ;
        }
        t.interrupt();
    }
}

运行结果:

后台线程(守护线程)

在 Java 程序中,线程分为用户线程和守护线程(后台线程),虚拟机必须确保用户线程执行完毕,不用等待守护线程执行完毕,如后台记录操作日志、监控内存、垃圾回收机制等

只要前台有一个线程在运行,则整个 Java 进程都不会消失,所以此时可以设置一个后台线程,这样即使 Java 线程结束了,此后台线程依然会继续执行

public class ThreadDemo implements Runnable {

    @Override
    public void run() {
        while (true){
            System.out.println(Thread.currentThread().getName() + "在运行...");
        }
    }


    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        Thread t = new Thread(td,"线程");
        t.setDaemon(true);//此线程在后台运行
        t.start();
    }
}

线程优先级

在 Java 的线程操作中,所有的线程在运行前都会保持在就绪状态,那么此时,哪个线程的优先级高,哪个线程就有可能会先被执行

  • 线程的优先级用数字表示,范围 1~10

    Thread.MIN_PRIORITY = 1;

    Thread.MAX_PRIORITY = 10;

    Thread.NORM_PRIORITY = 5;

public class ThreadDemo implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+ "运行,i = " + i);
        }
    }


    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        Thread t1 = new Thread(td,"线程A");
        Thread t2 = new Thread(td,"线程B");
        Thread t3 = new Thread(td,"线程C");

        t1.setPriority(Thread.MIN_PRIORITY);//优先级最低
        t2.setPriority(Thread.MAX_PRIORITY);// 优先级最高
        t3.setPriority(Thread.NORM_PRIORITY);// 优先级最中等

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

    }
}

运行结果:

从程序的运行结果中可以观察到,线程将根据其优先级的大小来决定哪个线程会先运行,但是需要注意并非优先级越高就一定会先执行,只是优先级高只是意味着获得调度的概率高,而优先级低只是意味着获得调度的概率低,哪个线程先执行将由 CPU 的调度决定。

线程同步

一个多线程的程序如果是通过 Runnable 接口实现的,则意味着类中的属性被多个线程共享,那么这样就会造成一种问题,如果这多个线程要操作同一个资源时就有可能出现资源同步问题。处理多线程问题时,多个线程同时访问同一个对象(并发),并且某些线程还想修改这个对象。这时候就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕,下一个线程再使用。

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制 synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,但会存在以下问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起

  • 在多线程竞争在,加锁--释放锁会导致较多的上下文切换和调度延时,引起性能问题

  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置引起性能问题

同步代码块

synchronized(obj){
    //同步代码块
}

obj叫做同步监视器(锁对象),任何线程进入下面同步代码块之前必须先获得对obj 的锁;其他线程无法获得锁,也就执行同步代码块。这种做法符合:“加锁­——修改——­释放锁”的逻辑。锁对象可以是任意对象,但必须保证是同一对象 任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后该线程会释放对该同步监视器的锁定。

public class ThreadDemo implements Runnable {

    private int ticket = 5;//假设一共有5张票
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            synchronized (this){// 要对当前对象进行同步
                if (ticket > 0){ // 还有票
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("卖票:ticket = " + ticket-- );
                }
            }
        }
    }

    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        Thread t1 = new Thread(td);
        Thread t2 = new Thread(td);
        Thread t3 = new Thread(td);

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

    }
}

运行结果:

同步方法

除了可以将需要的代码设置成同步代码块外,也可以使用 synchronized 关键字将一个方法声明为同步方法。对于synchronized修 饰的实例方法(非static方法),无须显式指定同步监视器,同步方法的同步监视器就是this,也就是调用该方法的对象。

synchronized 方法返回值 方法名称(参数列表){
    
}
public class ThreadDemo implements Runnable {

    private int ticket = 5;//假设一共有5张票
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            this.sale();
        }
    }

    // 声明同步方法
    public synchronized void sale(){
        if (ticket > 0){ // 还有票
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("卖票:ticket = " + ticket-- );
        }
    }

    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        Thread t1 = new Thread(td);
        Thread t2 = new Thread(td);
        Thread t3 = new Thread(td);

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

    }
}

同步锁(Lock)

从Java5开始,Java提供了一种功能更强大的线程同步机制——通过显式定义同步锁 对象来实现同步,在这种机制下,同步锁由Lock对象充当。

Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性。

Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得 Lock对象。

ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock(可重入锁)。使用该Lock 对象可以显式地加锁、释放锁。

import java.util.concurrent.locks.ReentrantLock;

class ClassName{
    //定义锁对象
    private final ReentrantLock lock=new ReentrantLock();
    
    //需要保证线程安全的方法
    public void methodName(){
        //加锁
        lock.lock();
        try{
        	//代码块
        }
        //使用finally块来保证一定可以释放锁
        finally{
            //释放锁
            lock.unlock();
        }
    }
}
import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo {
    public static void main(String[] args) {
        TestThread t = new TestThread();

        new Thread(t).start();
        new Thread(t).start();
        new Thread(t).start();
    }
}
class TestThread implements Runnable{
    int ticketNums = 10;
    //定义 lock
    private final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try{
                lock.lock();//加锁
                if (ticketNums > 0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ticketNums--);
                }else{
                    break;
                }
            }finally {
                lock.unlock();//释放锁
            }
        }
    }
}

synchronized 与 lock 的对比

  • Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized 是隐式锁,出了作用域自动释放

  • Lock 只有代码块锁,synchronized 有代码块锁和方法锁

  • 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

  • 优先使用顺序:Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)

死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也 没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。 在系统中出现多个同步监视器的情况下很容易发生死锁。

比如:现在张三想要李四的画,李四想要张三的书,张三对李四说“把你的画给我,我就给你书”,李四也对张三说“把你的书给我,我就给你画”两个人互相等对方先行动,就这么干等没有结果,这实际上就是死锁的概念。

public class ThreadDemo implements Runnable {
    private static ZhangSan zs = new ZhangSan();
    private static LiSi ls = new LiSi();

    private boolean flag = false;// 声明标志位,判断哪个先说话

    @Override
    public void run() {
        if (flag){
            synchronized (zs){
                zs.say();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (ls){
                    zs.get();
                }
            }
        }else{
            synchronized (ls){
                ls.say();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (zs){
                    ls.get();
                }
            }
        }
    }

    public static void main(String[] args) {
        ThreadDemo t1 = new ThreadDemo();// 控制张三
        ThreadDemo t2 = new ThreadDemo();// 控制李四
        t1.flag = true;
        t2.flag = false;
        new Thread(t1).start();
        new Thread(t2).start();
    }


}
class ZhangSan{
    public void say(){
        System.out.println("张三对李四说:“你给我画,我就把书给你。”");
    }
    public void get(){
        System.out.println("张三得到画了。");
    }
}
class LiSi{
    public void say(){
        System.out.println("李四对张三说:“你给我书,我就把画给你”");
    }
    public void get(){
        System.out.println("李四得到书了。");
    }
}

运行结果:程序不会结束,两个子线程一直在相互等待对方释放锁

产生死锁的四个必要条件:

  1. 互斥条件:一个资源每次只能被一个进程使用

  2. 请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放

  3. 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺

  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

只要想办法破其中的任意一个或多个条件就可以避免死锁发生

线程协作

应用场景:生产者和消费者问题

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费

  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止

  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止

 

这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件

  • 对于生产者,没有生产产品之前,要通知消费者等待。而生产了产品之后,又需要马上通知消费者消费

  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费

  • 在生产者消费者问题中,仅有 synchronized 是不够的

    synchronized 可阻止并发更新同一个共享资源,实现了同步

    synchronized 不能用来实现不同线程之间的消息传递(通信)

 

Java 提供了几个方法解决线程之间的通信问题

方法名作用
wait()表示线程一直等待,直到其他线程通知,与 sleep 不同,会释放锁
wait(long timeout)指定等待的毫秒数
notify()唤醒一个处于等待状态的线程
notifyAll()唤醒同一个对象上所有调用 wait() 方法的线程,优先级别高的线程优先调度

注:均是 Object 类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常 IllegalMonitorStateException  

  1. wait()、notify/notifyAll()方法是Object的final方法,无法被重写。

  2. wait()使当前线程阻塞,前提是必须先获得锁,一般配合synchronized关键字使 用,即,一般在synchronized同步代码块里使用wait()、notify/notifyAll()方法

  3. 由于wait()、notify/notifyAll()在synchronized代码块执行,说明当前线程一定是获 取了锁的。当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状 态。只有当notify/notifyAll()被执行时候,才会唤醒一个或多个正处于等待状态的线 程,然后继续往下执行,直到执行完synchronized代码块的代码或是中途遇到wait(), 再次释放锁。也就是说,notify/notifyAll()的执行只是唤醒沉睡的线程,而不会立即释 放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了 notify/notifyAll()后立即退出临界区,以唤醒其他线程。

  4. wait()需要被try...catch包围。

  5. notify和wait的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法, 那么B线程是无法被唤醒的

  6. notify和notifyAll的区别

    • notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作 系统对多线程管理的实现。

    • notifyAll会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操 作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll方法。

 

解决方式:

一、并发协作模型“生产者/消费者模式” ---> 管程法

  • 生产者:负责生产数据的模块(可能是方法,对象,线程,进程)

  • 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)

  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

public class ThreadDemo {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();

        new Producer(container).start();
        new Consumer(container).start();

    }
}
//生产者
class Producer extends Thread {
    SynContainer container;

    public Producer(SynContainer container){
        this.container = container;
    }

    //生产
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.push(new Product(i));
            System.out.println("生产了" + i + "个产品");
        }
    }
}

//消费者
class Consumer extends Thread{
     SynContainer container;
     public Consumer(SynContainer container){
         this.container = container;
     }

     //消费
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了 --->" + container.pop().id + "个产品");
        }
    }
}
//产品
class Product{
    int id;
    public Product(int id){
        this.id = id;
    }
}
//缓冲区
class SynContainer{
    //容器
    Product[] products = new Product[10];
    //计数器
    int count = 0;

    //生产者放入产品
    public synchronized void push(Product product){
        //如果容器满了,就需要等待消费者消费
        if (count == products.length){
            //通知消费者消费,生产等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果没有满,就丢入产品
        products[count] = product;
        count++;

        //可以通知消费者消费了
        this.notifyAll();
    }

    //消费者消费产品
    public synchronized Product pop(){
        //判断是否能消费
        if (count == 0){
            //等待生产者生产,消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果可以消费
        count--;
        Product product = products[count];

        //消费完了,通知生产者生产
        this.notifyAll();
        return product;
    }
}

二、并发协作模型“生产者/消费者模式” ---> 信号灯法

public class ThreadDemo {
    public static void main(String[] args) {
        TV tv =new TV();
        Player p = new Player(tv);
        Audience a = new Audience(tv);

        new Thread(p).start();
        new Thread(a).start();
    }
}
class TV{
    String voice;//内容

    //true表示观众可以看节目
    //false表示演员可以表演
    boolean flag = false;//信号灯

    //演员表演
    public synchronized void play(String voice){
        //true表示演员等待,观众观看
        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //可以表演,此时flag=false
        System.out.println("表演了" + voice);
        this.voice = voice;
        this.flag = !this.flag; //表演完毕,改变flag=true,通知观众观看,同时让演员停止表演
        this.notifyAll(); //通知观众观看
    }

    //观众观看
    public synchronized void watch(){
        //false,观众不能观看
        if (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //可以观看,此时flag=true;
        System.out.println("观看--->" + voice);
        this.flag = !this.flag; //观看完毕,改变flag=false
        this.notifyAll();
    }
}
//表演者
class Player implements Runnable{

    private TV tv;

    public Player(TV tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0){
                tv.play("动物世界" + i);
            }else{
                tv.play("探索宇宙" + i);
            }
        }
    }
}
//观众
class Audience implements Runnable{
    private TV tv;

    public Audience(TV tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用

好处:

  • 提高响应速度(减少了创建新线程的时间)

  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)

  • 便于线程管理

    • corePoolSize(int):核心池的大小

    • maximumPoolSize(int):最大线程数

    • keepAliveTime(long):线程没有任务时最多保持多长时间后会终止

    • unit(TimeUnit 枚举类):上面参数时间的单位,可以是分钟,秒,毫秒等等。

    • workQueue(BlockingQueue):任务队列,当线程任务提交到线程池以后,首先放入队列中,然后线程池按照该任务队列依次执行相应的任务。可以使用的 workQueue 有很多,比如:LinkedBlockingQueue 等等。

    • threadFactory(ThreadFactory 类):新线程产生工厂类。

    • handler(RejectedExecutionHandler 类):当提交线程拒绝执行、异常的时候,处理异常的类。

      该类取值如下:(注意都是内部类)

            ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。

            ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。

            ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务,重复此过程。

            ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

Java 5 提供了线程相关的API:ExecutorService 和 Executors

ExecutorService:真正的线程池接口。常见子类 ThreadPoolExecutor

  • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行 Runnable

  • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般用来执行 Callable

  • void shutdown():关闭连接池

Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

  • newFixedThreadPool:创建固定大小数量线程池,数量通过传入的参数决定。如果请求的线程数大于定义的固定大小,则会把多余的存储到阻塞队列中,线程交替从阻塞队列取来执行

  • newSingleThreadExecutor:创建一个线程容量的线程池,所有的线程依次执行,相当于创建固定数量为 1 的线程池。

  • newCachedThreadPool:创建可缓存的线程池,没有最大线程限制(实际上是 Integer.MAX_VALUE)。如果用空闲线程等待时间超过一分钟,就关闭该线程

  • newScheduledThreadPool:创建计划(延迟)任务线程池,线程池中的线程可以让其在特定的延迟时间之后执行,也可以以固定的时间重复执行(周期性执行)。相当于以前的 Timer 类的使用

  • newSingleThreadScheduledExecutor:创建单线程池延迟任务,创建一个线程容量的计划任务,每次只执行一个线程,多余的先存储到工作队列,一个一个执行,保证了线程的顺序执行。

public class ThreadDemo {
    public static void main(String[] args) {
        //创建线程池
        //newFixedThreadPool 参数:线程池大小
        ExecutorService service = Executors.newFixedThreadPool(10);

        //执行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        //关闭
        service.shutdown();
    }
}
class MyThread implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

自定义线程池

定义单例线程池

public class MyPool {

    private static MyPool myPool = null;
    //单例线程池中有两种具体的线程池
    private ThreadPoolExecutor threadPool = null;
    private ScheduledThreadPoolExecutor scheduledPool = null;

    public ThreadPoolExecutor getThreadPool() {
        return threadPool;
    }

    public ScheduledThreadPoolExecutor getScheduledPool() {
        return scheduledPool;
    }

    //设置线程池的各个参数的大小
    private int corePoolSize = 10;// 池中所保存的线程数,包括空闲线程。
    private int maximumPoolSize = 20;// 池中允许的最大线程数。
    private long keepAliveTime = 3;// 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
    private int scheduledPoolSize = 10;

    private static synchronized void create() {
        if (myPool == null)
            myPool = new MyPool();
    }

    public static MyPool getInstance() {
        if (myPool == null)
            create();
        return myPool;
    }

    private MyPool() {
        //实例化线程池,这里使用的 LinkedBlockingQueue 作为 workQueue ,使用 DiscardOldestPolicy 作为 handler
        this.threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
                keepAliveTime, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(),
                new ThreadPoolExecutor.CallerRunsPolicy());//不在新线程中执行任务,而是由调用者所在的线程来执行
        //实例化计划任务线程池
        this.scheduledPool = new ScheduledThreadPoolExecutor(scheduledPoolSize);
    }
}

获取线程池并添加任务

public void testThreadPool() {

        ThreadPoolExecutor pool1 = (ThreadPoolExecutor) Executors.newCachedThreadPool();

        pool1.execute(() -> System.out.println("快捷线程池中的线程!"));


        ThreadPoolExecutor pool2 = MyPool.getInstance().getThreadPool();
        pool2.execute(() -> {
            System.out.println("pool2 普通线程池中的线程!");
            try {
                Thread.sleep(30*1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println("pool2 poolSize:"+pool2.getPoolSize());
        System.out.println("pool2 corePoolSize:"+pool2.getCorePoolSize());
        System.out.println("pool2 largestPoolSize:"+pool2.getLargestPoolSize());
        System.out.println("pool2 maximumPoolSize:"+pool2.getMaximumPoolSize());

        ScheduledThreadPoolExecutor pool3 = MyPool.getInstance().getScheduledPool();
        pool3.scheduleAtFixedRate(() -> System.out.println("计划任务线程池中的线程!"), 0, 5000, TimeUnit.MILLISECONDS);
    }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值