JavaSE 第十三章 多线程

本文详细介绍了Java多线程的基本概念、线程的创建与使用、生命周期、同步机制等内容,包括线程池的使用和线程间的通信。

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

13.1 基本概念:程序、进程、线程

13.1.1 程序(program)

  • 程序是为了完成特定任务而用某种语言编写的一组指令的集合。即指一段静态代码,静态对象。

13.1.2 进程(process)

  • 进程是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程。进程有它自身的产生、存在和消亡的过程,而这个过程叫做生命周期。
  • 运行中的QQ,运行中的视频播放器等都是一个进程

  • 程序是静态的,而进程是动态的

  • 进程作为资源分派单位,系统在运行时会为每一个进程分配不同的内存区域。

13.1.3 线程(thread)

  • 进程可进一步划分为线程,线程是一个程序内部的一条执行路径

  • 若一个进程同一时间并行执行多个线程,就是支持多线程的

  • 线程作为调度和执行的单位,每个线程都拥有独立的运行栈和程序计数器,线程切换的开销小

  • 一个进程中的多个线程共享相同的内存单元/内存地址空间,它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程共享的系统资源可能会带来安全隐患。

13.1.4 单核CPU和多核CPU的理解

  • 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。但因为CPU时间单元特别短,我们感觉不出来。

  • 多核CPU才能更好的发挥出多线程的效率。

  • 一个Java应用程序java.exe,它至少有三个线程:main()主线程gc()垃圾回收线程异常处理线程。如果发生异常,会影响主线程。

13.1.5 并行和并发的概念

  • 并发:多个事情再同一时间段内发生。一个CPU同时执行多个任务。

  • 并行:多个事情再同一时间点发生。多个CPU同时执行多个任务。

13.1.6 使用多线程的优点

  • 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
  • 提高计算机系统CPU的利用率
  • 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

13.1.7何时需要多线程

  • 程序需要执行两个或多个任务
  • 程序需要实现一些需要等待的任务时
  • 需要一些后台运行的程序时

13.2 线程的创建和使用

  • Java语言的JVM允许程序运行多个线程,它通过java.lang.Thread类来体现

  • Thread类的特性

    • 每个线程都是通过某个特定Thread对象run()方法来完成操作的,经常把run()方法的主题称为线程体

    • 通过该Thread对象的start()方法来启动这个线程,而非直接调用run()

13.2.1 方式一:继承Thread类

  • 构造器:
    • Thread():创建新的Thread对象

    • Thread(String threadname):创建线程并指定线程实例名

    • Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接 口中的run方法

    • Thread(Runnable target, String name):创建新的Thread对象

  • 继承Thread类实现多线程的步骤
  • 第一步:定义子类继承Thread类

  • 第二步:在子类中重写Thread类的run()方法

  • 第三步:创建Thread类的子类对象,即创建了一个线程对象

  • 第四步:调用线程对象的start()方法,启动线程,调用run()方法

public class Demo1 {
    public static void main(String[] args) {
        // 创建MyThread类对象,调用start()方法,启动子线程
        Thread thread = new MyThread() ;
        thread.start();

        for (int i = 1; i <= 100; i++) {
            System.out.println("主线程 i = " + i);
        }
    }
}

// 创建子类MyThread类继承Thread类
class MyThread extends Thread {
    // 重写run()方法,打印1-100的数
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println("子线程 i = " + i);
        }
    }
}
  • 匿名内部类的方式创建线程对象
public class Demo2 {
    public static void main(String[] args) {

        // 使用匿名内部类的方式创建一个线程
        new Thread(){
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    System.out.println("子线程 i = " + i);
                }
            }
        }.start();

        for (int i = 1; i <= 100; i++) {
            System.out.println("主线程 i = " + i);
        }

    }
}

  • 注意:
  • 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。

  • run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU 调度决定。

  • 想要启动多线程,必须调用start()方法

  • 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上 的异常IllegalThreadStateException

在这里插入图片描述

13.2.2 方式二:实现Runnable接口

  • 第一步:定义子类,实现Runnable接口

  • 第二步:子类重写Runnable接口的run()方法

  • 第三步:通过Thread类的有参构造方法创建线程对象

  • 第四步:将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法

  • 第五步:调用Thread类的start()方法:开启线程,调用Runnable接口子类的run()方法

public class Demo2 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Thread1()) ;
        thread.start();

        for (int i = 0; i < 100; i++) {
            if (i % 2 != 0) {
                System.out.println("主线程 i = " + i);
            }
        }
    }
}

