深入理解java线程

1. 并发编程之多线程基础

1.1 线程与进程区别

进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。

线程:单个进程中执行中每个任务就是一个进程。线程是进程中执行运算的最小单位。

一个线程只能属于一个进程,但是一个进程可以有多个线程。多线程处理就是允许一个进程中在同一时刻执行多个任务。

线程是一种轻量级的进程,与进程相比,线程给操作系统带来的创建、维护和管理的负担要轻,意味着线程的代价或开销比较小。

每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。线程是一组命令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。

使用线程可以把占据时间长的程序中的任务放到后台去处理,程序的运行速度可能加快,在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了、在这种情况下可以释放一些针对的资源如内存占用等等。

如果有大量的线程,会影响性能,因为操作系统需要在他们之间切换,更多的线程需要更多的内存空间,线程的终止需要考虑其对程序运行的影响。通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生。

总结:进程是所有线程的集合,每一个线程是进程中的一条执行路径

1.2 为什么要使用多线程?

多线程的好处是提高程序的效率,充分发挥多核计算机的优势

每当创建一个线程的时候,系统都需要分配一定资源,并且去维护这个线程

CPU同一时间,只能处理一个线程

1.3 多线程应用场景?

答:主要体现在多线程提高程序效率

多线程最多的场景:tomcat的web服务器本身;各种专用服务器(如游戏服务器);
每次创建connector连接器时,tomcat默认会创建10个线程,每个服务器会有很多个用户来访问,每次来访问都需要独立去请求容器访问资源去请求数据库,每一个请求都独立进行这一轮,所以来一个请求都是单独开一个线程去接待这个请求。当线程到两三百的时候,机器的资源就不够分配了,所以tomcat的最大并发就是两三百(随着机器的性能来决定并发量)。

多线程的常见应用场景:
1、后台任务,例如:定时向大量(100w以上)的用户发送邮件;
2、异步处理,例如:统计结果、记录日志、发送短信等;
	add(){
		==> 创建一个新的线程去记录日志
		==> 直接返回
	}
3、分布式计算、分片下载、断点续传

小结:任务量比较大,通过多线程可以提高效率时
	 需要异步处理时
	 占用系统资源,造成阻塞的工作时
	 都可以采用多线程提高效率

1.4 多线程创建方式

1.4.1.继承Thread类
  • 重写run方法
public class A1_线程创建_继承Thread {

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

    static class MyThread extends Thread{
        @Override
        public void run() {
            for(int i = 0; i < 100; i++){
                System.out.println("打印输出 = " + i);
            }
        }
    }
}
1.4.2 实现Runnable接口

建议使用实现接口的方式,因为java不支持多继承,如果当前类继承thread类,就不能继承别的方法了,而且使用继承的方式还可以使用一个thread对象来构建多个任务

  • 方式一:原始方法

    public class A2_线程创建_实现Runnable {
    
        public static void main(String[] args) {
            Thread thread = new Thread(new MyRunnable());
            thread.start();
        }
    
        static class MyRunnable implements Runnable{
            public void run() {
                for(int i = 0; i < 100; i++){
                    System.out.println("打印输出 = " + i);
                }
            }
        }
    }
    
  • 方式二:匿名内部类

    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                for(int i = 0; i < 100; i++){
                    System.out.println("打印输出 = " + i);
                }
            }
        });
        thread.start();
    }
    
  • 方式三:lambda表达式

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for(int i = 0; i < 100; i++){
                System.out.println("打印输出 = " + i);
            }
        });
        thread.start();
    }
    
1.4.3 实现Callable接口

实现Callable接口,实现带返回值的任务

public class A3_线程创建_实现Callable {

    public static void main(String[] args) {
        //FutureTask用来包装任务,可以用于获取执行结果
        FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());

        //创建线程执行线程任务
        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            //获取线程的执行结果
            Integer num = futureTask.get();
            System.out.println("得到线程处理结果: " + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    //实现Callable接口,实现带返回值的任务
    static class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            int num = 0;
            for (int i = 0; i < 100; i++) {
                System.out.println("打印输出 = " + i);
                num += i;
            }
            return num;
        }
    }

}

结果:
    ...
    打印输出 = 98
	打印输出 = 99
	得到线程处理结果: 4950

1.5 用户线程与守护线程

java分为两种线程:用户线程和守护线程

  • 守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
  • 守护线程和用户线程没什么本质的区别:唯一不同的就是虚拟机的离开;如果用户线程已经全部退出运行,只剩下守护线程存在,虚拟机也就退出了。因为没有了被守护者,守护线程也就没有工作可做,也就没有继续运行程序的必要了。
  • 将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。在使用守护线程时需要注意:
    thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出一个IllegalThreadStateException异常。
    你不能把正在运行的常规线程设置为守护线程。

例子:

public class B1_线程基础_用户线程守护线程 {
    public static void main(String[] args) {
        //守护线程
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(200);
                    System.out.println("t1输出 = " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });
        t1.setDaemon(true);//设置为守护线程
        t1.start();

        //用户线程
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t2输出 = " + i);
            }
        });
        t2.start();

    }

}

按理说t1的休眠时间比t2时间长,等t2运行完毕后,t1会继续运行

但是设置了t1.setDaemon(true)后,等t2运行完,t1也就停止了,这就是守护线程

1.6 线程优先级

