多线程学习

1 基本概念

1.1 程序 进程 线程

  • 程序:完成一定功能的静态代码,对比火车站售票厅
  • 进程:正在执行的静态代码,称为进程,对比北京西站售票厅
  • 线程:并发执行一些异步任务,比对售票的中多个窗口

在这里插入图片描述

  • 单核cpu和多核cpu
  • java程启动后,main线程,gc线程,异常处理线程

1.2 并行和并发

  • 并行:多核cpu下,多线程同时执行
  • 并发:如果是单核cpu,采用抢占时cpu调度模型,让cpu在多个线程之间切换执行

完成一个任务需要多少个线程

  • io密集型 cpu核数*2
  • cpu密集型 cpu核数
  • 混合型经验

1.3 线程使用的场景

  • 网络连接tomcat,mysql,一个连接对一个一个线程, one connection one thread
  • 文件操作,文件下载,后台启动一个线程异步执行长时间的任务

2 多线程实战

2.1 Thread方法实现多线程

  1. 定义子类继承Thread类。
  2. 类中重写Thread类中的run方法。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,调用run方法。

启动一个线程,在线程中执行1-10000的偶数打印工作

public class MyThread extends Thread {

    @Override
    public void run() {
        for (int i = 1; i <= 10000; i++) {
            if (i % 2 == 0) {
                //Thread.currentThread().getName():得到线程的名字
                System.out.println(Thread.currentThread().getName() + "\t" + i);

            }
        }
    }
}

测试:

public class Test1 {

    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
        myThread1.start();

        MyThread myThread2 = new MyThread();
        myThread2.start();

        System.out.println(Thread.currentThread().getName() + "  main 线程 over");

    }

}

        JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞
        System.out.println("main over");

如果子线程执行,进程不会停止

2.2 Runable方式实现多线程

  • 定义子类,实现Runnable接口。
  • 类中重写Runnable接口中的run方法。
  • 通过Thread类含参构造器创建线程对象。
  • 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
  • 调用Thread类的start方法:开启线程, 调用Runnable子类接口的run方法。
public class ThreadDemo {

    public static void main(String[] args) {
        //方式1:Thread子类,启动
        MyThread thread1 = new MyThread();
        thread1.start();

        //方式2:Runable方式(推荐的方式)
        //优势:可以实现多继承,比如继承BaseDao,然后再实现Runnable
        MyTask task = new MyTask();
        Thread thread2 = new Thread(task);
        thread2.start();

        //方式3:匿名内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <= 100; i++) {
                    System.out.println(Thread.currentThread().getName() +  "\t" +  i);
                }
            }
        }).start();
    }

}

//方式1
class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            System.out.println(Thread.currentThread().getName() +  "\t" +  i);
        }
    }
}

//方式2
//优势:可以实现多继承,比如继承BaseDao,然后再实现Runnable
class MyTask implements Runnable {

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

2.3 Thread常见方法

  • 构造函数:

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

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

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

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

  • 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控制,使其他线程有机会被执行,时间到后重排队。

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

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

2.4 sleep

指定线程休眠的时间,单位毫秒,让出cpu时间片,其他线程可以抢占cpu时间片

public class MyTask implements Runnable {

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) {
                //Thread.currentThread().getName():得到线程的名字
                System.out.println(Thread.currentThread().getName() + "\t" + i);
                try {
                    Thread.sleep(1000);
                    Thread.yield();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }

    }
}

2.5 线程优先级

  • 线程的优先级等级
    • MAX_PRIORITY: 10
    • MIN _PRIORITY: 1
    • NORM_PRIORITY: 5
    public static void main(String[] args) {
        MyTask myTask = new MyTask();
        Thread thread1 = new Thread(myTask, "t1");
        thread1.setPriority(Thread.MIN_PRIORITY);
        thread1.start();

        Thread thread2 = new Thread(myTask, "t2");
        thread1.setPriority(Thread.MAX_PRIORITY);
        thread2.start();
    }

2.6 守护线程

  • 其他线程都执行结束,守护线程自动结束
  • 守护启动子线程,也是守护线程
  • 守护线程的语法thread.(setDaemon(true)设置守护线程
public class Test2 {

    public static void main(String[] args) {
        MyThread myThread1 = new MyThread();
//        myThread1.setDaemon(true);
        myThread1.start(); //守护线程, gc线程,jvm线程结束gc会自动结束
        JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞
        System.out.println("main over");
    }

}

class MyThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("a");
        }
    }
}

2.7 线程合并join

有两个线程1,2如下
在这里插入图片描述
执行join()方法后
在这里插入图片描述

public class Test1 {

    /**
     * CountDownLatch:可以实现相同的效果
     * @param args
     */
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    System.out.println(Thread.currentThread().getName() + "\t" + i);
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t1.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println(Thread.currentThread().getName() +  "  main orver");

    }

}

有join:
在这里插入图片描述
无join:
在这里插入图片描述

2.8 线程退出

2.8.1 stop

不推荐,线程退出方式粗暴,不管线程正在执行的任务,直接退出,可能丢失数据
在这里插入图片描述

public class Test1 {

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyTask());
        t1.start();

        Scanner in = new Scanner(System.in);
        System.out.println("输入1/0:0表示退出");
        int i = in.nextInt(); ///主线程进入IO阻塞

        if (i == 0) {
            t1.stop();
        }

        System.out.println("main over");
    }


    static class MyTask implements Runnable {

        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

}


2.8.2 中断信号interrupt

在这里插入图片描述

  • nterrupt():发送中断信号(true)
    • 如果线程在阻塞状态,比如sleep(),join(),wait(),这时接收到中断信号会抛出一个异常InterruptException,同时中断信号清除(false)
    • 只是发送信号,不会对线程产生影响
  • static interrupted():得到中断信号(true),然后把中断信号设置成false
  • isInterrupted():得到中断信号,不会清除中断信号
public class Test1 {

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyTask());
        t1.start();

        JOptionPane.showMessageDialog(null, "是否确认向下执行...."); //主线程进入IO阻塞
        t1.interrupt(); //发送中断信号给t1

        System.out.println("main over");
    }

    static class MyTask implements Runnable {

        @Override
        public void run() {
            while (true) {
                System.out.println("a");

                try {
                    Thread.sleep(500000);
                } catch (InterruptedException e) {
                    System.out.println("bbbbbbbbbbbbbbb");
                    e.printStackTrace();
                    Thread.currentThread().interrupt(); //再次发送中断信号,中断信号发给阻塞线程,抛出Interrupt异常,中断信号清除
//                    throw new RuntimeException(e);
                }

                //得到中断信号,优雅的退出
                if (Thread.interrupted()) {
                    break;
                }

            }
        }
    }

}

3 线程原理

3.1 线程的调度和时间片