class Thread1 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println("子线程 i = " + i);
            }
        }
    }
}
  • 匿名内部类方式实现
public class Demo3 {
    public static void main(String[] args) {

        // 使用Runnable接口的方式创建匿名内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i % 2 == 0) {
                        System.out.println("子线程 i = " + i);
                    }
                }
            }
        }).start();

        for (int i = 0; i < 100; i++) {
            if (i % 2 != 0) {
                System.out.println("主线程 i = " + i);
            }
        }
    }
}

13.2.4 实现Callable接口

  • 与使用Runnable相比, Callable功能更强大些

    • 相比run()方法,可以有返回值

    • 方法可以抛出异常

    • 支持泛型的返回值

    • 需要借助FutureTask类,比如获取返回结果

  • Future接口

    • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。

    • FutrueTask是Futrue接口的唯一的实现类

    • FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

public class Demo2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建Callable接口实现类对象
        ThreadDemo3 td = new ThreadDemo3();
        // 创建FutureTask对象,其实参为Callable实现类对象
        FutureTask<String> task = new FutureTask<>(td);

        // 将FutureTask对象作为实参传给线程对象
        Thread t1 = new Thread(task);

        t1.start();

        // get()方法获取返回值
        System.out.println(task.get());
    }
}

class ThreadDemo3 implements Callable<String> {

    @Override
    public String call() throws Exception {
        for (int i = 0 ; i < 100 ; i ++) {
            System.out.println("实现Callable接口的方式创建线程:" + i);
        }
        return "返回值";
    }
}

V get() :获取返回值的方法,需要等到call()方法中的代码执行完后才能调用此方法

13.2.4 继承和实现

  • 区别:

    • 继承Thread类时,线程代码是写在Thread类的子类run()方法中的

    • 实现Runnable时,线程代码写在接口实现类的run()方法中

  • 实现Runnable接口的好处

    • 避免单继承的局限

    • 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源

  • Thread类也是实现了Runnable接口的

13.2.5 Thread类中的方法

  • void start(): 启动线程,并执行对象的run()方法

  • void run(): 线程在被调度时执行的操作

  • String getName(): 返回线程的名称

  • void setName(String name):设置该线程名称

  • static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类

  • static void yield():线程让步 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
    若队列中没有同优先级的线程,忽略此方法

  • join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将 被阻塞,直到 join() 方法加入的 join 线程执行完为止 低优先级的线程也可以获得执行

  • static void sleep(long millis):(指定时间:毫秒) 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队抛出InterruptedException异常

  • stop(): 强制线程生命期结束,不推荐使用

  • boolean isAlive():返回boolean,判断线程是否还活着

  • 代码示例:
public class Demo3 {
    public static void main(String[] args) {
        // 调用currentThread()方法获取当前线程并输出
        System.out.println(Thread.currentThread());
        // getName() 获取当前线程的线程名
        System.out.println(Thread.currentThread().getName());
        // setName() 设置线程的线程名
        Thread.currentThread().setName("主线程");
        System.out.println(Thread.currentThread().getName());
    }
}

/*
	Thread[main,5,main]
	main
	主线程
*/
public class Demo4 {
    public static void main(String[] args) {
        /*
        static void yield():线程让步
            暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
            若队列中没有同优先级的线程,忽略此方法
         */

        Thread thread1 = new Thread(new Thread1()) ;
        thread1.setName("线程1---");
        thread1.start();

        for (int i = 0; i < 100; i++) {
            if (i == 50) {
                Thread.yield();
                System.out.println("yield()方法执行");
            }

            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + " i = " + i);
            }
        }
    }
}

在这里插入图片描述

  • yield()方法将CPU的控制权让出去后还能继续抢用
public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        /*
        join() :当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的线程执行完为止
        低优先级的线程也可以获得执行
         */

        Thread thread1 = new Thread(new Thread1()) ;
        thread1.setName("线程1---");
        thread1.start();

        for (int i = 0; i < 100; i++) {
            if (i == 20) {
                System.out.println("join()方法执行");
                thread1.join();
            }

            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + " i = " + i);
            }
        }
    }
}

在这里插入图片描述

public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        /*
        static void sleep(long millis):(指定时间:毫秒)
            令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
            抛出InterruptedException异常
         */
        Thread thread1 = new Thread(new Thread1()) ;
        thread1.setName("线程1---");
        thread1.start();

        for (int i = 0; i < 100; i++) {
            if (i == 20) {
                System.out.println("sleep()方法执行");
                Thread.sleep(10);
            }

            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + " i = " + i);
            }
        }
    }
}
  • 过去10毫秒后主线程将继续抢占CPU