线程的切换是由线程调度控制的,我们无法通过代码来干涉,但是我们通过提高线程的优先级来最大程度的改善线程获取时间片的几率。
线程的优先级被划分为10级,值分别为1-10,其中1最低,10最高。线程提供了3个常量来表示最低,最高,以及默认优先级:

    /**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;

    /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;

	void setPriority(int newPriority):设置线程的优先级

demo:

public class B1_线程基础_线程优先级 {
    public static void main(String[] args) {
        //守护线程
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                System.out.println("t1输出 = " + i);
            }
        });
        t1.setPriority(1);
        t1.start();

        //用户线程
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                System.out.println("t2输出 = " + i);
            }
        });
        t2.setPriority(10);
        t2.start();
    }
}

1.7 线程的停止

原先是使用stop()方法,现在该方法已经被废弃了,因为这是让线程内部抛异常的方式让其停止,如果该线程有事务,那么事务就会回滚,不符合要求

t.isInterrupted(); //线程是否中断
t.interrupt();	//中断线程

demo:

public class B4_线程基础_停止线程 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            long time = 0L;
            while (true) {
                boolean interrupted = Thread.currentThread().isInterrupted();
                if (interrupted) {
                    break;
                } else {
                    System.out.println("线程执行中……" + time);
                    time += 1;
                }
            }
        });
        t1.start();

        //2秒之后停止线程
        Thread.sleep(2000); 
        t1.interrupt();
    }
}

1.8 线程的生命周期

public enum State {
        
        NEW,		//新建了一个线程

        RUNNABLE,	//可运行状态

        BLOCKED,//阻塞 给某个代码上了一个锁,在等待其他线程争锁的时候,就处于阻塞状态
        WAITING,	//阻塞	调用wait()方法时,就处于该状态
        TIMED_WAITING,	//阻塞 有时间的等待 sleep()

        TERMINATED;
    }

在这里插入图片描述

  1. 【新建状态】new Thread(); 新建了一个线程,此时还没有运行,处于新建的窗状态。
  2. 【运行状态】调用start()方法后,处于就绪状态,与cpu争夺时间片,争取到了之后,处于运行状态,在有限的时间里,能运行多少运行多少,然后时间片结束,停止运行,又会处于就绪状态,争取下一次的时间片,这个两个过程就是可运行状态。
  3. 【阻塞状态】在运行状态的时候,线程被阻塞,当BLOCKED抢到了锁、WAITING被notify()方法唤醒、TIMED_WAITING时间结束时,就会处于就绪状态,此时又会去与cpu抢夺时间片,进行第二步的操作。
  4. 【销毁状态】当一个线程执行完毕后,任务就完毕了。

1.9 常用API - join方法

thread的方法 join():
线程调用了join方法,那么就要一直运行到该线程运行结束,才会运行其他线程,这样可以控制线程执行顺序。

A{
	B.join(); //让B线程先执行
}
B线程执行完之后,再运行A线程

demo:

//有三个线程,如何保证t1运行完,然后t2运行完,然后t3运行完
public class B3_线程基础_join方法 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("t1 ======> " + i);
            }
        });
        Thread t2 = new Thread(() -> {
            //先让t1执行完
            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println("t2 ======> " + i);
            }
        });
        Thread t3 = new Thread(() -> {
            //先让t2执行完
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            for (int i = 0; i < 10; i++) {
                System.out.println("t3 ======> " + i);
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

2. 并发编程之线程安全

2.1 什么是线程安全?

2.1.1 线程安全问题
 1. 什么是线程安全问题?
 	多线程操作共享变量,导致访问数据出问题
 	
 2. 出现线程安全问题的条件
 	有多个线程
 	有共享数据
 	其中一个线程修改了共享数据

demo:

public class C1_线程安全_售票案例 {

    //总票数
    static int ticket = 100;

    public static void main(String[] args) {

        Runnable runnable = () -> {
            //循环买票
            while(true){
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if (ticket>0) {
                    ticket--;
                    System.out.println(Thread.currentThread().getName() + "卖了一张票,剩余:" + ticket);
                }else {
                    System.out.println("卖完了,没票了……");
                    break;
                }
            }
        };

        Thread t1 = new Thread(runnable,"窗口1");
        Thread t2 = new Thread(runnable,"窗口2");
        Thread t3 = new Thread(runnable,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }

}

在这里插入图片描述

这个例子说明线程不安全,出现了超卖的现象

为什么会出现这种情况呢?要从JMM的内存模型设计开始。

2.2 JMM内存模型

java内存模型(即java Memory Model,简称JMM)。
JMM本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),用于存储线程私有的数据。而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。
线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写会主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:

在这里插入图片描述

2.3 java并发编程三大特性

正因为有了JMM内存模型,以及java语言的设计,所以在并发编程当中我们可能会遇到下面几种问题。
这几种问题,成为并发编程的三大特性:

2.3.1 原子性

原子性,寄一个操作或多个操作,要么全部执行完并且在执行的过程中不被打断,要么全部不执行。(提供了互斥访问,在同一时刻只有一个线程进行访问)

可以通过锁的方式解决:


synchronized (o) {
    if (ticket > 0) {
        ticket--;
        System.out.println(Thread.currentThread().getName() + "卖了一张票,剩余:" + ticket);
    } else {
        System.out.println("卖完了,没票了……");
        break;
    }
}
2.3.2 可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没有刷新到主存,线程2又使用了i,那么这个i肯定还是之前的,线程1对变量的修改线程2没看到

demo:

public class C2_线程安全_可见性案例 {

    private static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {

        new Thread(() -> {
            System.out.println("1号线程启动,执行while循环");
            long num = 0;
            while (!flag){
                num ++;
            }
            System.out.println("num = " + num);
        }).start();

        Thread.sleep(1000);
        new Thread(() -> {
            System.out.println("2号线程启动,更改变量 flag ");
            changeFlag();
        }).start();
    }
    
    private static void changeFlag(){
        flag = !flag;
    }
}

结果:
    1号线程启动,执行while循环
    (一秒后)
    2号线程启动,更改变量 flag
    (但是程序没有停止,num一直在++,并没有输出num)    

原因:线程之间是不可见的,他们自己内存中读取的是副本,没有及时读取到主存的结果,也没有监测主存信息的变化

2.3.3 有序性

程序执行的顺序按照代码的先后顺序执行。
一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不会保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。如下:

int a = 10;	//语句1
a = a + 3;	//语句2 
int r = 2;	//语句3 
r = a * a;	//语句4 

则因为执行重排序(happens-before),他还可能执行顺序为 2-1-3-4,1-3-2-4
但绝不可能 2-1-4-3,因为这打破了依赖关系。
显然重排序对单线程运行时不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。

demo:

public class C2_线程安全_有序性 {
    public static void main(String[] args) throws Exception {

        for (int i = 0; i < 500000; i++) {
            C2_线程安全_有序性.State state = new C2_线程安全_有序性.State();
            ThreadA threadA = new ThreadA(state);
            ThreadB threadB = new ThreadB(state);
            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();
        }
    }

    static class ThreadA extends Thread{
        private final C2_线程安全_有序性.State state;
        ThreadA(C2_线程安全_有序性.State state){
            this.state = state;
        }
        @Override
        public void run(){
            state.a = 1;
            state.b = 1;
            state.c = 1;
            state.d = 1;
        }
    }


    static class ThreadB extends Thread{
        private final C2_线程安全_有序性.State state;
        ThreadB(C2_线程安全_有序性.State state){
            this.state = state;
        }
        @Override
        public void run(){
            if (state.b==1 && state.a==0){
                System.out.println("b == 1");
            }
            if (state.c==1 && (state.b==0 || state.a==0)){
                System.out.println("c == 1");
            }
            if (state.d==1 && (state.a==0 || state.b==0 || state.c==0)){
                System.out.println("d == 1");
            }
        }
    }

    static class State{
        public int a  = 0;
        public int b  = 0;
        public int c  = 0;
        public int d  = 0;
    }

}

在这里插入图片描述

2.4 volatile

volatile关键字的作用是变量在多个线程之间可见。并且能够保证所修饰变量的有序性:

  1. 保证变量的可见性:
    当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改后的结果。
    当一个线程被volatile关键字修饰的变量写入数据的时候,虚拟机会强制它被值刷新到主存中。
    当一个线程被volatile关键字修饰的值的时候,虚拟机会强制要求它从主存中读取

  2. 屏蔽指令重排序:指令重排序是编译期和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果是正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。非常经典的例子是在单例方法中同时对字段加入volatile,就是为了防止指令重排序。

  • 此时把2.3.2的案例中变量加一个关键字 private static volatile boolean flag = false;

    1秒后,结果num就可以立刻打印出来了

  • volatile可以保证可见性和指令重排序,但是不能保证原子性,不能控制别的线程改不改同一个变量

2.5 synchronized

jdk1.0开始就提供了这个锁

关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile

2.5.1 使用方法
1. 普通同步方法(实例方法),锁是当前实例对象,进入同步代码前要获得当前实例的锁
2. 静态同步方法,锁是当前类的class对象,进入同步代码前要获得当前类对象的锁
3. 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

一定要传入一个对象作为锁
public class C1_线程安全_售票案例 {

    //总票数
    static int ticket = 100;

    public static void main(String[] args) {

        Object o = new Object();
        
        Runnable runnable = () -> {
            //循环买票
            while (true) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o) {
                    if (ticket > 0) {
                        ticket--;
                        System.out.println(Thread.currentThread().getName() + 
                    "卖了一张票,剩余:" + ticket);
                    } else {
                        System.out.println("卖完了,没票了……");
                        break;
                    }
                }
            }
        };

        Thread t1 = new Thread(runnable, "窗口1");
        Thread t2 = new Thread(runnable, "窗口2");
        Thread t3 = new Thread(runnable, "窗口3");
        t1.start();
        t2.start();
        t3.start();
    }

}
2.5.2 工作原理

JVM 是通过进入、退出对象监视器(monitor)来实现对方法、同步块的同步的。
具体是现实在编译之后,在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入 monitor.exit的指令。
其本质就是对一个对象监视器(monitor)进行获取,而这个获取过程具有排他性,从而达到了同一时刻只能一个线程访问的目的。
而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit之后才能尝试继续获取锁。

  1. 一段代码块被上了锁,当他被编译之后,他会形成一个对象监视器的入口和出口,monitorEnter 和 monitorExit
  2. synchronized必须使用对象来作为锁,是因为使用了监视器的机制后,这个监视器有个排他性,当两个线程过来访问同一个资源的时候,都需要这个监视器的入口,监视器会去查看锁的状态,判断对象有没有上锁,没上锁就会上锁,然后运行完毕之后,会在exit处释放锁,下一个线程就抢到了锁。
  3. monitor是怎么实现的不用考虑,这是jvm通过操作系统在底层实现的,只要加个关键字就可以

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nc3g99gB-1594367080147)(synchronize锁原理.png)]

static class A{
    public static void main(String[] args) {
        new A().fun1();
        A.fun2();
    }

    //普通方法是使用当前对象作为锁  this
    public synchronized void fun1(){
        //同步代码
    }

    //会使用当前的类对象  A.class作为锁
    public synchronized static void fun2(){
        //同步静态代码块
    }
    
}


如果想保证两个代码的线程同步,他们必须使用同一把锁

  1. 两个方法同步,可以直接在方法上加锁,使用的就是当前对象的锁,this
  synchronized void fun1(){
      
  }
  synchronized void fun2(){
      
  }
  
  2.如果是两个不同方法里面的代码块,那么就在外面声明一个对象,让他们用同一把锁
      
  private Object obj = new Object();
  void fun3(){
      synchronized(obj){
          //同步代码
      }
  }
  void fun4(){
      synchronized(obj){
          //同步代码
      }
  }
小结:

注意:
对象如同锁,持有锁的线程可以在同步中执行
没持有锁的线程即使获取CPU的执行权,也进不去

同步的前提:
1. 必须要有两个或以上的线程
2. 必须是多个线程使用同一个锁

优缺点:
好处:解决了多线程的安全问题。
弊端:多个线程需要判断锁,较为消耗资源、抢锁的资源。

修饰方法注意点:
synchronized 修饰方法使用的是 this锁
synchronized 修饰静态方法使用的是当前类的字节码文件
2.5.3 多线程死锁

互相持有对方的锁,两边都不能释放锁,程序就卡在这里了。

public class C4_线程安全_死锁 {
    public static void main(String[] args) throws Exception {

        Object o1 = new Object();   //1号锁
        Object o2 = new Object();   //2号锁

        new Thread(() -> {
            synchronized (o1){ //获取o1锁
                try {
                    Thread.sleep(1000);
                    synchronized(o2){
                        System.out.println("线程1执行");
                    }
                }catch (Exception e){

                }
            }
        }).start();

        new Thread(() -> {
            synchronized (o2){ //获取o2锁
                try {
                    Thread.sleep(1000);
                    synchronized(o1){
                        System.out.println("线程2执行");
                    }
                }catch (Exception e){

                }
            }
        }).start();

    }
}

2.6 J.U.C 之lock锁

在 jdk1.5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,Lock接口提供了与synchronized关键字类似的同步功能,但需要在使用时手动获取锁和释放锁。

2.6.1 lock锁用法
  • 一定要配合try-finally语句块
Lock lock = new ReentrantLock();
lock.lock();
try {
    //可能会出现线程安全的代码块
} finally {
    //一定在finally中释放锁
    //也不能把获取锁的操作在try中进行,因为有可能在获取锁的时候抛出异常
    lock.unlock();
}
方法 - tryLock()

问题:如果多个线程抢一把锁,对于synchronized锁来说,没抢到锁的必须得等,而lock提供了一个tryLock()的方法,尝试获取锁,如果没获取到,那么就去做别的事情。

public class C4_线程安全_Lock锁 {

    private Lock lock = new ReentrantLock();

    public void insert(Thread thread) {
        if (lock.tryLock()) {
            try {
                System.out.println(thread.getName() + "抢到锁啦!");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println(thread.getName() + "释放了锁~");
                lock.unlock();
            }
        } else {
            System.out.println(thread.getName() + "做别的事情……");
        }
    }

    public static void main(String[] args) throws Exception {
        C4_线程安全_Lock锁 test = new C4_线程安全_Lock锁();
        new Thread(() -> test.insert(Thread.currentThread())).start();
        new Thread(() -> test.insert(Thread.currentThread())).start();
        new Thread(() -> test.insert(Thread.currentThread())).start();
        new Thread(() -> test.insert(Thread.currentThread())).start();
    }
}
支持读写锁
  • 读锁也叫共享锁,读锁和读锁之间是共享的,加了读锁以后别人也可以使用读锁来操作代码
  • 读锁和写锁是互斥的
public class C4_线程安全_lock锁3 {

    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void read(Thread thread) {
        readWriteLock.readLock().lock();
        try {
            System.out.println(thread.getName() + "正在读操作……");
            Thread.sleep(500);
        } catch (Exception e) {
            System.out.println("error...");
        } finally {
            System.out.println(thread.getName() + "已经读完……");
            readWriteLock.readLock().unlock();
        }

    }

    public void write(Thread thread) {
        readWriteLock.writeLock().lock();
        try {
            System.out.println(thread.getName() + "正在写操作……");
            Thread.sleep(1000);
        } catch (Exception e) {
            System.out.println("error...");
        } finally {
            System.out.println(thread.getName() + "已经写完……");
            readWriteLock.writeLock().unlock();
        }
    }

    public static void main(String[] args) throws Exception {
        final C4_线程安全_lock锁3 test = new C4_线程安全_lock锁3();

        //读操作
        new Thread(() -> { test.read(Thread.currentThread()); }).start();
        new Thread(() -> { test.read(Thread.currentThread()); }).start();
        new Thread(() -> { test.read(Thread.currentThread()); }).start();

        Thread.sleep(2000);
        System.out.println();

        //写操作
        new Thread(() -> { test.write(Thread.currentThread()); }).start();
        new Thread(() -> { test.write(Thread.currentThread()); }).start();
        new Thread(() -> { test.write(Thread.currentThread()); }).start();

    }
}

结果:

Thread-0正在读操作……
Thread-1正在读操作……
Thread-2正在读操作……
Thread-1已经读完……
Thread-2已经读完……
Thread-0已经读完……

Thread-3正在写操作……
Thread-3已经写完……
Thread-4正在写操作……
Thread-4已经写完……
Thread-5正在写操作……
Thread-5已经写完……
2.6.2 lock与synchronized区别
  1. Lock是一个接口,而synchronized是java中的关键字,synchronized是内置的语言实现;
    synchronized关键字可以直接修饰方法,也可以修饰代码块,而lock只能修饰代码块;
  2. synchronize在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  3. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。(提供tryLock)
  5. Lock可以提高多个线程进行读操作的效率。(提供读写锁)

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

3. 并发编程之线程间的通信

3.1 什么是多线程之间通信

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。

于是我们引出了等候唤醒机制:( wait()、notify() )

wait()、notify()、notifyAll()是三个定义在object类里面的方法,可以用来控制线程的状态。

这三个方法最终调用的都是jvm级的native方法。随着jvm运行平台的不同,可能有些许差异。

如果对象调用了wait方法就会使持有该对象的线程把该对象的控制权交出去,然后处于等待状态。

如果对象调用了notify方法就会通知某个正在等待这个对象的控制权的线程可以继续运行。

如果对象调用了notifyAll方法就会通知所有等待这个对象控制权的线程继续运行。

	注意:wait()方法的调用必须放在synchronized方法或synchronized块中。

3.2 wait与sleep区别

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。
而wait()方法,则是属于Object()类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法过程中,线程不会释放对象锁。

而当调用wait()方法时,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后,本线程才会进入对象锁定池准备

获取对象锁进入运行状态。

3.3 实战面试题

3.3.1 两个线程,交替打印1-100 A线程负责打印奇数 B线程负责打印偶数。

public class C5_线程通信_奇偶数 {

    private static int num = 1;

    public static void main(String[] args) {

        //锁对象
        Object obj = new Object();

        //奇数线程
        new Thread(() -> {
            while (true) {
                if (num > 100) { break; }
                synchronized (obj) {
                    if (num % 2 != 0) {
                        System.out.println(Thread.currentThread().getName() + "奇数:" + num++);
                        obj.notify(); //唤醒另一个线程
                    } else {
                        //偶数  奇数线程等待
                        try {
                            //wait()必须要在synchronized代码块中,并且必须使用同一把锁对象
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();

        //偶数线程
        new Thread(() -> {
            while (true) {
                if (num > 100) { break; }
                synchronized (obj) {
                    if (num % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + "偶数:" + num++);
                        obj.notify(); //唤醒另一个线程
                    } else {
                        //奇数  偶数线程等待
                        try {
                            //必须要在synchronized代码块中,并且必须使用同一把锁对象
                            obj.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();

    }

}

3.3.2 多线程实现生产消费者模式(wait、notify实现)

生产者消费者概念

生产者消费者模式是并发、多线程编程中经典的设计模式,生产者和消费者通过分离的执行工作解耦,简化了开发模式,生产者和消费者可以以不同的速度生产和消费数据。

生产者消费者模式的好处:

  1. 它简化开发,你可以独立的或并发的编写消费者和生产者,它仅仅只需知道共享对象是谁
  2. 生产者不需要知道谁是消费者或者有多少消费者,消费者也是
  3. 生产者和消费者可以以不同的速度执行
  4. 分离的消费者和生产者在功能上能写出更简洁、可读、易维护的代码

在这里插入图片描述

demo:

/**
 * @author zhouR.
 * @date 2020-07-07 21:45
 * @function: 经典  生产消费者面试题(wait、notify实现)
 * 4S店存放生产的汽车
 * <p>
 * 生产者线程生产汽车
 * 消费者线程购买汽车
 * <p>
 * 4S店最多只能放4台车,如果达到4台车,就停止生产
 * 如果4S店没有库存车,消费需等待有库存后,方可购买
 */
