JavaSE进阶12:多线程、线程同步(锁)、线程池、

系列文章目录

JavaSE进阶01:继承、修饰符
JavaSE进阶02:多态、抽象类、接口
JavaSE进阶03:内部类、Lambda表达式
JavaSE进阶04:API中常用工具类
JavaSE进阶05:包装类、递归、数组的高级操作、异常
JavaSE进阶06:Collection集合、迭代器、List、ArrayList、LinkedList
JavaSE进阶07:泛型、Set集合、TreeSet、二叉树、红黑树
JavaSE进阶08:HashSet、Map集合、HashMap、TreeMap、可变参数、不可变集合
JavaSE进阶09:Stream流、File类
JavaSE进阶10:IO流、字节流、字节缓冲流
JavaSE进阶11:字符流、字符缓冲流、转换流、对象操作流、Properties集合
JavaSE进阶12:多线程、线程同步、线程池
JavaSE进阶13:网络编程入门、UDP通信程序、TCP通信程序、日志、枚举
JavaSE进阶14:类加载器、反射
JavaSE进阶15:XML、注解、单元测试
JavaSE进阶扩充:JDK8 HashMap底层分析(了解)
JavaSE进阶扩充:JDK8 ArrayList线程安全问题和源码分析、集合常见面试题
Java进阶作业


文章目录

  • 系列文章目录
    • 1.多线程的概念
      • 1.1 并发和并行【重点】
      • 1.2 进程和线程【重点】
    • 2.多线程的实现方式
      • 2.1实现多线程方式一:继承Thread类【重点】
      • 2.2 多线程的实现方式-两个小问题【了解】
      • 2.3实现多线程方式二:实现Runnable接口【重点】
      • 2.4 实现多线程方式三: 实现Callable接口【重点】
      • 2.5 三种实现方式的对比【重点】
    • 3.线程类中的常见方法
      • 3.1getName/setName获取设置线程名称【重点】
      • 3.2 currentThread获得当前线程对象【重点】
      • 3.3 sleep线程休眠【重点】
      • 3.4 getPriority/setPriority获取设置线程优先级【了解】
      • 3.5 setDaemon设置守护线程【了解】
    • 4.线程的生命周期
    • 5.线程同步
      • 5.1 卖票【难点】
      • 5.2 线程安全问题-原因分析【难点】
      • 5.3 解决方式一:synchronized同步代码块【重点】
      • 5.4 线程安全问题-锁对象唯一【重点】
      • 5.5 解决方式二:synchronized 同步方法【重点】
      • 5.6 解决方式三:Lock锁【重点】
      • 5.7 死锁【了解】
      • 5.8 可重入锁【掌握】
    • 6.线程的等待和唤醒
      • 6.1 生产者和消费者思路分析【了解】
      • 6.2 生产者和消费者案例【了解】
    • 7.线程池
      • 7.1 线程状态介绍
      • 7.2 线程池-基本原理
      • 7.3 JDK的线程池-Executors
      • 7.4 自定义线程池-ThreadPoolExecutor
      • 7.5 线程池的任务拒绝策略
  • 7.ThreadLocal


1.多线程的概念

1.1 并发和并行【重点】

并行:在同一时刻,有多个指令在多个CPU上同时执行
JavaSE进阶12:多线程、线程同步(锁)、线程池、_第1张图片
并发:在一段时间内,有多个指令在单个CPU上交替执行
如:以前淘宝双十一的高并发导致服务器崩溃了
JavaSE进阶12:多线程、线程同步(锁)、线程池、_第2张图片

1.2 进程和线程【重点】

  • 进程是正在运行的程序
    • 独立性:进程是一个能独立运行的基本单位,也是操作系统分配和调度的最小单元
    • 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的。
    • 并发性:任何进程都可以同其他进程一起并发制作
  • 线程:是进程中的单个顺序控制流,是一条执行路径
    • 程序中做的事情, 线程是CPU调度最小单元
  • 单线程:一个进程只有一条执行路径
  • 多线程:一个进程有多条执行路径
    • 采用多线程技术可以同时执行多个任务
    • 多线程需要硬件支持
  • 线程和进程的关系
    • 线程它是进程的一部分,不能独立存在
    • 一个进程,可以有多条线程,至少有一条线程
  • 多线程的作用
    • 可以让程序同时做不同的事情,提高程序的执行效率(例如迅雷同时下载多个文件)

JavaSE进阶12:多线程、线程同步(锁)、线程池、_第3张图片

2.多线程的实现方式

  • 实现多线程的方式有哪些?
    • 继承Thread类的方式进行实现
    • 实现Runnable接口的方式进行实现
    • 利用Callable和Future接口方式实现
  • 多线程执行特点
    运行的结果每次有可能不一样,表示有随机性,因为线程的执行是由CPU来执行的,由CPU说了算