在这里插入图片描述

public class Demo7 {
    public static void main(String[] args) {
      	// stop(): 强制线程生命期结束,不推荐使用
        Thread thread1 = new Thread(new Thread1()) ;
        thread1.setName("线程1---");
        thread1.start();

        for (int i = 0; i < 100; i++) {
            if (i == 20) {
                System.out.println("stop()方法执行");
                Thread.currentThread().stop();
            }

            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + " i = " + i);
            }
        }
    }
}

在这里插入图片描述
主线程直接强制结束,主线程中代码都不再执行

13.2.6 线程的调度

  • 调度策略
  • 时间片轮转

在这里插入图片描述

  • 抢占式:各线程抢占CPU,优先级越高抢占到CPU的概率越大
  • Java的调度方法

    • 同优先级线程组成先进先出队列(先到先服务),使用时间片策略

    • 对高优先级,使用优先调度的抢占式策略

13.2.7 线程优先级

  • 线程的优先级等级

    • MAX_PRIORITY:10

    • MIN _PRIORITY:1

    • NORM_PRIORITY:5

  • 涉及的方法

    • getPriority() :返回线程优先值
    • setPriority(int newPriority) :改变线程的优先级
  • 说明

    • 线程创建时继承父线程的优先级

    • 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

public class Demo9 {
    public static void main(String[] args) {
        // 主线程的优先级
        int priority = Thread.currentThread().getPriority();
        System.out.println("主线程优先级 = " + priority);     // 主线程优先级 = 5
        Thread.currentThread().setName("主线程");
        // 创建线程对象
        Thread thread = new Thread(new Thread1()) ;
        thread.setName("子线程");
        System.out.println("thread.getPriority() = " + thread.getPriority());   // thread.getPriority() = 5
        // 设置子线程优先级
        thread.setPriority(Thread.MAX_PRIORITY);
        System.out.println("thread.getPriority() = " + thread.getPriority());   // thread.getPriority() = 10

        thread.start();

        for (int i = 1 ; i <= 100 ; i ++) {
            System.out.println(Thread.currentThread().getName() + "i = " + i);
        }
    }
}

13.2.8 线程分类

  • Java线程分为用户线程守护线程

区别:

  • 守护线程是为用户线程服务的,为其他线程的运行提供便利服务。

  • JVM在用户线程没有结束前,会同守护线程一起运行

  • 用户线程全部结束后,JVM就会退出 ,相应的,守护线程也会被结束

  • 守护线程创建的线程也是守护线程,用户线程可以创建用户线程也可以创建守护线程。

  • 守护线程一般伴随着JVM的结束而结束,也可以手动结束。

  • 通过在start()方法前调用 thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
  • Java垃圾回收就是一个典型的守护线程。

示例:

public class Demo10 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Thread2()) ;
        Thread thread2 = new Thread(new Thread3()) ;

        // 设置线程thread2为守护线程
        thread2.setDaemon(true);

        thread1.start();
        thread2.start();

    }
}

class Thread2 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("用户线程:" + i);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Thread3 implements Runnable {

