【Java SE】多线程

本文详细介绍了Java中的多线程概念,包括线程生命周期、线程安全、守护线程、线程同步(如synchronized、volatile)、线程池和ThreadLocal。还探讨了JStack工具的使用,以及Java内存模型(JMM)中的可见性问题,强调了volatile关键字在确保可见性但不保证原子性方面的特性。文中通过实例和代码展示了线程的创建方式,如继承Thread类和实现Runnable接口,并分析了它们的区别。此外,还讨论了线程池的实现和使用,以及如何避免并发编程中的常见问题。
摘要由CSDN通过智能技术生成

——————————————————————————————

笔记

————————————

  • 线程睡眠/阻塞:用TimeUnit.SECONDS.sleep()取代Thread.sleep()
import java.util.concurrent.TimeUnit;

try {
    //Thread.sleep(10*1000); 等价于
    TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
    e.printStackTrace();
}

为什么说他好呢?因为他更灵活:
在这里插入图片描述

TimeUnit.SECONDS.sleep();
TimeUnit.MINUTES.sleep();
TimeUnit.HOURS.sleep();
TimeUnit.DAYS.sleep();

————————————
————————————
————————————
————————————
————————————
——————————————————————————————

通俗地说:
进程:比如IDEA或者QQ的运行,是程序(任务)的执行过程,持有资源(共享内存、共享文件)和线程;
说明是动态性的,单个文件并不是进程,只有点击文件使程序运行后才发生进程)
(进程是资源、线程的载体,脱离了线程谈进程无意义)

线程:比如QQ中的“文字聊天”“收发文件”,所有这些人物都可以理解为线程;

线程的交互互斥同步;比方说进程是班级,线程是学生,学生沟通就是线程,学生之间有时需要合作(同步)才能达到目的,有时需要竞争(互斥)才能达到目的;

——————————————————————————————

  • 程序、进程、线程的区别是什么?分别举例说明 (初中级面试)

程序是进程的超集,进程是线程的超集;(一个程序可以有多个进程,一个进程可以有多个线程)

  • 程序 Program:是一个指令的集合。程序不能独立执行,只有被加载到内存中,系统为他分配资源后才能执行;
  • 进程 Process:如上,一个执行中的程序叫做进程;
    进程是系统分配资源的独立单位,每个进程在系统内顺序执行的特定地址;
    程序是进程的静态文本描述,进程是程序在系统内顺序执行的动态活动;
  • 线程 Thread:是进程的“单一的连续控制流程”;
    线程是CPU调度和分配的基本单位,是比进程更小的,能独立运行的基本单位,也被称作轻量级的进程;
    能独立运行,但不能独立存在;
    线程不能独立存在,必须依附于某个进程;一个进程可以包括多个并行的线程,一个线程肯定属于一个进程;
    Java允许并发执行多个线程;

例如:一个车间是一个程序,一个正在进行生产任务的车间是一个进程,车间内每个从事不同工作的工人是一个线程;

——————————————————————————————
Java对线程的支持体现在:
Thread类Runnable接口 之上,他们都继承于java.lang包;
他们之间共通的方法是run(),此方法为我们提供了线程执行的代码;
在这里插入图片描述

  • Thread.activeCount() 返回活动线程的【当前线程】的线程组中的【数量】
    注意!若实际只运行了1个线程时,其他IDE中此方法返回的是1
    但IDEA中使用此方法返回的是2,因为IDEA中多了个Monitor Ctrl-Break监控线程,使用IDEA时注特别注意!!!!!!!!

————————————————————————

多线程

提问:实现多线程的方式?

1.继承Thread类创建线程;
2.实现Runnable接口创建线程;
(答出1、2点就够了;下面是两种特殊的方式,不用死记硬背,不重要)
3.实现Callable接口,通过FutureTask包装器来创建Thread线程;(少用)
4.使用ExecutorServiceCallableFuture实现由返回结果的线程;(线程池)(少用)

注:两者获取线程名称的方式有区别
Thread类用:getName()
Runnable接口用:Thread.currentThread().getName()

1.继承Thread类创建线程;
(写两个线程)

public class Test1 extends Thread{
    public void run(){
        System.out.println("Test1.run()");
    }

    public static void main(String[] args) {
        Test1 t1 = new Test1();
        Test1 t2 = new Test1();
        t1.start();
        t2.start();
    }
}
————————————
输出:
Test1.run()
Test1.run()

也可以简写:

public class Test1 extends Thread{
    public void run(){
        System.out.println("Test1.run()");
    }

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

2.实现Runnable接口创建线程;
(写两个线程)
易错点!!!要先实例化Test类,再把实例对象放在Thread的实例语句中,不要写反!!!

public class Test2 implements Runnable {
    public void run(){
        System.out.println("Test2.run()");
    }

    public static void main(String[] args) {
        Test2 t1 = new Test2();
        Test2 t2 = new Test2();
        Thread thread1 = new Thread(t1);
        Thread thread2 = new Thread(t2);
        thread1.start();
        thread2.start();
    }
}
————————————
输出:
Test2.run()
Test2.run()

3.实现Callable接口,通过FutureTask包装器来创建Thread线程;

public class Test3<Object> implements Callable<Object> {

    public Object call(){
        System.out.println(Thread.currentThread().getName()+"————我是通过Callable接口通过FutureTask包装器来实现的线程。");
        return null;
    }
}
public class Test2 {
    // 通过Callable和FutureTask创建线程
    /*
    * a.创建Callable接口的实现类,并实现Call方法
    * b.创建Callable实现类的实现,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的Call方法的返回值
    * c.使用FutureTask对象作为Thread对象的target创建并启动线程
    * d.调用FutureTask对象的get()来获取子线程执行结束后的返回值
    */

    public static void main(String[] args) {
        Callable<Object> oneCallable = new Test3<>();
        FutureTask<Object> oneTask = new FutureTask<Object>(oneCallable);
        Thread t = new Thread(oneTask);
        System.out.println(Thread.currentThread().getName());
        t.start();
    }
}
————————————
输出:
main
Thread-0————我是通过Callable接口通过FutureTask包装器来实现的线程。

4.使用ExecutorServiceCallableFuture实现由返回结果的线程;(线程池)(少用)

public class Test4 {

    // 使用`ExecutorService`、`Callable`、`Future`实现由返回结果的线程(线程池)

    private static int POOL_NUM = 10; // 线程池数量

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for(int i = 0; i < POOL_NUM ; i++){
            RunnableThread thread = new RunnableThread();
            executorService.execute(thread);
        }
        executorService.shutdown();
    }
}

class RunnableThread implements  Runnable{

    @Override
    public void run() {
        System.out.println("通过线程池方法创建的线程: "+Thread.currentThread().getName());
    }
}
————————————
输出:
通过线程池方法创建的线程: pool-1-thread-1
通过线程池方法创建的线程: pool-1-thread-2
通过线程池方法创建的线程: pool-1-thread-3
通过线程池方法创建的线程: pool-1-thread-4
通过线程池方法创建的线程: pool-1-thread-5
通过线程池方法创建的线程: pool-1-thread-1
通过线程池方法创建的线程: pool-1-thread-2
通过线程池方法创建的线程: pool-1-thread-3
通过线程池方法创建的线程: pool-1-thread-4
通过线程池方法创建的线程: pool-1-thread-4

附:实际中常用方法:

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