2.1实现多线程方式一:继承Thread类【重点】

  • Thread类:表示线程的类
方法名 说明
void run() 在线程开启后,此方法将被调用执行
void start() 使此线程开始执行,Java虚拟机会调用此线程的run方法()
public class MyThread extends Thread{//继承Thread类
    private String name;

    public MyThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {//重写run()方法
        //代码就是线程在开启之后执行的代码
        for (int i = 0; i < 100; i++) {
            System.out.println(name+"线程开启了" + i);
        }
    }
}
class Demo {
    public static void main(String[] args) {
        //创建一个线程对象
        MyThread t1 = new MyThread("线程t1");
        //创建一个线程对象
        MyThread t2 = new MyThread("线程t2");
        //开启一条线程
        t1.start();
        //开启第二条线程
        t2.start();
    }
}
/*
线程t2线程开启了0
线程t1线程开启了0
线程t2线程开启了1
线程t1线程开启了1
线程t2线程开启了2
线程t1线程开启了2
线程t2线程开启了3
线程t1线程开启了3
线程t2线程开启了4
线程t1线程开启了4
线程t2线程开启了5
线程t2线程开启了6
线程t2线程开启了7
线程t1线程开启了5
...
 */

2.2 多线程的实现方式-两个小问题【了解】

  • 为什么要重写run()方法?
    • 因为run()是用来封装被线程执行的代码
  • run()方法和start()方法的区别?
    • run():封装线程执行的代码,直接调用,相当于普通方法的调用
    • start():启动线程;然后由JVM调用此线程的run()方法

2.3实现多线程方式二:实现Runnable接口【重点】

1.Thread构造方法

方法名 说明
Thread(Runnable target) 分配一个新的Thread对象

2.实现步骤

  • 定义一个类MyRunnable实现Runnable接口
  • 在MyRunnable类中重写run()方法
  • 创建MyRunnable类的对象
  • 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
  • 启动线程
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //线程启动后执行的代码
        for (int i = 0; i < 100; i++) {
            System.out.println("第二种方式实现多线程" + i);
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        //创建了一个参数的对象
        MyRunnable mr = new MyRunnable();
        //创建了一个线程对象,并把参数传递给这个线程.
        //在线程启动之后,执行的就是参数里面的run方法
        Thread t1 = new Thread(mr);
        //开启线程
        t1.start();


        MyRunnable mr2 = new MyRunnable();
        Thread t2 = new Thread(mr2);
        t2.start();

    }
}
/*
第二种方式Runnable实现多线程0
第二种方式Runnable实现多线程0
第二种方式Runnable实现多线程1
第二种方式Runnable实现多线程1
第二种方式Runnable实现多线程2
第二种方式Runnable实现多线程2
第二种方式Runnable实现多线程3
第二种方式Runnable实现多线程3
第二种方式Runnable实现多线程4
第二种方式Runnable实现多线程4
第二种方式Runnable实现多线程5
第二种方式Runnable实现多线程6
...
*/

2.4 实现多线程方式三: 实现Callable接口【重点】

1.方法介绍

方法名 说明
V call() 计算结果,如果无法计算结果,则抛出一个异常
FutureTask(Callable callable) 创建一个 FutureTask,一旦运行就执行给定的 Callable
V get() 如有必要,等待计算完成,然后获取其结果

2.实现步骤

  • 定义一个类MyCallable实现Callable接口
  • 在MyCallable类中重写call()方法
  • 创建MyCallable类的对象
  • 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
  • 创建Thread类的对象,把FutureTask对象作为构造方法的参数
  • 启动线程
  • 再调用get方法,就可以获取线程结束之后的结果。

3.注意事项
get()方法的调用一定要在Thread类对象调用start()方法之后

import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 5; i++) {
            System.out.println("跟女孩表白" + i);
        }
        //返回值就表示线程运行完毕之后的结果
        return "答应";
    }
}


import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //线程开启之后需要执行里面的call方法
        MyCallable mc = new MyCallable();

        //Thread t1 = new Thread(mc);//报错,Thread的参数只有Runnable

        //可以获取线程执行完毕之后的结果.也可以作为参数传递给Thread对象
        FutureTask<String> ft = new FutureTask<>(mc);

        //创建线程对象
        Thread t1 = new Thread(ft);

        //String s = ft.get(); //若写在开启线程前,就会在此死等不向下继续运行。
        //开启线程
        t1.start();

        String s = ft.get();//返回值是callable接口的,所以用ft调用
        System.out.println(s);
    }
}
/*
跟女孩表白0
跟女孩表白1
跟女孩表白2
跟女孩表白3
跟女孩表白4
答应
 */

2.5 三种实现方式的对比【重点】