    @Override
    public void run() {
        while (true) {
            System.out.println("守护线程---");
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在这里插入图片描述
用户线程结束后,JVM会退出,守护线程也随之结束

13.3 线程的生命周期

  • JDK中用Thread.State类定义了线程的几种状态 要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
  • 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建 状态

  • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已 具备了运行的条件,只是没分配到CPU资源

  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能

  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态

  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

在这里插入图片描述

13.4 线程的同步

  • 问题提出

    • 多个线程执行的不确定性,引起执行结果的不稳定
  • 示例1:银行取钱问题
    创建两个线程去取钱,银行中一共3000元,每个线程取2000,结果就是余额为-1000
public class Demo2 {
    public static void main(String[] args) {
        // 模拟取钱问题
        Bank bank = new Bank();

        Thread thread1 = new Thread(bank) ;
        Thread thread2 = new Thread(bank) ;

        thread1.setName("线程1 :取钱");
        thread2.setName("线程2 :取钱");

        thread1.start();
        thread2.start();

        try {
            Thread.sleep(100);
            System.out.println("剩余金额:money = " + bank.money);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 银行类
 */
class Bank implements Runnable {

    int money = 3000 ;

    @Override
    public void run() {
        try {
            if (money > 2000) {
                Thread.sleep(10);
                System.out.println(Thread.currentThread().getName());
                money = money - 2000 ;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

线程1 :取钱
线程2 :取钱
剩余金额:money = -1000

  • 示例2:模拟火车站售票程序,开启三个窗口售票
public class Demo3 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        Thread thread1 = new Thread(ticket) ;
        Thread thread2 = new Thread(ticket) ;
        Thread thread3 = new Thread(ticket) ;

        thread1.setName("窗口一售票");
        thread2.setName("窗口二售票");
        thread3.setName("窗口三售票");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class Ticket implements Runnable {

    // 初始化车票为100张
    private int ticket = 100 ;

    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                System.out.println( Thread.currentThread().getName() + ticket);
                ticket -- ;
            } else {
                break;
            }
        }
    }
}

在这里插入图片描述

在这里插入图片描述

  • 多线程出现了安全问题

  • 问题出现的原因:当多个线程在操作一个共享数据时,一个线程对多条语句只执行了一部分,还没执行完成,另一个线程前瞻到CPU资源。导致共享数据出错

  • 解决办法:多条线程操作共享数据时,在一个线程执行过程中,其他线程不能参与相关操作。

13.4.1 Synchronized的使用

  • Java对于多线程的安全问题提供了专业的解决方式:同步机制

  • synchronized使用在代码块上——同步代码块

synchronized(对象) {
	// 需要被同步的代码;
}
  • synchronized使用在方法上——同步方法
public synchronized void method() {
	...
}

如图:
在这里插入图片描述

13.4.2 同步机制中的锁

  • 同步机制:

在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防 止两个任务访问相同的资源(其实就是共享资源竞争)。防止这种冲突的方法 就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁 之时,另一个任务就可以锁定并使用它了。

示例:把卖票操作加上synchronized关键字修饰

  • synchronized的锁是什么?

    • 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。

    • 同步方法的锁:静态方法(类名.class)、非静态方法(this

    • 同步代码块:自己指定,很多时候也是指定为this类名.class

  • 注意:

    • 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就 无法保证共享资源的安全

    • 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方 法共用同一把锁(this),同步代码块(指定需谨慎)

13.4.3 同步的范围

  • 如何找问题,即代码是否存在线程安全?

    • 明确哪些代码是多线程运行的代码

    • 明确多个线程是否有共享数据

    • 明确多线程运行代码中是否有多条语句操作共享数据

  • 如何解决呢?

    • 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中
  • 范围太小:没锁住所有有安全问题的代码

  • 范围太大:不能很好的发挥多线程的功能。

13.4.4 会释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、 该方法的继续执行。
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导 致异常结束。
  • 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

13.4.5 不会释放锁的操作

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()Thread.yield()方法暂停当前线程的执行

  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会自动释放锁(同步监视器)。

    • 应尽量避免使用suspend()resume()来控制线程
  • 对于买票案例的同步代码块操作:继承Thread类的方式
public class Demo5 {
    public static void main(String[] args) {

        Ticket2 t1 = new Ticket2() ;
        Ticket2 t2 = new Ticket2() ;
        Ticket2 t3 = new Ticket2() ;

        t1.setName("窗口一售票");
        t2.setName("窗口二售票");
        t3.setName("窗口三售票");

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

}
class Ticket2 extends Thread {

    // 初始化车票为100张
    private static int ticket = 100 ;

    static Object obj = new Object() ;

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
            // synchronized (this) {  // 错误
            // synchronized (Ticket2.class) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}
  • 对于买票案例的同步代码块操作:实现Runnable接口的方式
public class Demo4 {
    public static void main(String[] args) {
        Ticket1 ticket1 = new Ticket1();

        Thread thread1 = new Thread(ticket1) ;
        Thread thread2 = new Thread(ticket1) ;
        Thread thread3 = new Thread(ticket1) ;

        thread1.setName("窗口一售票");
        thread2.setName("窗口二售票");
        thread3.setName("窗口三售票");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class Ticket1 implements Runnable {

    // 初始化车票为100张
    private int ticket = 100 ;
    Object obj = new Object() ;

    @Override
    public void run() {
        while (true) {
          	// synchronized(this) { // 正确的
          	// synchronized(Ticket1.class) { 	// 正确的
            synchronized(obj) {     // synchronized修饰的同步代码块
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (ticket > 0) {   // 判断ticket的大小
                    System.out.println( Thread.currentThread().getName() + ticket);
                    ticket -- ;
                } else {
                    break;
                }
            }
        }
    }
}
  • 对于上述同步代码块中的锁,我们是创建了一个Object类的对象,在main方法中创建了一个Ticket1类的对象,将其作为实参传入到Thread类的构造方法,三个Thread对象使用的是一个Ticket1对象,它们的锁也都为Object类的对象obj
  • 同步方法:实现Runnable接口
public class Demo6 {
    public static void main(String[] args) {
        Ticket3 ticket3 = new Ticket3();

        Thread thread1 = new Thread(ticket3) ;
        Thread thread2 = new Thread(ticket3) ;
        Thread thread3 = new Thread(ticket3) ;

        thread1.setName("窗口一售票");
        thread2.setName("窗口二售票");
        thread3.setName("窗口三售票");

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

class Ticket3 implements Runnable {

    // 初始化车票为100张
    private int ticket = 100 ;

    @Override
    public void run() {
        while (ticket > 0) {
            show() ;
        }
    }

    private synchronized void show() {  // 此时的同步监视器为this,即 ticket3
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (ticket > 0) {   // 判断ticket的大小
            System.out.println( Thread.currentThread().getName() + ticket);
            ticket -- ;
        }
    }
}
  • 同步方法:继承Thread类
public class Demo7 {
    public static void main(String[] args) {

        Ticket4 t1 = new Ticket4() ;
        Ticket4 t2 = new Ticket4() ;
        Ticket4 t3 = new Ticket4() ;

        t1.setName("窗口一售票");
        t2.setName("窗口二售票");
        t3.setName("窗口三售票");

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

}

class Ticket4 extends Thread {

    // 初始化车票为100张
    private static int ticket = 100 ;


    @Override
    public void run() {
        while (ticket > 0) {
            show() ;
        }
    }

    // 需要把方法定义为静态的
    private static synchronized void show() {   // 此时的同步监视器为 Ticket4.class

        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + ticket);
            ticket--;
        }
    }
}
  • 对于静态方法和非静态方法:

    • 非静态方法是属于对象的,在实现Runnable接口的方法中,只需要定义一个Ticket3类型的对象,在调用同步方法show()时,同步监视器为this是没问题的,三个线程的同步监视器是同一个对象。

    • 静态方法是属于类的,对于继承Thread类的实现方法,需要定义三个Ticket4类型的对象,使用非静态方法时,this指向不同的对象,即不能保证同步监视器唯一,所以需要使用到静态方法,此时,同步监视器就为Ticket4.class

13.4.6 单例设计模式之懒汉模式(线程安全)

  • 线程不安全的单例模式:
class SingletonTest {
    public static void main(String[] args) {
        Thread1 t = new Thread1() ;

        Thread t1 = new Thread(t) ;
        Thread t2 = new Thread(t) ;
        Thread t3 = new Thread(t) ;

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

class Singleton {
    private static Singleton instance = null ;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Singleton() ;
        }
        return instance ;
    }
}

class Thread1 implements Runnable {

    @Override
    public void run() {
        Singleton singleton = Singleton.getInstance();
        System.out.println("singleton = " + singleton);
    }
}

singleton = cn.pdsu.edu._thread.Singleton@570d413f
singleton = cn.pdsu.edu._thread.Singleton@25e63a54
singleton = cn.pdsu.edu._thread.Singleton@570d413f

  • 打印结果可以看出,这个单例设计模式并没有完全单例,它存在有不同的对象,这是因为线程不安全的原因。在第一次造对象时,先抢到CPU资源的线程判断到instance为null,进入if语句,在创建对象前,CPU资源被其另外一个线程抢占,另外一个线程判断instance也为null,进入if语句,这样,两条线程获取的对象就不是同一个对象了。

使用线程同步机制解决线程不安全的问题:

class Singleton {
    private static Singleton instance = null ;

    private Singleton() {}

    public static Singleton getInstance() {
        synchronized (Singleton.class) {
            if (instance == null) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                instance = new Singleton();
            }
        }
        return instance;
    }
}
  • 使用synchronized代码块将if语句块包起来,这样就能解决线程安全的问题了,每次都只能有一个线程去操作instance对象。但是,这样的效率会因为判断为空的操作在synchronized代码块中而大大降低,我们应该在synchronized代码块外再嵌套一层空判断,这样后续的进程就不需要再等待判断instance是否为空了,可以直接判断,然后拿着对象走了。
class Singleton {
    private static Singleton instance = null ;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

13.4.7 线程的死锁

  • 死锁

    • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃 自己需要的同步资源,就形成了线程的死锁

    • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

  • 解决方法

    • 专门的算法、原则

    • 尽量减少同步资源的定义

    • 尽量避免嵌套同步

  • 死锁代码演示:
public class Demo4 {
    public static void main(String[] args) {

        StringBuffer sb1 = new StringBuffer() ;
        StringBuffer sb2 = new StringBuffer() ;

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (sb1) {
                    sb1.append(1) ;
                    sb2.append('a') ;
                }

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (sb2) {
                    sb1.append(2) ;
                    sb2.append('b') ;
                }
                System.out.println("sb1 = " + sb1);
                System.out.println("sb2 = " + sb2);
            }

        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (sb2) {
                    sb1.append(3) ;
                    sb2.append('c') ;
                }

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (sb1) {
                    sb1.append(4) ;
                    sb2.append('d') ;
                }
                System.out.println("sb1 = " + sb1);
                System.out.println("sb2 = " + sb2);
            }
        }).start();
    }
}
  • 执行这段代码,程序台没有输出内容,且程序也没有结束,这是因为,上面的线程拿着同步锁sb1,等待同步锁sb2;下面的线程拿着同步锁sb2,等待同步锁sb1,进入了死锁的状态。

13.4.8 Lock(锁)

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

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

  • ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以 显式加锁释放锁

  • 示例代码:
import java.util.concurrent.locks.ReentrantLock;

public class Demo5 {

    private static int ticket = 100;
    private static ReentrantLock lock = new ReentrantLock() ;

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                sell();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                sell();
            }
        }).start();
    }

    public static void sell() {
        while (ticket > 0) {
            lock.lock();
            try {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + " : " + ticket);
                    ticket -- ;
                }
            }finally {
                lock.unlock();
            }
        }
    }

}
  • synchronizedLock的异同:
    • 二者都是用来解决线程安全问题的

    • synchronized在执行完相应的同步代码块后会自动释放同步监视器,而Lock需要手动调用unlock()方法

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

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

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

优先使用Lock锁----->同步代码块------>同步方法

13.5 线程通信

  • 问题引出:使用两个线程打印1-100,线程1和线程2交替打印
public class Demo6 {
    public static void main(String[] args) {
        ThreadTest t = new ThreadTest() ;

        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t1.setName("线程1");
        t2.setName("线程2");

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

class ThreadTest implements Runnable {

    int i = 1 ;
    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                notify();
                if (i <= 100) {
                    System.out.println(Thread.currentThread().getName() + " : " + i ++);
                } else {
                    break;
                }
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

线程1 : 1
线程2 : 2
线程1 : 3
线程2 : 4
线程1 : 5
线程2 : 6
线程1 : 7

  • 对于上面的代码,以及运行结果我们可以看出,是线程1先抢占到了CPU资源,当它执行run()方法,进入synchronized代码块后,首先执行了notify()方法,但此时并没有线程处于等待状态,所以也没有线程被唤醒,当线程1执行输出后会执行一个wait()方法,此时线程1放弃了CPU和同步资源并进入等待状态。线程2开始执行synchronized代码块中的代码,首先执行了notify()方法,把正在等待的线程1唤醒,然后执行下面的代码,打印输出过后,线程2执行了wait()方法进入等待状态;往复循环,两个线程能实现交替打印

13.5.1 wait()方法

  • 在当前线程中调用方法: 对象名.wait()
  • 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify (或notifyAll) 为止
  • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
  • 调用此方法后,当前线程将释放对象监控权 ,然后进入等待
  • 在当前线程被notify()后,要重新获得监控权,然后从断点处继续代码的执行。

13.5.2 notify()/notifyAll()

  • 在当前线程中调用方法: 对象名.notify()
  • 功能:唤醒等待该对象监控权的一个/所有线程
  • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

13.5.3 生产者消费者问题

  • 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处 取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图 生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通 知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如 果店中有产品了再通知消费者来取走产品。

  • 这里可能出现两个问题:

    • 生产者比消费者快时,消费者会漏掉一些数据没有取到。
    • 消费者比生产者快时,消费者会取相同的数据。

代码实现:

/**
 * 生产者(Producer)将产品交给店员(Clerk),而消费者(Customer)从店员处
 * 取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图
 * 生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通
 * 知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如
 * 果店中有产品了再通知消费者来取走产品。
 */

public class Clerk {

    private int produce = 0 ;

    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        Thread t1 = new Thread(new Producer(clerk)) ;
        Thread t2 = new Thread(new Customer(clerk)) ;

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

    // 生产产品
    public synchronized void addProduct() {
        // 判断,当产品大于等于20时,调用wait()方法
        if (produce >= 20) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            produce ++ ;
            System.out.println("生产者生产产品 :" + produce);
            // 唤醒线程
            notify();
        }
    }

    // 消费产品
    public synchronized void consumption() {
        // 判断,当产品小于1时,调用wait()方法
        if (produce < 1) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("消费者消费产品 :" + produce);
            produce -- ;
            // 唤醒线程
            notify();
        }
    }

}

/**
 * 生产者类:
 *  负责生产产品,当产品达到20后,会调用wait()方法,释放资源进入等待状态
 *
 */
class Producer implements Runnable {

    Clerk clerk ;

    public Producer(Clerk clerk) {
        this.clerk = clerk ;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.addProduct();
        }
    }
}

/**
 * 消费者类:
 *  负责消费产品,当产品数量大于1时,消费者都是可以进行消费的操作的,
 *             当产品数量小于1时,消费者执行wait()方法,释放资源进入等待状态
 */
class Customer implements Runnable {

    Clerk clerk ;

    public Customer(Clerk clerk) {
        this.clerk = clerk ;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumption();
        }
    }
}

生产者生产产品 :1
生产者生产产品 :2
消费者消费产品 :2
生产者生产产品 :2
消费者消费产品 :2
生产者生产产品 :2
生产者生产产品 :3
消费者消费产品 :3
生产者生产产品 :3
生产者生产产品 :4

13.6 线程池

13.6.1 线程池的概念

  • 线程池就是装线程的容器
  • 创建线程池对象

  • 创建线程的任务对象

  • 把线程的任务对象交付给线程池对象

  • 线程池对象委派线程对象去完成线程任务

  • 当线程对象完成线程任务时,线程对象会回到线程池中等待下一次委派

13.6.2 系统提供的线程池

  • 使用Executors工具类中的静态方法创建线程池对象
  • static ExecutorService newCachedThreadPool():创建一个不指定线程上限的线程池对象
  • static ExecutorService newFixedThreadPool(int nThreads) : 创建一个指定线程上限的线程池对象
  • 向线程池提交任务的方法 : ExecutorService 接口
    • Future<?> submit(Runnable task)
    • <T> Future<T> submit(Callable<T> task)
    • <T> Future<T> submit(Runnable task, T result)
public class ExecutorsDemo {
    public static void main(String[] args) {
        // 创建一个不指定线程上限的线程池
        ExecutorService es = Executors.newCachedThreadPool();

        // 使用for循环创建多个线程对象
        for (int i = 0 ; i < 100 ; i ++) {
            int finalI = i ;
            // submit()方法用来提交线程任务
            es.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "这是第" + finalI + "个线程任务");
            });

        }
        // 关闭线程池
        es.shutdown();
    }
}
public class Demo {
    public static void main(String[] args) {
        // 创建一个指定线程上限的线程池对象
        ExecutorService es = Executors.newFixedThreadPool(5);

        for (int i = 0 ; i < 30 ; i ++) {
            int finalI = i;
            // 提交线程任务
            es.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "这是第" + finalI + "个线程任务") ;
            }) ;
        }
        // 关闭线程池
        es.shutdown();
    }
}