由于 CPU 的计算频率非常高,每秒计算数十亿次,于是,可以将 CPU 的时间从毫秒的维度进行分段,每一小段叫做一个 CPU 时间片。不同的操作系统、不同的处理器,线程的 CPU 时间片长度都不同。假定操作系统的线程一个时间片的时间长度为 20 毫秒(比如 Windows XP),在一个 2GHz 的 CPU 上,那么一个时间片可以进行计算的次数是: 20 亿/(1000/20) =4 千万次,也就是说,一个时间片内的计算量是非常巨大的。 目前操作系统中主流的线程调度方式大都是:基于 CPU 时间片方式进行线程调度。线程只有得到 CPU 时间片,才能执行指令,处于执行状态;没有得到时间片的线程,处于就绪状态,等待系统分配下一个 CPU 时间片。由于时间片非常短,在各个线程之间快速地切换,表现出来特征是很多个线程在“同时执行”或者“并发执行”。线程的调度模型,目前主要分为两种调度模型:分时调度模型、抢占式调度模型。
(1)分时调度模型——系统平均分配 CPU 的时间片,所有线程轮流占用 CPU。分时调度模型在时间片调度的分配上,所有线程人人平等。
下图就是一个分时调度的简单例子:三个线程,轮流得到 CPU 时间片;一个线程执行时,另外两个线程处于就绪状态
在这里插入图片描述
(2)抢占式调度模型——系统按照线程优先级分配 CPU 时间片。优先级高的线程,优先分配 CPU 时间片;如果所有的就绪线程的优先级相同,那么会随机选择一个;优先级高的线程获取的 CPU 时间片相对多一些。 由于目前大部分操作系统都是使用抢占式调度模型进行线程调度。 Java 的线程管理和调度是委托给了操作系统完成的,与之相对应, Java 的线程调度也是使用抢占式调度模型。

3.2 线程状态

操作系统,线程的状态
在这里插入图片描述
java的线程状态
得到java的线程状态

public Thread.State getState(); //返回当前线程的执行状态,一个枚举类型值

Thread.State 是一个内部枚举类,定义了 6 个枚举常量,分别代表 Java 线程的 6 种状态,具体如下:

public static enum State {
     NEW, //新建
     RUNNABLE, //可执行:包含操作系统的就绪、运行两种状态
     BLOCKED, //阻塞  -> 操作系统线程中的阻塞
     WAITING, //等待  -> 操作系统线程中的阻塞
     TIMED_WAITING, //计时等待 -> 操作系统线程中的阻塞
     TERMINATED; //终止
 }

在Thread.State 定义的 6 种状态中,有四种是比较常见的状态,它们是: NEW 状态、RUNNABLE状态、 TERMINATED 状态、 TIMED_WAITING 状态。
接下来,将线程的 6 种状态以及各种状态的进入条件,做一个总结。
1. NEW 状态 通过 new Thread(…)已经创建线程,但尚未调用 start()启动线程,该线程处于 NEW(新建)状态。虽然前面介绍了4种方式创建线程,但是其中的其他三种方式,本质上都是通过new Thread( )创建的线程,仅仅是创建了不同的 target 执行目标实例(如 Runnable 实例)。
2. RUNNABLE 状态 Java 把就绪(Ready)和执行(Running)两种状态合并为一种状态:可执行(RUNNABLE)状态(或者可运行状态)。调用了线程的 start()实例方法后,线程就处于就绪状态;此线程获取到 CPU 时间片后,开始执行 run( )方法中的业务代码,线程处于执行状态。
在这里插入图片描述
(1)就绪状态就绪状态仅仅表示线程具备运行资格,如果没有被操作系统的调度程序挑选中,线程就永远是就绪状态;当前线程进入就绪状态的条件,大致包括以下几种:

  • 调用线程的 start()方法,此线程进入就绪状态。
  • 当前线程的执行时间片用完。
  • 线程睡眠(sleep)操作结束。
  • 对其他线程合入(join)操作结束。
  • 等待用户输入结束。
  • 线程争抢到对象锁(Object Monitor)。
  • 当前线程调用了 yield 方法出让 CPU 执行权限。

(2)执行状态
线程调度程序从就绪状态的线程中选择一个线程,作为当前线程时线程所处的状态。这也是线程进入执行状态的唯一方式。
3. BLOCKED 状态 处于阻塞(BLOCKED)状态的线程并不会占用 CPU 资源,以下情况会让线程进入阻塞状态:
(1)线程等待获取锁 等待获取一个锁,而该锁被其他线程持有,则该线程进入阻塞状态。当其他线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。
(2) IO 阻塞 线程发起了一个阻塞式 IO 操作后,如果不具备 IO 操作的条件,线程会进入阻塞状态。 IO 包括磁盘 IO、 网络 IO 等。 IO 阻塞的一个简单例子:线程等待用户输入内容后继续执行。
4. WAITING 状态
处于 WAITING(无限期等待)状态的线程不会被分配 CPU 时间片,需要被其他线程显式地唤醒,才会进入就绪状态。线程调用以下 3 种方法,会让自己进入无限等待状态:

  • Object.wait() 方法,对应的唤醒方式为: Object.notify() / Object.notifyAll()。
  • Thread.join() 方法,对应的唤醒方式为:被合入的线程执行完毕。
  • LockSupport.park() 方法,对应的唤醒方式LockSupport.unpark(Thread)。

5. TIMED_WAITING 状态 处于 TIMED_WAITING(限时等待)状态的线程不会被分配 CPU 时间片,如果指定时间之内没有被唤醒,限时等待的线程会被系统自动唤醒,进入就绪状态。以下 3 个方法会让线程进入限时等待状态:

  • Thread.sleep(time) 方法,对应的唤醒方式为: sleep 睡眠时间结束。

  • Object.wait(time) 方 法 , 对 应 的 唤 醒 方 式 为 : 调 用 Object.notify()
    /Object.notifyAll()去主动唤醒,或者限时结束。

  • LockSupport.parkNanos(time)/parkUntil(time) 方法,对应的唤醒方式为:线程调用配套的 LockSupport.unpark(Thread)方法结束,或者线程停止(park)时限结束。

进入 BLOCKED 状态、 WAITING 状态、 TIMED_WAITING 状态的线程都会让出 CPU 的使用权;另外,等待或者阻塞状态的线程被唤醒后,进入 Ready 状态,需要重新获取时间片才能接着运行。
6. TERMINATED 状态 线程结束任务之后,将会正常进入 TERMINATED(死亡)状态;或者说在线程执行过程中发生了异常(而没有被处理),也会导致线程进入死亡状态。

3.3 多线程自增i++和线程执行原理

4个线程自增一个堆(共享的)里的对象的值

public class Test {

    private int i =0;

    public void f1() {
        i++;
    }

}

i++的反编译结果

javap -c Test.class

在这里插入图片描述
在这里插入图片描述
线程产生为没有一个虚拟机栈分配1M内存
-Xss128K:指定没有线程的栈大小是128K
自增类:

public class Plus {

    private int amount = 0;

    public void selfPlus() {
            amount ++;
    }

    public int getAmount() {
        return amount;
    }

}

自增任务类:

public class PlusTask implements Runnable {

    private Plus plus;

    public PlusTask() {
    }

    public PlusTask(Plus plus) {
        this.plus = plus;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100000000; i++) {
            plus.selfPlus();
        }
    }

}