实现方式 优点 缺点
实现Runnable、Callable接口 扩展性强,实现该接口的同时还可以继承其他的类 编程相对复杂,不能直接使用Thread类中的方法
继承Thread类 编程比较简单,可以直接使用Thread类中的方法 可以扩展性较差,不能再继承其他的类
  • 实际开发过程中如何选择?
    • 如果不需要任务的返回结果,就实现Runnable接口,需要的话,实现Callable接口
  • 如何去写多线程程序
    • 1.确定要做什么,执行什么任务
    • 2.要不要用多线程,要任务返回的结果,如果要实现Callable接口,如果不要实现Runnable接口
    • 3.实现call方法或run方法
    • 4.交给线程类,执行start(),开启线程干活

3.线程类中的常见方法

3.1getName/setName获取设置线程名称【重点】

方法名 说明
void setName(String name) 将此线程的名称更改为等于参数name
String getName() 返回此线程的名称
public class MyThread extends Thread {
    public MyThread() {
    }

    //构造方法不能继承,快捷键加入有参构造
    public MyThread(String name) {
        super(name);
    }
    //getName方法获取线程名
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName() + "@@@" + i);
        }
    }
}

class Demo {
    //1,线程是有默认名字的,格式:Thread-编号
    public static void main(String[] args) {
        //方式一:有参构造设置线程名
        MyThread t1 = new MyThread("小蔡");
        MyThread t2 = new MyThread("小强");
        //方法二:setName设置线程名
        //t1.setName("小蔡");
        //t2.setName("小强");

        t1.start();
        t2.start();
    }
}
/*
小蔡@@@0
小强@@@0
小蔡@@@1
小强@@@1
小蔡@@@2
小强@@@2
小蔡@@@3
小强@@@3
小强@@@4
小蔡@@@4
 */

3.2 currentThread获得当前线程对象【重点】

作用:因为用接口实现多线程不能使用Thread类中的方法,但通过获取当前线程对象方法currentThread就可以。

方法名 说明
static Thread currentThread() 返回对当前正在执行的线程对象的引用
public class Demo {
    public static void main(String[] args) {
        String name = Thread.currentThread().getName();//getName是Thread类中的静态方法,Demo没有继承线程类
        System.out.println(name);
    }
}
/*
main
 */

3.3 sleep线程休眠【重点】

方法名 说明
static void sleep(long millis) 使当前正在执行的线程停留(暂停执行)指定的毫秒数

1秒=1000毫秒

package sleep;

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);//接口实现时只能捕获异常
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}

class Demo {
    public static void main(String[] args)  {

        MyRunnable mr = new MyRunnable();
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        t1.start();
        t2.start();
    }
}

注意:
Runnable接口中的run方法没有抛出异常,所以其所有实现类和子类都不能抛出异常,只能自己捕获异常

3.4 getPriority/setPriority获取设置线程优先级【了解】

1.线程调度
线程有两种调度模型:

  • 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
  • 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
  • Java使用的是抢占式调度模型

2.优先级相关方法
项目中一般很少使用,一般都是公平竞争。

方法名 说明
final int getPriority() 返回此线程的优先级
final void setPriority(int newPriority) 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
        return "线程执行完毕了";
    }
}


class Demo {
    public static void main(String[] args) {
        //优先级: 1 - 10 默认值:5
        MyCallable mc = new MyCallable();

        FutureTask<String> ft = new FutureTask<>(mc);
        Thread t1 = new Thread(ft);
        t1.setName("飞机");
        t1.setPriority(10);
        System.out.println(t1.getPriority());//10
        t1.start();

        FutureTask<String> ft2 = new FutureTask<>(mc);
        Thread t2 = new Thread(ft2);
        t2.setName("坦克");
        t2.setPriority(1);
        System.out.println(t2.getPriority());//1
        t2.start();
    }
}
/*
10
1
坦克---0
飞机---0
坦克---1
飞机---1
坦克---2
飞机---2
坦克---3
飞机---3
坦克---4
飞机---4
坦克---5
飞机---5
飞机---6
飞机---7
飞机---8
飞机---9
...
 */

3.5 setDaemon设置守护线程【了解】

1.什么是守护线程?

  • 守护其它线程,当普通线程执行完毕了,守护线程就没有存在的必要了

2.如何使用守护线程?

相关方法

方法名 说明
void setDaemon(boolean on) 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出

代码演示

public class MyThread1 extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(this.getName()+"---"+i);
        }
    }
}

class MyThread2 extends Thread{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(this.getName()+"---"+i);
        }
    }
}