13.6.3 自定义线程池对象

线程池对象对应的类:ThreadPoolExecutor

  • 构造方法:

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) :根据给定的初始参数创建线程池对象

int corePoolSize :核心线程数量
int maximumPoolSize:总线程数量(总线程数量 = 核心线程数量 + 临时线程数量)
long keepAliveTime:临时线程存活时间
TimeUnit unit:临时线程存货时间的单位
BlockingQueue<Runnable> workQueue:阻塞队列
ThreadFactory threadFactory:线程工厂,为线程池提供线程对象
RejectedExecutionHandler handler:任务拒绝策略

13.6.3.1 TimeUnit 枚举类

TimeUnit是一个时间单位枚举类

DAYS
HOURS
MICROSECONDS
MILLISECONDS
MINUTES
NANOSECONDS
SECONDS

13.6.3.2 BlockingQueue<E>
  • BlockingQueue是阻塞队列的根节点,是一个接口,单列集合

  • ArrayBlockingQueue是其具体实现类,数组阻塞队列

    • 构造方法: ArrayBlockingQueue(int capacity) :创建一个给定容量和默认访问策略的ArrayBlockingQueue对象

    • 进队列的方法:void put(E e)

    • 出队列的方法:E take()

13.6.3.3 ThreadFactory
  • 线程池中线程对象的来源
  • Executors工具类完成线程工厂对象的创建 :static ThreadFactory defaultThreadFactory()