(本质上,实现多线程唯一的方式是实现Thread类的run()方法;)
——————————————————————————————

  • 两种Thread创建方法的区别?(extends Threadimplements Runnable

extends Thread:
(1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
(2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。(因为Java的单继承

implements Runnable:
(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源(指同一个Runnable对象)的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

总结:

  1. Runnable方式可以避免Thread方式因为Java单继承特性带来的缺陷;
  2. Runnable代码可以被多个线程(Thread实例)共享,适合于多个线程处理同一资源的情况;
    (建议:多采用Runnable这种方式创建线程)

实现Runnable接口,只创建了一个类的实例,而且被多个线程共享了。因此Counter递增。而继承Thread类, 你必须为每一个线程创建不同的实例。因此每个类的实例分配了不同的内存空间,每一个有不同的Counter,它们的值相同。这意味着没有增加因为没有一个对象的引用是相同的。

什么时候用Runnable接口呢?
当你想要在一组线程中访问相同的资源时,使用Runnable接口。在这种情况下要避免使用Thread类,因为多对象的创建会占用更多的内存,会导致大的性能花费。

PS:Thread类内部实现了Runnable接口

最后,哪种方式最好用呢?
显而易见,当然是实现Runnable接口更好。
https://www.cnblogs.com/JohnTsai/p/3979482.html
———————————————
为了实践这些差别,下列模拟一个情境:
火车站有3个卖票窗口,他们一起售出3张火车票,然后输出查看这些窗口分别卖出几张票;
(注意!安全的买票程序中run方法需要加入同步 Synchronized!!!)
(注意!安全的买票程序中run方法需要加入同步 Synchronized!!!)
(注意!安全的买票程序中run方法需要加入同步 Synchronized!!!)
(注意!安全的买票程序中run方法需要加入同步 Synchronized!!!)
(注意!安全的买票程序中run方法需要加入同步 Synchronized!!!)
extends Thread因为单线程,所以写不写无所谓;

Thread

/*
* 情境:5张火车票,由3个售票窗口(3个线程)卖出
* extends Thread
*
* 卖票逻辑:当票数>0时,就可以一直卖下去;<0时则售罄
*/

class MyThread extends Thread{
    private int ticketCount = 5; // 5张火车票
    private String name; // 窗口名,即线程名

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

    @Override
    public void run() {
        while(ticketCount > 0){
                ticketCount -- ; // 若还有票,则卖掉一张
                System.out.println(name + " 卖了一张票,剩余票数为:"+ticketCount);
        }
    }
}

// 考虑接下来的线程实例名 如何和之前定义的 String name 挂上钩?
// 因为MyThread类中定义了构造器,加入了参数String name;所以在Test1类中,要把name写进线程创建参数中

public class Test1{
    public static void main(String[] args) {
        MyThread mt1 = new MyThread("窗口1");
        MyThread mt2 = new MyThread("窗口2");
        MyThread mt3 = new MyThread("窗口3");
        mt1.start();
        mt2.start();
        mt3.start();
    }
}
———————————————
输出:
窗口3 卖了一张票,剩余票数为:4
窗口2 卖了一张票,剩余票数为:4
窗口1 卖了一张票,剩余票数为:4
窗口3 卖了一张票,剩余票数为:3
窗口1 卖了一张票,剩余票数为:3
窗口2 卖了一张票,剩余票数为:3
窗口1 卖了一张票,剩余票数为:2
窗口3 卖了一张票,剩余票数为:2
窗口2 卖了一张票,剩余票数为:2
窗口3 卖了一张票,剩余票数为:1
窗口1 卖了一张票,剩余票数为:1
窗口2 卖了一张票,剩余票数为:1
窗口3 卖了一张票,剩余票数为:0
窗口1 卖了一张票,剩余票数为:0
窗口2 卖了一张票,剩余票数为:0

居然卖出了15张票,很显然这不合理;

———————————————
Runnable

/*
 * 情境:5张火车票,由3个售票窗口(3个线程)卖出
 * implements Runnable
 *
 * 卖票逻辑:当票数>0时,就可以一直卖下去;<0时则售罄
 */

class MyThread2 implements Runnable{

    private int ticketCount = 5;

    // implements Runnable 不用定义String name,因为Thread类中有构造方法可以直接添加名字

    @Override
    public synchronized void run(){
        while(ticketCount > 0){
            ticketCount -- ;
            System.out.println(Thread.currentThread().getName() + "卖了一张票,余下票数为:"+ ticketCount);
        }
    }
}

public class Test2 {

    public static void main(String[] args) {
        MyThread2 mt1 = new MyThread2();
        Thread t1 = new Thread(mt1,"窗口1");
        Thread t2 = new Thread(mt1,"窗口2");
        Thread t3 = new Thread(mt1,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
———————————————
输出:
窗口2卖了一张票,余下票数为:4
窗口3卖了一张票,余下票数为:3
窗口1卖了一张票,余下票数为:2
窗口3卖了一张票,余下票数为:0
窗口2卖了一张票,余下票数为:1

过程:
线程1启动,没有获得CPU使用权,于是等待;
线程2启动,获得了CPU使用权,于是进入run()运行;输出 窗口2卖了一张票,余下票数为:2
线程2把资源让出来了,线程3启动,获得了CPU使用权...

为什么顺序是:第四行剩下0,第五行剩下1?反过来了?
因为实际上第五行先执行,第四行后执行;
当第五行还没来得及输出时,就被第四行执行了并输出;

易错!!!
1.容易写成这样:

        Test2 t1 = new Test2("窗口1");
        Test2 t2 = new Test2("窗口2");
        Test2 t3 = new Test2("窗口3");

        Thread thd1 = new Thread(t1);
        Thread thd2 = new Thread(t2);
        Thread thd3 = new Thread(t3);

        thd1.start();
        thd2.start();
        thd3.start();

2.而且容易忘记:
implements Runnable 不用定义String name,因为Thread类中有构造方法可以直接添加名字;
3.编写XX卖出了几张票时,这个窗口名!!
要用Thread.getCurrentThread().getName()(而且!即使写了这个,也容易忘记写上getName

结果是很随机的,哪个线程获得了CPU使用权,谁就可以卖出一张票;
但是这个结果是合理的,一共5张票;
———————————————
回到总结:
Runnable代码可以被多个线程(Thread实例)共享,适合于多个线程处理同一资源的情况;
验证的就是多个线程处理同一资源的情况:ticketCount是同一资源;
———————————————
分析:

Runnable中:
MyThread2是一个唯一的一个实现Runnable接口的对象,然后我们把他作为参数,传递给三个线程对象;
因此三个线程用的都是同一个Runnable对象中的代码,所以三线程共享了同个资源:ticketCount

Thread中:
新建了三个线程,每个线程都有自己的ticketCountnamerun()(这些资源对于每个线程来说:是独立的,不是共享的)
因此三个线程各自卖了各自的五张票,共15张票;
———————————————
修改下代码?

把Runnable中:
唯一创建的Runnable对象,改成三个Runnable对象,且把这三个不同的参数传给三个不同线程,结果会不会也变成卖了15张票?(3个线程分别卖了5张票)?

public class Test2 {

    public static void main(String[] args) {
        MyThread2 mt1 = new MyThread2();
        MyThread2 mt2 = new MyThread2();
        MyThread2 mt3 = new MyThread2();
        Thread t1 = new Thread(mt1,"窗口1");
        Thread t2 = new Thread(mt2,"窗口2");
        Thread t3 = new Thread(mt3,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

果然结果如同预料,卖了15张;

——————————————————————————————

守护线程 Daemon Thread

Java线程有两类:用户线程 守护线程

  • 用户线程 User Thread:
    运行在【前台】(看得清、摸不到),【执行具体任务】;
    程序的主线程、连接网络的字线程等都是用户线程;

  • 守护线程 Daemon Thread: (顾名思义,某些线程的守护者)
    运行在【后台】,为【其他前台线程】服务;
    特点:
    一旦所有用户线程都结束运行,守护线程就会随着JVM一起结束工作(要守护的对象都消失了,他觉得自己没有存在的必要了);
    应用:
    数据库连接池中的监控线程;JVM虚拟机启动后的监控线程;
    (如:监测连接池的线程个数,超时时间;监测虚拟机的使用情况、锁持有的情况)
    最常见的守护线程:
    垃圾回收线程

提问:守护线程和用户线程有什么区别?
Java 线程分为守护线程(Daemon Thread) 和 用户线程(User Thread)两类.
通常情况下,我们使用Thread 创建的线程在默认情况下都属于用户线程, 当在启动线程之前, 执行thread.setDaemon(true)时, 线程会变成守护线程。
其实在本质上,用户线程和守护线程并没有太大区别,唯一的区别就是会影响虚拟机的退出(程序的终止)。
当jvm中只剩下守护线程时,虚拟机会退出,及程序终止;
而当jvm中至少拥有一个用户线程时,jvm都不会退出。

———————————————
如何设置守护线程 Daemon Thread?

可以通过调用Thread类的setDaemon(true)方法来设置当前的线程为守护线程;

注意事项:

  • setDaemon(true)方法必须在start()之前调用,否则会抛出IllgalThreadStateException异常;
  • 在守护线程中产生的新线程也是守护线程;
  • 不是所有的人物都可以分配给守护线程来执行,比如【读写】或【计算】逻辑;
    (因为守护线程依附于用户线程而存在,如果守护:读写/计算到一半,用户程序退出,那么读写/计算也会退出,那么程序就崩溃了)

———————————————
模拟一个设计不合理的程序

/*
* 情境:主线程 + 守护线程
* 守护线程:往文件中不断写数据;(模拟一个设计不合理的程序)
* 主线程:阻塞等待来自键盘的输入,一旦获取到了键盘输入,阻塞便解除,
* 当然,主线程一旦结束,守护线程就消失(即使写入数据还未进行完,守护线程也会随着JVM一起结束运行)
*
*/

import java.io.*;
import java.util.Scanner;

class DaemonThread implements Runnable{
    @Override
    public void run() {
        System.out.println("进入守护线程" + Thread.currentThread().getName());
        try {
            writeToFile();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("退出守护线程" + Thread.currentThread().getName());
    }

    private void writeToFile() throws Exception {
        File filename = new File("/Users/chiu/Desktop"+File.separator+"daemon.txt");
        OutputStream os = new FileOutputStream(filename,true);
        int count = 0;
        while(count < 999){
            os.write(("\r\nword" + count).getBytes());
            System.out.println("守护线程" + Thread.currentThread().getName()
            + "向文件中写入了word" + count++);
            Thread.sleep(1000);
        }
    }
}

public class Test1 {
    public static void main(String[] args) {
        System.out.println("进入主线程" + Thread.currentThread().getName());

        DaemonThread daemonThread = new DaemonThread();
        Thread thd = new Thread(daemonThread);
        thd.setDaemon(true); // 设置成守护线程
        thd.start();

        Scanner sc = new Scanner(System.in);
        sc.next(); // 主线程被阻塞;若有输入操作,则解除阻塞,继续执行,从而打印下一句话

        System.out.println("退出主线程" + Thread.currentThread().getName());
    }
}

/*
* 若守护线程正常退出,那么会输出到999句,且输出:"退出守护线程" + Name
* 若主线程意外退出,守护线程终止,不会输出到999句,且不会输出:"退出守护线程" + Name
*
*/
——————————————
输出:
进入主线程main
进入守护线程Thread-0
守护线程Thread-0向文件中写入了word0
守护线程Thread-0向文件中写入了word1
...
守护线程Thread-0向文件中写入了word30
守护线程Thread-0向文件中写入了word31
aaaaah  // 键盘输入了任意字符
退出主线程main

——————————————
桌面的Daemon.txt

word0
word1
word2
...
word29
word30
word31

——————————————————————————————

JStack (生成线程快照)

JStack生成线程快照

除了用JStack,还可以用jstat.exe(命令行工具) jvisualvm.exe(界面化工具)

JStack:
作用:生成JVM当前时刻线程的快照(threaddump,即当前进程中所有线程的信息);
目的:帮助定位程序问题出现的原因,如长时间停顿、CPU占用率过高等;(也可以很容易看出哪些是守护线程和用户线程)

总结:

  1. JStack中,若有 daemon标识 ,则表示是守护线程;若无,则表示用户线程;
  2. 通过查看 Thread State线程状态 ,可以让我们了解到导致某个线程死锁、阻塞的原因;(辅以查阅JDK文档)
    java.lang.Thread.State: TIMED_WAITING (sleeping)
  3. tid nid 可以让我们直观看到哪些线程CPU占有率过高;

1.在Terminal查看 JStack 信息:

$ jstack
Usage:
    jstack [-l] <pid>
        (to connect to running process)
        // 如果输入【-l】:指显示关于锁的额外信息
    jstack -F [-m] [-l] <pid>
        (to connect to a hung process)
    jstack [-m] [-l] <executable> <core>
        (to connect to a core file)
    jstack [-m] [-l] [server_id@]<remote server IP or hostname>
        (to connect to a remote debug server)

Options:
    -F  to force a thread dump. Use when jstack <pid> does not respond (process is hung)
    -m  to print both java and native frames (mixed mode)
    -l  long listing. Prints additional information about locks
    -h or -help to print this help message

2.输入-l + 项目的PID:标示查看该项目当前线程的快照,+ 锁的信息

打开Activity Monitor,找到java列表:
再启动IDE中的程序,
java列表中多出一个进程,比如PID为:71970
(若表中无PID列,页面搜索:https://blog.csdn.net/weixin_42915286/article/details/84335176)
(通过这个方法找到新PID有点蠢,不知道有没有更方便的方法;Windows可以通过找到新进程javaw.exe来定位,但mac中显然不可以)
在这里插入图片描述
Terminal:
$ jstack -l 72007

2019-06-12 18:37:30
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.131-b11 mixed mode):

"Attach Listener" #11 daemon prio=9 os_prio=31 tid=0x00007ffb8b865800 nid=0x1507 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
  - None

"Thread-0" #10 daemon prio=5 os_prio=31 tid=0x00007ffb8b0b4000 nid=0x3b03 waiting on condition [0x000070000f1d5000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
  at java.lang.Thread.sleep(Native Method)
  at com.imooc.collection.Thread.Daemon.DaemonThread.writeToFile(Test1.java:34)
  at com.imooc.collection.Thread.Daemon.DaemonThread.run(Test1.java:19)
  at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
  - None

// 【"Thread-0" #10 daemon】 :daemon 表示是守护线程
// 【prio=5】 :优先级 
// 【tid + nid】 :都是16进制,也是标识线程的参数,结合top/h指令可以方便找到CPU占有率高的线程
// 【java.lang.Thread.State: TIMED_WAITING (sleeping) 】:线程状态 (查看JDK文档可知 timed_waiting 指有时间限制的等待
// 【at java.lang.Thread.sleep】 指出 timed_waiting 原因是因为调用了sleep(守护进程中.sleep(1000)
// 【Locked ownable synchronizers】 (若未写 -l 就不会出现此信息):当前线程是否处于同步块?(none因为程序中未添加synchronized关键字)

"Service Thread" #9 daemon prio=9 os_prio=31 tid=0x00007ffb8c085000 nid=0x3803 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
  - None

"C1 CompilerThread2" #8 daemon prio=9 os_prio=31 tid=0x00007ffb8b0a6800 nid=0x4503 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
  - None

"C2 CompilerThread1" #7 daemon prio=9 os_prio=31 tid=0x00007ffb8b823800 nid=0x4703 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
  - None

"C2 CompilerThread0" #6 daemon prio=9 os_prio=31 tid=0x00007ffb8a83d000 nid=0x3603 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
  - None

"Monitor Ctrl-Break" #5 daemon prio=5 os_prio=31 tid=0x00007ffb8a818000 nid=0x4903 runnable [0x000070000ebc3000]
   java.lang.Thread.State: RUNNABLE
  at java.net.SocketInputStream.socketRead0(Native Method)
  at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
  at java.net.SocketInputStream.read(SocketInputStream.java:171)
  at java.net.SocketInputStream.read(SocketInputStream.java:141)
  at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
  at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
  at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
  - locked <0x000000076ac83310> (a java.io.InputStreamReader)
  at java.io.InputStreamReader.read(InputStreamReader.java:184)
  at java.io.BufferedReader.fill(BufferedReader.java:161)
  at java.io.BufferedReader.readLine(BufferedReader.java:324)
  - locked <0x000000076ac83310> (a java.io.InputStreamReader)
  at java.io.BufferedReader.readLine(BufferedReader.java:389)
  at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)

   Locked ownable synchronizers:
  - None

"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007ffb8a802800 nid=0x3403 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
  - None

"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007ffb8b823000 nid=0x2f03 in Object.wait() [0x000070000e9bd000]
   java.lang.Thread.State: WAITING (on object monitor)
  at java.lang.Object.wait(Native Method)
  - waiting on <0x000000076ab08ec8> (a java.lang.ref.ReferenceQueue$Lock)
  at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
  - locked <0x000000076ab08ec8> (a java.lang.ref.ReferenceQueue$Lock)
  at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
  at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

   Locked ownable synchronizers:
  - None

"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007ffb8b006000 nid=0x2e03 in Object.wait() [0x000070000e8ba000]
   java.lang.Thread.State: WAITING (on object monitor)
  at java.lang.Object.wait(Native Method)
  - waiting on <0x000000076ab06b68> (a java.lang.ref.Reference$Lock)
  at java.lang.Object.wait(Object.java:502)
  at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
  - locked <0x000000076ab06b68> (a java.lang.ref.Reference$Lock)
  at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

   Locked ownable synchronizers:
  - None

"main" #1 prio=5 os_prio=31 tid=0x00007ffb8c000000 nid=0x1903 runnable [0x000070000e2a8000]
   java.lang.Thread.State: RUNNABLE
  at java.io.FileInputStream.readBytes(Native Method)
  at java.io.FileInputStream.read(FileInputStream.java:255)
  at java.io.BufferedInputStream.read1(BufferedInputStream.java:284)
  at java.io.BufferedInputStream.read(BufferedInputStream.java:345)
  - locked <0x000000076ab1de10> (a java.io.BufferedInputStream)
  at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
  at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
  at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
  - locked <0x000000076adbe098> (a java.io.InputStreamReader)
  at java.io.InputStreamReader.read(InputStreamReader.java:184)
  at java.io.Reader.read(Reader.java:100)
  at java.util.Scanner.readInput(Scanner.java:804)
  at java.util.Scanner.next(Scanner.java:1369)
  at com.imooc.collection.Thread.Daemon.Test1.main(Test1.java:49)

   Locked ownable synchronizers:
  - None
  // 主线程【main Thread】没有daemon标记,说明是用户线程

"VM Thread" os_prio=31 tid=0x00007ffb8b822000 nid=0x2c03 runnable 
"GC task thread#0 (ParallelGC)" os_prio=31 tid=0x00007ffb8c00a800 nid=0x2207 runnable 
"GC task thread#1 (ParallelGC)" os_prio=31 tid=0x00007ffb8c00b000 nid=0x2a03 runnable 
"GC task thread#2 (ParallelGC)" os_prio=31 tid=0x00007ffb8a800800 nid=0x5303 runnable 
"GC task thread#3 (ParallelGC)" os_prio=31 tid=0x00007ffb8a803800 nid=0x5103 runnable 
"VM Periodic Task Thread" os_prio=31 tid=0x00007ffb8b824800 nid=0x3a03 waiting on condition 
JNI global references: 22

——————————————————————————————

  • Thread类有没有实现Runnable接口?
    有。

一个类可以同时集成Thread和实现Runnable接口
源码中就有实现:class Thread implements Runnable{...}

run方法的修饰符可以写成protected

public class Test extends Thread implements Runnable{
    public static void main(String[]a args){
        Thread t = new Thread(new Test());
        t.start();
    }
}
public class X extends Thread implements Runnable{
    public void run(){
        System.out.println("this is run()");
    }
    public static void main(String[] args){
        Thread t = new Thread(new X());
        t.start();
    }
}

正常运行;
结果为:
his is run()

——————————————————————————————
面试提问:run()start() 有什么区别?

只有调用线程类的start()才能异步调用run(),真正达到多线程的目的;
直接调用run()是同步的,无法达到多线程的目的;

系统调用start()来启动一个线程,此线程现在处于就绪状态,而非运行状态,意味着这个线程可以被JVM调度执行;调度过程中,JVM通过调用run()方法来完成实际的操作,当run()结束后,该线程才会终止;
当直接调用线程类的run(),这会被当作一个普通函数调用,程序中只有一个线程;

该问题也可变成:
当调用一个线程对象的start方法后,线程马上进入运行状态吗?

不是,只是进入就绪(可运行)状态,等待分配CPU时间片,一旦得到CPU时间片,即进入运行状态;
————————————————————————
面试提问:sleep()wait() 有什么区别?

  • 1.原理不同:
    sleep是Thread的静态类方法,线程控制自身,可以设定时间,时间一到自动苏醒;
    wait来自Object,也可设置苏醒时间,用于通信;
  • 2.使用区域不同:
    sleep随便;
    wait由于本身特殊性,只能在同步语句块/同步方法内使用;
  • 3.锁的处理机制不同:
    sleep不涉及通信,所以不会释放锁;
    wait涉及通信,启用后,锁会释放,线程所在对象的其他synchronized数据会被其他线程使用;

提问:关于线程的说法,正确的是?
A. sleep执行时会释放对象锁;
B. wait执行时会释放对象锁;
C. sleep必须写在同步方法或同步块中;
D. wait必须写在同步方法或同步块中;

答:
BD
————————————————————————
提问:什么叫线程安全?
线程安全就是限制(通常是让请求排队)多个线程同时修改共享数据,从而保证数据的一致性或数据的正确性。
————————————————————————
提问:下列哪个方法可以创建一个可运行的类?
A.public class X implements Runnable {public void run(){......}}
B.public class X implements Thread {public void run(){......}}
C.public class X implements Runnable {public int run(){......}}
D.public class X implements Runnable {protected void run(){......}}

——————————
答:
AD

————————————————————————

  • 下面的代码,实际有几个线程在运行?
public static void main(String[]args) thows Exception{
    Runnable r = new Thread6();
    Thread t = new Thread(r,"Name test");
    t.start();
}

两个:

    Thread t = new Thread(r,"Name test");
    t.start();

和main方法(主线程);
————————————————————————

线程生命周期(5种 或 6种)

之所以有5种或6种的差异,是因为有无把Blocked区分地更细的缘故;
——————————————
5种

  • 线程的几种状态?(生命周期)

五种:创建、就绪、运行、阻塞、死亡;

在这里插入图片描述

  • 1.创建 New:新创建了一个线程对象;
    Thread thd = new Thread();
  • 2.就绪 Runnable:线程对象创建后,其他线程调用了该对象的start(),该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权;(只是具备了运行条件,不一定开始运行了!!因为CPU这个时候可能在执行其他线程,本线程还没获得CPU使用权,仍在等待)
  • 3.运行 Running:就绪状态的线程获取了CPU,执行程序代码;(stop()已经被淘汰了)
  • 4.阻塞 Blocked:阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行,直到线程进入就绪状态,才有机会转到运行状态;(阻塞一定要在运行状态后才可能会发生)(比如wait(),join(),sleep()
    Thread.sleep()
  • 5.死亡 Dead:线程执行完了或者因异常退出了run(),该线程结束生命周期;

阻塞又分成三种:
1.等待阻塞:(不是等着被阻塞,而是这种阻塞的方式是:等待着的,等着被唤醒 nofity)运行的线程执行wait(),该线程会释放占用的所有资源,JVM会把该线程放入等待池中,这个等待不能被自动唤醒,必须依靠notify()notifyAll()才能被唤醒,wait()是Object类的方法
2.同步阻塞:运行的线程在获取对象的同步琐时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中;
3.其他阻塞:运行的线程执行sleep()join(),或者发出了I/O请求时,JVM会把该线程设置为阻塞状态,当sleep() 状态超时、 join()等待线程终止或超时,或I/O(读取数据)处理完毕时,线程重新转入就绪状态,sleep()是Object类的方法

——————————————
6种:

Java的线程生命周期有六种状态:

1.New(初始化状态)
2.Runnable(可运行/运行状态)
3.Blocked(阻塞状态)
4.Waiting(无时间限制的等待状态)
5.Timed_Waiting(有时间限制的等待状态)
6.Terminated(终止状态)

  • 1.New(初始化状态):
    指的是在高级语言,比如Java。在Java层面的线程被创建了,而在操作系统中的线程其实是还没被创建的,所以这个时候是不可能分配CPU执行这个线程的!所以这个状态是高级语言独有的,操作系统的线程没这个状态。我们New了一个线程,那时候它就是这个状态。

  • 2.Runnable(可运行/运行状态):
    这个状态下是可以分配CPU执行的,在New状态时候我们调用start()方法后线程就处于这个状态。

  • 3.Blocked(阻塞状态):
    这个状态下是不能分配CPU执行的,只有一种情况会导致线程阻塞,就是synchronized!我们知道被synchronized修饰的方法或者代码块同一时刻只能有一个线程执行,而其他竞争锁的线程就从Runnable到了Blocked状态!当某个线程竞争到锁了它就变成了Runnable状态。注意并发包中的Lock,是会让线程属于等待状态而不是阻塞,只有synchronized是阻塞。(感觉是历史遗留问题,没必要多一个阻塞状态,和等待没差啊)。

  • 4.Waiting(无时间限制的等待状态):
    这个状态下也是不能分配CPU执行的。有三种情况会使得Runnable状态到waiting状态

(1).调用无参的Object.wait()方法。等到notifyAll()或者notify()唤醒就会回到Runnable状态。
(2).调用无参的Thread.join()方法。也就是比如你在主线程里面建立了一个线程A,调用A.join(),那么你的主线程是得等A执行完了才会继续执行,这是你的主线程就是等待状态。
(3).调用LockSupport.park()方法。LockSupport是Java6引入的一个工具类Java并发包中的锁都是基于它实现的,再调用LocakSupport.unpark(Thread thread),就会回到Runnable状态。

  • 5.Timed_Waiting(有时间限制的等待状态):
    其实这个状态和Waiting就是有没有超时时间的差别,这个状态下也是不能分配CPU执行的。有五种情况会使得Runnable状态到waiting状态

(1).Object.wait(long timeout)。
(2).Thread.join(long millis)。
(3).Thread.sleep(long millis)。注意 Thread.sleep(long millis, int nanos) 内部调用的其实也是Thread.sleep(long millis)。
(4).LockSupport.parkNanos(Object blocked,long deadline)。
(5).LockSupport.parkUntil(long deadline)。

  • 6.Terminated(终止状态):
    在我们的线程正常run结束之后或者run一半异常了就是终止状态!
    注意有个方法Thread.stop()是让线程终止的,但是这个方法已经被废弃了,不推荐使用,因为比如你这个线程得到了锁,你stop了之后这个锁也随着没了,其它线程就都拿不到这个锁了!这不玩完了么!所以推荐使用interrupt()方法。

interrupt()会使得线程Waiting和Timed_Waiting状态的线程抛出 interruptedException异常,使得Runnabled状态的线程如果是在I/O操作会抛出其它异常。

如果Runnabled状态的线程没有阻塞在I/O状态的话,那只能主动检测自己是不是被中断了,使用isInterrupted()。

————————————————————————

  • 线程名字除了维持初始化的本名,还可以自定义

获取线程的名字方法是:String name = Thread.currentThread().getName();

若不作改动,第一个线程初始化的本名会是:Thread-0
如果想自定义线程名字?
implements Runnable中可以写:Thread thread = new Thread(runnable,"名字");
extends Thread中可以先定义有参构造器,其中定义一个String name,然后main方法里写:Thread thread = new Thread("名字");

extends Thread中还可以写成两行:

Thread thd = new Thread();
thd.setName("名字");

——————————————————————————————

sleep yield join wait 方法

  • sleep yield join wait 方法的区别?
    (90% 初中高级面试 高频;可能一辈子也用不到,但是必须知道)

sleep

public class TestSleep {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("hello");
        Thread.sleep(2000);
        System.out.println("world");
    }
}

———————————————
join

Join()的例1:

public class TestJoin {

    public static void main(String[] args) throws InterruptedException {
        TestJoin2 t1 = new TestJoin2("小明");
        TestJoin2 t2 = new TestJoin2("小东");
        t1.start();
        t1.join();
        t2.start();
    }
}

class TestJoin2 extends Thread{
    public TestJoin2(String name){
        super(name);
    }
    @Override
    public void run(){
        for(int i = 0; i<50 ; i++){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.getName()+":"+i);
        }
    }
}

输出的结果是:

小明:0
小明:1
小明:2
小明:3
小明:4
小明:5
...
小明:47
小明:48
小明:49
小东:0
小东:1
小东:2
...
小东:47
小东:48
小东:49

// 即:小明先输出完,再轮到小东输出

这一行t1.join();对输出起了关键作用;
如果没有这一行,那么结果会是:小明一行+小东一行+小明一行+小东一行...(轮询着输出)
所以
1.join()的作用是:让该线程执行完,再执行下一个线程(最初级理解,可以拿1W月薪封顶);

join(0)中还能放参数,目的是让目标线程(上一级线程)阻塞指定时间;
(如果不加时间参数,意思就是无限大:等到调用线程完全执行完毕,再执行下一个操作)
(不设置时间参数,就相当于写:.join(Long.MAX_VALUE)

改造一下,join()中加上时间:

TestJoin2 t1 = new TestJoin2("小明");
        TestJoin2 t2 = new TestJoin2("小东");
        t1.start();
        t1.join(5000);
        System.out.println("xxoo");
        t2.start();
...
小明:45
小明:46
小明:47
xxoo
小明:48
小东:0
小明:49
小东:1
小东:2
...

因为t1.join(5000)规定把t1阻塞5秒钟:那么运行到小明:47这一行时,5秒到了;
尽管此时t1还没有完整输出,但程序还是要释放t1的锁,执行下面的语句xxoo,然后再继续执行完t1,后面接着执行t2;

2.join()的作用是:阻塞(wait())调用的线程,让调用方法执行完毕,再释放锁,执行下面的线程; :(进一步的理解)
(调谁就阻塞谁!!!)

白话:

t1.start();
t1.join(); // 括号内还能写毫秒时间
t2.start();

join写在这两个start方法之间,join会阻塞t2,让t1执行完毕后,再释放锁,让t2执行;
如果没有写join,t1和t2可能会交替执行;

查看源码,join()里调用的就是wait();结合例子,他阻塞(wait)的是t1,其实阻塞的是main方法;
所以意思是:t1.start()执行第一行后,执行t1.join(),它会采用wait的原理,阻塞t1.start(),让t1.start()执行完毕之后再释放锁,执行之后的语句;

另外,如果t1.join()放在t1.start()t2.start()之后执行,或者放在他俩之前执行,是没有效果的,即输出结果会轮询t1和t2;

另外,比如这么写:

t1.start();
t1.join();
System.out.println("xxoo");
t2.start();

结果为:

...
小明:47
小明:48
小明:49
xxoo
小东:0
小东:1
小东:2
...

Join()的例2:

public class TestJoin3 implements Runnable{

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + "开始执行.");

        for(int i=1;i<11;i++){
            System.out.println(name+"执行了[ "+i+" ]次");

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

    public static void main(String[] args) throws InterruptedException {
        TestJoin3 runnable = new TestJoin3();
        Thread thread1 = new Thread(runnable,"线程1");

        System.out.println("主线程开始执行.");
        thread1.start();
        thread1.join();

        System.out.println("主线程执行结束.");
    }
}
——————————
输出:
主线程开始执行.
Thread-0开始执行.
Thread-0执行了[ 1 ]次
Thread-0执行了[ 2 ]次
Thread-0执行了[ 3 ]次
Thread-0执行了[ 4 ]次
Thread-0执行了[ 5 ]次
Thread-0执行了[ 6 ]次
Thread-0执行了[ 7 ]次
Thread-0执行了[ 8 ]次
Thread-0执行了[ 9 ]次
Thread-0执行了[ 10 ]次
主线程执行结束.

如果把thread1.join();注释掉?会如何输出?

主线程开始执行.
线程1开始执行.
线程1执行了[ 1 ]次
主线程执行结束.
线程1执行了[ 2 ]次
线程1执行了[ 3 ]次
线程1执行了[ 4 ]次
线程1执行了[ 5 ]次
线程1执行了[ 6 ]次
线程1执行了[ 7 ]次
线程1执行了[ 8 ]次
线程1执行了[ 9 ]次
线程1执行了[ 10 ]次

相当于做了一个异步操作;
———————————————
yield
仅仅释放线程所占有的CPU资源,从而让其他线程有机会运行,但是并不能保证某个特定的线程能获得CPU资源;谁能获得CPU完全取决于调度器,在有些情况下调用本方法的线程甚至会再次得到CPU资源;所以,依赖于本方法是不可靠的,他只能尽力而为;作用于线程;

———————————————
wait
wait只能在同步环境中被调用,而sleep不需要;
进入wait状态的线程能够被nofity和notifyAll线程唤醒,但进入sleep状态的线程不能被notify唤醒;
wait通常有条件地执行,线程会一直处于wait状态,直到某个条件变成真;但sleep仅仅让你的线程进入睡眠状态;
wait状态会释放对象的锁,但sleep不会;

wait方法是针对一个被同步代码块加锁的对象;

(上文内容:wait来自Object,也可设置苏醒时间,用于通信;
wait由于本身特殊性,只能在同步语句块/同步方法内使用;
wait涉及通信,启用后,锁会释放,线程所在对象的其他synchronized数据会被其他线程使用;)
——————————————————————————————

  • synchronized修饰在方法前是什么意思?
    一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(即在synchronized方法内部的线程)执行完该方法后,别的线程才能进入;
    他保证此方法线程安全、同步,白话就是让这个方法获得一个锁;

同步:并发(多个线程访问同一份资源,确保资源安全,线程安全)
(重点是多个线程访问同一份资源,如果多个资源分别访问多个资源,那就不在讨论范围内)
之前会说到Hashtable是线程安全,xxx是线程不安全,因为Hashtable源码里方法前写了synchronized(线程安全、同步);StringBuffer前也加了synchronized;
——————————————————————————————

  • 使用TimerTimerTask实现定时执行,定时在每天下午17:00执行

  • Timer:定时器,实际上是个线程,定时调度所拥有的TimeTasks;

  • TimerTask:一个拥有run方法的类,需要定时执行的代码放在run方法体中,TimerTasks一般以匿名类的方式创建;
    ——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————

线程池 ThreadPool

  • 为什么用线程池?

达到复用;
有时,系统要处理很多执行时间很短的请求,如果每个请求都开一个新线程的话,系统要进行很多销毁,有时创建和销毁线程的时间会比线程执行时间还长;
而且当线程数量太多,系统会受不了;
线程池为了解决:
通过重用线程池中的线程,减少每个线程创建和销毁的性能开销;
对线程进行一些维护和管理,比如定时开始,周期执行,并发数控制等;
——————————————————————————————

  • 线程池参数是什么意思?(就是问线程池怎么用)

比如:去火车站买票,有10个售票窗口,但只有5个对外开放
核心线程数 corePoolSize:对外开放的5个窗口;
最大线程数 maximumPoolSize:10个窗口(共10个窗口,所以只多能开10个窗口);
线程队列已满:如果5个窗口都被占用,那么后来的人必须在后面排队,但售票厅的人越来越多,直至塞爆;
线程异常处理策略:这时火车站站长安排把剩下的5个窗口都打开,所以目前有10个窗口同时运行;而后面又来了更多批人,10个窗口也处理不过来了,且售票厅人满为患,站长下令不再允许乘客进站;
线程存活时间 keepAliveTime:允许售票员休息的最长时间,以此限制售票员偷懒现象;
——————
【任务队列】【WorkQueue

常用三种:SynchronousQueue LinkedBlockingDeque ArrayBlockingQueue

报错情况:
1.SynchronousQueue:当线程数超过maximumPoolSize时;
2.LinkedBlockingDeque:当队列数超过capacity时;

下面讲maximumPoolSize时有介绍;
——————————————————————————————

  • 线程池中的ThreadPoolExecutor,每个参数是干什么的?

Executor是一个接口,跟线程池有关的基本都要和他打交道,ThreadPoolExecutor的关系:
ThreadPoolExecutor - (abstract)AbstractExecutorService - (interface)ExecutorService - (interface)Executor 子(使用线程池时用到的主类)

使用线程池就要用到如下代码:

ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, // 核心线程数
                      maximumPoolSize, // 最大线程数
                      keepAliveTime, // 限制线程存活时间:秒/小时/分钟
                      TimeUnit.MILLISECONDS, // 时间单位
                      new LinkedBlockingDeque<Runnable>, // 线程队列
                      Executors.defaultThreadFactory(), // 线程工厂
                      new AbortPolicy() // 队列已满,且当前线程数已经超过最大线程数时的异常处理
           );

——————
corePoolSize
默认情况下线程会一直存活,即使处于闲置状态也不受到keepAliveTime限制,除非将allowCoreThread设置成true;
——————
maximumPoolSize:(易错!!!!!!!!!!!!!!)
最大线程数(两个队列若报错?都跟他有关系)

1.LinkedBlockingDeque时,若未设置capacity(队列容量),最大线程数就无意义;

ThreadPoolExecutor executor = new ThreadPoolExecutor
      (3,4,5, TimeUnit.SECONDS,new LinkedBlockingDeque<Runnable>(capacity));

未设置capacity(队列容量),意味着队列没有上限;
此时,LinkedBlockingDeque 完全不受maximumPoolSize的影响
进入的线程数可以超过核心线程数,多出来的部分就全部堆在队列中,无上限;
(最大线程数不包括队列数量)

2.LinkedBlockingDeque时,若设置了capacity,最大线程数有意义;
超过最大线程数后,多出来的线程会放到队列中;
当队列中的数字超过capacity后,报错:RejectedExecutionException;

3.SynchronousQueue时,最大线程数有意义;
若线程数多出核心线程数(但没超过最大线程数时),没问题,多出部分放在PoolSize中;
而SynchronusQueue没有队列这一说
但还超过了最大线程数的话,多出来的线程会失效(因为无队列可存放),会报错:RejectedExecutionException

——————
keepAliveTime
【非核心线程】的闲置【超时时间】,超过这个时间就会被回收;
非核心线程的意思是:
Synchronous中,即使是【超过了核心线程,但是没有超过最大线程】的这点线程,超时后也会失效!!!
——————
(易错方法!!!!!!)
getCorePoolSize():返回的是【定值】,设置了核心线程数是几就返回几!即使现在未开线程,这个数也不变;
getPoolSize():返回的是【实际线程数】,【不包括队列里的数量】!!!
getQueue().size():当处于Synchronous队列任务时,这个参数始终为0!!!!!!

——————
keepAliveTime的易错点:
比如在此代码中:

public static void main(String[] args) throws InterruptedException {
        Runnable myRunnable = new Runnable(){
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        };

        ThreadPoolExecutor executor = new ThreadPoolExecutor(3,4,5,TimeUnit.SECONDS,new LinkedBlockingDeque<Runnable>());

        executor.execute(myRunnable); // 注1
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        System.out.println("开6个线程");
        System.out.println("QueueSize: "+executor.getQueue().size()); // 3
        Thread.sleep(8000); // 注2
        System.out.println("QueueSize: "+executor.getQueue().size());  // 0
    }

在此式中,keepAliveTime作用的地点不是注1,而是注2!!
run()方法本身耗费的2s不用考虑,考虑的是Thread(8000)!!!

——————
unit
指定keepAliveTime的单位,如TimeUnit.SECONDS;(当allowCoreThreadTimeOut设置成true时对corePoolSize生效)

——————
threadFactory
线程工厂,提供创建新线程的功能,ThreadFactory是一个接口,只有一个方法:

public interface ThreadFactory{
    Thread new Thread(Runnable r);
}

——————
.getPoolSize
返回线程池实际的线程数
——————
RejectExecutionHandler 异常处理
也是一个接口,只有一个方法:

public interface RejectExecutorHandler{
    void rejectedExecutor(Runnable var1,ThreadPoolExecutor var2);
}

——————————————————————————————

  • Test1:标准线程池使用方法
public class ThreadPoolExecutorTest1 {
    // 标准线程池使用方法

    public static void main(String[] args) throws InterruptedException {
        Runnable myRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName()+"run");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 线程池主类:初始化数据
        ThreadPoolExecutor executor = new ThreadPoolExecutor(6,10,5,TimeUnit.SECONDS,new SynchronousQueue<Runnable>());

        // corePoolSize:6,maximumPoolSize:10,keepAliveTime:5s

        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        System.out.println("以上先开三个线程————————————");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());
        System.out.println("poolSize: "+executor.getPoolSize());
        System.out.println("QueueSize: "+executor.getQueue().size());
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        System.out.println("以上又开三个线程————————————");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());
        System.out.println("poolSize: "+executor.getPoolSize());
        System.out.println("QueueSize: "+executor.getQueue().size());
        Thread.sleep(8000);
        System.out.println("AFTER 8 SECONDS......");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());
        System.out.println("poolSize: "+executor.getPoolSize());
        System.out.println("QueueSize: "+executor.getQueue().size());
    }
}
————————————————————
输出:
以上先开三个线程————————————
corePoolSize: 6
poolSize: 3  // 因为写了三行,创建了三个线程
QueueSize: 0
以上又开三个线程————————————
corePoolSize: 6
poolSize: 6
QueueSize: 0
pool-1-thread-2run  // 这时突然跑出来run()里语句?是因为Thread.sleep(8000); 休息的8秒到期了
pool-1-thread-3run  // 先执行了线程池的语句,休息8秒时,run()里的机会来了,于是他就输出在这里
pool-1-thread-1run
pool-1-thread-4run
pool-1-thread-6run
pool-1-thread-5run
AFTER 8 SECONDS......
corePoolSize: 6
poolSize: 6
QueueSize: 0

**易错:

  • getCorePoolSize是一个【定值】,不会变化!!!设置了多少corePoolSize,他就返回多少!!!
    (即使线程池无线程运行时,getCorePoolSize 也等于设置的值!!!)
    至始至终,getCorePoolSize 都等于 设置的corePoolSize值;**
  • getPoolSize是【动态】的,当前在跑几个线程就返回几;

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

  • Test2:LinkedBlockingDeque
public class ThreadPoolExecutorTest2 {

    public static void main(String[] args) throws InterruptedException {
        Runnable myRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName()+" run");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 线程池主类:初始化数据
        // 队列换成了 LinkedBlockingDeque
        // 特点:当任务数超过corePoolSize时,会把超出的任务放在QueueSize中,只会创建3个线程重复利用
        ThreadPoolExecutor executor = new ThreadPoolExecutor(3,6,5, TimeUnit.SECONDS,new LinkedBlockingDeque<Runnable>());

        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        System.out.println("以上先开三个线程————————————");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  // 3
        System.out.println("poolSize: "+executor.getPoolSize());          // 3
        System.out.println("QueueSize: "+executor.getQueue().size());     // 0
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        System.out.println("以上又开三个线程————————————");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  // 3
        System.out.println("poolSize: "+executor.getPoolSize());          // 3 没开新线程,因为有线程复用
        System.out.println("QueueSize: "+executor.getQueue().size());     // 3
        Thread.sleep(8000);  // 8>5,所以Queue里的3个(亦是非corePoolSize)都被回收
        System.out.println("AFTER 8 SECONDS......");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  // 3
        System.out.println("poolSize: "+executor.getPoolSize());          // 3
        System.out.println("QueueSize: "+executor.getQueue().size());     // 0
    }
}

—————————————————————————
输出:
以上先开三个线程————————————
corePoolSize: 3
poolSize: 3
QueueSize: 0
以上又开三个线程————————————
corePoolSize: 3
poolSize: 3
QueueSize: 3
pool-1-thread-1run    // 注意:输出是123 123 说明线程复用了(而不是123 456)
pool-1-thread-2run
pool-1-thread-3run
pool-1-thread-1run
pool-1-thread-2run
pool-1-thread-3run
AFTER 8 SECONDS......
corePoolSize: 3
poolSize: 3
QueueSize: 0

如果这里理解有误,页面搜索:(易错方法!!!!!!)
——————————————————————————————

  • Test3:SynchronousQueue
public class ThreadPoolExecutorTest3 {

    public static void main(String[] args) throws InterruptedException {
        Runnable myRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName()+" run");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 队列换成了 SynchronousQueue
        // 特点:无复用

        ThreadPoolExecutor executor = new ThreadPoolExecutor(3,6,5, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());

        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        System.out.println("以上先开三个线程————————————");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  // 3
        System.out.println("poolSize: "+executor.getPoolSize());          // 3
        System.out.println("QueueSize: "+executor.getQueue().size());     // 0
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        System.out.println("以上又开三个线程————————————");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  // 3
        System.out.println("poolSize: "+executor.getPoolSize());          // 6 SynchronousQueue 没有复用
        System.out.println("QueueSize: "+executor.getQueue().size());     // 0
        Thread.sleep(8000);
        System.out.println("AFTER 8 SECONDS......");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  // 3
        System.out.println("poolSize: "+executor.getPoolSize());          // 3
        System.out.println("QueueSize: "+executor.getQueue().size());     // 0
    }
}

—————————————————————————
输出:
以上先开三个线程————————————
corePoolSize: 3
poolSize: 3
QueueSize: 0
以上又开三个线程————————————
corePoolSize: 3
poolSize: 6
QueueSize: 0
pool-1-thread-6run
pool-1-thread-2run
pool-1-thread-1run
pool-1-thread-3run
pool-1-thread-5run
pool-1-thread-4run
AFTER 8 SECONDS......
corePoolSize: 3
poolSize: 3
QueueSize: 0

——————————————————————————————

  • Test4:LinkedBlockingDeque
public class ThreadPoolExecutorTest4 {
    public static void main(String[] args) throws InterruptedException {
        Runnable myRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName()+" run");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // LinkedBlockingDeque

        ThreadPoolExecutor executor = new ThreadPoolExecutor(3,4,5, TimeUnit.SECONDS,new LinkedBlockingDeque<Runnable>());

        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        System.out.println("以上先开6个线程————————————");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  // 3
        System.out.println("poolSize: "+executor.getPoolSize());          // 3
        System.out.println("QueueSize: "+executor.getQueue().size());     // 3
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        executor.execute(myRunnable);
        System.out.println("以上又开6个线程————————————");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  // 3
        System.out.println("poolSize: "+executor.getPoolSize());          // 3
        System.out.println("QueueSize: "+executor.getQueue().size());     // 9
        Thread.sleep(8000);
        System.out.println("AFTER 8 SECONDS......");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  // 3
        System.out.println("poolSize: "+executor.getPoolSize());          // 3
        System.out.println("QueueSize: "+executor.getQueue().size());     // 0
    }
}
—————————————————————————
输出:
以上先开6个线程————————————
corePoolSize: 3
poolSize: 3
QueueSize: 3
以上又开6个线程————————————
corePoolSize: 3
poolSize: 3
QueueSize: 9
pool-1-thread-2 run  // 因为核心线程数是3,所以只有这三个核心线程在复用
pool-1-thread-3 run
pool-1-thread-1 run
pool-1-thread-2 run
pool-1-thread-3 run
pool-1-thread-1 run
pool-1-thread-2 run
pool-1-thread-3 run
pool-1-thread-1 run
AFTER 8 SECONDS......
corePoolSize: 3
poolSize: 3
QueueSize: 0
pool-1-thread-2 run
pool-1-thread-3 run
pool-1-thread-1 run

要点:
LinkedBlockingDeque设置了corePoolSize之后,就不用管maximumPoolSize了;只考虑corePoolSize即可;

——————————————————————————————

  • Test5:LinkedBlockingDeque

注:LinkedBlockingDeque添加的capacity数字,代表队列限制的最大数字;

若队列数超过capacity,程序会报错RejectedExecutionException!!!

public class ThreadPoolExecutorTest5 {
    public static void main(String[] args) throws InterruptedException {
        Runnable myRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName()+" run");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // LinkedBlockingDeque 且队列设置了capacity:1

        // LinkedBlockingDeque 完全不受maximumPoolSize的影响(意思是虽然MPS虽然有限制,但是队列capacity无限制,可以随意添加线程)
        // 但是当LinkedBlockingDeque的队列有大小限制时(capacity),程序就会报错
        // 首先为3个任务开了3个corePoolSize:1,2,3 然后第4,5个task加入队列,第6个因为队列已满就直接创建新线程4
        // 至此一共4个线程,未超过maximumPoolSize,8秒后,non-corePoolSize受到keepAliveTime影响而被回收,因此线程池只剩下3个线程了

        ThreadPoolExecutor executor = new ThreadPoolExecutor(3,4,5, TimeUnit.SECONDS,new LinkedBlockingDeque<Runnable>(1));

        executor.execute(myRunnable); // 线程1 OK
        executor.execute(myRunnable); // 线程2 OK
        executor.execute(myRunnable); // 线程3 OK 至此 corePoolSize满
        System.out.println("以上先开3个线程————————————");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  // 3
        System.out.println("poolSize: "+executor.getPoolSize());          // 3
        System.out.println("QueueSize: "+executor.getQueue().size());     // 0
        executor.execute(myRunnable); // 线程4 OK 至此 maximumPoolSize满 corePoolSize: 3 poolSize: 4 QueueSize: 0
        executor.execute(myRunnable); // 线程5 OK 至此 Deque满 corePoolSize: 3 poolSize: 4 QueueSize: 1
        executor.execute(myRunnable); // 线程6 
        System.out.println("以上又开3个线程————————————");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  //
        System.out.println("poolSize: "+executor.getPoolSize());          //
        System.out.println("QueueSize: "+executor.getQueue().size());     //
        Thread.sleep(8000);
        System.out.println("AFTER 8 SECONDS......");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  //
        System.out.println("poolSize: "+executor.getPoolSize());          //
        System.out.println("QueueSize: "+executor.getQueue().size());     //
    }
}
—————————————————————————
输出:
Exception in thread "main" 以上先开3个线程————————————
corePoolSize: 3
poolSize: 3
QueueSize: 0
java.util.concurrent.RejectedExecutionException: Task com.imooc.collection.Thread.ThreadPoolExecutorTest5$1@66d3c617 rejected from java.util.concurrent.ThreadPoolExecutor@63947c6b[Running, pool size = 4, active threads = 4, queued tasks = 1, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
	at com.imooc.collection.Thread.ThreadPoolExecutorTest5.main(ThreadPoolExecutorTest5.java:39)
pool-1-thread-1 run
pool-1-thread-2 run
pool-1-thread-3 run
pool-1-thread-4 run
pool-1-thread-1 run

——————————————————————————————

  • Test6:SynchronousQueue

报错:RejectedExecutionException
当线程数超过maximumPoolSize时

public class ThreadPoolExecutorTest6 {
    public static void main(String[] args) throws InterruptedException {
        Runnable myRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName()+" run");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        
        // SynchronousQueue
        // 添加第5个线程的时候会报错,因为SynchronousQueue不保存队列,收到一个任务就会去执行

        ThreadPoolExecutor executor = new ThreadPoolExecutor(3,4,5, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());

        executor.execute(myRunnable); // 线程1 OK
        executor.execute(myRunnable); // 线程2 OK
        executor.execute(myRunnable); // 线程3 OK 至此 corePoolSize满
        System.out.println("以上先开3个线程————————————");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  // 3
        System.out.println("poolSize: "+executor.getPoolSize());          // 3
        System.out.println("QueueSize: "+executor.getQueue().size());     // 0
        executor.execute(myRunnable); // 线程4 OK 至此 maximumPoolSize满 corePoolSize: 3 poolSize: 4 QueueSize: 0
        executor.execute(myRunnable); // 线程5 报错
        executor.execute(myRunnable); // 线程6
        System.out.println("以上又开3个线程————————————");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  //
        System.out.println("poolSize: "+executor.getPoolSize());          //
        System.out.println("QueueSize: "+executor.getQueue().size());     //
        Thread.sleep(8000);
        System.out.println("AFTER 8 SECONDS......");
        System.out.println("corePoolSize: "+executor.getCorePoolSize());  //
        System.out.println("poolSize: "+executor.getPoolSize());          //
        System.out.println("QueueSize: "+executor.getQueue().size());     //
    }
}

—————————————————————————
输出:
以上先开3个线程————————————
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.imooc.collection.Thread.ThreadPoolExecutorTest6$1@63947c6b rejected from java.util.concurrent.ThreadPoolExecutor@2b193f2d[Running, pool size = 4, active threads = 4, queued tasks = 0, completed tasks = 0]
corePoolSize: 3
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
poolSize: 3
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
QueueSize: 0
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
	at com.imooc.collection.Thread.ThreadPoolExecutorTest6.main(ThreadPoolExecutorTest6.java:34)
pool-1-thread-3 run
pool-1-thread-2 run
pool-1-thread-1 run
pool-1-thread-4 run

——————————————————————————————

  • 线程池的内部使用规则?

在这里插入图片描述
其实就是上面的6个例子;

线程池的线程执行规则跟任务队列有很大的关系;

任务队列没有大小限制时:
如果线程数量<=核心线程数量,那么直接启动一个核心线程来执行任务,不会放入队列;
如果线程数量>核心线程数,但<=最大线程数,并且任务队列是LinkedBlockingDeques时,超过核心线程数的任务会放入队列中排队;
如果线程数量>核心线程数,但<=最大线程数,并且任务队列是Synchronous时,线程池会创建新线程执行任务,也不会被放入任务队列中;这些线程属于非核心线程,在任务完成后,限制时间达到了超过时间就会被回收;
如果线程数量>核心线程数,并且>最大线程数,当任务队列是LinkedBlockingDeque,会将超过核心线程的任务放在任务队列中;也就是当任务队列是LinkedBlockingDeques时,并且没有大小限制时,线程池的最大线程数设置是无效的,他的线程数最多不会超过线程数;
如果线程数量>核心线程数,并且>最大线程数,当任务队列是Synchronous时,会因为线程池拒绝添加任务而抛错;

任务队列大小有限时:
当LinkedBlockingDeques塞满时,新增的任务会直接创建新线程来执行,当创建的线程数量超过最大线程数时会抛异常;
SynchronousQueue没有数量限制,因为他根本不会保持这些任务,而是直接交给线程池去执行。

——————————————————————————————

ThreadLocal

  • 用过ThreadLocal吗?怎么用? (Java中高级时 高频)

JDK1.2中就提供了java.lang.ThreadLocal,为解决多线程并发问题提供了一种方法,可以简洁的编写出优美的多线程程序;

线程中的安全问题都是由于共享问题造成的,有了ThreadLocal局部变量就可以解决

很多人望文生义,以为这是个“本地线程”,其实不是!
ThreadLocal是Thread的局部变量:实现当前线程的共享变量
也许把它理解成ThreadLocalVar更易理解;
他为变量在每个线程中都创建了一个副本,那么每个线程都可以访问自己内部的副本变量;

ThreadLocal是一个本地线程副本变量工具类,主要用于私有线程和该线程存放的副本对象做一个映射,在高并发场景下,可以实现无状态调用,特别适用于各个线程依赖不同的变量值完成造作的场景。

以上是概念,已经足够面试。

ThreadLocal<范型> 实例 = new ThreadLocal()

.get() 获取当前线程的副本value
.set() 保存当前线程的副本value
initialValue() 当前线程初时副本变量值
remove() 移除当前线程的副本变量值

例子:

private ThreadLocal<Integer> count = new ThreadLocal(){
    @Override
    protected Integer initialValue(){
        return 0; 
    }
};

	// 因为下方getNext中的 count.get(); 会报空指针异常
 	  // 所以在上方定义initialValue()初始值为0

public int getNext(){
    Integer value = count.get();  // 小心空指针异常
    value++;
    count.set(value);
    return value;
}

public static void main(String[] args) {
    ThreadLocalTest1 t = new ThreadLocalTest1();
    new Thread(new Runnable(){
        @Override
        public void run(){
            while(true){
                System.out.println(Thread.currentThread().getName()+
                " "+t.getNext());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();

    new Thread(new Runnable(){
        @Override
        public void run(){
            while(true){
                System.out.println(Thread.currentThread().getName()+
                " "+t.getNext());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();

    new Thread(new Runnable(){
        @Override
        public void run(){
            while(true){
                System.out.println(Thread.currentThread().getName()+" "+t.getNext());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();
}
————————————————————————-
输出:
Thread-0 1
Thread-1 1
Thread-2 1
Thread-0 2
Thread-1 2
Thread-2 2
Thread-0 3
Thread-2 3
Thread-1 3
Thread-0 4
Thread-1 4
Thread-2 4
Thread-1 5
Thread-2 5
Thread-0 5
Thread-2 6
Thread-1 6
Thread-0 6
Thread-2 7
Thread-0 7
Thread-1 7
Thread-2 8
Thread-1 8
Thread-0 8
Thread-2 9
Thread-0 9
Thread-1 9
......

https://www.bilibili.com/video/av38448736?from=search&seid=7986154456761801928
62分钟:年薪15 - 30W参考面试题

——————————————————————————————

数据争用 JMM(Java内存模型) 可见性

JMM = Java Memory Model

如果是单线程,无需考虑安全性和可见性问题;
如果是多线程,必须考虑安全性和可见性问题;

问题概要:
什么是内存可见性?
Java内存模型(JMM)?
实现可见性的方法:synchronized和volatile
(final也可保证内存可见性)
synchronized和volatile实现可见性的原理?
 - synchronized实现可见性:
(指令重排序)
(as - if - serial语义)
 - volatile实现可见性:
(volatile可保证可见性)
(volatile不可保证原子性)
(volatile使用注意事项)
synchronized和volatile比较:
(volatile比synchronized轻量级)
(volatile没synchronized使用广泛)

即使没有保证可见性的措施,
为什么很多时候共享变量依然能在主内存和工作内存之间得到及时更新?
(页面搜索)

另外,了解一下
对于64位的long,double变量的读写可能不是原子操作?

附:(了解)
对于64位的long,double变量的读写可能不是原子操作?
Java内存模型允许JVM将没有被volatile修饰的64位数据类型的读写划分为两次32次读写操作来进行;
导致问题:有可能会读到【半个变量】的情况(32位读到一半,CPU使用权被抢了)
解决方法:加volatile关键字;
但现在大多数JVM都把64位变量做了一个原子性处理,所以这个问题无需太关注;
————————————————

共享变量:
如果一个【变量】在【多个线程】的【工作内存】中都存在【副本】,那么这个【变量】就是这几个线程的共享变量;

可见性:
一个线程对【共享变量】值的修改,能够及时的被其他线程看到;

工作内存:
【Java内存模型 JMM】抽象出来的概念;

JMM(Java内存模型)
Java Memory Model 描述了Java程序中各种【变量】(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和内存中读取出【变量】这样的底层细节;
(多线程,带来共享变量问题,带来线程争用问题,带来可见性和安全性问题)

所有变量都存储在主内存中;
每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝);

在这里插入图片描述
JMM的规定:

  1. 线程对共享变量的所有操作都必须在自己的【工作内存】中进行,不能直接从主内存中读写;
  2. 不同【线程】之间无法直接访问【其他线程】工作内存中的变量,线程间变量值的传递需要通过【主内存】来完成;

【共享变量可见性】实现的原理:必须满足【两点】
比如线程1对共享变量的修改要想被线程2及时看到,必须经过下面两个步骤:

  1. 线程修改后的共享变量值能及时从工作内存刷新到主内存中;
    (把工作内存1中更新过的共享变量刷新到主内存中)
  2. 其他线程能够及时把共享变量的最新值从主内存更新到自己的工作内存中;
    (把主内存中最新的共享变量的值更新到工作内存2中)

在这里插入图片描述
多线程中,共享变量不可见是一个很严重的问题!!!
上图中任何一个环节出错都会导致这个问题,造成线程不安全!!!

——————————————————————————————
Java语言层面支持的可见性实现方式:
Synchronized
Volatile
(包括JDK1.5后引入的机制还有 ReentrantLockAtomicInteger
———————————————

Synchronized

单点服务器加锁使用synchronized和volatile,但分布式部署时,可使用Redis锁!

作用:

  • 原子性(同步)
  • 可见性

JMM中关于Synchronized的两条规定:

  1. 线程 解锁前 ,必须把共享变量的最新值刷新到主内存中;
  2. 线程 加锁时 ,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁于解锁需要同一把锁)
    这两条规定保证了:线程解锁前对共享变量的修改在下次加锁时对其他线程可见;

线程执行互斥代码的过程:

  1. 获得互斥锁
  2. 清空工作内存
  3. 从主内存拷贝变量的最新副本到工作内存
  4. 执行代码
    (期间可能更改了新的共享变量值)
  5. 将更改后的共享变量的值刷新到主内存
  6. 释放互斥锁

重排序: (了解)
代码书写的顺序 与 实际执行的顺序 不同,指令重排序 是 编译器或处理器 为了提高程序性能 而做的优化;(有数据依赖关系就禁止重排序)
(由于有些代码翻译成机器语言后,重排序后的风格可能更符合CPU的执行特点,即可最大性能发挥CPU的性能)

  1. 【编译器优化】的重排序(编译器优化)
    单线程中保证结果正确的前提下,安排代码执行顺序;
  2. 【指令级并行】的重排序(处理器优化)
    双核处理器很多都采用了此重排序;
  3. 【内存系统】的重排序(处理器优化)
    处理器对读写缓存进行优化;

在这里插入图片描述

as-if-serial (Java在【单线程】下遵循的语义)

无论如何重排序,程序执行的结果都应该和代码顺序执行的结果一致;
(Java编译器、运行时和处理器都会保证Java在【单线程】下遵循本语义)

因此,重排序不会给单线程带来内存可见性问题;
但在【多线程】中程序【交错执行】时,重排序可能会造成内存可见性问题;

单线程情况:

int num1 = 1;
int num2 = 2;
int sum = num1 + num2;
单线程中,第1、2行顺序可以重拍,但第3行不能;
第3行会一直在第1、2行之后;

可见性实现的例子:

此例子目的:
分析 Read线程 与 Write线程之间线程的可见性;
即 若Write线程改变了,那Read线程能否及时看到?

程序跑一遍:顺序是 先Read后Write,或者先Write后Read;
如果先Read后Write,且线程间有可见性:那么num会等于2,再等于6;
若不可见,则num会等于1,再等于3

(勿修改此段代码的行数)

public class SynchronizedDemo{

    // 共享变量
    private boolean ready = false;
    private int result = 0;
    private int number = 1;

    //写操作
    public void write(){
        ready = true;
        number = 2;
    }

    //读操作
    public void read(){
        if(ready){
            result = number*3;
        }
        System.out.println("result的值为:"+result);
    }

    // 内部线程类

    private class ReadWriteThread extends Thread{
        // 根据构造方法中传入的flag参数,确定线程执行读操作/写操作
        private  boolean flag;
        public ReadWriteThread(boolean flag){
            this.flag = flag;
        }

        @Override
        public void run() {
            if(flag){
                write();
            }else{
                read();
            }
        }
    }

    public static void main(String[] args) {
        SynchronizedDemo synDemo = new SynchronizedDemo();
        synDemo.new ReadWriteThread(true).start();
        synDemo.new ReadWriteThread(false).start();
    }
}
——————————
输出:
有时为6,有时为0

执行顺序的分析:(数字为行数)

  • 1.10 - 16 - 17 - 11
    (result = 6)
  • 2.11 - 16 - 17 - 10 (第1与2步有重排序,第2步时ready还是false,所以第3步不会执行,result输出初始结果)
    (result = 0)
  • 3.10 - 11 - 16 - 17
    这是我们期望的顺序
    (result = 6)

总之有各种排列顺序,导致输出结果result的不稳定;
【但有个部分总没错】:这一定是实现了总内存和工作内存的及时更新,那么前一个方法中更新的内容才会及时传递到下一个内存中;

【疑问】:此时还未添加synchronized,为什么他也实现了可见性?
(即使没有保证可见性的措施,为什么很多时候共享变量依然能在主内存和工作内存之间得到及时更新?)

一般情况下,短时间内高并发情况下才会出现变量得不到及时更新的情况,因为CPU在执行时会很快刷新缓存,所以一般情况下很难看到这种问题;
他是不可预测的,而正是因为不可预测,所以我们才需要加以保护措施;

(JMM虽然规定了synchronized会实现可见性,但他没有说,未添加他就一定不能实现;
实际上,在大多数情况下,没有添加synchronized的方法间,共享变量仍可以被对方看到及更新;这是因为IDE做出了优化,揣摩了程序员的意图,所以达到程序员目的的情况大大提高;
但只要有一次未达到目的的情况发生,这种情况就是线程不安全的,是不允许发生的;
程序员最好在需要保证可见性的地方加入synchronizedvolatile;)

导致共享变量在线程之间不可见的原因:
1.线程的交叉执行;
2.重排序结合线程交叉执行;
3.共享变量更新后的值没有在工作内存与主内存之间及时更新;

所以,解决这些问题的方法就是:在方法前加上synchronized修饰;
如:public synchronized void write(){}

——————————————————————————————

Volatile (可见性)

单点服务器加锁使用synchronized和volatile,但分布式部署时,可使用Redis锁!

作用:

  • 保证volatile变量的【可见性】
  • 【不能保证原子性】

———————————————
volatile如何实现内存可见性?
深入来说
通过加入【内存屏障】和【禁止重排序优化】来实现;
volatile变量执行【写操作】时,会在写操作后加入一条【store屏障指令】;
(把CPU中写好的新volatile变量强制刷新到主内存去;还会禁止重排序)
volatile变量执行【读操作】时,会在读操作后加入一条【load屏障指令】;
(把CPU中的老volatile变量强制失效,只能去主内存中读取新volatile变量;也会禁止重排序)

storeload是JMM中定义的8条指令其中两条,感兴趣可以查资料)

通俗来说:
volatile变量在每次被线程访问时,都会强迫从主内存中重读该变量的值;
而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。
这种情况下,不同的线程总能看到该变量的最新值;
———————————————
线程【写】volatile变量的过程:

  • 1.改变线程工作内存中volatile变量副本的值;
  • 2.将改编后的副本的值从工作内存刷新到主内存;

线程【读】volatile变量的过程:

  • 1.从主内存中读取volatile变量的最新值到线程的工作内存中;
  • 2.从工作内存中读取volatile变量的副本;

———————————————
为什么【volatile无法保证原子性】?

写一个程序详细验证:

public class VolatileDemo {

    private volatile int number = 0;

    public int getNumber() {
        return this.number;
    }

    public void increase(){
        this.number ++;
    }

    public static void main(String[] args) {
        final VolatileDemo volDemo = new VolatileDemo();
        for(int i = 0 ; i<500 ; i++){
            new Thread(new Runnable(){
                @Override
                public void run() {
                    volDemo.increase();
                }
            }).start();
        }

        // 如果还有子线程在运行,主线程就让出CPU
        // 直到所有主线程都运行完了,主线程再继续往下执行
        // (确保子线程都执行完毕)
        // (>2 是因为除了一个线程外,IDEA还会另外启动一个监控线程)
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println("number = "+volDemo.getNumber());
    }
}

while(Thread.activeCount()>2)
意思是,保证子线程都运行完了,再执行主线程,输出最后那一句;
其他IDE中,写while(Thread.activeCount()>1)就可以了,而IDEA需要写2,因为IDEA中会启动一个监控线程,若IDEA中此方法写1,程序将不会终止;

多次验证结果,输出是500

如果在increase()中添加Thread.sleep(100);
结果突然就变成了490,且不稳定;
原因出在number ++;上;
因为volatile不保证原子性;
———————————————
分析number ++;在三种情况下的操作步骤:

无修饰时:

private int number = 0;
number ++;

1.读取number值;
2.将number值+1;
3.写入最新的number值;
很明显不是原子操作;

synchronized时:

synchronized(this){
    number ++;
}
原子操作;

volatile时:

private volatile intnumber = 0;
变为volatile变量,无法保证原子性;

———————————————
程序分析:
假设volatile int number = 5; number++

  • 1.线程A读取number的值;(读到A工作内存中为5;因volatile无原子性,所以可能还没+1,CPU使用权就被夺走了,A阻塞了)
  • 2.线程B读取number的值;(读到B工作内存中为5
  • 3.线程B执行+1操作;(B工作内存中为6
  • 4.线程B写入最新的number值;(主内存中number值为6;因volatilestore指令强制更新主内存)
    (而第一步A由于被阻塞,所以他无法被volatilestore指令强制更新主内存)
    (下一步,线程A又获取了CPU使用权)
  • 5.线程A执行+1操作;(A工作内存还是5的情况下,+1变成6
  • 6.线程A写入最新的number值;

———————————————
volatile适用场合

要在多线程中安全使用volatile变量,必须同时满足:

  • 1.对变量的写入操作不依赖于其当前值
    不满足的情况:number++ count = count*5
    满足的情况:最典型就是boolean变量、记录温度变化的变量等 (这些变量与上一次的值无任何关系)
  • 2.该变量没有包含在具有其他变量的不变式中
    不满足的情况:不变式low < up
    (意思就是low和up是两个volatile变量,他们会在多线程中更改值,如果代码中会拿它们来进行大小比较,那么这个比较会不安全)
    本质:因volatile只能保证代码的可见性,而不能保证代码的原子性。所以,当代码包含其他共享变量时,如果被其他线程执行,那么值就会发生改变。

总结
volatile的使用场景更多的是:不再引用之前的变量 和 不变式中的变量;
更轻量级(轻量级代表执行速度比synchronized更快)(volatile没synchronized使用广泛)
———————————————

synchronized和volatile比较:

volatile不需加锁,比synchronized更轻量级,不会阻塞线程;
内存可见性角度来看,volatile【读】相当于加锁,volatile【写】相当于解锁;
synchronized既保证可见性,也保证原子性;volatile只保证可见性,不保证原子性;
———————————————

(因volatile可保证可见性,但不能保证自增操作原子性,所以…)
保证number自增操作的【原子性】解决方案:

  • 1.使用synchronized关键字(要加锁)
    保证原子性和可见性;
  • 2.使用ReentrantLock关键字(要加锁;JDK1.5后 java.util.concurrent.locks
    读音:re-entrant 重进入;保证原子性和可见性;
    他比synchronized功能更强大,可实现更多锁,感兴趣可在文档中了解;
  • 3.使用AtomicInteger(JDK1.5后 java.utik.concurrent.atomic

提问:正确的是?
A. volatile是保证被修饰变量的可见性,同时也保证原子操作
B. Java中没有提供检测与避免死锁的专门机制,但应用程序员可以采用某些策略防止死锁的发生
C. JAVA中对共享数据操作的并发控制是采用加锁技术
D. 共享数据的访问权限都必须定义为private

答:
A
另外,D遵循了Java封装思想,共享数据最好私有化;

———————————————
1.使用synchronized关键字

public synchronized void increase(){  // 新增synchronized
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.number ++;
    }

但是!!由于循环500次,而每次休眠100毫秒,整个方法使用synchronized修饰会让程序变得很慢;所以改变下锁定范围:缩小锁粒度synchonized只锁定到number++

public void increase(){  // 新增synchronized
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchonized(this){
            this.number ++;
        }
    }

———————————————
2.使用ReentrantLock关键字
声明:Lock lock = new ReentrantLock();
try finally 进入锁,执行代码,释放锁:lock.lock(); try{...} finally{lock.unlock();}
(JDK文档中推荐:一定要释放锁,因为锁内部可能抛出异常)

private Lock lock = new ReentrantLock();
private volatile int number = 0;
...
public void increase(){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.lock();
        try{
            this.number ++;
        }finally{
            lock.unlock();
        }
    }

———————————————

  • 用过AtomicInteger吗?怎么用的? (高频,特别是金融公司)
    java.util.concurrent.atomic.AtomicInteger;

AtomicInteger:Atomic + Integer
白话:保证多线程下,count++输出的结果是一个定值,不会出错(保证线程安全)
(多线程下普通for循环中count++输出的值会一直变化)

使用状况:
如果存在数据库中,数据库本身就支持原子性处理;
如果存在内存中,比如有1W个订单进来,那么就肯定需要考虑到这种情况;(1W个订单进来肯定要考虑多线程;不能用单线程,那么就只能等别人处理完了再处理,不现实)

AtomicInteger是int类型的原子操作类,对于全局变量的数值类型操作 num++,若没有加synchronized关键字则是线程不安全,num++被解析为num=num+1;
很明显,这个操作不具备原子性,多线程时一定会出现问题;

  • AtomicIntegerTest1:public static int count

输出的结果是count: 999x,这个x值是不确定的(结果值会由不同的线程去跑一遍)每次测试结果可能都不一样
多线程跑++操作,结果并没有像预测那样count: 10000
AtomicInteger是为了解决多线程里的count问题而产生的(解决count值的原子性 Atomicity)

public class AtomicIntegerTest1 {
    public static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<10000;i++){
            new Thread(){
                public void run(){
                    count ++;
                }
            }.start();
        }
        System.out.println("count: "+count);
    }
}
—————————————————————————
输出结果可能是:10000或者9999或者9998...
  • AtomicIntegerTest2:public static int count
    变量添加volatile修饰?
    volatile修饰的变量能在线程间保持可见性,能被多个线程同时读但又能保证植被单线程写,并且不会读取到过期值,volatile修饰的字段写入操作总是优先于操作,即使多个线程同时修改volatile字段…
public class AtomicIntegerTest2 {

    // volatile 可以保证变量在线程间保持可见性,但却依然不能保证非原子性的操作

    public static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<10000;i++){
            new Thread(){
                public void run(){
                    count ++;
                }
            }.start();
        }
        System.out.println("count: "+count);
    }
}

—————————————————————————
输出结果可能是:10000或者9999或者9998...
【说明还是在碰运气】...
  • AtomicIntegerTest3:public static int count

注意:
1.变量定义的方式:AtomicInteger count = new AtomicInteger(0);
2.run()中count增加的方式:.getAndIncrement()会给变量+1

public class AtomicintegerTest3 {

    // AtomicInteger

    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<10000;i++){
            new Thread(){
                public void run(){
                    count.getAndIncrement();
                }
            }.start();
        }
        System.out.println("count: "+count);
    }
}
—————————————————————————
输出的值是定值

在这里插入图片描述
注:incrementAndGet()getAndIncrement()的区别?
通过源码分析可知:
incrementAndGet()getAndIncrement() 都调用了 Unsafe 类中的 getAndAddInt() 方法,意思是一样的;
区别是:
incrementAndGet():先+1,再返回
getAndIncrement():先返回,再 +1
这就好比 count++++count 的区别(前者先自增后输出,后者先输出后自增)

但当我实验的时候,发现这么写的话,输出的结果是一样的,看不出区别:

AtomicInteger first = new AtomicInteger(5);
first.getAndIncrement();
System.out.println(first);  // 6
 
AtomicInteger second = new AtomicInteger(5);
second.getAndIncrement();
System.out.println(second);  // 6

其实应该通过一个变量存放一下再输出,才看得到区别:

        AtomicInteger first = new AtomicInteger(5);
        System.out.println("original value: "+first);      // 5
        int a = first.getAndIncrement();
        System.out.println("getAndIncrement value: "+ a);  // 5
 
        AtomicInteger second = new AtomicInteger(5);
        System.out.println("original value: "+second);     // 5
        int b = second.incrementAndGet();
        System.out.println("incrementAndGet value: "+ b);  // 6

————————————————
有人说java线程锁淘汰了,有负载均衡,Java线程锁这东西没用?

有人理解是:负载均衡,都是Framework提供的功能;它们提供简单的API而把复杂性隐藏起来;如果想要理解它们的原理,有可能还是得研究“多线程”和“并发”;

线程间通信的方法还是挺多的,不一定非要通过共享变量通信。

各人关注点不同,无需反驳;你觉得有用就继续钻研,如果你觉得不掌握这些知识也够用(使用一些框架),那就好好研究下那些框架的用法;能解决问题就行了。
——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————


——————————————————————————————

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值