class Test {

    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();//主人
        MyThread2 t2 = new MyThread2();//保镖
        t2.setDaemon(true);//用
        t1.setName("主人");
        t2.setName("保镖");
        t2.start();
        t1.start();
    }
}
/*
主人---0
保镖---0
主人---1
保镖---1
主人---2
主人---3
主人---4
保镖---2
保镖---3
保镖---4
保镖---5
保镖---6
保镖---7
保镖---8
保镖---9
保镖---10//没有普通线程,守护现在再运行一会儿就提前结束了
 */

4.线程的生命周期

JavaSE进阶12:多线程、线程同步(锁)、线程池、_第4张图片

5.线程同步

5.1 卖票【难点】

  • 案例需求
    • 某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

没注意线程安全的错误示范

public class Ticket implements Runnable {
    //票的数量
    private int ticket = 10;//三个线程共有

    @Override
    public void run() {
        while(true){
            if(ticket <= 0){//卖完了
                break;
            }else{
                try {
                    Thread.sleep(100);//机器出票时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket--;//出票成功
                System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
            }
        }

    }
}

class Demo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

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

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

        t1.start();
        t2.start();
        t3.start();
    }
}
/*
窗口一在卖票,还剩下7张票
窗口三在卖票,还剩下7张票
窗口二在卖票,还剩下7张票
窗口二在卖票,还剩下5张票
窗口三在卖票,还剩下4张票
窗口一在卖票,还剩下5张票
窗口三在卖票,还剩下3张票
窗口二在卖票,还剩下2张票
窗口一在卖票,还剩下2张票
窗口三在卖票,还剩下-1张票
窗口二在卖票,还剩下-1张票
窗口一在卖票,还剩下-1张票
 */

5.2 线程安全问题-原因分析【难点】

1.什么是线程安全问题?
所谓的线程安全问题就是多线程环境下,出现数据结果与预期不一致的情况就称为线程安全问题

2.卖票出现了问题
三个线程几乎同时出票时,ticket减1同时执行了3次后再打印。

3.出现重复票和负号票的原因

卖票的过程中有多条线程操作了共享数据

5.3 解决方式一:synchronized同步代码块【重点】

1.如何解决上述问题呢?

  • 任意时刻只有一条线程可以**操作(增、删、改、查)**共享变量

  • Java中如何解决?使用同步代码块。

//同步代码块格式:
synchronized(任意对象) { 
	操作共享数据的代码 
}
  • 同步的好处和弊端
    • 好处:解决了多线程的数据安全问题
    • 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁

2.synchronized同步代码块的特点?

  • 默认情况下是打开的,只要有一个线程进去执行代码了,锁就会关闭
  • 当锁中代码执行完,锁才会自动打开

3.什么情况下会出现线程安全问题呢?

  • 多条线程操作共享数据(可以拆分为三个条件):
    • 1.多线程环境
    • 2.有共享数据
    • 3.多条线程操作了共享数据

因此,在卖票的时候,加不加Thread.sleep()代码中都会有线程安全问题,只不过加上之后,线程执行的速度慢了下来,我们可以观察到问题而已

正确代码演示

package Ticket;

public class Ticket implements Runnable {
    //票的数量
    private int ticket = 10;
    private Object obj = new Object();

    @Override
    public void run() {
        while(true){
            synchronized (obj){//多个线程必须使用同一把锁
                if(ticket <= 0){//卖完了
                    break;
                }else{
                    try {
                        Thread.sleep(100);//机器出票时间
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket--;//出票成功
                    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
                }
            }
        }
    }
}

class Demo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

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

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

        t1.start();
        t2.start();
        t3.start();
    }
}
/*
窗口一在卖票,还剩下9张票
窗口一在卖票,还剩下8张票
窗口二在卖票,还剩下7张票
窗口二在卖票,还剩下6张票
窗口二在卖票,还剩下5张票
窗口二在卖票,还剩下4张票
窗口二在卖票,还剩下3张票
窗口二在卖票,还剩下2张票
窗口三在卖票,还剩下1张票
窗口三在卖票,还剩下0张票
 */

5.4 线程安全问题-锁对象唯一【重点】

锁对象为什么要唯一?
相当于在操作共享数据的房子中,每个线程都有对应的门,都可以通过自己的门进入房子中。
JavaSE进阶12:多线程、线程同步(锁)、线程池、_第5张图片

public class MyThread extends Thread {
    private static int ticketCount = 100;
    private static final Object obj = new Object();//同一把锁不想被修改加final
    @Override
    public void run() {
        while(true){
            synchronized (obj){ //锁唯一
                if(ticketCount <= 0){
                    //卖完了
                    break;
                }else{
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticketCount--;
                    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
                }
            }
        }
    }
}

class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("窗口一");
        t2.setName("窗口二");

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

  • 注意
    • 重写父类Thread 的run方法:锁对象为this时,每个Thread实现类的锁对象是Thread实现类本身,锁对象不唯一。
    • 重写接口Runnable 的run方法:锁对象为this时,每个Thread的锁对象都是Runnable 的实现类,锁对象唯一。