13.6.3.4 RejectedExecutionHandler
  • RejectedExecutionHandler:任务拒绝策略的父接口

  • 其实现类是ThreadPoolExecutor类的静态成员内部类

    • static class ThreadPoolExecutor.AbortPolicy:直接拒绝并报错(默认方案)
    • static class ThreadPoolExecutor.DiscardPolicy:直接拒绝但是不报错
    • static class ThreadPoolExecutor.DiscardOldestPolicy:随机移除阻塞队列中的某个等待任务,并把阻塞队列外的等待最久的一个线程任务添加到阻塞队列中
    • static class ThreadPoolExecutor.CallerRunsPolicy : 请其他的线程对象完成此线程池完成不了的任务
  • ThreadPoolExecutor.AbortPolicy()

public class Demo1 {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2 , // 核心线程数为 2
                3 , // 总线程数为 3,临时线程数为 1
                2 , // 临时线程的存活时常为 2分钟
                TimeUnit.MINUTES ,
                new ArrayBlockingQueue(2) , // 阻塞队列空间为 2
                Executors.defaultThreadFactory() ,  // 默认的线程工厂
                new ThreadPoolExecutor.AbortPolicy());  // 默认的拒绝策略(直接拒绝并报错)

        for (int i = 0 ; i < 6 ; i ++) {
            int finalI = i;
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "任务 " + finalI);
            }) ;
        }
        threadPoolExecutor.shutdown();
    }
}