public class D2_线程通信_生产消费4S店案例 {
    static final int CAR_STORE_NUM = 4;
    static int CAR_NUM = 1;

    public static void main(String[] args) {
        Shop4S car4S = new Shop4S();
        ConsumerTask consumer = new ConsumerTask(car4S);
        ProductTask product = new ProductTask(car4S);

        //可以控制生产者和消费者比例,来决定供求关系
        
        //生产者
        new Thread(product).start();
        new Thread(product).start();
        new Thread(product).start();
        
        //消费者
        new Thread(consumer).start();
        new Thread(consumer).start();
    }

    //消费者任务
    static class ConsumerTask implements Runnable {
        Shop4S shop4S;

        public ConsumerTask(Shop4S shop4S) { this.shop4S = shop4S; }

        @Override
        public void run() {
            while (true) {
                //只要没有到4辆车,就会生产汽车
                synchronized (shop4S) {  //使用4s店作为对象锁
                    try {
                        Thread.sleep(500);
                        if (shop4S.carList.size() > 0) {
                            //4s店有车,可以继续购买
                            Car car = shop4S.carList.poll();//减少一辆汽车
                            System.out.println("恭喜用户"+ Thread.currentThread().getName() + "喜提一辆豪车[ " + car.getName()+ " ]");
                            shop4S.notifyAll();
                            shop4S.wait();
                        } else {
                            System.out.println("4S店没车,停止购买");
                            shop4S.wait();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    //生产者任务
    static class ProductTask implements Runnable {
        Shop4S shop4S;

        public ProductTask(Shop4S shop4S) { this.shop4S = shop4S; }

        @Override
        public void run() {
            while (true) {
                //只要没有到4辆车,就会生产汽车
                synchronized (shop4S) {  //使用4s店作为对象锁
                    try {
                        Thread.sleep(700);
                        if (shop4S.carList.size() < CAR_STORE_NUM) {
                            //4s店没满,可以继续生产
                            Car car = new Car();
                            car.setName("(" + CAR_NUM++ + ")号");
                            String name = Thread.currentThread().getName();
                            shop4S.carList.push(car); //放到4S店
                            System.out.println("工厂["+name+"]生产了汽车" + car.getName()+ ",当前4S店有 "+shop4S.carList.size()+" 辆车");
                            shop4S.notifyAll();
                            shop4S.wait();
                        } else {
                            System.out.println("库存已满,停止生产");
                            shop4S.wait();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    //4s店  (共享资源)
    static class Shop4S {
        LinkedList<Car> carList = new LinkedList<>();
    }
    //车
    static class Car {
        private String name;
        public String getName() {return name;}
        public void setName(String name) {this.name = name;}
    }

}

4. 并发编程之J.U.C - 线程池API

4.1 什么是线程池

java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序,都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。

4.2 线程池作用

线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少创建和销毁线程所需的时间,从而提高效率。

如果一个线程的时间非常长,就没有必要使用线程池了(不是不能长时间操作,而是不宜),况且我们还不能控制线程池中线程的开始、挂起和终止。

4.3 线程池体系结构

最基础的一个接口:Executor
里面只有一个方法:void execute(Runnable command); 执行任务

ExecutorService是子接口,这是工作中常用的接口

ThreadPoolExecutor是核心实现类,通过他new出线程池对象
    
ExecutorService service = new ThreadPoolExecutor();
service.execute();  //执行任务,或者放在for循环里面,循环执行
service.shutdown();

4.4 线程池工作原理

4.4.1 各个参数的含义
ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2,      //corePoolSize 核心线程数
                5,  //maximumPoolSize 最大线程数
                10,     //keepAliveTime保持存活的时间
                TimeUnit.SECONDS,       //时间单位
                new ArrayBlockingQueue<Runnable>(10),//任务队列
                new ThreadPoolExecutor.AbortPolicy()    //饱和策略
        );
4.4.2 线程池工作流程

主要流程:

  1. 判断核心线程数
  2. 判断任务队列
  3. 判断最大线程数
  4. 检查饱和策略。
public class E2_线程池_线程池运行原理 {
    public static void main(String[] args) {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2,      //corePoolSize 核心线程数
            5,  //maximumPoolSize 最大线程数
            10,     //keepAliveTime保持存活的时间
            TimeUnit.SECONDS,       //时间单位
            new ArrayBlockingQueue<Runnable>(10),//任务队列
            new ThreadPoolExecutor.AbortPolicy()    //饱和策略
        );

        for (int i = 0; i < 20; i++) {
            try {
                executor.execute(new MyRunnable("第" + (i + 1) + "号线程"));
            } catch (Exception e) {
                System.out.println("丢弃任务:" + (i + 1));
               // e.printStackTrace();
            }
        }
    }

    static class MyRunnable implements Runnable {
        private String name;
        public MyRunnable(String name) { this.name = name; }
        @Override
        public void run() {
            System.out.println("name = " + name);
            while (true) {
				//让任务一直在执行
            }
        }
    }

}
  1. 创建线程池,会创建一个任务队列,队列的特点就是先入先出,队列的作用就是存储待执行的任务,每一个任务存进来以后,会被线程池内空余的线程执行,上面案例中创建的队列是10个任务队列。

  2. 创建完线程池后,executor对象会调用execute()执行任务的方法,目前demo中会运行20个任务,这里写的是死循环,让任务一直运行

  3. 当第一个任务【任务1】来时,先判断当前线程池中,线程的数量有没有小于核心线程数corePoolSize,如果小于核心线程数,那么就会创建一个线程,在这个线程里面执行【任务1】

  4. 当第二个任务【任务2】来时,跟上面执行一样的流程,因为corePoolSize设置的是2,所以【任务2】也在运行

  5. 当第三个任务【任务3】来时,线程池中的线程的数量大于核心线程数,就不会创建线程来执行【任务3】,然后会把【任务3】装到任务队列中,队列满足先入先出的逻辑,所以3号任务会在队列第一个位置等着,等【任务1】或者【任务2】任意一个执行完,就会执行3号

  6. 后面的任务就一直积压在任务队列中,队列总共会装10个。

在这里插入图片描述

  1. 当第13个任务来时

    • 判断最核心线程数,满了
    • 判断任务队列,满了
    • 检查最大线程数,判断线程池里面的线程有没有小于最大线程数,如果小于最大线程数,就会再创建线程,来执行【任务13】

在这里插入图片描述

  1. 当第16个线程来时,一顿判断之后发现最大线程数都满了,就要判断饱和策略。

    new ThreadPoolExecutor.AbortPolicy()  //默认的,直接抛出异常,丢弃任务,拒绝执行
    

    所以16-20号任务都会被丢弃,并抛出5个异常。运行上面的代码,结果如下:

    丢弃任务:16
    丢弃任务:17
    丢弃任务:18
    丢弃任务:19
    丢弃任务:20
    name =1号线程
    name =2号线程
    name =13号线程
    name =14号线程
    name =15号线程
    
  2. 因为写的是死循环,所以正常的应该是1号2号运行完后,3号4号会去运行

  3. 存活时间 和 时间单位,仅限于备胎,当最大线程区的三个备胎都把任务完成了,并且没有新任务的前提下,他只能存活这么长时间,如果写的是10s,只要10秒内没有新任务,这些线程就会被释放掉。而核心线程数是不会被释放的

4.4.3 线程池的三种队列
1.SynchronousQueue

SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。
使用SynchronousQueue阻塞队列一般要求maximumPoolSize为无界,避免线程拒绝执行操作。

2. LinkedBlockingQueue

LinkedBlockingQueue是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时maximumPoolSize就相当于无效了),每个线程完全独立于其他线程。生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并行操作队列中的数据

3. ArrayBlockingQueue

ArrayBlockingQueue是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续运行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSize时,再有新的元素尝试加入ArrayBlockingQueue时会报错。

demo:

public class E3_线程池_阻塞队列 {
    public static void main(String[] args) {
		/**
         *	存一个数据,取一个数据,存的时候会阻塞,取的时候也会阻塞
         */
        SynchronousQueue<Integer> queue = new SynchronousQueue<>();
        
        /**
         *	一下子全部把数据装进队列,然后慢慢取消费
         */
        LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>();
        
        /**
         *	指定容量为4,队列一次性装4个数据,然后取一个数据,存一个数据
         */
        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(4);

        //生产者线程
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                try {
                    queue.put(i);
                    System.out.println("装入数据 = " + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        //消费者线程
        Thread t2 = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000);
                    System.out.println("取出数据 = " + queue.take());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

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

4.4.4 饱和策略
  1. AbortPolicy:抛出异常,丢弃任务
  2. CallerRunsPolicy:不抛弃任务,调用线程池的那个线程帮忙执行任务,比如上面4.4.2中的demo,换成这个策略后,调用线程池的线程是main方法,于是main方法这个线程会执行16-20号任务。
  3. DiscardPolicy:默默抛弃任务,连异常都不抛。【抛弃新加的任务】把16-20丢弃
  4. DiscardOldestPolicy:默默抛弃任务,连异常都不抛。【抛弃创建较早的任务】把3-7丢弃

4.5 Executors线程池工具类

Executors是线程池的工具类,提供了四种快捷创建线程池的方法:

  • newCachedThreadPool
    创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
  • newFixedThreadPool
    创建一个定长线程池,可控制线程最大并发数,超出线程会在队列中等待
  • newSingleThreadExecutor
    创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。
  • newScheduledThreadPool
    创建一个定长线程池,支持定时及周期性任务执行
4.5.1 newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

//可缓存线程池
public static void pool1(){
    ExecutorService es = Executors.newCachedThreadPool();

    //创建10个线程  分别执行任务
    for (int i = 0; i < 10; i++) {
        es.execute(() -> {
            for (int j = 0; j < 10; j++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":" + j);
            }
        });
    }
    es.shutdown();
}

运行结果:

开启了10个线程,每个线程都打印一遍数字,打印了10遍。

原理:

 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, 
        							Integer.MAX_VALUE,
                                    60L, 
                                    TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
    }
   
没有核心线程数,最大核心线程数为integer最大值,队列只能装一个任务  

只要来一个任务,就会创建一个新的线程去执行,来多少任务创建多少线程。
当任务执行完毕,还在存活期内,线程就可以复用。
如果时间过了,线程就可以自动回收
但是要慎用,当有大量任务的时候,就会一下子创建很多线程
4.5.2 newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出线程会在队列中等待

//定长线程池
public static void newFixedThreadPool() {
    ExecutorService es = Executors.newFixedThreadPool(2);
    //创建10个线程  分别执行任务
    for (int i = 0; i < 10; i++) {
        es.execute(() -> {
            for (int j = 0; j < 10; j++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":" + j);
            }
        });
    }
    es.shutdown();
}

结果:

创建两个线程,由这两个线程执行任务

原理:

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, 
                                      nThreads,
                                      0L, 
                                      TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

设置的数字都赋值给了核心数和最大值,队列使用的是无限大的队列,存的所有任务都会被存到这个队列,通过nThreads个线程把这些任务全部完成
4.5.3 newSingleThreadExecutor
  • 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。
//单例线程池
public static void newSingleThreadExecutor() {
    ExecutorService es = Executors.newSingleThreadExecutor();
    //创建10个线程  分别执行任务
    for (int i = 0; i < 10; i++) {
        es.execute(() -> {
            for (int j = 0; j < 10; j++) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":" + j);
            }
        });
    }
    es.shutdown();
}

结果:一个线程全部运行完毕

原理:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1,
                                1,
                                0L,
                                TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

核心线程和最大线程数全部设置成1,其他和定长一样,任务全都装到无限队列里,然后由这一个线程慢慢执行任务
4.5.4 newScheduledThreadPool
  • 创建一个定长线程池,支持定时及周期性任务执行
//调度线程池
public static void newScheduledThreadPool() {
    ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);

    //延时执行的线程池
    //在指定的时间过后才执行,比如下面这段代码会在4秒之后执行打印任务
    //实际案例:订单创建成功后30分钟检查支付状态
    pool.schedule(
        () -> System.out.println("i:" + 1), //任务
        4,  //延时时间
        TimeUnit.SECONDS);  //时间单位

    //周期性执行
    //3秒执行一次,过5秒还会执行一次
    pool.scheduleAtFixedRate(
        () -> System.out.println("i:" + 1), //任务
        3,  //延时时间
        5,  //间隔时间
        TimeUnit.SECONDS);  //时间单位
}

5. 多线程要注意的问题

  1. 线程安全问题

  2. 线程的性能

    • 创建线程和维护线程都需要消耗系统资源
    • 线程的上下文切换也消耗时间,就是一个cpu从一个线程切换到下一个线程需要消耗时间
    • 所以线程不是越多越好,cmd->wmic->cpu get * 查看当前电脑的cpu信息,比如 NumberOfCores=4,NumberOfLogicalProcessors=8,就是4核8线程,那么最好就是创建8个线程,这样就可以同时执行8个线程,而且还可以不用来回切换,节省资源也节省时间,效率利用到最高
  3. 如果自己写代码,创建8个线程,还不能关闭这个线程,如果要复用的话还需要很多代码来实现,java已经给我们提供了解决办法了,在jdk1.5之后,在java.util.concurrent 线程池的框架。

线程池的作用分析

通过创建固定数量的线程来执行大量的任务

这样可以很好地复用线程,减少线程的创建和维护时间的消耗

线程池体系结构

java.util.concurrent.Executor (I) 线程池的顶级接口,定义了线程池的最基本方法

java.util.concurrent.ExecutorService (I) 继承顶级接口,定义了一些常用方法

java.util.concurrent.ThreadPoolExecutor (C)线程池核心实现类

java.util.concurrent.ScheduledThreadPoolExecutor (C)子类—可调度线程池,加了一些延时的处理,定期执行的处理

java.util.concurrent.Executors 线程池的工具类

使用:

// 创建线程
ExecutorService es = new ThreadPoolExecutor();
// 执行任务
es.execute(任务);

demo:使用线程池和不使用线程池的对比

public class E5_线程池_时间对比 {
    public static void main(String[] args) throws Exception {
        testNoThread();		//结果:总耗时 = 13137
        //testUseThread();	//结果:总耗时 = 60
    }

    //不使用线程池,执行大量任务
    static void testNoThread() throws InterruptedException {
        long startTime = System.currentTimeMillis();
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            Thread thread = new Thread(() -> {
                list.add((int)Math.random());
            });
            thread.start();
            thread.join();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("总耗时 = " + (endTime-startTime));
    }

    //使用线程池,执行大量任务
    static void testUseThread(){
        ExecutorService es = new ThreadPoolExecutor(
            6,
            6,
            20,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>());
        long startTime = System.currentTimeMillis();
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            es.execute(() -> {list.add((int)Math.random());}); }
        long endTime = System.currentTimeMillis();
        System.out.println("总耗时 = " + (endTime-startTime));
        es.shutdown();
    }
}

线程停止

临时线程:在指定时间内没有获取到任务,那么临时线程会被销毁

核心线程:默认情况下是不超时的。

  • 除非将他设置为可超时es.allowCoreThreadTimeOut(true)
  • 或者调用es.shutdown(),停止,这是将队列里的方法全部执行完之后停止
  • 或者使用es.shutdownNow(),强制停止

6. 源码分析

Worker是一个工作的线程,线程池实现的核心,线程为什么能够复用,都跟这个worker有关
Worker是核心线程的内部类,他实现了Runnable接口,代表他是一个可执行的线程,内部有一个属性,当我们启动线程的时候,就是启动它内部的线程,而内部的线程是把自己传进去了,只要线程启动,worker的run方法就会启动

ThreadPoolExecutor类中

    这个才是真正存放工作线程的集合
    private final HashSet<Worker> workers = new HashSet<Worker>();

    //实现了Runnable接口
    private final class Worker extends AbstractQueuedSynchronizer implements Runnable {

        private static final long serialVersionUID = 6138294804551838833L;

        final Thread thread; //线程,指的就是这个线程,具体工作的线程
        Runnable firstTask;	//第一次要执行的任务,创建worker的时候,会给这个传递一个任务
        volatile long completedTasks;

        /**
         * worker构造器
         *  new的时候会传递一个任务给firstTask
         * 然后通过线程池工厂新建一个线程,把自己本身传给了线程
         */
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

        /** 只要线程启动,worker的run方法就会启动  */
        public void run() {
            runWorker(this);
        }

        //...
    }

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;	//首先将第一次任务交给task变量
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        //while循环
        while (task != null || (task = getTask()) != null) {
            w.lock();
            //判断线程池状态的代码
            if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && 
                runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    task.run();  //在这里调用任务的run方法
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;  
                /*把任务清空  此时处于while循环当中,进行下一轮判断,此时task已经是null
                于是进入了(task = getTask()) != null 这个判断,如果这个线程还有任务要执行,
                那么就继续执行,这里便是线程复用的原因所在
                */
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}


//从任务队列中获取任务
private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        /*从任务队列中获取任务
        为什么要用两种方法?poll() 和 take()
        1. 有的线程是临时线程,如果是临时线程就使用上面的方法workQueue.poll(),
           这不是阻塞的方法,如果在keepAliveTime这么长时间内没取到任务,就超时了,是null
        
        2. workQueue.take();这是核心线程使用的方法,使用的是阻塞的take方法,
          如果取不到任务,就阻塞在这里
        */
        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
            workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

那么,worker什么时候被创建?

就要看execute()方法了

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();	//判断工作池的工作状态,不用管
    /*
    workerCountOf(c) 当前线程池里面有几个线程在工作,判断一下核心线程数
    如果数量小于核心线程数,就add一个worker
    */
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))	//在这里创建一个worker,并且把任务传递进去
            return;
        c = ctl.get();
    }
    
    //如果核心线程数满了,就往队列中添加一个任务offer(),添加成功会返回一个true
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    //如果任务队列也满了,就继续添加worker,由第二个参数控制 true和false来控制
    else if (!addWorker(command, false))
        reject(command);	//如果临时任务添加失败,拒绝任务,拒绝策略就在这里执行
}


private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);	//取出工作线程的数量 
            if (wc >= CAPACITY ||
                //如果第二个参数是true的话,判断核心线程,否则就判断最大线程池
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    //判断完之后,准备创建worker

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        //new了一个worker,并把当前任务交给worker
        w = new Worker(firstTask);
        //创建线程
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w); //set集合,刚创建完就加进去
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true; //添加成功
                }
            } finally {
                mainLock.unlock();
            }
            //如果添加成功
            if (workerAdded) {
                t.start();	//线程执行,worker里面的run方法就执行了
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值