5.5 解决方式二:synchronized 同步方法【重点】

1.同步方法的格式

同步方法:就是把synchronized关键字加到方法上

修饰符 synchronized 返回值类型 方法名(方法参数) { 
	方法体;
}

2.同步方法的锁对象是:this
3.静态同步方法的锁对象是:类名.class。static方法不能用this和super等关键字。
4.静态同步方法的格式
同步静态方法:就是把synchronized关键字加到静态方法上

修饰符 static synchronized 返回值类型 方法名(方法参数) { 
	方法体;
}

同步方法示例代码:

public class MyRunnable implements Runnable {
    private int ticketCount = 100;
    @Override
    public void run() {
        while(true){
            if("窗口一".equals(Thread.currentThread().getName())){
                //同步方法
                boolean result = synchronizedMthod();
                if(result){
                    break;
                }
            }

            if("窗口二".equals(Thread.currentThread().getName())){
                //同步代码块
                synchronized (this){//同步方法锁对象this
                    if(ticketCount == 0){
                        break;
                    }else{
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        ticketCount--;
                        System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
                    }
                }
            }

        }
    }

    private synchronized boolean synchronizedMthod() {
        if(ticketCount == 0){
            return true;
        }else{
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticketCount--;
            System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
            return false;
        }
    }
}