pool-1-thread-1任务 0
pool-1-thread-3任务 4
pool-1-thread-2任务 1
pool-1-thread-3任务 3
Exception in thread “main” pool-1-thread-1任务 2
java.util.concurrent.RejectedExecutionException:…

  • ThreadPoolExecutor.DiscardPolicy()
public class Demo2 {
    public static void main(String[] args) {
        // 自定义线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2 ,     // 核心线程数
                3 , // 总线程数
                2 ,     // 临时线程的存活时间
                TimeUnit.MINUTES ,  // 临时线程存活时间的单位
                new ArrayBlockingQueue(2) , // 阻塞队列的大小
                Executors.defaultThreadFactory() ,  // 线程工厂
                new ThreadPoolExecutor.DiscardPolicy());    // 拒绝策略,直接拒绝但是不报错

        for (int i = 0; i < 6; i++) {
            int finalI = i;
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "任务" + finalI);
            }) ;
        }
        threadPoolExecutor.shutdown();
    }
}

pool-1-thread-3任务4
pool-1-thread-2任务1
pool-1-thread-1任务0
pool-1-thread-2任务2
pool-1-thread-3任务3

  • ThreadPoolExecutor.DiscardOldestPolicy()
public class Demo3 {
    public static void main(String[] args) {
        // 自定义线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2 ,
                3 ,
                2 ,
                TimeUnit.MINUTES ,
                new ArrayBlockingQueue(2) ,
                Executors.defaultThreadFactory() ,
                new ThreadPoolExecutor.DiscardOldestPolicy()
        );

        for (int i = 0; i < 6; i++) {
            int finalI = i;
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "任务" + finalI);
            }) ;
        }
        threadPoolExecutor.shutdown();
    }
}

pool-1-thread-1任务0
pool-1-thread-3任务4
pool-1-thread-1任务3
pool-1-thread-2任务1
pool-1-thread-3任务5

  • ThreadPoolExecutor.CallerRunsPolicy()
public class Demo4 {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2 ,
                3 ,
                2 ,
                TimeUnit.MINUTES ,
                new ArrayBlockingQueue(2) ,
                Executors.defaultThreadFactory() ,
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        for (int i = 0; i < 6; i++) {
            int finalI = i;
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "任务" + finalI);
            }) ;
        }
        threadPoolExecutor.shutdown();
    }
}

pool-1-thread-1任务0
main任务5
pool-1-thread-3任务4
pool-1-thread-2任务1
pool-1-thread-3任务3
pool-1-thread-1任务2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值