测试:

    public static void main(String[] args) throws InterruptedException {
        Plus plus = new Plus();
        PlusTask plusTask = new PlusTask(plus);

        Thread t1 = new Thread(plusTask);
        Thread t2 = new Thread(plusTask);
        Thread t3 = new Thread(plusTask);
        Thread t4 = new Thread(plusTask);

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

        t1.join();
        t2.join();
        t3.join();
        t4.join();

        System.out.println("实际的值 = " + plus.getAmount());

    }

4 线程同步

4.1 多窗口买票

在这里插入图片描述
Ticket.java

package l_ticket;

public class Ticket {

    private int count = 100;

    /**
     * 查票
     * @return
     */
    public int getCount() {
        return count;
    }

    public int out() {
        return this.count--;
    }

}

WindowTask

package l_ticket;

public class WindowTask implements Runnable {

    private Ticket ticket;

    public WindowTask() {
    }

    public WindowTask(Ticket ticket) {
        this.ticket = ticket;
    }

    @Override
    public void run() {
        while (true) {
            //票数小于0,窗口退出
            if (ticket.getCount() <= 0) {
                break;
            }

            //阻塞,会有超卖
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread().getName() + "\t" + ticket.out());
        }
    }
}

测试代码:

package l_ticket;

public class Test1 {

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

        Thread window1 = new Thread(task, "1窗口");
        Thread window2 = new Thread(task, "2窗口");
        Thread window3 = new Thread(task, "3窗口");
        Thread window4 = new Thread(task, "4窗口");
        Thread window5 = new Thread(task, "2窗口");
        Thread window6 = new Thread(task, "3窗口");
        Thread window7 = new Thread(task, "4窗口");
        Thread window8 = new Thread(task, "4窗口");


        window1.start();
        window2.start();
        window3.start();
        window4.start();
        window5.start();
        window6.start();
        window7.start();
        window8.start();

    }

}

结果:

1窗口	3
2窗口	2
3窗口	0
3窗口	1
4窗口	-1
2窗口	2
4窗口	2
4窗口	-2
1窗口	-3

理想状态如下:
在这里插入图片描述
极端状态:
在这里插入图片描述

  • 超卖问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
  • 解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

在这里插入图片描述

  • 临界区代码块:多个线程同时访问的代码块
  • 共享数据:临界区代码块中,多个线程共享访问的堆里面的数据

加锁:通过共享对象给临界区代码块加锁

4.2 synchronized内置锁

解释两个概念:
1. 临界区资源(共同数据)
表示一种可以被多个线程使用的公共资源或共享数据,但是每一次只能有一个线程使用它。一旦临界区资源被占用,想使用该资源的其他线程必须等待。
2. 临界区代码段(Critical Section)
是每个线程中访问临界资源的那段代码,多个线程必须互斥地对临界区资源进行访问。线程进入临界区代码段之前,必须在进入区申请资源,申请成功之后进行临界区代码段,执行完成之后释放资源。临界区代码段(Critical Section)的进入和退出
在这里插入图片描述
在 Hotspot 虚拟机中, Monitor 是由 C++类 ObjectMonitor 实现, ObjectMonitor 类定义在ObjectMonitor.hpp 文件中,其构造器代码大致如下:


/Monitor 结构体
ObjectMonitor::ObjectMonitor() {
    _header = NULL;
    _count = 0;
    _waiters = 0,
    
    //线程的重入次数
    _recursions = 0;
    _object = NULL;
    
    //标识拥有该 monitor 的线程
    _owner = NULL;
    
    //等待线程组成的双向循环链表
    _WaitSet = NULL;
    _WaitSetLock = 0 ;
    _Responsible = NULL ;
    _succ = NULL ;
    
    //多线程竞争锁进入时的单向链表
    cxq = NULL ;
    FreeNext = NULL ;
    
    //_owner 从该双向循环链表中唤醒线程节点
    _EntryList = NULL ;
    _SpinFreq = 0 ;
    _SpinClock = 0 ;
    OwnerIsThread = 0 ;
}

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

4.2.1 synchronized语法

1. 代码块

synchronized (共享对象(plus,ticket)) {//临界区代码块
    //对共对象的访问(plus)(ticket)
}

比如多窗口售票

  synchronized (ticket) {
                //票数小于0,窗口退出
                if (ticket.getCount() <= 0) {
                    break;
                }

                //阻塞,会有超卖
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                System.out.println(Thread.currentThread().getName() + "\t" + ticket.out());
            }
}

自增1亿次案例
在这里插入图片描述
2. 实例方法上加锁
相当于synchronized(this)

public class SynDemo1 {
    public static void main(String[] args) {
        Car a = new Car();
        Thread t1 = new Thread1(a);
        Thread t2 = new Thread2(a);
        t1.start();
        t2.start();
    }
}

class Thread1 extends Thread {
    private Car a;

    public Thread1(Car a) {
        this.a = a;
    }
    public void run() {
        a.fun1();
    }
}

class Thread2 extends Thread {
    private Car a;

    public Thread2(Car a) {
        this.a = a;
    }
    public void run() {
        a.fun2();
    }
}

class Car {
    public synchronized  void fun1() {
        //synchronized (this) {
            System.out.println("开始打蜡");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("打蜡结束");
        //}
    }

    public void fun2() {
        synchronized (this) {
            try {
                Thread.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("开始抛光");
            System.out.println("抛光结束");
        }
    }
}

3. 静态方法加锁
相当于syncronized(class)

public class SynDemo1 {
    public static void main(String[] args) {
        Car a = new Car();
        Thread t1 = new Thread1(a);
        Thread t2 = new Thread2(a);
        t1.start();
        t2.start();
    }
}

class Thread1 extends Thread {
    private Car a;

    public Thread1(Car a) {
        this.a = a;
    }
    public void run() {
        a.fun1();
    }
}

class Thread2 extends Thread {
    private Car a;

    public Thread2(Car a) {
        this.a = a;
    }
    public void run() {
        a.fun2();
    }
}

class Car {
    public synchronized static void fun1() {
            System.out.println("开始打蜡");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("打蜡结束");
    }

    public static void fun2() {
        synchronized (Car.class) {
            try {
                Thread.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("开始抛光");
            System.out.println("抛光结束");
        }
    }
}

4.2.2 必须有相同的锁

public class MyThread2 extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized (Integer.class) {
                System.out.println("1");

                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                System.out.println("2");
            }
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}

public class MyThread1 extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized (String.class) {
                System.out.println("a");

                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                System.out.println("b");
            }
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}
public class Test1 {

    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();

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

}

4.2.3 死锁

synchronized嵌套时,比如张三在A电话亭想去B电话亭,李四在B电话亭想去A电话亭,这是会发生死锁
a线程锁定一个资源,同时想获取b线程的资源,b线程锁定一个资源,同时想获取a线程的资源。

public class DeadLock {

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (String.class) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                    synchronized (Integer.class) {
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (Integer.class) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                    synchronized (String.class) {

                    }

                }
            }
        });


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

}

4.2.4 CountDownLatch

juc的公共锁

  • 构造函数new CountDownLatch(perms 10)授权数量
  • countDown():授权数量-1
  • await():执行后线程进入阻塞,知道授权数量=0