class Demo {
    public static void main(String[] args) {
        MyRunnable myRunnable=new MyRunnable();
        Thread t1 = new Thread(myRunnable);
        Thread t2 = new Thread(myRunnable);

        t1.setName("窗口一");
        t2.setName("窗口二");

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

静态同步方法示例:


public class MyRunnable implements Runnable {
    private static int ticketCount = 100;//变更处1:静态方法访问静态变量
    @Override
    public void run() {
        while(true){
            if("窗口一".equals(Thread.currentThread().getName())){
                //同步方法
                boolean result = synchronizedMthod();
                if(result){
                    break;
                }
            }

            if("窗口二".equals(Thread.currentThread().getName())){
                //同步代码块
                synchronized (MyRunnable.class){//变更处2:静态同步方法的锁对象:类名.class
                    if(ticketCount == 0){
                        break;
                    }else{
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        ticketCount--;
                        System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
                    }
                }
            }

        }
    }

    private static synchronized boolean synchronizedMthod() {//变更处3:在静态方法加synchronized 
        if(ticketCount == 0){
            return true;
        }else{
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticketCount--;
            System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
            return false;
        }
    }
}

class Demo {
    public static void main(String[] args) {
        MyRunnable myRunnable=new MyRunnable();
        Thread t1 = new Thread(myRunnable);
        Thread t2 = new Thread(myRunnable);

        t1.setName("窗口一");
        t2.setName("窗口二");

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

5.6 解决方式三:Lock锁【重点】

1.如何手动开关锁呢?答:使用lock

2.如何使用Lock?答:Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化


ReentrantLock构造方法

方法名 说明
ReentrantLock() 创建一个ReentrantLock的实例

加锁解锁方法

方法名 说明
void lock() 获得锁
void unlock() 释放锁
package lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable {
    //票的数量
    private int ticket = 100;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                if (ticket <= 0) {
                    break;
                } else {
                    Thread.sleep(100);
                    ticket--;
                    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();//不管是否异常,一定释放锁
            }
        }
    }
}

class Demo {
    public static void main(String[] args) {

        Ticket ticket = new Ticket();

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

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

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

5.7 死锁【了解】

1.什么是死锁:线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
2.导致死锁的原因:进行了锁的嵌套以后不准写锁的嵌套
JavaSE进阶12:多线程、线程同步(锁)、线程池、_第6张图片
代码演示
JavaSE进阶12:多线程、线程同步(锁)、线程池、_第7张图片

5.8 可重入锁【掌握】

ReentrantLock是可重入锁

可重入性:就是一个线程不用释放,可以重复的获取一个锁n次,只是在释放的时候,也需要相应的释放n次。(简单来说:A线程在某上下文中或得了某锁,当A线程想要再次获取该锁时,不会应为锁已经被自己占用,而需要先等到锁的释放)假使A线程即获得了锁,又在等待锁的释放,就会造成死锁。”

ReentrantLock lock = new ReentrantLock();//非公平重入锁
ReentrantLock lock = new ReentrantLock(true);//公平重入锁

6.线程的等待和唤醒

6.1 生产者和消费者思路分析【了解】

目的:两个线程轮流执行。
JavaSE进阶12:多线程、线程同步(锁)、线程池、_第8张图片

6.2 生产者和消费者案例【了解】

Object类的等待和唤醒方法

方法名 说明
void wait() 导致当前线程等待同时释放锁,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
void notify() 唤醒正在等待单个线程,并不立即释放锁
void notifyAll() 唤醒正在等待所有线程,并不立即释放锁
  • 案例需求

    • 桌子类(Desk):定义表示包子数量的变量,定义锁对象变量,定义标记桌子上有无包子的变量

    • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务

      1.判断是否有包子,决定当前线程是否执行

      2.如果有包子,就进入等待状态,如果没有包子,继续执行,生产包子

      3.生产包子之后,更新桌子上包子状态,唤醒消费者消费包子

    • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务

      1.判断是否有包子,决定当前线程是否执行

      2.如果没有包子,就进入等待状态,如果有包子,就消费包子

      3.消费包子后,更新桌子上包子状态,唤醒生产者生产包子

    • 测试类(Demo):里面有main方法,main方法中的代码步骤如下

      创建生产者线程和消费者线程对象

      分别开启两个线程

  • 代码实现

/*桌子类*/
public class Desk {

    //定义一个标记
    //true 就表示桌子上有汉堡包的,此时允许吃货执行
    //false 就表示桌子上没有汉堡包的,此时允许厨师执行
    public static boolean flag = false;

    //能吃汉堡包的总数量
    public static int count = 3;

    //锁对象
    public static final Object lock = new Object();
}
/*厨师类*/
public class Cooker extends Thread {
    @Override
    public void run() {
        while(true){
            synchronized (Desk.lock){
                if(Desk.count == 0){//吃不下不生产了
                    break;
                }else{
                    if(!Desk.flag){
                        //生产
                        System.out.println("厨师正在生产汉堡包");
                        Desk.flag = true;
                        Desk.lock.notifyAll();//唤醒消费者
                    }else{
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}
/*消费者类*/
public class Foodie extends Thread {
    @Override
    public void run() {
        //套路:
        //1. while(true)死循环
        //2. synchronized 锁,锁对象要唯一
        //3. 判断,共享数据是否结束. 结束
        //4. 判断,共享数据是否结束. 没有结束
        while(true){
            synchronized (Desk.lock){
                if(Desk.count == 0){//吃不下了
                    break;
                }else{
                    if(Desk.flag){
                        //有
                        System.out.println("吃货在吃汉堡包");
                        Desk.flag = false;
                        Desk.lock.notifyAll();唤醒厨师
                        Desk.count--;
                    }else{
                        //没有就等待
                        try {
                            Desk.lock.wait();//使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}
/*测试类*/
public class Demo {
    public static void main(String[] args) {
        Foodie f = new Foodie();
        Cooker c = new Cooker();
        f.start();
        c.start();
    }
}
/*
厨师正在生产汉堡包
吃货在吃汉堡包
厨师正在生产汉堡包
吃货在吃汉堡包
厨师正在生产汉堡包
吃货在吃汉堡包
*/

7.线程池

7.1 线程状态介绍

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。那么Java中的线程存在哪几种状态呢?
Java中的线程状态被定义在了java.lang.Thread.State枚举类中,State枚举类的源码如下:

public class Thread {
    
    public enum State {
    
        /* 新建 */
        NEW , 

        /* 可运行状态 */
        RUNNABLE , 

        /* 阻塞状态 */
        BLOCKED , 

        /* 无限等待状态 */
        WAITING , 

        /* 计时等待 */
        TIMED_WAITING , 

        /* 终止 */
        TERMINATED;
    
	}
    
    // 获取当前线程的状态
    public State getState() {
        return jdk.internal.misc.VM.toThreadState(threadStatus);
    }
    
}

通过源码我们可以看到Java中的线程存在6种状态,每种线程状态的含义如下

线程状态 具体含义
NEW 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。
RUNNABLE 当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。
BLOCKED 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
WAITING 一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。
TIMED_WAITING 一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。
TERMINATED 一个完全运行完成的线程的状态。也称之为终止状态、结束状态

各个状态的转换,如下图所示:
JavaSE进阶12:多线程、线程同步(锁)、线程池、_第9张图片

7.2 线程池-基本原理

概述 :
线程池也是可以看做成一个池子,在该池子中存储很多个线程

为什么使用线程池 :
池化的思想是为了减少每次获取和结束资源的消耗,提高对资源的利用率

线程池存在的意义:
系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理是对系统资源的消耗,这样就有点"舍本逐末"了。针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。

线程池的好处 :

  1. 降低资源消耗:通过重复利用现有的线程来执行任务,避免了多次创建和销毁线程
  2. 提高响应速度:因为省去了创建线程这个步骤,所以拿到任务时就可以立即开始执行任务
  3. 提供附加功能:线程池的可扩展性是我们自己可以加入新的功能,不如定时或延时来执行某些任务

7.3 JDK的线程池-Executors

概述 : JDK对线程池也进行了相关的实现,在真实企业开发中我们也很少去自定义线程池,而是使用JDK中自带的线程池。

我们可以使用Executors中所提供的静态方法来创建线程池。

方法名 作用
static ExecutorService newCachedThreadPool() 创建一个默认的线程池
static newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池

创建一个默认的线程池 :

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {
        //创建一个默认的线程池对象.池子中默认是空的.默认最多可以容纳int类型的最大值.
        ExecutorService executorService = Executors.newCachedThreadPool();
        //Executors --- 可以帮助我们创建线程池对象
        //ExecutorService --- 可以帮助我们控制线程池

        //匿名内部类
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "在执行了");
            }
        });
		
		//Thread.sleep(2000);
		
        //lambda表达式
        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });

        executorService.shutdown();//直接关闭线程池,也就关闭了池里的所有线程
		//shutdown线程池关闭后,再submit提交任务会报错。
    }
}
/*
pool-1-thread-1在执行了
pool-1-thread-2在执行了

加入Thread.sleep(2000)后:因为线程1执行完后就归还到线程池后线程1已经空闲了
pool-1-thread-1在执行了
pool-1-thread-1在执行了
 */

创建一个指定最多线程数量的线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class MyThreadPoolDemo2 {
    public static void main(String[] args) {
        //创建一个默认的线程池对象.池子中默认是空的.默认最多可以容纳int个线程
        //int参数不是初始值而是最大值
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        ThreadPoolExecutor pool = (ThreadPoolExecutor) executorService;
        System.out.println("池中线程数:"+pool.getPoolSize());//0

        //匿名内部类
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "在执行了");
            }
        });
        //lambda表达式
        executorService.submit(()->{
            System.out.println(Thread.currentThread().getName() + "在执行了");
        });

        System.out.println("池中线程数:"+pool.getPoolSize());//2
        executorService.shutdown();
    }
}
/*
池中线程数:0
池中线程数:2
pool-1-thread-1在执行了
pool-1-thread-2在执行了
 */

7.4 自定义线程池-ThreadPoolExecutor

创建线程池对象 :

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,空闲线程最大存活时间,任务队列,创建线程工厂,任务的拒绝策略);

代码实现 :

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyThreadPoolDemo3 {
//    参数一:核心线程数量-如:饭店正式员工
//    参数二:最大线程数-如:饭店最大员工数量
//    参数三:空闲线程最大存活时间-如:临时员工空闲多长时间被辞退
//    参数四:时间单位-TimeUnit里选分秒、秒、分等
//    参数五:任务队列-如:排队的客户在哪排队、能排几个,new ArrayBlockingQueue<>(10)
//    参数六:创建线程工厂-如:去哪里招人,Executors.defaultThreadFactory()默认工厂
//    参数七:任务的拒绝策略-new ThreadPoolExecutor.AbortPolicy()默认拒绝策略
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(2,5,2,TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        pool.submit(()-> System.out.println(Thread.currentThread().getName()+"正在招待客人"));
        pool.submit(()-> System.out.println(Thread.currentThread().getName()+"正在招待客人"));
        pool.submit(()-> System.out.println(Thread.currentThread().getName()+"正在招待客人"));

        pool.shutdown();
    }
}
/*
pool-1-thread-1正在招待客人
pool-1-thread-2正在招待客人
pool-1-thread-1正在招待客人
 */

ThreadPoolExecutor 线程池-参数详解
JavaSE进阶12:多线程、线程同步(锁)、线程池、_第10张图片

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
    
corePoolSize:   核心线程的最大值,不能小于0
maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
keepAliveTime:  空闲线程最大存活时间,不能小于0
unit:           时间单位
workQueue:      任务队列,不能为null
threadFactory:  创建线程工厂,不能为null      
handler:        任务的拒绝策略,不能为null。
				 什么时候拒绝:	

7.5 线程池的任务拒绝策略

1.什么时候拒绝:当提交的任务>池子中最大线程数+任务队列容量
2.如何拒绝:RejectedExecutionHandler是jdk提供的一个任务拒绝策略接口,它下面存在4个子类。

ThreadPoolExecutor.AbortPolicy: 		    丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy: 		    丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy:    抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.CallerRunsPolicy:        调用任务的run()方法绕过线程池直接执行。

注:明确线程池对最多可执行的任务数 = 队列容量 + 最大线程数

案例演示1:ThreadPoolExecutor.AbortPolicy任务处理策略
丢弃任务并抛出RejectedExecutionException异常。是默认的策略,用的最多。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo01 {
    public static void main(String[] args) {
        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy()) ;

        // 提交5个任务,而该线程池最多可以处理4个任务,当我们使用AbortPolicy这个任务处理策略的时候,就会抛出异常
        for(int x = 0 ; x < 5 ; x++) {
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
            });
        }
        threadPoolExecutor.shutdown();
    }
}
/*
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@1b2c6ec2[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@7cd84586[Wrapped task = ThreadPoolExecutorDemo01$$Lambda$14/0x00000008011f4840@30dae81]] rejected from java.util.concurrent.ThreadPoolExecutor@4edde6e5[Running, pool size = 3, active threads = 3, queued tasks = 1, completed tasks = 0]
pool-1-thread-3---->> 执行了任务3
pool-1-thread-1---->> 执行了任务0
pool-1-thread-2---->> 执行了任务2
pool-1-thread-3---->> 执行了任务1
*/

控制台报错是因为线程池对最多可执行的任务数为3+1=4,任务4被丢弃了。
注意:核心线程数量为1,本例中有3-1=2个临时线程

案例演示2:演示ThreadPoolExecutor.DiscardPolicy任务处理策略
丢弃任务,但是不抛出异常,这是不推荐的做法。

public class ThreadPoolExecutorDemo02 {
    public static void main(String[] args) {
        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.DiscardPolicy()) ;

        // 提交5个任务,而该线程池最多可以处理4个任务,当我们使用DiscardPolicy这个任务处理策略的时候,控制台不会报错
        for(int x = 0 ; x < 5 ; x++) {
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
            });
        }
    }
}
/*
pool-1-thread-3---->> 执行了任务3
pool-1-thread-1---->> 执行了任务0
pool-1-thread-1---->> 执行了任务1
pool-1-thread-2---->> 执行了任务2
*/

控制台没有报错,仅仅执行了4个任务,任务4被丢弃了

案例演示3:演示ThreadPoolExecutor.DiscardOldestPolicy任务处理策略
抛弃队列中等待最久的任务 然后把当前任务加入队列中。这是不推荐的做法。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo01 {
    public static void main(String[] args) {
        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.DiscardOldestPolicy()) ;

        // 提交5个任务,而该线程池最多可以处理4个任务,当我们使用AbortPolicy这个任务处理策略的时候,就会抛出异常
        for(int x = 0 ; x < 10 ; x++) {
            int y=x;
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务"+y);
            });
        }
        threadPoolExecutor.shutdown();
    }
}
/*
pool-1-thread-1---->> 执行了任务0
pool-1-thread-3---->> 执行了任务3
pool-1-thread-2---->> 执行了任务2
pool-1-thread-2---->> 执行了任务9
*/