public class Plus {

    private int count = 0;

    public void selfPlus() {
        count ++;
    }

    public int getCount() {
        return count;
    }
}

package q_countdown;

import java.util.concurrent.CountDownLatch;

public class MyTask implements Runnable {

    Plus plus;
    CountDownLatch countDownLatch;

    public MyTask() {
    }

    public MyTask(Plus plus, CountDownLatch countDownLatch) {
        this.plus = plus;
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        synchronized (plus) {
            for (int i = 0; i < 100000000; i++) {
                plus.selfPlus();
            }
            countDownLatch.countDown(); //-1
        }
    }
}

package q_countdown;

import i_plus.PlusTask;

import java.util.concurrent.CountDownLatch;

public class Test1 {

    public static void main(String[] args) {

        Plus plus = new Plus();
        CountDownLatch countDownLatch = new CountDownLatch(4);
        MyTask task = new MyTask(plus, countDownLatch);

        for (int i = 0; i < 4; i++) {
            new Thread(task).start();
        }

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("理论值 = 400000000");
        System.out.println("实际值 = " + plus.getCount());
        System.out.println("理论值和实际值相差 = " + (400000000 - plus.getCount()));
    }

}

5 线程通讯

5.1 等待唤醒机制

线程的等待唤醒机制
在这里插入图片描述
synchronized(内置锁)

  • EntryList:双向队列:等待锁的多列
  • Owner:正在执行的线程
  • WaitSet:双向队列
    • locko.wait:wait方法一定在synchronize代码块中,把当前线程放到waitset中
    • locko.notify:把waitset队列中的一个随机线程放到entrylist中
    • locko.notifyAll:把waitset队列中的所有线程都放到entrylist中

所有对象都会关联一个c++结构,这个结构ObjectMonier


/Monitor 结构体
ObjectMonitor::ObjectMonitor() {
    _header = NULL;
    _count = 0;
    _waiters = 0,
    
    //线程的重入次数
    _recursions = 0;
    _object = NULL;
    
    //标识拥有该 monitor 的线程
    _owner = NULL;
    
    //等待线程组成的双向循环链表
    _WaitSet = NULL;
    _WaitSetLock = 0 ;
    _Responsible = NULL ;
    _succ = NULL ;
    
    //多线程竞争锁进入时的单向链表
    cxq = NULL ;
    FreeNext = NULL ;
    
    //_owner 从该双向循环链表中唤醒线程节点
    _EntryList = NULL ;
    _SpinFreq = 0 ;
    _SpinClock = 0 ;
    OwnerIsThread = 0 ;
}

5.2 生产者消费模式

  • 包子铺线程负责生产包子
  • 吃货线程负责消费包子
  • 包子对象
    在这里插入图片描述
    1. 共享对象
    Baozi.java
package x_baozi;

public class Baozi {

    private String pier;
    private String xianer;
    private boolean flag = false ;//包子资源 是否存在  包子资源状态

    public String getPier() {
        return pier;
    }

    public void setPier(String pier) {
        this.pier = pier;
    }

    public String getXianer() {
        return xianer;
    }

    public void setXianer(String xianer) {
        this.xianer = xianer;
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public String toString() {
        return "Baozi{" +
                "pier='" + pier + '\'' +
                ", xianer='" + xianer + '\'' +
                ", flag=" + flag +
                '}';
    }
}

2. 生产者
包子铺线程

package x_baozi;

/**
 * 包子铺
 */
public class Baozipu extends Thread {

    private Baozi baozi;

    public Baozipu() {
    }

    public Baozipu(Baozi baozi) {
        this.baozi = baozi;
    }

    @Override
    public void run() {
        int count = 0;
        while (true) {
            synchronized (baozi) {
                if (baozi.isFlag()) {
                    try {
                        baozi.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }

                if (count % 2 == 0) {
                    baozi.setPier("米皮");
                    baozi.setXianer("韭菜鸡蛋");
                } else {
                    baozi.setPier("面皮");
                    baozi.setXianer("猪肉大葱");
                }

                baozi.setFlag(true); //包子已经生成好了的标记
                System.out.println("包子已经生产完毕," + baozi);
                System.out.println("吃货来吃包子吧");
                count++;

                baozi.notify();
            }
        }
    }
}

3. 消费者
吃货线程

package x_baozi;

public class ChiHuo extends Thread {

    private Baozi baozi;

    public ChiHuo() {
    }

    public ChiHuo(Baozi baozi) {
        this.baozi = baozi;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (baozi) {
                if (!baozi.isFlag()) {
                    try {
                        baozi.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }

                System.out.println("吃货正在吃 " + baozi);
                baozi.setFlag(false);
                baozi.notify();
            }
        }
    }
}

4. 测试

package x_baozi;

public class Test1 {

    public static void main(String[] args) {
        Baozi baozi = new Baozi();

        Baozipu baozipu = new Baozipu(baozi); //包子铺线程
        ChiHuo chiHuo = new ChiHuo(baozi); //吃货线程

        chiHuo.start();
        baozipu.start();
    }

}

6 juc显示锁

  • 内置锁:synchronized
  • 现实锁:juc锁

JDK5 版本引入了 java.util.concurrent 并发包,简称为 JUC 包,JUC 出自并发大师 Doug Lea 之手, Doug Lea 对 Java 并发性能的提升做出了巨大的贡献。

6.1 Lock接口

Java 对象锁还存在性能问题。在竞争稍微激烈的情况下, Java 对象锁会膨胀为重量级锁(基于操作系统的 Mutex Lock 实现),而重量级锁的线程阻塞和唤醒操作,需要进程在内核态和用户态之间来回切换,导致其性能非常低。**JUC显示锁因为是纯粹Java语言实现,避免了这些问题,**同时JUC显示锁具备了对象锁不具备的高级特性r如限时抢锁、可中断抢锁、多个等待队列。
显式锁不再作为 Java 内置特性来实现,而是作为 Java 语言可编程特性来实现
java.util.concurrent.locks.Lock接口的主要抽象方法如下
在这里插入图片描述
从 Lock 提供的接口方法可以看出, 显式锁至少比 Java 内置锁多了以下优势:
1. 可中断获取锁
使用 synchronized 关键字获取锁的时候,如果线程没有获取到被阻塞,阻塞期间该线程是不响应中断信号(interrupt)的;而使用 Lock.lockInterruptibly( )方法获取锁时,如果线程被中断,线程将抛出中断异常。
2. 可非阻塞获取锁
使用 synchronized 关键字获取锁时,如果没有成功获取,线程只有被阻塞;而使用Lock.tryLock( )方法获取锁时,如果没有获取成功,线程也不会被阻塞,而是直接返回 false。
3. 可限时抢锁
使用 Lock.tryLock(long time, TimeUnit unit)方法, 显式锁可以设置限定抢占锁的超时时间。而在使用 synchronized 关键字获取锁时,如果不能抢到锁,线程只能无限制阻塞。

6.2 ReentrantLock的基本用法

public class ReenterLockTest implements Runnable {
    public static ReentrantLock lock = new ReentrantLock();
    public static int i = 0;