抛弃在线程池中等待时间长的145678任务,把最后一个任务9加入队列

案例演示4:演示ThreadPoolExecutor.CallerRunsPolicy任务处理策略
调用任务的run()方法绕过线程池直接执行。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo01 {
    public static void main(String[] args) {
        /**
         * 核心线程数量为1 , 最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
         */
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1 , 3 , 20 , TimeUnit.SECONDS ,
                new ArrayBlockingQueue<>(1) , Executors.defaultThreadFactory() , new ThreadPoolExecutor.CallerRunsPolicy()) ;

        // 提交5个任务,而该线程池最多可以处理4个任务,当我们使用AbortPolicy这个任务处理策略的时候,就会抛出异常
        for(int x = 0 ; x < 10 ; x++) {
            int y=x;
            threadPoolExecutor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务"+y);
            });
        }
        threadPoolExecutor.shutdown();
    }
}
/*
pool-1-thread-2---->> 执行了任务2
main---->> 执行了任务4
pool-1-thread-1---->> 执行了任务0
pool-1-thread-3---->> 执行了任务3
main---->> 执行了任务5
pool-1-thread-2---->> 执行了任务1
main---->> 执行了任务7
pool-1-thread-2---->> 执行了任务6
main---->> 执行了任务9
pool-1-thread-2---->> 执行了任务8
*/

任务过多自己执行不了就交给别人执行。

7.ThreadLocal

  • 简介:
    • ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。可将ThreadLocal理解为Map,但不是Map。
  • 使用场景:
    • 后端中ServletRequest作用域都在方法内,因为若ServletRequest定义在成员变量处,它将被共用,引发线程安全问题。但Controller层、Service层、Dao层无法共享数据。

JavaSE进阶12:多线程、线程同步(锁)、线程池、_第11张图片

public class ThreadLocalTest {
    static ThreadLocal<Integer> threadLocal=new ThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set(111);
        new Thread(()->{
            threadLocal.set(222);
            Integer aInt = threadLocal.get();
            System.out.println(Thread.currentThread().getName()+":"+aInt);
        },"线程A").start();

        new Thread(()->{
            Integer bInt = threadLocal.get();
            System.out.println(Thread.currentThread().getName()+":"+bInt);
        },"线程B").start();

        System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
    }
}
/*
线程A:222
main:111
线程B:null
 */

你可能感兴趣的:(JAVA进阶笔记,JAVA)