    public void run() {
        lock.lock();
        lock.lock();
        try {
            for (int j = 0; j < 10000000; j++) {
                i++;
            }
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReenterLockTest r1 = new ReenterLockTest();
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r1);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

6.3 读写锁

读写锁的内部包含了两把锁:一把是为读锁,是一种共享锁;一把写锁,是一种独占锁。读写锁适用于读多写少的并发情况
  • 读、读共享
  • 读、写互斥
  • 写、写互斥
public class ReadWriteLockDemo {
    static CountDownLatch countDownLatch;

    private static Lock lock = new ReentrantLock();
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private int value;

    public Object handleRead(Lock lock) throws InterruptedException {
        lock.lock(); //阻塞
        try {
            Thread.sleep(1000);
            System.out.println("read success");
            countDownLatch.countDown();
            return value;
        } finally {
            lock.unlock();
        }
    }

    public void handleWrite(Lock lock, int index) throws InterruptedException {
        lock.lock(); //阻塞
        try {
            Thread.sleep(1000);
            value = index;
            System.out.println("write success");
            countDownLatch.countDown();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final ReadWriteLockDemo demo = new ReadWriteLockDemo();
        Runnable readRunnable = new Runnable() {
            public void run() {
                try {
                    demo.handleRead(readLock);
//                    demo.handleRead(lock);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Runnable writeRunnable = new Runnable() {

            public void run() {
                try {
                    demo.handleWrite(writeLock, new Random().nextInt());
//                    demo.handleWrite(lock, new Random().nextInt());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        countDownLatch = new CountDownLatch(20);

        long t1 = System.currentTimeMillis(); //1970-1-1 0:0:0:0到现在的毫秒

        for (int i = 0; i < 18; i++) {
            new Thread(readRunnable).start();
        }

        for (int i = 18; i < 20; i++) {
            new Thread(writeRunnable).start();
        }

        countDownLatch.await();
        long t2 = System.currentTimeMillis(); //1970-1-1 0:0:0:0到现在的毫秒
        System.out.println(t2 - t1); //期待20s
    }
}

6.4 Semaphore

Semaphore 是一个是许可管理器,可以用来控制在同一时刻访问共享资源的线程数量,Semaphore 维护了一组虚拟许可,其数量可以通过构造函数的参数指定。线程在访问共享资源前,必须使用 Semaphore 的 acquire 方法获得许可,如果许可数量为 0,该线程则一直阻塞。线程访问完成资源后,必须使用 Semaphore 的 release 方法去释放许可。
1. Semaphore 的主要方法

  • Semaphore(permits):构造一个 Semaphore 实例,初始化其管理的许可数量为 permits 参数值。
  • Semaphore(permits,fair):是否以公平模式(fair 参数是否为 true)进行许可的发放。
  • availablePermits( ):获取 Semaphore 对象可用的许可数量。
  • acquire( ):尝试获取 1 个许可。而当前线程被中断,则会抛出 InterruptedException 异常并终止阻塞
  • acquire(permits):尝试去阻塞的获取 permits 个许可
  • acquierUninterruptibly( ):阻塞的过程不可中断,直到成功获取 1 许可。
  • acquireUninterruptibly(permits):获取 permits 个许可,阻塞的过程不可中断
  • tryAcquire():非阻塞获取1个许可
  • tryAcquire(permits):非阻塞获取 permits 个许可。
  • tryAcquire(timeout,TimeUnit):限时获取许可
  • release( ):释放 1 个可用的许可
  • release(permits):释放 permits 个可用的许可。
  • drainPermits( ):当前线程获得剩余的所有可用许可。
  • hasQueuedThreads( ):判断当前 Semaphore 对象上是否存在正在等待许可的线程。
  • getQueueLength( ):获取当前 Semaphore 对象上正在等待许可的线程数量。

2. Semaphore 示例
假设有 10 个人在银行办理业务,只有 2 个工作窗口,使用 Semaphore 模拟银行排队,代码如下:

public class SemaphoreTest {

    public static void main(String[] args) throws InterruptedException {
        //线程池,用于多线程模拟测试
        final CountDownLatch countDownLatch = new CountDownLatch(10);
        //创建信号量,含有2个许可
        final Semaphore semaphore = new Semaphore(2);
        AtomicInteger index = new AtomicInteger(0);
        //创建Runnable可执行实例
        Runnable r = () ->
        {
            try
            {
                //抢占一个许可
                semaphore.acquire(1);

                //模拟业务操作: 处理排队业务
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                System.out.println(sdf.format(new Date()) + ", 受理处理中...,服务号: " + index.incrementAndGet());
                Thread.sleep(1000);
                //释放一个信号
                semaphore.release(1);
            } catch (Exception e)
            {
                e.printStackTrace();
            }
            countDownLatch.countDown();
        };


        //创建4条线程
        Thread[] tArray = new Thread[10];
        for (int i = 0; i < 10; i++)
        {
            tArray[i] = new Thread(r, "线程" + i);
        }
        //启动4条线程
        for (int i = 0; i < 10; i++)
        {
            tArray[i].start();
        }


        countDownLatch.await();
    }

}

7 线程池原理和实战

Java 线程需要 JVM 和 操作系统配合完成大量的工作:

  1. 必须为线程堆栈分配和初始化大量内存块,其中包含至少 1M 的栈内存。
  2. 需要进行系统调用,以便在操作系统中创建和注册本地线程。

频繁创建和销毁线程非常低效。通过使用线程池可以提升性能,方便线程的管理。
在这里插入图片描述

7.1 JUC的线程池结构

JUC中的线程池如图所示:

在这里插入图片描述
**说明:**JUC 就是 java.util .concurrent 工具包的简称,该工具包是从 JDK 1.5 开始加入到 JDK,用于完成高并发、处理多线程的一个工具包。

1. Executor
它是 Java 异步目标任务的“执行者”接口,其目标是来执行目标任务。“执行者” Executor提供了 execute()接口来执行已提交的 Runnable 执行目标实例。 Executor 作为执行者的角色,存在的目的是“任务提交者”与“任务执行者”分离开来的机制。它只包含一个函数式方法:

void execute(Runnable command)

2. ExecutorService
ExecutorService 继承于 Executor。它是 Java 异步目标任务的“执行者服务“接口,它对外提供异步任务的接收服务, ExecutorService 提供了“接收异步任务、并转交给执行者”的方法,如submit 系列方法、 invoke 系列方法等等。具体如下:

//向线程池提交单个异步任务
 Future submit(Callable task);
//向线程池提交批量异步任务
 List<Future> invokeAll(Collection<? extends Callable> tasks) throws InterruptedException;

3. AbstractExecutorService
AbstractExecutorService 是 一 个 抽 象 类 , 它 实 现 了 ExecutorService 接 口 。AbstractExecutorService 存在的目的是为 ExecutorService 中的接口提供了默认实现。
4. ThreadPoolExecutor
ThreadPoolExecutor 就是“线程池”实现类,它继承于 AbstractExecutorService 抽象类。 ThreadPoolExecutor 是 JUC 线程池的核心实现类。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了一些基础的数据统计,方便线程的管理和监控。
5. ScheduledExecutorService
ScheduledExecutorService 是一个接口,它继承于于 ExecutorService。它是一个可以完成“延时”“周期性”任务的调度线程池接口,其功能和 Timer/TimerTask 类似。
6. ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor 继承于 ThreadPoolExecutor,它提供了 ScheduledExecutorService线程池接口中“延时执行”和“周期执行”等抽象调度方法的具体实现。
ScheduledThreadPoolExecutor 类 似 于 Timer , 但 是 在 高 并 发 程 序 中ScheduledThreadPoolExecutor 的性能要优于 Timer。
7. Executors
Executors 是 个 静 态 工 厂 类 , 它 通 过 静 态 工 厂 方 法 返 回 ExecutorService 、
ScheduledExecutorService 等线程池实例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法。

7.2 Executors创建线程池

Java 通过 Executors 工厂类提供四种快捷创建线程池的方法,具体如表所示。

在这里插入图片描述
1. newSingleThreadExecutor 创建“单线程化线程池”

该方法用于创建一个“单线程化线程池”,也就是只有一条线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池,能保证所有任务按照指定顺序(如FIFO)执行。代码具体如下:
@Slf4j
public class ThreadPoolDemo {

    public static final int SLEEP_GAP = 500;
    public static final int MAX_TURN = 5;

    //异步的执行目标类
    public static class TargetTask implements Runnable {
        static AtomicInteger taskNo = new AtomicInteger(1);
        protected String taskName;

        public TargetTask() {
            taskName = "task-" + taskNo.get();
            taskNo.incrementAndGet();
        }

        public void run() {
            log.info("任务:{} doing", taskName);
            ThreadUtil.sleepMilliSeconds(SLEEP_GAP);
            log.info("{} 运行结束.", taskName);
        }

        @Override
        public String toString() {
            return "TargetTask{" + taskName + '}';
        }
    }

    //测试用例:只有一条线程的线程池
    @Test
    public void testSingleThreadExecutor() {
        ExecutorService pool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < MAX_TURN; i++) {
            pool.execute(new TargetTask());
        }
        ThreadUtil.sleepSeconds(1000);
        //关闭线程池
        pool.shutdown();
    }
}

运行以上代码,部分结果截取如下:

10:14:00.798 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-1 doing
10:14:01.304 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-1 运行结束.
10:14:01.304 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-2 doing
10:14:01.804 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-2 运行结束.
10:14:01.804 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-3 doing
10:14:02.305 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-3 运行结束.
10:14:02.305 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-4 doing
10:14:02.807 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-4 运行结束.
10:14:02.807 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-5 doing
10:14:03.308 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-5 运行结束.

newSingleThreadExecutor线程池特点:

  • 单线程化的线程池中的任务,是按照提交的次序,顺序执行的。
  • 池中的唯一线程的存活时间是无限的。
  • 当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列(无界的阻塞队列)。

2. newFixedThreadPool 创建“固定数量的线程池”
该方法用于创建一个“固定数量的线程池”,其唯一的参数用于设置池中线程的“固定数量”。演示代码如下:

    //测试用例:只有3条线程固定大小的线程池
    @Test
    public void testNewFixedThreadPool() throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < MAX_TURN; i++) {
            pool.execute(new TargetTask());
        }
        ThreadUtil.sleepSeconds(1000);
        //关闭线程池
        pool.shutdown();
    }

执行以上测试用例,部分结果截取如下:

10:29:06.186 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-1 doing
10:29:06.186 [pool-1-thread-3] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-3 doing
10:29:06.186 [pool-1-thread-2] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-2 doing
10:29:06.690 [pool-1-thread-3] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-3 运行结束.
10:29:06.690 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-1 运行结束.
10:29:06.690 [pool-1-thread-2] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-2 运行结束.
10:29:06.690 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-5 doing
10:29:06.690 [pool-1-thread-3] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-4 doing
10:29:07.191 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-5 运行结束.
10:29:07.191 [pool-1-thread-3] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-4 运行结束.

newFixedThreadPool线程池的特点:

  • 如果线程数没有达到“固定数量”,则每次提交一个任务池内就创建一个新线程,直到线程达到线程池的固定的数量。
  • 线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  • 在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)。

3. newCachedThreadPool 创建“可缓存线程池”

该方法用于创建一个“可缓存线程池”,如果线程池内的某些线程无事可干成为空闲线程,“可缓存线程池”可灵活回收这些空闲线程。

    //测试用例:“可缓存线程池”
    @Test
    public void testNewCacheThreadPool() {
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            pool.execute(new TargetTask());
            pool.submit(new TargetTask());
        }
        sleepSeconds(1000);
        //关闭线程池
        pool.shutdown();
    }

运行以上测试用例,结果如下:

[pool-1-thread-9]:任务:task-9 doing
[pool-1-thread-1]:任务:task-1 doing
[pool-1-thread-3]:任务:task-3 doing
[pool-1-thread-5]:任务:task-5 doing
[pool-1-thread-6]:任务:task-6 doing
[pool-1-thread-2]:任务:task-2 doing
[pool-1-thread-7]:任务:task-7 doing
[pool-1-thread-8]:任务:task-8 doing
[pool-1-thread-10]:任务:task-10 doing
[pool-1-thread-4]:任务:task-4 doing
[pool-1-thread-3]:task-3 运行结束.
[pool-1-thread-8]:task-8 运行结束.
[pool-1-thread-10]:task-10 运行结束.
[pool-1-thread-4]:task-4 运行结束.
[pool-1-thread-1]:task-1 运行结束.
[pool-1-thread-7]:task-7 运行结束.
[pool-1-thread-9]:task-9 运行结束.
[pool-1-thread-6]:task-6 运行结束.
[pool-1-thread-2]:task-2 运行结束.
[pool-1-thread-5]:task-5 运行结束.

newCachedThreadPool 特点

  • 在接收新的异步任务 target 执行目标实例时,如果池内所有线程繁忙,此线程池会添加新线程来处理任务。
  • 此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小
  • 如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,那么就会回收空闲(60 秒不执行任务)线程。
  • “可缓存线程池”的弊端:线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能导致创线程过多会而导致资源耗尽。

4. newScheduledThreadPool 创建“可调度线程池”

该方法用于创建一个“可调度线程池”,一个提供“延时”和“周期性”任务的调度功能的ScheduledExecutorService 类型的线程池。 Executors 提供了多个创建“可调度线程池”工厂方法,
部分如下:

//方法一:创建一个可调度线程池,池内仅含有一条线程
public static ScheduledExecutorService newSingleThreadScheduledExecutor();
//方法二:创建一个可调度线程池,池内含有 N 条线程, N 的值为输入参数 corePoolSize
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) ;

newSingleThreadScheduledExecutor 工厂方法所创建的仅含有一条线程的可调度线程池,适用于 调 度 串 行 化 任 务 , 也 就 是 一 个 一 个 任 务 的 串 行 化 调 度 执 行 。 使 用Executors.newScheduledThreadPool(int corePoolSize)快捷工厂方法创建一个“可调度线程池”的测试用例,其代码具体如下:

    //测试用例:“可调度线程池”
    @Test
    public void testNewScheduledThreadPool() {
        ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
        for (int i = 0; i < 2; i++) {
            scheduled.scheduleAtFixedRate(new TargetTask(),
                    0, 5000, TimeUnit.MILLISECONDS);
        }
        ThreadUtil.sleepSeconds(1000);
        //关闭线程池
        scheduled.shutdown();
    }

运行程序,部分结果截取如下:

10:51:52.389 [pool-1-thread-2] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-2 doing
10:51:52.389 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-1 doing
10:51:52.894 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-1 运行结束.
10:51:52.894 [pool-1-thread-2] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-2 运行结束.
10:51:57.387 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-1 doing
10:51:57.387 [pool-1-thread-2] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-2 doing
10:51:57.889 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-1 运行结束.
10:51:57.889 [pool-1-thread-2] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-2 运行结束.

scheduleAtFixedRate 方法的定义如下:
public ScheduledFuture<?> scheduleAtFixedRate(
    Runnable command, //异步任务 target 执行目标实例;
    long initialDelay, //首次执行延时;
    long period, //两次开始执行最小间隔时间;
    TimeUnit unit //所设置的时间的计时单位,如 TimeUnit.SECONDS 常量;
);
scheduleWithFixedDelay方法的定义如下:
public ScheduledFuture<?> scheduleWithFixedDelay(
    Runnable command,//异步任务 target 执行目标实例;
    long initialDelay, //首次执行延时;
    long delay, //前一次执行结束到下一次执行开始的间隔时间(间隔执行延迟时间);
    TimeUnit unit //所设置的时间的计时单位,如 TimeUnit.SECONDS 常量;
);

两个方法区别:

  • scheduleAtFixedRate 是从任务开始时算起
  • scheduleWithFixedDelay 是从任务结束时算起

7.3 线程池的标准创建方式

大部分企业的开发规范,都会禁止使用快捷线程池,要求通过标准构造器 ThreadPoolExecutor 去构造工作线程池。要求使用ThreadPoolExecutor创建线程池
在这里插入图片描述

// 使用标准构造器,构造一个普通的线程池
public ThreadPoolExecutor(
    int corePoolSize, // 核心线程数,即使线程空闲(Idle),也不会回收;
    int maximumPoolSize, // 线程数的上限;
    long keepAliveTime, TimeUnit unit, // 线程最大空闲(Idle)时长
    BlockingQueue workQueue, // 任务的排队队列
    ThreadFactory threadFactory, // 新线程的产生方式
    RejectedExecutionHandler handler) // 拒绝策略

1. 核心和最大线程数量

corePoolSize 核心线程池数,maximumPoolSize 最大线程数。线程池规则如下:

(1)在线程池接收到的新任务,并且当前工作线程数少于corePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求,直到线程数达到 corePoolSize。
(2)当前工作线程数多于 corePoolSize 数量,但小于 maximumPoolSize 数量,则仅当任务排队队列已满时,才会创建新线程。 通过设置 corePoolSize 和 maximumPoolSize 相同,可以创建一个固定大小的线程池。
(3)当 maximumPoolSize 被设置为无界值(如 Integer.MAX_VALUE)时,线程池可以接收任意数量的并发任务。
( 4 ) corePoolSize 和 maximumPoolSize 不 仅 能 在 线 程 池 构 造 时 设 置 , 也 可 以 使 用setCorePoolSize 和 setMaximumPoolSize 两个方法进行动态更改。

2. BlockingQueue

BlockingQueue(阻塞队列)的实例用于暂时接收到的异步任务,如果线程池的核心线程都在忙,则所接收到的目标任务,缓存在阻塞队列中。

3. keepAliveTime
默认情况下, Idle 超时策略仅适用于存在超过 corePoolSize 线程的情况。 但是如果调用了allowCoreThreadTimeOut(boolean)方法,并且传入了参数 true,则 keepAliveTime 参数所设置的 Idle超时策略也将被应用于核心线程。

7.4 线程池的任务调度流程

线程池的任务调度流程如下:
在这里插入图片描述

(1)当前工作线程数小于核心线程池数量,执行器总是优先创建一个任务线程,而不是从线程队列中取一个空闲线程。
(2)线程池中任务数大于核心线程池数,任务将被加入到阻塞队列中,一直到阻塞队列满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不是为新任务创建一个新线程。
(3)当完成一个任务的执行时,执行器总是优先从阻塞队列中取下一个任务,并开始其执行,一直到阻塞队列为空,其中所有的缓存任务被取光。
(4)在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。
(5)在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出 maximumPoolSize。如果线程池的线程总数超时 maximumPoolSize,则线程池会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。

在创建线程池时,如果线程池的参数如核心线程数量、最大线程数量、 BlockingQueue 等配置不合理,就会出现任务不能被正常调度的问题。
下面是一个错误的线程池配置示例:

    @org.junit.Test
    public void testThreadPoolExecutor() {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, //corePoolSize
                100, //maximumPoolSize
                100, //keepAliveTime
                TimeUnit.SECONDS, //unit
                new LinkedBlockingDeque<>(100));//workQueue

        for (int i = 0; i < 5; i++) {
            final int taskIndex = i;
            executor.execute(() ->
            {
                Print.tco("taskIndex = " + taskIndex);
                try {
                    Thread.sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        //每隔 1 秒,输出线程池的工作任务数量、总计的任务数量
        while (true) {
            Print.tco("- activeCount:" + executor.getActiveCount() +
                    " - taskCount:" + executor.getTaskCount());
            sleepSeconds(1);
        }
    }

在这里插入图片描述
运行程序,结果如下:

12:01:49.667 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - taskIndex = 0
12:01:49.667 [pool-1-thread-2] INFO com.lxs.demo.a_thread.ThreadPoolDemo - taskIndex = 1
12:01:50.671 [main] INFO com.lxs.demo.a_thread.ThreadPoolDemo - - activeCount:2 - taskCount:5
12:01:51.671 [main] INFO com.lxs.demo.a_thread.ThreadPoolDemo - - activeCount:2 - taskCount:5

虽然设置了maximumPoolSize为100的线程池,提交了2个任务只会执行2个

7.5 线程池的拒绝策略

使用有界队列的时候,如果队列满了,提交任务到线程池的时候就会被拒绝。总体来说,任务被拒绝有两种情况:

(1)线程池已经被关闭。
(2)工作队列已满且 maximumPoolSize 已满。
无 论 以 上 哪 种 情 况 任 务 被 拒 , 线 程 池 都 会 调 用 RejectedExecutionHandler 实 例 的rejectedExecution 方法。 RejectedExecutionHandler 是拒绝策略的接口, JUC 为该接口提供了以下几种实现:

  • AbortPolicy:拒绝策略
  • DiscardPolicy:抛弃策略
  • DiscardOldestPolicy:抛弃最老任务策略
  • CallerRunsPolicy:调用者执行策略
  • 自定义策略

JUC 线程池拒绝策略的接口与类之间的关系图
在这里插入图片描述
(1) AbortPolicy
使用该策略时,如果线程池队列满了则新任务被拒绝,并且会抛出 RejectedExecutionException异常。该策略是线程池的默认的拒绝策略。
(2) DiscardPolicy
该策略是 AbortPolicy 的 Silent(安静)版本,如果线程池队列满了,新任务会直接被丢掉,并且不会有任何异常抛出。
(3) DiscardOldestPolicy
抛弃最老任务策略,也就是说如果队列满了,会将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列。因为队列是队尾进队头出,队头元素是最老的,所以每次都是移除对头元素后再尝试入队。
(4) CallerRunsPolicy
调用者执行策略。在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。
在以上的四种内置策略中,线程池默认的拒绝策略为 AbortPolicy,如果提交的任务被拒绝,线程池抛出 RejectedExecutionException 异常,该异常是非受检异常(运行时异常),很容易忘记捕获。如果关心任务被拒绝的事件,需要在提交任务时捕获 RejectedExecutionException 异常。
(5)自定义策略
如果以上拒绝策略都不符合需求,则可自定义一个拒绝策略,实现 RejectedExecutionHandler接口的 rejectedExecution 方法即可。
自定义拒绝策略的例子,代码如下:

    //自定义拒绝策略
    public static class CustomIgnorePolicy implements RejectedExecutionHandler {
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            // 可做日志记录等
            Print.tco(r + " rejected; " + " - getTaskCount: " + e.getTaskCount());
        }
    }

    @org.junit.Test
    public void testCustomIgnorePolicy() {
        int corePoolSize = 2; //核心线程数
        int maximumPoolSize = 4;  //最大线程数
        long keepAliveTime = 10;
        TimeUnit unit = TimeUnit.SECONDS;
        //最大排队任务数
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
        //线程工厂
        ThreadFactory threadFactory = new SimpleThreadFactory();
        //拒绝和异常策略
        RejectedExecutionHandler policy = new CustomIgnorePolicy();
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime, unit,
                workQueue,
                threadFactory,
                policy);

        // 预启动所有核心线程
        pool.prestartAllCoreThreads();
        for (int i = 1; i <= 10; i++) {
            pool.execute(new TargetTask());
        }
        //等待10秒
        sleepSeconds(10);
        Print.tco("关闭线程池");
        pool.shutdown();
    }

运行以上代码,大致结果如下:

在这里插入图片描述

7.6 线程池状态

1. 线程池的状态
线程池总共存在 5 种状态,定义在 ThreadPoolExecutor 类中,具体代码如下:

package java.util.concurrent;
...省略 import
public class ThreadPoolExecutor extends AbstractExecutorService {
    // runState is stored in the high-order bits
    private static final int RUNNING = -1 << COUNT_BITS;
    private static final int SHUTDOWN = 0 << COUNT_BITS;
    private static final int STOP = 1 << COUNT_BITS;
    private static final int TIDYING = 2 << COUNT_BITS;
    private static final int TERMINATED = 3 << COUNT_BITS;
    …省略其他
}

线程池的 5 种状态,具体如下:
(1) RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
(2)SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。
(3) STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。
(4) TIDYING:该状态下所有任务都已终止或者处理完成,将会执行terminated( )钩子方法。
(5) TERMINATED:执行完terminated( )钩子方法之后的状态。

在这里插入图片描述
线程池的状态转换规则为:
(1)线程池创建之后状态为 RUNNING。
(2)执行线程池的 shutdown 实例方法,会使线程池状态从 RUNNING 转变为 SHUTDOWN。
(3)执行线程池的 shutdownNow 实例方法,会使线程池状态从 RUNNING 转变为 STOP。
(4)当线程池处于 SHUTDOWN 状态,执行器 shutdownNow 方法,会将其状态转变为 STOP状态。
(5)等待线程池的所有工作线程停止,工作队列清空之后,线程池状态会从 STOP 转变为TIDYING。
(6)执行完 terminated( ) 钩子方法之后,线程池状态从 TIDYING 转变为 TERMINATED 。

public class ThreadPoolDemo {

    CountDownLatch countDownLatch = new CountDownLatch(MAX_TURN);
    public static final int SLEEP_GAP = 500;
    public static final int MAX_TURN = 5;

    //异步的执行目标类
    public static class TargetTask implements Runnable {
        CountDownLatch countDownLatch;
        static AtomicInteger taskNo = new AtomicInteger(1);
        protected String taskName;

        public TargetTask() {
            taskName = "task-" + taskNo.get();
            taskNo.incrementAndGet();
        }

        public TargetTask(CountDownLatch countDownLatch) {
            this();
            this.countDownLatch = countDownLatch;
        }

        public void run() {
            System.out.println("任务:" + taskName + " doing");

            try {
                Thread.sleep(SLEEP_GAP);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(taskName + " 运行结束.");
            countDownLatch.countDown();
        }

        @Override
        public String toString() {
            return "TargetTask{" + taskName + '}';
        }
    }

    //测试用例:只有一条线程的线程池
    @Test
    public void testSingleThreadExecutor() {
        ExecutorService pool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < MAX_TURN; i++) {
            pool.execute(new TargetTask(countDownLatch));
        }

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

//        //关闭线程池
        pool.shutdown();
    }

    //测试用例:只有3条线程固定大小的线程池
    @Test
    public void testNewFixedThreadPool() throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < MAX_TURN; i++) {
            pool.execute(new TargetTask(countDownLatch));
        }

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //关闭线程池
        pool.shutdown();
    }

    /**
     * CachedThredPool:线程数没有限制
     * @throws InterruptedException
     */
    @Test
    public void testNewCachedThreadPool() throws InterruptedException {
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < MAX_TURN; i++) {
            pool.execute(new TargetTask(countDownLatch));
        }

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //关闭线程池
        pool.shutdown();
    }


    //测试用例:“可调度线程池”
    @Test
    public void testNewScheduledThreadPool() {
        ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
        for (int i = 0; i < 2; i++) {
            scheduled.scheduleAtFixedRate(new TargetTask(countDownLatch),
                    0, 5000, TimeUnit.MILLISECONDS);
        }

        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //关闭线程池
        scheduled.shutdown();
    }


    @Test
    public void testThreadPoolExecutor() {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, //corePoolSize
                4, //maximumPoolSize
                100, //keepAliveTime
                TimeUnit.SECONDS, //unit
                new LinkedBlockingDeque<>(2), new ThreadPoolExecutor.CallerRunsPolicy());//workQueue

        for (int i = 0; i < 7; i++) {
            final int taskIndex = i;

            Runnable task = new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() +  "   taskIndex = " + taskIndex );
                    try {
                        Thread.sleep(10000);
//                        Thread.sleep(Long.MAX_VALUE);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };

            executor.execute(task);
        }
        while (true) {
            //每隔 1 秒,输出线程池的工作任务数量、总计的任务数量
            System.out.printf("- activeCount: %d - taskCount: %d\r\n", executor.getActiveCount(), executor.getTaskCount());
            ThreadUtil.sleepSeconds(1);
        }
    }

}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值