精华内容
下载资源
问答
  • 多线程就一定吗?天真!

    千次阅读 2019-12-31 09:18:14
    在《多线程排序》中介绍了多线程排序,似乎看起来多线程快了很多,然而多线程就一定更快吗? 为什么多线程就不一定快? 还是拿《多线程排序-真香!》中的程序举例,下面是各个线程数量的排序结果: 线程数 时间...

     

     

    在《多线程排序》中介绍了多线程排序,似乎看起来多线程快了很多,然而多线程就一定更快吗?

    为什么多线程就不一定快?

    还是拿《多线程排序-真香!》中的程序举例,下面是各个线程数量的排序结果:

    线程数时间/s

    1

    2.393644

    2

    1.367392

    3

    1.386448

    4

    1.036919

    5

    1.097992

    6

    1.218000

    7

    1.184615

    8

    1.176258

    以上结果可能不准确,但是体现了一些变化趋势,即并不是线程数量越多越快,也不是单线程最快,而是线程数为4的时候最快。

    为什么呢?

    原因在于我的机器只有4个逻辑CPU,因此4是最合适的。为了不解释太多术语,简单解释一下。一个CPU就像一条流水线,会执行一系列指令,当你很多指定拆成4份(4线程)的时候,它是正好最合适的,少的时候,有一个闲着;而多了,就会存在抢占的情况。举个简单的例子,假设有4个水管可以出水,你现在去接水,那么你在每个水管下放一个桶去接水,自然要比只在一个水管下去接水要快的,但是如果你的水桶数量多于水管数,为了每个水桶都要有水,你在这个过程中就需要去切换水桶,每个水桶换一下,才能都接得上,而换的这个过程就像线程的上下文切换带来的开销

    因此,不是线程越多越快,最合适的才最快

    单线程有时候反而更快

    说到这你可能更会奇怪了,为什么单线程有时候反而会更快呢?还是拿接水为例,假设虽然有4个水管,但是你只有一个桶,因此你一个人从这个水管里一直接水是最快的;而如果你拿两个桶,这个接一点,又换一下,那个接一点,又换一下,中间显然有切换导致的中断,相同时间内单个桶接的比较多;这就是单核CPU妄图使用多线程提高效率或者每个线程都需要竞争同一把锁而实际可能会导致更慢的缘故。

    举个绑核的例子:

     

    $ taskset -c 1 taskset -c 1 ./multiThread 4
    thread num:4
    time 2.378558

    我使用taskset将程序绑定在一个CPU上运行,可以看其时间足足是不绑核的时候的两倍有余。

    什么叫都需要竞争呢?举个极端的例子,我们修改前面的工作线程代码如下:

     

    /*比较线程,采用快速排序*/

    void * workThread(void *arg)
    {
        pthread_mutex_lock(&mutex);
        SortInfo *sortInfo = (SortInfo*)arg;
        long idx = sortInfo->startIdx;
        long num = sortInfo->num;
        qsort(&nums[idx],num,sizeof(long),compare);
        pthread_mutex_unlock(&mutex);
        pthread_barrier_wait(&b);
        return ((void*)0);
    }

    这里的例子比较极端,在排序的时候都给它们加上了锁(关于锁,后面会有文章进行更加详细的介绍。),即哪个线程拿到了锁,就可以继续工作,没有拿到的继续等待。使用完成后再释放。
    在这样的情况下,看看4线程还有效果吗?

     

    $ ./multiThread 4
    thread num:4
    time 2.480588

    是最快的时候两倍多的时间!而且还比单个线程的时候要慢!!!

    而另外一种情况,比如说从队列中取出数据,然后进行耗时处理,那么对取出数据的操作进行加锁是可行的,多线程的情况仍然能提高处理速度。但如果你仅仅是读取数据,那么单线程的情况可能会比多线程要快,因为它避免了线程上下文切换的开销

    扩展介绍-绑核

    为什么要绑核?

    • 充分利用CPU,减少CPU之间上下文切换

    • 指定程序运行在指定CPU,便于区分

     

    $ taskset -c 1 ./proName

    将proName绑定在第二个核。

     

    $ taskset -c 1-3  ./proName

    绑定运行在第二个到第四个核。

     

    $ taskset -p 3569
    pid 3569's current affinity mask: f

    查看进程3569当前运行在哪个核上。

    mask f转为二进制即为1111,因此四个核都有运行。

    当然除了命令行,还有函数接口可以使用,这里就不再扩展了。

    如何查看机器的CPU数量

    物理CPU个数,就是你实际CPU的个数:

     

    $ cat /proc/cpuinfo | grep "physical id" | sort -u | wc -l 
    1

    CPU物理核数,就是你的一个CPU上有多少个核心,现在很多CPU都是多核:

     

    $ cat /proc/cpuinfo | grep "core id" | sort -u | wc -l
    2

    CPU逻辑核数,一颗物理CPU可以有多个物理内核,加上超线程技术,会让CPU看起来有很多个:

    $ cat /proc/cpuinfo | grep "processor" | sort -u | wc -l
    4

    总结

    线程上下文切换是有开销的,如果它的收益不能超过它的开销,那么使用多线程来提高效率将得不偿失。因此不要盲目推崇多线程。如果为了提高效率采用多线程,那么线程中最多应为逻辑CPU数。也就是说如果你的程序绑在一个核上或者你只有一个CPU一个核,那么采用多线程只能提高同时处理的能力,而不能提高处理效率。

    https://www.yanbinghu.com/2019/12/25/46016.html

    来源:公众号【编程珠玑】

    作者:守望先生

    ID:shouwangxiansheng

     

    相关精彩推荐

    多线程排序-真香!

    我有一个问题,用了多线程后,两个问题有了现在

    系统编程-文件读写这件小事

    生成随机数的方式你选对了吗?

     

    关注公众号【编程珠玑】,获取更多Linux/C/C++/数据结构与算法/计算机基础/工具等原创技术文章。后台免费获取经典电子书和视频资源

     

    展开全文
  • 多线程并发一定比单线程吗?

    千次阅读 2019-08-02 19:19:54
    确实多线程在一定情况下比单线程更快。 下面的代码演示串行和并发执行并累加操作的时间,请分析:当count的数量增加 1万 -> 10万 -> 100万 -> 1000万 -> 1亿,下面的代码并发执行一定比串行执行快吗?...

    很多时候我们都以为要想处理速度更快,那就多开几个线程跑! 确实多线程在一定情况下比单线程更快。

    下面的代码演示串行和并发执行并累加操作的时间,请分析:当count的数量增加 1万 -> 10万 -> 100万 -> 1000万 -> 1亿,下面的代码并发执行一定比串行执行快吗?

    public class ConcurrencyTest {
        private static final long count = 10000l;
    
        public static void main(String[] args) throws InterruptedException {
            for(int i=0; i<10; i++) {
                concurrency();
                serial();
                System.out.println("------------------------------------------");
            }
    
        }
    
        private static void concurrency() throws InterruptedException {
            long start = System.currentTimeMillis();
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    int a = 0;
                    for (long i = 0; i < count; i++) {
                        a += 5;
                    }
                }
            });
            thread.start();
            int b = 0;
            for (long i = 0; i < count; i++) {
                b--;
            }
            long time = System.currentTimeMillis() - start;
            thread.join();
            System.out.println("concurrency :" + time + "ms,b=" + b);
        }
    
        private static void serial() {
            long start = System.currentTimeMillis();
            int a = 0;
            for (long i = 0; i < count; i++) {
                a += 5;
            }
            int b = 0;
            for (long i = 0; i < count; i++) {
                b--;
            }
            long time = System.currentTimeMillis() - start;
            System.out.println("serial:" + time + "ms,b=" + b + ",a=" + a);
        }
    }

    测试结果

    这是count=1万的结果,基本上串行要快些

    并发 :1ms,b=-10000
    串行:0ms,b=-10000,a=50000
    ------------------------------------------
    并发 :0ms,b=-10000
    串行:1ms,b=-10000,a=50000
    ------------------------------------------
    并发 :0ms,b=-10000
    串行:1ms,b=-10000,a=50000
    ------------------------------------------
    并发 :0ms,b=-10000
    串行:0ms,b=-10000,a=50000
    ------------------------------------------
    并发 :1ms,b=-10000
    串行:0ms,b=-10000,a=50000
    ------------------------------------------
    并发 :1ms,b=-10000
    串行:0ms,b=-10000,a=50000
    ------------------------------------------
    并发 :0ms,b=-10000
    串行:1ms,b=-10000,a=50000
    ------------------------------------------
    并发 :0ms,b=-10000
    串行:0ms,b=-10000,a=50000
    ------------------------------------------
    并发 :1ms,b=-10000
    串行:0ms,b=-10000,a=50000
    ------------------------------------------

    这是count=10万l的结果,基本上串行要快点点

    并发 :4ms,b=-100000
    串行:3ms,b=-100000,a=500000
    ------------------------------------------
    并发 :0ms,b=-100000
    串行:1ms,b=-100000,a=500000
    ------------------------------------------
    并发 :1ms,b=-100000
    串行:0ms,b=-100000,a=500000
    ------------------------------------------
    并发 :0ms,b=-100000
    串行:1ms,b=-100000,a=500000
    ------------------------------------------
    并发 :1ms,b=-100000
    串行:0ms,b=-100000,a=500000
    ------------------------------------------
    并发 :0ms,b=-100000
    串行:0ms,b=-100000,a=500000
    ------------------------------------------
    并发 :1ms,b=-100000
    串行:0ms,b=-100000,a=500000
    ------------------------------------------
    并发 :0ms,b=-100000
    串行:0ms,b=-100000,a=500000
    ------------------------------------------
    并发 :0ms,b=-100000
    串行:1ms,b=-100000,a=500000
    ------------------------------------------
    并发 :0ms,b=-100000
    串行:0ms,b=-100000,a=500000
    ------------------------------------------

    这是count=100万l的结果,基本上并发就要比串行要快了

    并发 :5ms,b=-1000000
    串行:5ms,b=-1000000,a=5000000
    ------------------------------------------
    并发 :3ms,b=-1000000
    串行:7ms,b=-1000000,a=5000000
    ------------------------------------------
    并发 :1ms,b=-1000000
    串行:7ms,b=-1000000,a=5000000
    ------------------------------------------
    并发 :1ms,b=-1000000
    串行:2ms,b=-1000000,a=5000000
    ------------------------------------------
    并发 :1ms,b=-1000000
    串行:2ms,b=-1000000,a=5000000
    ------------------------------------------
    并发 :2ms,b=-1000000
    串行:2ms,b=-1000000,a=5000000
    ------------------------------------------
    并发 :1ms,b=-1000000
    串行:3ms,b=-1000000,a=5000000
    ------------------------------------------
    并发 :1ms,b=-1000000
    串行:2ms,b=-1000000,a=5000000
    ------------------------------------------
    并发 :1ms,b=-1000000
    串行:3ms,b=-1000000,a=5000000
    ------------------------------------------
    并发 :1ms,b=-1000000
    串行:2ms,b=-1000000,a=5000000
    ------------------------------------------

    余下的读者可以自己去调试哦。

     

    结论

     1.在数量比较小的时候,很明显串行要比并发更快。

     2.因为数量较小的时候,并发会花费很多时间在线程调度上。

    展开全文
  • 万字图解Java多线程

    万次阅读 多人点赞 2020-09-06 14:45:07
    java多线程我个人觉得是javaSe中最难的一部分,我以前也是感觉学会了,但是真正有多线程的需求却不知道怎么下手,实际上还是对多线程这块知识了解不深刻,不知道多线程api的应用场景,不知道多线程的运行流程等等,...

    前言

    授权Java面试者精选独家原创发布

    java多线程我个人觉得是javaSe中最难的一部分,我以前也是感觉学会了,但是真正有多线程的需求却不知道怎么下手,实际上还是对多线程这块知识了解不深刻,不知道多线程api的应用场景,不知道多线程的运行流程等等,本篇文章将使用实例+图解+源码的方式来解析java多线程。

    文章篇幅较长,大家也可以有选择的看具体章节,建议多线程的代码全部手敲,永远不要相信你看到的结论,自己编码后运行出来的,才是自己的。

    什么是java多线程?

    进程与线程

    进程

    • 当一个程序被运行,就开启了一个进程, 比如启动了qq,word
    • 程序由指令和数据组成,指令要运行,数据要加载,指令被cpu加载运行,数据被加载到内存,指令运行时可由cpu调度硬盘、网络等设备

    线程

    • 一个进程内可分为多个线程
    • 一个线程就是一个指令流,cpu调度的最小单位,由cpu一条一条执行指令

    并行与并发

    并发:单核cpu运行多线程时,时间片进行很快的切换。线程轮流执行cpu

    并行:多核cpu运行 多线程时,真正的在同一时刻运行

    java提供了丰富的api来支持多线程。

    为什么用多线程?

    多线程能实现的都可以用单线程来完成,那单线程运行的好好的,为什么java要引入多线程的概念呢?

    多线程的好处:

    1. 程序运行的更快!快!快!

    2. 充分利用cpu资源,目前几乎没有线上的cpu是单核的,发挥多核cpu强大的能力

    多线程难在哪里?

    单线程只有一条执行线,过程容易理解,可以在大脑中清晰的勾勒出代码的执行流程

    多线程却是多条线,而且一般多条线之间有交互,多条线之间需要通信,一般难点有以下几点

    1. 多线程的执行结果不确定,受到cpu调度的影响
    2. 多线程的安全问题
    3. 线程资源宝贵,依赖线程池操作线程,线程池的参数设置问题
    4. 多线程执行是动态的,同时的,难以追踪过程
    5. 多线程的底层是操作系统层面的,源码难度大

    有时候希望自己变成一个字节穿梭于服务器中,搞清楚来龙去脉,就像无敌破坏王一样(没看过这部电影的可以看下,脑洞大开)。

    java多线程的基本使用

    定义任务、创建和运行线程

    任务: 线程的执行体。也就是我们的核心代码逻辑

    定义任务

    1. 继承Thread类 (可以说是 将任务和线程合并在一起)
    2. 实现Runnable接口 (可以说是 将任务和线程分开了)
    3. 实现Callable接口 (利用FutureTask执行任务)

    Thread实现任务的局限性

    1. 任务逻辑写在Thread类的run方法中,有单继承的局限性
    2. 创建多线程时,每个任务有成员变量时不共享,必须加static才能做到共享

    Runnable和Callable解决了Thread的局限性

    但是Runbale相比Callable有以下的局限性

    1. 任务没有返回值
    2. 任务无法抛异常给调用方

    如下代码 几种定义线程的方式

    @Slf4j
    class T extends Thread {
        @Override
        public void run() {
            log.info("我是继承Thread的任务");
        }
    }
    @Slf4j
    class R implements Runnable {
    
        @Override
        public void run() {
            log.info("我是实现Runnable的任务");
        }
    }
    @Slf4j
    class C implements Callable<String> {
    
        @Override
        public String call() throws Exception {
            log.info("我是实现Callable的任务");
            return "success";
        }
    }
    

    创建线程的方式

    1. 通过Thread类直接创建线程
    2. 利用线程池内部创建线程

    启动线程的方式

    • 调用线程的start()方法
    // 启动继承Thread类的任务
    new T().start();
    
    // 启动继承Thread匿名内部类的任务 可用lambda优化
    Thread t = new Thread(){
      @Override
      public void run() {
        log.info("我是Thread匿名内部类的任务");
      }
    };
    
    //  启动实现Runnable接口的任务
    new Thread(new R()).start();
    
    //  启动实现Runnable匿名实现类的任务
    new Thread(new Runnable() {
        @Override
        public void run() {
            log.info("我是Runnable匿名内部类的任务");
        }
    }).start();
    
    //  启动实现Runnable的lambda简化后的任务
    new Thread(() -> log.info("我是Runnable的lambda简化后的任务")).start();
    
    // 启动实现了Callable接口的任务 结合FutureTask 可以获取线程执行的结果
    FutureTask<String> target = new FutureTask<>(new C());
    new Thread(target).start();
    log.info(target.get());
    
    

    以上各个线程相关的类的类图如下

    上下文切换

    多核cpu下,多线程是并行工作的,如果线程数多,单个核又会并发的调度线程,运行时会有上下文切换的概念

    cpu执行线程的任务时,会为线程分配时间片,以下几种情况会发生上下文切换。

    1. 线程的cpu时间片用完
    2. 垃圾回收
    3. 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

    当发生上下文切换时,操作系统会保存当前线程的状态,并恢复另一个线程的状态,jvm中有块内存地址叫程序计数器,用于记录线程执行到哪一行代码,是线程私有的。

    idea打断点的时候可以设置为Thread模式,idea的debug模式可以看出栈帧的变化

    线程的礼让-yield()&线程的优先级

    yield()方法会让运行中的线程切换到就绪状态,重新争抢cpu的时间片,争抢时是否获取到时间片看cpu的分配。

    代码如下

    // 方法的定义
    public static native void yield();
    
    Runnable r1 = () -> {
        int count = 0;
        for (;;){
           log.info("---- 1>" + count++);
        }
    };
    Runnable r2 = () -> {
        int count = 0;
        for (;;){
            Thread.yield();
            log.info("            ---- 2>" + count++);
        }
    };
    Thread t1 = new Thread(r1,"t1");
    Thread t2 = new Thread(r2,"t2");
    t1.start();
    t2.start();
    
    // 运行结果
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129504
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129505
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129506
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129507
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129508
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129509
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129510
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129511
    11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129512
    11:49:15.798 [t2] INFO thread.TestYield -             ---- 2>293
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129513
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129514
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129515
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129516
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129517
    11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129518
    

    如上述结果所示,t2线程每次执行时进行了yield(),线程1执行的机会明显比线程2要多。

    线程的优先级

    ​ 线程内部用1~10的数来调整线程的优先级,默认的线程优先级为NORM_PRIORITY:5

    ​ cpu比较忙时,优先级高的线程获取更多的时间片

    ​ cpu比较闲时,优先级设置基本没用

     public final static int MIN_PRIORITY = 1;
    
     public final static int NORM_PRIORITY = 5;
    
     public final static int MAX_PRIORITY = 10;
     
     // 方法的定义
     public final void setPriority(int newPriority) {
     }
    

    cpu比较忙时

    Runnable r1 = () -> {
        int count = 0;
        for (;;){
           log.info("---- 1>" + count++);
        }
    };
    Runnable r2 = () -> {
        int count = 0;
        for (;;){
            log.info("            ---- 2>" + count++);
        }
    };
    Thread t1 = new Thread(r1,"t1");
    Thread t2 = new Thread(r2,"t2");
    t1.setPriority(Thread.NORM_PRIORITY);
    t2.setPriority(Thread.MAX_PRIORITY);
    t1.start();
    t2.start();
    
    // 可能的运行结果
    11:59:00.696 [t1] INFO thread.TestYieldPriority - ---- 1>44102
    11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135903
    11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135904
    11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135905
    11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135906
    

    cpu比较闲时

    Runnable r1 = () -> {
        int count = 0;
        for (int i = 0; i < 10; i++) {
            log.info("---- 1>" + count++);
        }
    };
    Runnable r2 = () -> {
        int count = 0;
        for (int i = 0; i < 10; i++) {
            log.info("            ---- 2>" + count++);
    
        }
    };
    Thread t1 = new Thread(r1,"t1");
    Thread t2 = new Thread(r2,"t2");
    t1.setPriority(Thread.MIN_PRIORITY);
    t2.setPriority(Thread.MAX_PRIORITY);
    t1.start();
    t2.start();
    
    // 可能的运行结果 线程1优先级低 却先运行完
    12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>7
    12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>8
    12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>9
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>2
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>3
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>4
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>5
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>6
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>7
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>8
    12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>9
    
    

    守护线程

    默认情况下,java进程需要等待所有线程都运行结束,才会结束,有一种特殊线程叫守护线程,当所有的非守护线程都结束后,即使它没有执行完,也会强制结束。

    默认的线程都是非守护线程。

    垃圾回收线程就是典型的守护线程

    // 方法的定义
    public final void setDaemon(boolean on) {
    }
    
    Thread thread = new Thread(() -> {
        while (true) {
        }
    });
    // 具体的api。设为true表示未守护线程,当主线程结束后,守护线程也结束。
    // 默认是false,当主线程结束后,thread继续运行,程序不停止
    thread.setDaemon(true);
    thread.start();
    log.info("结束");
    

    线程的阻塞

    线程的阻塞可以分为好多种,从操作系统层面和java层面阻塞的定义可能不同,但是广义上使得线程阻塞的方式有下面几种

    1. BIO阻塞,即使用了阻塞式的io流
    2. sleep(long time) 让线程休眠进入阻塞状态
    3. a.join() 调用该方法的线程进入阻塞,等待a线程执行完恢复运行
    4. sychronized或ReentrantLock 造成线程未获得锁进入阻塞状态 (同步锁章节细说)
    5. 获得锁之后调用wait()方法 也会让线程进入阻塞状态 (同步锁章节细说)
    6. LockSupport.park() 让线程进入阻塞状态 (同步锁章节细说)

    sleep()

    ​ 使线程休眠,会将运行中的线程进入阻塞状态。当休眠时间结束后,重新争抢cpu的时间片继续运行

    // 方法的定义 native方法
    public static native void sleep(long millis) throws InterruptedException; 
    
    try {
       // 休眠2秒
       // 该方法会抛出 InterruptedException异常 即休眠过程中可被中断,被中断后抛出异常
       Thread.sleep(2000);
     } catch (InterruptedException异常 e) {
     }
     try {
       // 使用TimeUnit的api可替代 Thread.sleep 
       TimeUnit.SECONDS.sleep(1);
     } catch (InterruptedException e) {
     }
    

    join()

    ​ join是指调用该方法的线程进入阻塞状态,等待某线程执行完成后恢复运行

    // 方法的定义 有重载
    // 等待线程执行完才恢复运行
    public final void join() throws InterruptedException {
    }
    // 指定join的时间。指定时间内 线程还未执行完 调用方线程不继续等待就恢复运行
    public final synchronized void join(long millis)
        throws InterruptedException{}
    
    
    Thread t = new Thread(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        r = 10;
    });
    
    t.start();
    // 让主线程阻塞 等待t线程执行完才继续执行 
    // 去除该行,执行结果为0,加上该行 执行结果为10
    t.join();
    log.info("r:{}", r);
    
    // 运行结果
    13:09:13.892 [main] INFO thread.TestJoin - r:10
    

    线程的打断-interrupt()

    // 相关方法的定义
    public void interrupt() {
    }
    public boolean isInterrupted() {
    }
    public static boolean interrupted() {
    }
    

    打断标记:线程是否被打断,true表示被打断了,false表示没有

    isInterrupted() 获取线程的打断标记 ,调用后不会修改线程的打断标记

    interrupt()方法用于中断线程

    1. 可以打断sleep,wait,join等显式的抛出InterruptedException方法的线程,但是打断后,线程的打断标记还是false
    2. 打断正常线程 ,线程不会真正被中断,但是线程的打断标记为true

    interrupted() 获取线程的打断标记,调用后清空打断标记 即如果获取为true 调用后打断标记为false (不常用)

    interrupt实例: 有个后台监控线程不停的监控,当外界打断它时,就结束运行。代码如下

    @Slf4j
    class TwoPhaseTerminal{
        // 监控线程
        private Thread monitor;
    
        public void start(){
            monitor = new Thread(() ->{
               // 不停的监控
                while (true){
                    Thread thread = Thread.currentThread();
                 	// 判断当前线程是否被打断
                    if (thread.isInterrupted()){
                        log.info("当前线程被打断,结束运行");
                        break;
                    }
                    try {
                        Thread.sleep(1000);
                    	// 监控逻辑中被打断后,打断标记为true
                        log.info("监控");
                    } catch (InterruptedException e) {
                        // 睡眠时被打断时抛出异常 在该处捕获到 此时打断标记还是false
                        // 在调用一次中断 使得中断标记为true
                        thread.interrupt();
                    }
                }
            });
            monitor.start();
        }
    
        public void stop(){
            monitor.interrupt();
        }
    }
    

    线程的状态

    上面说了一些基本的api的使用,调用上面的方法后都会使得线程有对应的状态。

    线程的状态可从 操作系统层面分为五种状态 从java api层面分为六种状态。

    五种状态

    1. 初始状态:创建线程对象时的状态
    2. 可运行状态(就绪状态):调用start()方法后进入就绪状态,也就是准备好被cpu调度执行
    3. 运行状态:线程获取到cpu的时间片,执行run()方法的逻辑
    4. 阻塞状态: 线程被阻塞,放弃cpu的时间片,等待解除阻塞重新回到就绪状态争抢时间片
    5. 终止状态: 线程执行完成或抛出异常后的状态

    六种状态

    Thread类中的内部枚举State

    public enum State {
    	NEW,
    	RUNNABLE,
    	BLOCKED,
    	WAITING,
    	TIMED_WAITING,
    	TERMINATED;
    }
    
    1. NEW 线程对象被创建
    2. Runnable 线程调用了start()方法后进入该状态,该状态包含了三种情况
      1. 就绪状态 :等待cpu分配时间片
      2. 运行状态:进入Runnable方法执行任务
      3. 阻塞状态:BIO 执行阻塞式io流时的状态
    3. Blocked 没获取到锁时的阻塞状态(同步锁章节会细说)
    4. WAITING 调用wait()、join()等方法后的状态
    5. TIMED_WAITING 调用 sleep(time)、wait(time)、join(time)等方法后的状态
    6. TERMINATED 线程执行完成或抛出异常后的状态

    六种线程状态和方法的对应关系

    线程的相关方法总结

    主要总结Thread类中的核心方法

    方法名称是否static方法说明
    start()让线程启动,进入就绪状态,等待cpu分配时间片
    run()重写Runnable接口的方法,线程获取到cpu时间片时执行的具体逻辑
    yield()线程的礼让,使得获取到cpu时间片的线程进入就绪状态,重新争抢时间片
    sleep(time)线程休眠固定时间,进入阻塞状态,休眠时间完成后重新争抢时间片,休眠可被打断
    join()/join(time)调用线程对象的join方法,调用者线程进入阻塞,等待线程对象执行完或者到达指定时间才恢复,重新争抢时间片
    isInterrupted()获取线程的打断标记,true:被打断,false:没有被打断。调用后不会修改打断标记
    interrupt()打断线程,抛出InterruptedException异常的方法均可被打断,但是打断后不会修改打断标记,正常执行的线程被打断后会修改打断标记
    interrupted()获取线程的打断标记。调用后会清空打断标记
    stop()停止线程运行 不推荐
    suspend()挂起线程 不推荐
    resume()恢复线程运行 不推荐
    currentThread()获取当前线程

    Object中与线程相关方法

    方法名称方法说明
    wait()/wait(long timeout)获取到锁的线程进入阻塞状态
    notify()随机唤醒被wait()的一个线程
    notifyAll();唤醒被wait()的所有线程,重新争抢时间片

    同步锁

    线程安全

    • 一个程序运行多个线程本身是没有问题的
    • 问题有可能出现在多个线程访问共享资源
      • 多个线程都是读共享资源也是没有问题的
      • 当多个线程读写共享资源时,如果发生指令交错,就会出现问题

    临界区: 一段代码如果对共享资源的多线程读写操作,这段代码就被称为临界区。

    注意的是 指令交错指的是 java代码在解析成字节码文件时,java代码的一行代码在字节码中可能有多行,在线程上下文切换时就有可能交错。

    线程安全指的是多线程调用同一个对象的临界区的方法时,对象的属性值一定不会发生错误,这就是保证了线程安全。

    如下面不安全的代码

    // 对象的成员变量
    private static int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
      // t1线程对变量+5000次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count++;
            }
        });
      // t2线程对变量-5000次
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count--;
            }
        });
    
        t1.start();
        t2.start();
    
        // 让t1 t2都执行完
        t1.join();
        t2.join();
        System.out.println(count);
    }
    
    // 运行结果 
    -1399
    

    上面的代码 两个线程,一个+5000次,一个-5000次,如果线程安全,count的值应该还是0。

    但是运行很多次,每次的结果不同,且都不是0,所以是线程不安全的。

    线程安全的类一定所有的操作都线程安全吗?

    开发中经常会说到一些线程安全的类,如ConcurrentHashMap,线程安全指的是类里每一个独立的方法是线程安全的,但是方法的组合就不一定是线程安全的

    成员变量和静态变量是否线程安全?

    • 如果没有多线程共享,则线程安全
    • 如果存在多线程共享
      • 多线程只有读操作,则线程安全
      • 多线程存在写操作,写操作的代码又是临界区,则线程不安全

    局部变量是否线程安全?

    • 局部变量是线程安全的
    • 局部变量引用的对象未必是线程安全的
      • 如果该对象没有逃离该方法的作用范围,则线程安全
      • 如果该对象逃离了该方法的作用范围,比如:方法的返回值,需要考虑线程安全

    synchronized

    同步锁也叫对象锁,是锁在对象上的,不同的对象就是不同的锁。

    该关键字是用于保证线程安全的,是阻塞式的解决方案。

    让同一个时刻最多只有一个线程能持有对象锁,其他线程在想获取这个对象锁就会被阻塞,不用担心上下文切换的问题。

    注意: 不要理解为一个线程加了锁 ,进入 synchronized代码块中就会一直执行下去。如果时间片切换了,也会执行其他线程,再切换回来会紧接着执行,只是不会执行到有竞争锁的资源,因为当前线程还未释放锁。

    当一个线程执行完synchronized的代码块后 会唤醒正在等待的线程

    synchronized实际上使用对象锁保证临界区的原子性 临界区的代码是不可分割的 不会因为线程切换所打断

    基本使用

    // 加在方法上 实际是对this对象加锁
    private synchronized void a() {
    }
    
    // 同步代码块,锁对象可以是任意的,加在this上 和a()方法作用相同
    private void b(){
        synchronized (this){
    
        }
    }
    
    // 加在静态方法上 实际是对类对象加锁
    private synchronized static void c() {
    
    }
    
    // 同步代码块 实际是对类对象加锁 和c()方法作用相同
    private void d(){
        synchronized (TestSynchronized.class){
            
        }
    }
    
    // 上述b方法对应的字节码源码 其中monitorenter就是加锁的地方
     0 aload_0
     1 dup
     2 astore_1
     3 monitorenter
     4 aload_1
     5 monitorexit
     6 goto 14 (+8)
     9 astore_2
    10 aload_1
    11 monitorexit
    12 aload_2
    13 athrow
    14 return
    

    线程安全的代码

    private static int count = 0;
    
    private static Object lock = new Object();
    
    private static Object lock2 = new Object();
    
     // t1线程和t2对象都是对同一对象加锁。保证了线程安全。此段代码无论执行多少次,结果都是0
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (lock) {
                    count++;
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                synchronized (lock) {
                    count--;
                }
            }
        });
     
        t1.start();
        t2.start();
    
        // 让t1 t2都执行完
        t1.join();
        t2.join();
        System.out.println(count);
    }
    

    重点:加锁是加在对象上,一定要保证是同一对象,加锁才能生效

    线程通信

    wait+notify

    线程间通信可以通过共享变量+wait()&notify()来实现

    wait()将线程进入阻塞状态,notify()将线程唤醒

    当多线程竞争访问对象的同步方法时,锁对象会关联一个底层的Monitor对象(重量级锁的实现)

    如下图所示 Thread0,1先竞争到锁执行了代码后,2,3,4,5线程同时来执行临界区的代码,开始竞争锁

    1. Thread-0先获取到对象的锁,关联到monitor的owner,同步代码块内调用了锁对象的wait()方法,调用后会进入waitSet等待,Thread-1同样如此,此时Thread-0的状态为Waitting
    2. Thread2、3、4、5同时竞争,2获取到锁后,关联了monitor的owner,3、4、5只能进入EntryList中等待,此时2线程状态为 Runnable,3、4、5状态为Blocked
    3. 2执行后,唤醒entryList中的线程,3、4、5进行竞争锁,获取到的线程即会关联monitor的owner
    4. 3、4、5线程在执行过程中,调用了锁对象的notify()或notifyAll()时,会唤醒waitSet的线程,唤醒的线程进入entryList等待重新竞争锁

    注意:

    1. Blocked状态和Waitting状态都是阻塞状态

    2. Blocked线程会在owner线程释放锁时唤醒

    3. wait和notify使用场景是必须要有同步,且必须获得对象的锁才能调用,使用锁对象去调用,否则会抛异常

    • wait() 释放锁 进入 waitSet 可传入时间,如果指定时间内未被唤醒 则自动唤醒
    • notify()随机唤醒一个waitSet里的线程
    • notifyAll()唤醒waitSet中所有的线程
    static final Object lock = new Object();
    new Thread(() -> {
        synchronized (lock) {
            log.info("开始执行");
            try {
              	// 同步代码内部才能调用
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("继续执行核心逻辑");
        }
    }, "t1").start();
    
    new Thread(() -> {
        synchronized (lock) {
            log.info("开始执行");
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("继续执行核心逻辑");
        }
    }, "t2").start();
    
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    log.info("开始唤醒");
    
    synchronized (lock) {
      // 同步代码内部才能调用
        lock.notifyAll();
    }
    // 执行结果
    14:29:47.138 [t1] INFO TestWaitNotify - 开始执行
    14:29:47.141 [t2] INFO TestWaitNotify - 开始执行
    14:29:49.136 [main] INFO TestWaitNotify - 开始唤醒
    14:29:49.136 [t2] INFO TestWaitNotify - 继续执行核心逻辑
    14:29:49.136 [t1] INFO TestWaitNotify - 继续执行核心逻辑
    

    wait 和 sleep的区别?

    二者都会让线程进入阻塞状态,有以下区别

    1. wait是Object的方法 sleep是Thread的方法
    2. wait会立即释放锁 sleep不会释放锁
    3. wait后线程的状态是Watting sleep后线程的状态为 Time_Waiting

    park&unpark

    LockSupport是juc下的工具类,提供了park和unpark方法,可以实现线程通信

    与wait和notity相比的不同点

    1. wait 和notify需要获取对象锁 park unpark不要
    2. unpark 可以指定唤醒线程 notify随机唤醒
    3. park和unpark的顺序可以先unpark wait和notify的顺序不能颠倒

    生产者消费者模型

    指的是有生产者来生产数据,消费者来消费数据,生产者生产满了就不生产了,通知消费者取,等消费了再进行生产。
    

    消费者消费不到了就不消费了,通知生产者生产,生产到了再继续消费。

      public static void main(String[] args) throws InterruptedException {
            MessageQueue queue = new MessageQueue(2);
    		
    		// 三个生产者向队列里存值
            for (int i = 0; i < 3; i++) {
                int id = i;
                new Thread(() -> {
                    queue.put(new Message(id, "值" + id));
                }, "生产者" + i).start();
            }
    
            Thread.sleep(1000);
    
    		// 一个消费者不停的从队列里取值
            new Thread(() -> {
                while (true) {
                    queue.take();
                }
            }, "消费者").start();
    
        }
    }
    
    
    // 消息队列被生产者和消费者持有
    class MessageQueue {
        private LinkedList<Message> list = new LinkedList<>();
    
        // 容量
        private int capacity;
    
        public MessageQueue(int capacity) {
            this.capacity = capacity;
        }
    
        /**
         * 生产
         */
        public void put(Message message) {
            synchronized (list) {
                while (list.size() == capacity) {
                    log.info("队列已满,生产者等待");
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                list.addLast(message);
                log.info("生产消息:{}", message);
                // 生产后通知消费者
                list.notifyAll();
            }
        }
    
        public Message take() {
            synchronized (list) {
                while (list.isEmpty()) {
                    log.info("队列已空,消费者等待");
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Message message = list.removeFirst();
                log.info("消费消息:{}", message);
                // 消费后通知生产者
                list.notifyAll();
                return message;
            }
        }
    
    
    }
     // 消息
    class Message {
    
        private int id;
    
        private Object value;
    }
    

    同步锁案例

    为了更形象的表达加同步锁的概念,这里举一个生活中的例子,尽量把以上的概念具体化出来。

    这里举一个每个人非常感兴趣的一件东西。 钱!!!(马老师除外)。

    现实中,我们去银行门口的自动取款机取钱,取款机的钱就是共享变量,为了保障安全,不可能两个陌生人同时进入同一个取款机内取钱,所以只能一个人进入取钱,然后锁上取款机的门,其他人只能在取款机门口等待。

    取款机有多个,里面的钱互不影响,锁也有多个(多个对象锁),取钱人在多个取款机里同时取钱也没有安全问题。

    假如每个取钱的陌生人都是线程,当取钱人进入取款机锁了门后(线程获得锁),取到钱后出门(线程释放锁),下一个人竞争到锁来取钱。

    假设工作人员也是一个线程,如果取钱人进入后发现取款机钱不足了,这时通知工作人员来向取款机里加钱(调用notifyAll方法),取钱人暂停取钱,进入银行大堂阻塞等待(调用wait方法)。

    银行大堂里的工作人员和取钱人都被唤醒,重新竞争锁,进入后如果是取钱人,由于取款机没钱,还得进入银行大堂等待。

    当工作人员获得取款机的锁进入后,加了钱后会通知大厅里的人来取钱(调用notifyAll方法)。自己暂停加钱,进入银行大堂等待唤醒加钱(调用wait方法)。

    这时大堂里等待的人都来竞争锁,谁获取到谁进入继续取钱。

    和现实中不同的就是这里没有排队的概念,谁抢到锁谁进去取。

    ReentrantLock

    可重入锁 : 一个线程获取到对象的锁后,执行方法内部在需要获取锁的时候是可以获取到的。如以下代码

    private static final ReentrantLock LOCK = new ReentrantLock();
    
    private static void m() {
        LOCK.lock();
        try {
            log.info("begin");
          	// 调用m1()
            m1();
        } finally {
            // 注意锁的释放
            LOCK.unlock();
        }
    }
    public static void m1() {
        LOCK.lock();
        try {
            log.info("m1");
            m2();
        } finally {
            // 注意锁的释放
            LOCK.unlock();
        }
    }
    

    synchronized 也是可重入锁,ReentrantLock有以下优点

    1. 支持获取锁的超时时间
    2. 获取锁时可被打断
    3. 可设为公平锁
    4. 可以有不同的条件变量,即有多个waitSet,可以指定唤醒

    api

    // 默认非公平锁,参数传true 表示未公平锁
    ReentrantLock lock = new ReentrantLock(false);
    // 尝试获取锁
    lock()
    // 释放锁 应放在finally块中 必须执行到
    unlock()
    try {
        // 获取锁时可被打断,阻塞中的线程可被打断
        LOCK.lockInterruptibly();
    } catch (InterruptedException e) {
        return;
    }
    // 尝试获取锁 获取不到就返回false
    LOCK.tryLock()
    // 支持超时时间 一段时间没获取到就返回false
    tryLock(long timeout, TimeUnit unit)
    // 指定条件变量 休息室 一个锁可以创建多个休息室
    Condition waitSet = ROOM.newCondition();
    // 释放锁  进入waitSet等待 释放后其他线程可以抢锁
    yanWaitSet.await()
    // 唤醒具体休息室的线程 唤醒后 重写竞争锁
    yanWaitSet.signal()
    
    

    实例:一个线程输出a,一个线程输出b,一个线程输出c,abc按照顺序输出,连续输出5次

    这个考的就是线程的通信,利用 wait()/notify()和控制变量可以实现,此处使用ReentrantLock即可实现该功能。

      public static void main(String[] args) {
            AwaitSignal awaitSignal = new AwaitSignal(5);
            // 构建三个条件变量
            Condition a = awaitSignal.newCondition();
            Condition b = awaitSignal.newCondition();
            Condition c = awaitSignal.newCondition();
            // 开启三个线程
            new Thread(() -> {
                awaitSignal.print("a", a, b);
            }).start();
    
            new Thread(() -> {
                awaitSignal.print("b", b, c);
            }).start();
    
            new Thread(() -> {
                awaitSignal.print("c", c, a);
            }).start();
    
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            awaitSignal.lock();
            try {
                // 先唤醒a
                a.signal();
            } finally {
                awaitSignal.unlock();
            }
        }
    
    
    }
    
    class AwaitSignal extends ReentrantLock {
    
        // 循环次数
        private int loopNumber;
    
        public AwaitSignal(int loopNumber) {
            this.loopNumber = loopNumber;
        }
    
        /**
         * @param print   输出的字符
         * @param current 当前条件变量
         * @param next    下一个条件变量
         */
        public void print(String print, Condition current, Condition next) {
    
            for (int i = 0; i < loopNumber; i++) {
                lock();
                try {
                    try {
                        // 获取锁之后等待
                        current.await();
                        System.out.print(print);
                    } catch (InterruptedException e) {
                    }
                    next.signal();
                } finally {
                    unlock();
                }
            }
        }
    

    死锁

    说到死锁,先举个例子,

    下面是代码实现

    static Beer beer = new Beer();
    static Story story = new Story();
    
    public static void main(String[] args) {
        new Thread(() ->{
            synchronized (beer){
                log.info("我有酒,给我故事");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (story){
                    log.info("小王开始喝酒讲故事");
                }
            }
        },"小王").start();
    
        new Thread(() ->{
            synchronized (story){
                log.info("我有故事,给我酒");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (beer){
                    log.info("老王开始喝酒讲故事");
                }
            }
        },"老王").start();
    }
    class Beer {
    }
    
    class Story{
    }
    

    死锁导致程序无法正常运行下去

    检测工具可以检查到死锁信息

    java内存模型(JMM)

    jmm 体现在以下三个方面

    1. 原子性 保证指令不会受到上下文切换的影响
    2. 可见性 保证指令不会受到cpu缓存的影响
    3. 有序性 保证指令不会受并行优化的影响

    可见性

    停不下来的程序

    static boolean run = true;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (run) {
                // ....
            }
        });
        t.start();
        Thread.sleep(1000);
       // 线程t不会如预想的停下来
        run = false; 
    }
    

    如上图所示,线程有自己的工作缓存,当主线程修改了变量并同步到主内存时,t线程没有读取到,所以程序停不下来

    有序性

    JVM在不影响程序正确性的情况下可能会调整语句的执行顺序,该情况也称为 指令重排序

      static int i;
      static int j;
    // 在某个线程内执行如下赋值操作
            i = ...;
            j = ...;
      有可能将j先赋值
    

    原子性

    原子性大家应该比较熟悉,上述同步锁的synchronized代码块就是保证了原子性,就是一段代码是一个整体,原子性保证了线程安全,不会受到上下文切换的影响。

    volatile

    该关键字解决了可见性和有序性,volatile通过内存屏障来实现的

    • 写屏障

    会在对象写操作之后加写屏障,会对写屏障的之前的数据都同步到主存,并且保证写屏障的执行顺序在写屏障之前

    • 读屏障

    会在对象读操作之前加读屏障,会在读屏障之后的语句都从主存读,并保证读屏障之后的代码执行在读屏障之后

    注意: volatile不能解决原子性,即不能通过该关键字实现线程安全。

    volatile应用场景:一个线程读取变量,另外的线程操作变量,加了该关键字后保证写变量后,读变量的线程可以及时感知。

    无锁-cas

    cas (compare and swap) 比较并交换

    为变量赋值时,从内存中读取到的值v,获取到要交换的新值n,执行 compareAndSwap()方法时,比较v和当前内存中的值是否一致,如果一致则将n和v交换,如果不一致,则自旋重试。

    cas底层是cpu层面的,即不使用同步锁也可以保证操作的原子性。

    private AtomicInteger balance;
    
    // 模拟cas的具体操作
    @Override
    public void withdraw(Integer amount) {
        while (true) {
            // 获取当前值
            int pre = balance.get();
            // 进行操作后得到新值
            int next = pre - amount;
            // 比较并设置成功 则中断 否则自旋重试
            if (balance.compareAndSet(pre, next)) {
                break;
            }
        }
    }
    

    无锁的效率是要高于之前的锁的,由于无锁不会涉及线程的上下文切换

    cas是乐观锁的思想,sychronized是悲观锁的思想

    cas适合很少有线程竞争的场景,如果竞争很强,重试经常发生,反而降低效率

    juc并发包下包含了实现了cas的原子类

    1. AtomicInteger/AtomicBoolean/AtomicLong
    2. AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray
    3. AtomicReference/AtomicStampedReference/AtomicMarkableReference

    AtomicInteger

    常用api

    new AtomicInteger(balance)
    get()
    compareAndSet(pre, next)
    //        i.incrementAndGet() ++i
    //        i.decrementAndGet() --i
    //        i.getAndIncrement() i++
    //        i.getAndDecrement() ++i
     i.addAndGet()
      // 传入函数式接口 修改i
      int getAndUpdate(IntUnaryOperator updateFunction)
      // cas 的核心方法
      compareAndSet(int expect, int update)
    

    ABA问题

    cas存在ABA问题,即比较并交换时,如果原值为A,有其他线程将其修改为B,在有其他线程将其修改为A。

    此时实际发生过交换,但是比较和交换由于值没改变可以交换成功

    解决方式

    AtomicStampedReference/AtomicMarkableReference

    上面两个类解决ABA问题,原理就是为对象增加版本号,每次修改时增加版本号,就可以避免ABA问题

    或者增加个布尔变量标识,修改后调整布尔变量值,也可以避免ABA问题

    线程池

    线程池的介绍

    线程池是java并发最重要的一个知识点,也是难点,是实际应用最广泛的。

    线程的资源很宝贵,不可能无限的创建,必须要有管理线程的工具,线程池就是一种管理线程的工具,java开发中经常有池化的思想,如 数据库连接池、Redis连接池等。

    预先创建好一些线程,任务提交时直接执行,既可以节约创建线程的时间,又可以控制线程的数量。

    线程池的好处

    1. 降低资源消耗,通过池化思想,减少创建线程和销毁线程的消耗,控制资源
    2. 提高响应速度,任务到达时,无需创建线程即可运行
    3. 提供更多更强大的功能,可扩展性高

    线程池的构造方法

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
     
    }
    

    构造器参数的意义

    参数名参数意义
    corePoolSize核心线程数
    maximumPoolSize最大线程数
    keepAliveTime救急线程的空闲时间
    unit救急线程的空闲时间单位
    workQueue阻塞队列
    threadFactory创建线程的工厂,主要定义线程名
    handler拒绝策略

    线程池案例

    下面 我们通过一个实例来理解线程池的参数以及线程池的接收任务的过程

    如上图 银行办理业务。

    1. 客户到银行时,开启柜台进行办理,柜台相当于线程,客户相当于任务,有两个是常开的柜台,三个是临时柜台。2就是核心线程数,5是最大线程数。即有两个核心线程
    2. 当柜台开到第二个后,都还在处理业务。客户再来就到排队大厅排队。排队大厅只有三个座位。
    3. 排队大厅坐满时,再来客户就继续开柜台处理,目前最大有三个临时柜台,也就是三个救急线程
    4. 此时再来客户,就无法正常为其 提供业务,采用拒绝策略来处理它们
    5. 当柜台处理完业务,就会从排队大厅取任务,当柜台隔一段空闲时间都取不到任务时,如果当前线程数大于核心线程数时,就会回收线程。即撤销该柜台。

    线程池的状态

    线程池通过一个int变量的高3位来表示线程池的状态,低29位来存储线程池的数量

    状态名称高三位接收新任务处理阻塞队列任务说明
    Running111YY正常接收任务,正常处理任务
    Shutdown000NY不会接收任务,会执行完正在执行的任务,也会处理阻塞队列里的任务
    stop001NN不会接收任务,会中断正在执行的任务,会放弃处理阻塞队列里的任务
    Tidying010NN任务全部执行完毕,当前活动线程是0,即将进入终结
    Termitted011NN终结状态
    // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;
    

    线程池的主要流程

    线程池创建、接收任务、执行任务、回收线程的步骤

    1. 创建线程池后,线程池的状态是Running,该状态下才能有下面的步骤
    2. 提交任务时,线程池会创建线程去处理任务
    3. 当线程池的工作线程数达到corePoolSize时,继续提交任务会进入阻塞队列
    4. 当阻塞队列装满时,继续提交任务,会创建救急线程来处理
    5. 当线程池中的工作线程数达到maximumPoolSize时,会执行拒绝策略
    6. 当线程取任务的时间达到keepAliveTime还没有取到任务,工作线程数大于corePoolSize时,会回收该线程

    注意: 不是刚创建的线程是核心线程,后面创建的线程是非核心线程,线程是没有核心非核心的概念的,这是我长期以来的误解。

    拒绝策略

    1. 调用者抛出RejectedExecutionException (默认策略)
    2. 让调用者运行任务
    3. 丢弃此次任务
    4. 丢弃阻塞队列中最早的任务,加入该任务

    提交任务的方法

    // 执行Runnable
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        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);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
    // 提交Callable
    public <T> Future<T> submit(Callable<T> task) {
      if (task == null) throw new NullPointerException();
       // 内部构建FutureTask
      RunnableFuture<T> ftask = newTaskFor(task);
      execute(ftask);
      return ftask;
    }
    // 提交Runnable,指定返回值
    public Future<?> submit(Runnable task) {
      if (task == null) throw new NullPointerException();
      // 内部构建FutureTask
      RunnableFuture<Void> ftask = newTaskFor(task, null);
      execute(ftask);
      return ftask;
    } 
    //  提交Runnable,指定返回值
    public <T> Future<T> submit(Runnable task, T result) {
      if (task == null) throw new NullPointerException();
       // 内部构建FutureTask
      RunnableFuture<T> ftask = newTaskFor(task, result);
      execute(ftask);
      return ftask;
    }
    
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
            return new FutureTask<T>(runnable, value);
    }
    

    Execetors创建线程池

    注意: 下面几种方式都不推荐使用

    1.newFixedThreadPool

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    
    • 核心线程数 = 最大线程数 没有救急线程
    • 阻塞队列无界 可能导致oom

    2.newCachedThreadPool

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    
    • 核心线程数是0,最大线程数无限制 ,救急线程60秒回收
    • 队列采用 SynchronousQueue 实现 没有容量,即放入队列后没有线程来取就放不进去
    • 可能导致线程数过多,cpu负担太大

    3.newSingleThreadExecutor

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    
    • 核心线程数和最大线程数都是1,没有救急线程,无界队列 可以不停的接收任务
    • 将任务串行化 一个个执行, 使用包装类是为了屏蔽修改线程池的一些参数 比如 corePoolSize
    • 如果某线程抛出异常了,会重新创建一个线程继续执行
    • 可能造成oom

    4.newScheduledThreadPool

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    • 任务调度的线程池 可以指定延迟时间调用,可以指定隔一段时间调用

    线程池的关闭

    shutdown()

    会让线程池状态为shutdown,不能接收任务,但是会将工作线程和阻塞队列里的任务执行完 相当于优雅关闭

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }
    

    shutdownNow()

    会让线程池状态为stop, 不能接收任务,会立即中断执行中的工作线程,并且不会执行阻塞队列里的任务, 会返回阻塞队列的任务列表

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }
    

    线程池的正确使用姿势

    线程池难就难在参数的配置,有一套理论配置参数

    cpu密集型 : 指的是程序主要发生cpu的运算

    ​ 核心线程数: CPU核心数+1

    IO密集型: 远程调用RPC,操作数据库等,不需要使用cpu进行大量的运算。 大多数应用的场景

    ​ 核心线程数=核数*cpu期望利用率 *总时间/cpu运算时间

    但是基于以上理论还是很难去配置,因为cpu运算时间不好估算

    实际配置大小可参考下表

    cpu密集型io密集型
    线程数数量核数<=x<=核数*2核心数*50<=x<=核心数 *100
    队列长度y>=1001<=y<=10

    1.线程池参数通过分布式配置,修改配置无需重启应用

    线程池参数是根据线上的请求数变化而变化的,最好的方式是 核心线程数、最大线程数 队列大小都是可配置的

    主要配置 corePoolSize maxPoolSize queueSize

    java提供了可方法覆盖参数,线程池内部会处理好参数 进行平滑的修改

    public void setCorePoolSize(int corePoolSize) {
    }
    

    2.增加线程池的监控

    3.io密集型可调整为先新增任务到最大线程数后再将任务放到阻塞队列

    代码 主要可重写阻塞队列 加入任务的方法

    public boolean offer(Runnable runnable) {
        if (executor == null) {
            throw new RejectedExecutionException("The task queue does not have executor!");
        }
    
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            int currentPoolThreadSize = executor.getPoolSize();
           
            // 如果提交任务数小于当前创建的线程数, 说明还有空闲线程,
            if (executor.getTaskCount() < currentPoolThreadSize) {
                // 将任务放入队列中,让线程去处理任务
                return super.offer(runnable);
            }
    		// 核心改动
            // 如果当前线程数小于最大线程数,则返回 false ,让线程池去创建新的线程
            if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
                return false;
            }
    
            // 否则,就将任务放入队列中
            return super.offer(runnable);
        } finally {
            lock.unlock();
        }
    }
    

    3.拒绝策略 建议使用tomcat的拒绝策略(给一次机会)

    // tomcat的源码
    @Override
    public void execute(Runnable command) {
        if ( executor != null ) {
            try {
                executor.execute(command);
            } catch (RejectedExecutionException rx) {
                // 捕获到异常后 在从队列获取,相当于重试1取不到任务 在执行拒绝任务
                if ( !( (TaskQueue) executor.getQueue()).force(command) ) throw new RejectedExecutionException("Work queue full.");
            }
        } else throw new IllegalStateException("StandardThreadPool not started.");
    }
    

    建议修改从队列取任务的方式: 增加超时时间,超时1分钟取不到在进行返回

    public boolean offer(E e, long timeout, TimeUnit unit){}
    

    结语

    工作三四年了,还没有正式的写过博客,自学一直都是通过笔记的方式积累,最近重新学了一下java多线程,想着周末把这部分内容认真的写篇博客分享出去。

    文章篇幅较长,给看到这里的小伙伴点个大大的赞!由于作者水平有限,加之第一次写博客,文章中难免会有错误之处,欢迎小伙伴们反馈指正。

    如果觉得文章对你有帮助,麻烦 点赞、评论、转发、在看 走起

    你的支持是我最大的动力!!!

    展开全文
  • 在Windows编程中,多线程一定比单线程吗?什么时候该用多线程?什么时候该用单线程?它们各自的应用场景是什么?
  • 多线程(一):创建线程和线程的常用方法

    万次阅读 多人点赞 2018-09-01 19:14:23
    一:为什么要学多线程 应付面试 :多线程几乎是面试中必问的题,所以掌握一定的基础知识是必须的。 了解并发编程:实际工作中很少写多线程的代码,这部分代码一般都被人封装起来了,在业务中使用多线程的机会也...
    分享一个朋友的人工智能教程(请以“右键”->"在新标签页中打开连接”的方式访问)。比较通俗易懂,风趣幽默,感兴趣的朋友可以去看看。

    一:为什么要学多线程

    1. 应付面试 :多线程几乎是面试中必问的题,所以掌握一定的基础知识是必须的。
    2. 了解并发编程:实际工作中很少写多线程的代码,这部分代码一般都被人封装起来了,在业务中使用多线程的机会也不是很多(看具体项目),虽然代码中很少会自己去创建线程,但是实际环境中每行代码却都是并行执行的,同一时刻大量请求同一个接口,并发可能会产生一些问题,所以也需要掌握一定的并发知识

    二:进程与线程

    1. 进程

    进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。

    2. 线程

    线程是一条执行路径,是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

    一个正在运行的软件(如迅雷)就是一个进程,一个进程可以同时运行多个任务( 迅雷软件可以同时下载多个文件,每个下载任务就是一个线程), 可以简单的认为进程是线程的集合。

    线程是一条可以执行的路径。

    • 对于单核CPU而言:多线程就是一个CPU在来回的切换,在交替执行。
    • 对于多核CPU而言:多线程就是同时有多条执行路径在同时(并行)执行,每个核执行一个线程,多个核就有可能是一块同时执行的。

    3. 进程与线程的关系

    一个程序就是一个进程,而一个程序中的多个任务则被称为线程。进程是表示资源分配的基本单位,又是调度运行的基本单位。,亦即执行处理机调度的基本单位。 进程和线程的关系:

    • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。

    • 资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量,即每个线程都有自己的堆栈和局部变量。

    • 处理机分给线程,即真正在处理机上运行的是线程。

    • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

    如果把上课的过程比作进程,把老师比作CPU,那么可以把每个学生比作每个线程,所有学生共享这个教室(也就是所有线程共享进程的资源),上课时学生A向老师提出问题,老师对A进行解答,此时可能会有学生B对老师的解答不懂会提出B的疑问(注意:此时可能老师还没有对A同学的问题解答完毕),此时老师又向学生B解惑,解释完之后又继续回答学生A的问题,同一时刻老师只能向一个学生回答问题(即:当多个线程在运行时,同一个CPU在某一个时刻只能服务于一个线程,可能一个线程分配一点时间,时间到了就轮到其它线程执行了,这样多个线程在来回的切换)

    4. 为什么要使用多线程

    多线程可以提高程序的效率。

    实际生活案例:村长要求喜洋洋在一个小时内打100桶水,可以喜洋洋一个小时只能打25桶水,如果这样就需要4个小时才能完成任务,为了在一个小时能够完成,喜洋洋就请美洋洋、懒洋洋、沸洋洋,来帮忙,这样4只羊同时干活,在一小时内完成了任务。原本用4个小时完成的任务现在只需要1个小时就完成了,如果把每只羊看做一个线程,多只羊即多线程可以提高程序的效率。

    5. 多线程应用场景

    • 一般线程之间比较独立,互不影响
    • 一个线程发生问题,一般不影响其它线程

    三:多线程的实现方式

    1. 顺序编程

    顺序编程:程序从上往下的同步执行,即如果第一行代码执行没有结束,第二行代码就只能等待第一行执行结束后才能结束。

    public class Main {
        // 顺序编程 吃喝示例:当吃饭吃不完的时候,是不能喝酒的,只能吃完晚才能喝酒
        public static void main(String[] args) throws Exception {
    		// 先吃饭再喝酒
            eat();
            drink();
        }
    
        private static void eat() throws Exception {
            System.out.println("开始吃饭?...\t" + new Date());
            Thread.sleep(5000);
            System.out.println("结束吃饭?...\t" + new Date());
        }
    
        private static void drink() throws Exception {
            System.out.println("开始喝酒?️...\t" + new Date());
            Thread.sleep(5000);
            System.out.println("结束喝酒?...\t" + new Date());
        }
    }
    

    这里写图片描述

    2. 并发编程

    并发编程:多个任务可以同时做,常用与任务之间比较独立,互不影响。
    线程上下文切换:

    同一个时刻一个CPU只能做一件事情,即同一时刻只能一个线程中的部分代码,假如有两个线程,Thread-0和Thread-1,刚开始CPU说Thread-0你先执行,给你3毫秒时间,Thread-0执行了3毫秒时间,但是没有执行完,此时CPU会暂停Thread-0执行并记录Thread-0执行到哪行代码了,当时的变量的值是多少,然后CPU说Thread-1你可以执行了,给你2毫秒的时间,Thread-1执行了2毫秒也没执行完,此时CPU会暂停Thread-1执行并记录Thread-1执行到哪行代码了,当时的变量的值是多少,此时CPU又说Thread-0又该你,这次我给你5毫秒时间,去执行吧,此时CPU就找出上次Thread-0线程执行到哪行代码了,当时的变量值是多少,然后接着上次继续执行,结果用了2毫秒就Thread-0就执行完了,就终止了,然后CPU说Thread-1又轮到你,这次给你4毫秒,同样CPU也会先找出上次Thread-1线程执行到哪行代码了,当时的变量值是多少,然后接着上次继续开始执行,结果Thread-1在4毫秒内也执行结束了,Thread-1也结束了终止了。CPU在来回改变线程的执行机会称之为线程上下文切换。

    public class Main {
        public static void main(String[] args) {
    	    // 一边吃饭一边喝酒
            new EatThread().start();
            new DrinkThread().start();
        }
    }
    
    class EatThread extends Thread{
        @Override
        public void run() {
            System.out.println("开始吃饭?...\t" + new Date());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("结束吃饭?...\t" + new Date());
        }
    }
    
    class DrinkThread extends Thread {
        @Override
        public void run() {
            System.out.println("开始喝酒?️...\t" + new Date());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("结束喝酒?...\t" + new Date());
        }
    }
    

    并发编程,一边吃饭一边喝酒总共用时5秒,比顺序编程更快,因为并发编程可以同时运行,而不必等前面的代码运行完之后才允许后面的代码

    这里写图片描述

    本示例主要启动3个线程,一个主线程main thread、一个吃饭线程(Thread-0)和一个喝酒线程(Thread-1),共三个线程, 三个线程并发切换着执行。main线程很快执行完,吃饭线程和喝酒线程会继续执行,直到所有线程(非守护线程)执行完毕,整个程序才会结束,main线程结束并不意味着整个程序结束。
    这里写图片描述

    • 顺序:代码从上而下按照固定的顺序执行,只有上一件事情执行完毕,才能执行下一件事。就像物理电路中的串行,假如有十件事情,一个人来完成,这个人必须先做第一件事情,然后再做第二件事情,最后做第十件事情,按照顺序做。

    • 并行:多个操作同时处理,他们之间是并行的。假如十件事情,两个人来完成,每个人在某个时间点各自做各自的事情,互不影响

    • 并发:将一个操作分割成多个部分执行并且允许无序处理,假如有十件事情,如果有一个人在做,这个人可能做一会这个不想做了,再去做别的,做着做着可能也不想做了,又去干其它事情了,看他心情想干哪个就干哪个,最终把十件事情都做完。如果有两个人在做,他们俩先分一下,比如张三做4件,李四做6件,他们各做自己的,在做自己的事情过程中可以随意的切换到别的事情,不一定要把某件事情干完再去干其它事情,有可能一件事做了N次才做完。

    通常一台电脑只有一个cpu,多个线程属于并发执行,如果有多个cpu,多线程并发执行有可能变成并行执行。
    这里写图片描述

    3. 多线程创建方式

    • 继承 Thread
    • 实现 Runable
    • 实现 Callable
    ①:继成java.lang.Thread, 重写run()方法
    public class Main {
        public static void main(String[] args) {
            new MyThread().start();
        }
    }
    
    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
        }
    }
    

    Thread 类

    package java.lang;
    public class Thread implements Runnable {
    	// 构造方法
    	public Thread(Runnable target);
    	public Thread(Runnable target, String name);
    	
    	public synchronized void start();
    }
    

    Runnable 接口

    package java.lang;
    
    @FunctionalInterface
    public interface Runnable {
        pubic abstract void run();
    }
    

    ②:实现java.lang.Runnable接口,重写run()方法,然后使用Thread类来包装

    public class Main {
        public static void main(String[] args) {
        	 // 将Runnable实现类作为Thread的构造参数传递到Thread类中,然后启动Thread类
            MyRunnable runnable = new MyRunnable();
            new Thread(runnable).start();
        }
    }
    
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
        }
    }
    

    可以看到两种方式都是围绕着Thread和Runnable,继承Thread类把run()写到类中,实现Runnable接口是把run()方法写到接口中然后再用Thread类来包装, 两种方式最终都是调用Thread类的start()方法来启动线程的。
    两种方式在本质上没有明显的区别,在外观上有很大的区别,第一种方式是继承Thread类,因Java是单继承,如果一个类继承了Thread类,那么就没办法继承其它的类了,在继承上有一点受制,有一点不灵活,第二种方式就是为了解决第一种方式的单继承不灵活的问题,所以平常使用就使用第二种方式

    其它变体写法:

    public class Main {
        public static void main(String[] args) {
            // 匿名内部类
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
                }
            }).start();
    
            // 尾部代码块, 是对匿名内部类形式的语法糖
            new Thread() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());
                }
            }.start();
    
            // Runnable是函数式接口,所以可以使用Lamda表达式形式
            Runnable runnable = () -> {System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId());};
            new Thread(runnable).start();
        }
    }
    

    ③:实现Callable接口,重写call()方法,然后包装成java.util.concurrent.FutureTask, 再然后包装成Thread

    Callable:有返回值的线程,能取消线程,可以判断线程是否执行完毕

    public class Main {
        public static void main(String[] args) throws Exception {
        	 // 将Callable包装成FutureTask,FutureTask也是一种Runnable
            MyCallable callable = new MyCallable();
            FutureTask<Integer> futureTask = new FutureTask<>(callable);
            new Thread(futureTask).start();
    
            // get方法会阻塞调用的线程
            Integer sum = futureTask.get();
            System.out.println(Thread.currentThread().getName() + Thread.currentThread().getId() + "=" + sum);
        }
    }
    
    
    class MyCallable implements Callable<Integer> {
    
        @Override
        public Integer call() throws Exception {
            System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tstarting...");
    
            int sum = 0;
            for (int i = 0; i <= 100000; i++) {
                sum += i;
            }
            Thread.sleep(5000);
    
            System.out.println(Thread.currentThread().getName() + "\t" + Thread.currentThread().getId() + "\t" + new Date() + " \tover...");
            return sum;
        }
    }
    

    Callable 也是一种函数式接口

    @FunctionalInterface
    public interface Callable<V> {
        V call() throws Exception;
    }
    

    FutureTask

    public class FutureTask<V> implements RunnableFuture<V> {
    	// 构造函数
    	public FutureTask(Callable<V> callable);
    	
    	// 取消线程
    	public boolean cancel(boolean mayInterruptIfRunning);
    	// 判断线程
    	public boolean isDone();
    	// 获取线程执行结果
    	public V get() throws InterruptedException, ExecutionException;
    }
    

    RunnableFuture

    public interface RunnableFuture<V> extends Runnable, Future<V> {
        void run();
    }
    

    三种方式比较:

    • Thread: 继承方式, 不建议使用, 因为Java是单继承的,继承了Thread就没办法继承其它类了,不够灵活
    • Runnable: 实现接口,比Thread类更加灵活,没有单继承的限制
    • Callable: Thread和Runnable都是重写的run()方法并且没有返回值,Callable是重写的call()方法并且有返回值并可以借助FutureTask类来判断线程是否已经执行完毕或者取消线程执行
    • 当线程不需要返回值时使用Runnable,需要返回值时就使用Callable,一般情况下不直接把线程体代码放到Thread类中,一般通过Thread类来启动线程
    • Thread类是实现Runnable,Callable封装成FutureTask,FutureTask实现RunnableFuture,RunnableFuture继承Runnable,所以Callable也算是一种Runnable,所以三种实现方式本质上都是Runnable实现

    四:线程的状态

    1. 创建(new)状态: 准备好了一个多线程的对象,即执行了new Thread(); 创建完成后就需要为线程分配内存
    2. 就绪(runnable)状态: 调用了start()方法, 等待CPU进行调度
    3. 运行(running)状态: 执行run()方法
    4. 阻塞(blocked)状态: 暂时停止执行线程,将线程挂起(sleep()、wait()、join()、没有获取到锁都会使线程阻塞), 可能将资源交给其它线程使用
    5. 死亡(terminated)状态: 线程销毁(正常执行完毕、发生异常或者被打断interrupt()都会导致线程终止)

    这里写图片描述

    这里写图片描述

    这里写图片描述

    五:Thread常用方法

    Thread

    public class Thread implements Runnable {
        // 线程名字
        private volatile String name;
        // 线程优先级(1~10)
        private int priority;
        // 守护线程
        private boolean daemon = false;
        // 线程id
        private long tid;
        // 线程组
        private ThreadGroup group;
        
        // 预定义3个优先级
        public final static int MIN_PRIORITY = 1;
        public final static int NORM_PRIORITY = 5;
        public final static int MAX_PRIORITY = 10;
        
        
        // 构造函数
        public Thread();
        public Thread(String name);
        public Thread(Runnable target);
        public Thread(Runnable target, String name);
        // 线程组
        public Thread(ThreadGroup group, Runnable target);
        
        
        // 返回当前正在执行线程对象的引用
        public static native Thread currentThread();
        
        // 启动一个新线程
        public synchronized void start();
        // 线程的方法体,和启动线程没毛关系
        public void run();
        
        // 让线程睡眠一会,由活跃状态改为挂起状态
        public static native void sleep(long millis) throws InterruptedException;
        public static void sleep(long millis, int nanos) throws InterruptedException;
        
        // 打断线程 中断线程 用于停止线程
        // 调用该方法时并不需要获取Thread实例的锁。无论何时,任何线程都可以调用其它线程的interruptf方法
        public void interrupt();
        public boolean isInterrupted()
        
        // 线程是否处于活动状态
        public final native boolean isAlive();
        
        // 交出CPU的使用权,从运行状态改为挂起状态
        public static native void yield();
        
        public final void join() throws InterruptedException
        public final synchronized void join(long millis)
        public final synchronized void join(long millis, int nanos) throws InterruptedException
        
        
        // 设置线程优先级
        public final void setPriority(int newPriority);
        // 设置是否守护线程
        public final void setDaemon(boolean on);
        // 线程id
        public long getId() { return this.tid; }
        
        
        // 线程状态
        public enum State {
            // new 创建
            NEW,
    
            // runnable 就绪
            RUNNABLE,
    
            // blocked 阻塞
            BLOCKED,
    
            // waiting 等待
            WAITING,
    
            // timed_waiting
            TIMED_WAITING,
    
            // terminated 结束
            TERMINATED;
        }
    }
    
    public static void main(String[] args) {
        // main方法就是一个主线程
    
        // 获取当前正在运行的线程
        Thread thread = Thread.currentThread();
        // 线程名字
        String name = thread.getName();
        // 线程id
        long id = thread.getId();
        // 线程优先级
        int priority = thread.getPriority();
        // 是否存活
        boolean alive = thread.isAlive();
        // 是否守护线程
        boolean daemon = thread.isDaemon();
    
        // Thread[name=main, id=1 ,priority=5 ,alive=true ,daemon=false]
        System.out.println("Thread[name=" + name + ", id=" + id + " ,priority=" + priority + " ,alive=" + alive + " ,daemon=" + daemon + "]");
    }
    
    0. Thread.currentThread()
    public static void main(String[] args) {
        Thread thread = Thread.currentThread();
        // 线程名称
        String name = thread.getName();
        // 线程id
        long id = thread.getId();
        // 线程已经启动且尚未终止
        // 线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的
        boolean alive = thread.isAlive();
        // 线程优先级
        int priority = thread.getPriority();
        // 是否守护线程
        boolean daemon = thread.isDaemon();
        
        // Thread[name=main,id=1,alive=true,priority=5,daemon=false]
        System.out.println("Thread[name=" + name + ",id=" + id + ",alive=" + alive + ",priority=" + priority + ",daemon=" + daemon + "]");
    }
    
    1. start() 与 run()
    public static void main(String[] args) throws Exception {
       new Thread(()-> {
           for (int i = 0; i < 5; i++) {
               System.out.println(Thread.currentThread().getName() + " " + i);
               try { Thread.sleep(200); } catch (InterruptedException e) { }
           }
       }, "Thread-A").start();
    
       new Thread(()-> {
           for (int j = 0; j < 5; j++) {
               System.out.println(Thread.currentThread().getName() + " " + j);
               try { Thread.sleep(200); } catch (InterruptedException e) { }
           }
       }, "Thread-B").start();
    }
    

    start(): 启动一个线程,线程之间是没有顺序的,是按CPU分配的时间片来回切换的。
    这里写图片描述

    public static void main(String[] args) throws Exception {
        new Thread(()-> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
                try { Thread.sleep(200); } catch (InterruptedException e) { }
            }
        }, "Thread-A").run();
    
        new Thread(()-> {
            for (int j = 0; j < 5; j++) {
                System.out.println(Thread.currentThread().getName() + " " + j);
                try { Thread.sleep(200); } catch (InterruptedException e) { }
            }
        }, "Thread-B").run();
    }
    

    注意:执行结果都是main主线程
    这里写图片描述

    run(): 调用线程的run方法,就是普通的方法调用,虽然将代码封装到两个线程体中,可以看到线程中打印的线程名字都是main主线程,run()方法用于封装线程的代码,具体要启动一个线程来运行线程体中的代码(run()方法)还是通过start()方法来实现,调用run()方法就是一种顺序编程不是并发编程。

    有些面试官经常问一些启动一个线程是用start()方法还是run()方法,为了面试而面试。

    2. sleep() 与 interrupt()
    public static native void sleep(long millis) throws InterruptedException;
    public void interrupt();
    

    sleep(long millis): 睡眠指定时间,程序暂停运行,睡眠期间会让出CPU的执行权,去执行其它线程,同时CPU也会监视睡眠的时间,一旦睡眠时间到就会立刻执行(因为睡眠过程中仍然保留着锁,有锁只要睡眠时间到就能立刻执行)。

    • sleep(): 睡眠指定时间,即让程序暂停指定时间运行,时间到了会继续执行代码,如果时间未到就要醒需要使用interrupt()来随时唤醒
    • interrupt(): 唤醒正在睡眠的程序,调用interrupt()方法,会使得sleep()方法抛出InterruptedException异常,当sleep()方法抛出异常就中断了sleep的方法,从而让程序继续运行下去
    public static void main(String[] args) throws Exception {
        Thread thread0 = new Thread(()-> {
            try {
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t太困了,让我睡10秒,中间有事叫我,zZZ。。。");
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t被叫醒了,又要继续干活了");
            }
        });
        thread0.start();
    
        // 这里睡眠只是为了保证先让上面的那个线程先执行
        Thread.sleep(2000);
    
        new Thread(()-> {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t醒醒,醒醒,别睡了,起来干活了!!!");
            // 无需获取锁就可以调用interrupt
            thread0.interrupt();
        }).start();
    }
    

    这里写图片描述

    3. wait() 与 notify()

    wait、notify和notifyAll方法是Object类的final native方法。所以这些方法不能被子类重写,Object类是所有类的超类,因此在程序中可以通过this或者super来调用this.wait(), super.wait()

    • wait(): 导致线程进入等待阻塞状态,会一直等待直到它被其他线程通过notify()或者notifyAll唤醒。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。wait(long timeout): 时间到了自动执行,类似于sleep(long millis)
    • notify(): 该方法只能在同步方法或同步块内部调用, 随机选择一个(注意:只会通知一个)在该对象上调用wait方法的线程,解除其阻塞状态
    • notifyAll(): 唤醒所有的wait对象

    注意:

    • Object.wait()和Object.notify()和Object.notifyall()必须写在synchronized方法内部或者synchronized块内部
    • 让哪个对象等待wait就去通知notify哪个对象,不要让A对象等待,结果却去通知B对象,要操作同一个对象

    Object

    public class Object {
    	public final void wait() throws InterruptedException;
    	public final native void wait(long timeout) throws InterruptedException;
    	public final void wait(long timeout, int nanos) throws InterruptedException;
    	
    	
    	public final native void notify();
    	public final native void notifyAll();
    }
    

    WaitNotifyTest

    public class WaitNotifyTest {
        public static void main(String[] args) throws Exception {
            WaitNotifyTest waitNotifyTest = new WaitNotifyTest();
            new Thread(() -> {
                try {
                    waitNotifyTest.printFile();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            }).start();
    
            new Thread(() -> {
                try {
                    waitNotifyTest.printFile();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            }).start();
    
            new Thread(() -> {
                try {
                    System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t睡觉1秒中,目的是让上面的线程先执行,即先执行wait()");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                waitNotifyTest.notifyPrint();
            }).start();
        }
    
        private synchronized void printFile() throws InterruptedException {
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t等待打印文件...");
            this.wait();
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t打印结束。。。");
        }
    
        private synchronized void notifyPrint() {
            this.notify();
            System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t通知完成...");
        }
    }
    
    

    这里写图片描述

    wait():让程序暂停执行,相当于让当前,线程进入当前实例的等待队列,这个队列属于该实例对象,所以调用notify也必须使用该对象来调用,不能使用别的对象来调用。调用wait和notify必须使用同一个对象来调用。
    这里写图片描述

    this.notifyAll();
    这里写图片描述

    4. sleep() 与 wait()
    ① Thread.sleep(long millis): 睡眠时不会释放锁
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
    
        new Thread(() -> {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
                    try { Thread.sleep(1000); } catch (InterruptedException e) { }
                }
            }
        }).start();
    
        Thread.sleep(1000);
    
        new Thread(() -> {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
                }
            }
        }).start();
    }
    

    因main方法中Thread.sleep(1000)所以上面的线程Thread-0先被执行,当循环第一次时就会Thread.sleep(1000)睡眠,因为sleep并不会释放锁,所以Thread-1得不到执行的机会,所以直到Thread-0执行完毕释放锁对象lock,Thread-1才能拿到锁,然后执行Thread-1;
    这里写图片描述

    5. wait() 与 interrupt()

    wait(): 方法的作用是释放锁,加入到等待队列,当调用interrupt()方法后,线程必须先获取到锁后,然后才抛出异常InterruptedException 。注意: 在获取锁之前是不会抛出异常的,只有在获取锁之后才会抛异常

    所有能抛出InterruptedException的方法都可以通过interrupt()来取消的

    public static native void sleep(long millis) throws InterruptedException;
    public final void wait() throws InterruptedException;
    public final void join() throws InterruptedException;
    public void interrupt()

    notify()和interrupt()
    从让正在wait的线程重新运行这一点来说,notify方法和intterrupt方法的作用有些类似,但仍有以下不同之处:

    • notify/notifyAll是java.lang.Object类的方法,唤醒的是该实例的等待队列中的线程,而不能直接指定某个具体的线程。notify/notifyAll唤醒的线程会继续执行wait的下一条语句,另外执行notify/notifyAll时线程必须要获取实例的锁

    • interrupte方法是java.lang.Thread类的方法,可以直接指定线程并唤醒,当被interrupt的线程处于sleep或者wait中时会抛出InterruptedException异常。执行interrupt()并不需要获取取消线程的锁。

    • 总之notify/notifyAll和interrupt的区别在于是否能直接让某个指定的线程唤醒、执行唤醒是否需要锁、方法属于的类不同

    6. interrupt()

    有人也许认为“当调用interrupt方法时,调用对象的线程就会InterruptedException异常”, 其实这是一种误解,实际上interrupt方法只是改变了线程的“中断状态”而已,所谓中断状态是一个boolean值,表示线程是否被中断的状态。

    public class Thread implements Runnable {
    	public void interrupt() {
    		中断状态 = true;
    	}
    	
    	// 检查中断状态
    	public boolean isInterrupted();
    	
    	// 检查中断状态并清除当前线程的中断状态
    	public static boolean interrupted() {
    		// 伪代码
    		boolean isInterrupted = isInterrupted();
    		中断状态 = false;
    	}
    }	
    

    假设Thread-0执行了sleep、wait、join中的一个方法而停止运行,在Thread-1中调用了interrupt方法,此时线程Thread-0的确会抛出InterruptedException异常,但这其实是sleep、wait、join中的方法内部会对线程的“中断状态”进行检查,如果中断状态为true,就会抛出InterruptedException异常。假如某个线程的中断状态为true,但线程体中却没有调用或者没有判断线程中断状态的值,那么线程则不会抛出InterruptedException异常。

    isInterrupted() 检查中断状态
    若指定线程处于中断状态则返回true,若指定线程为非中断状态,则反回false, isInterrupted() 只是获取中断状态的值,并不会改变中断状态的值。

    interrupted()
    检查中断状态并清除当前线程的中断状态。如当前线程处于中断状态返回true,若当前线程处于非中断状态则返回false, 并清除中断状态(将中断状态设置为false), 只有这个方法才可以清除中断状态,Thread.interrupted的操作对象是当前线程,所以该方法并不能用于清除其它线程的中断状态。

    interrupt()与interrupted()

    • interrupt():打断线程,将中断状态修改为true
    • interrupted(): 不打断线程,获取线程的中断状态,并将中断状态设置为false

    这里写图片描述

    public class InterrupptTest {
        public static void main(String[] args) {
            Thread thread = new Thread(new MyRunnable());
            thread.start();
            boolean interrupted = thread.isInterrupted();
            // interrupted=false
            System.out.println("interrupted=" + interrupted);
    
            thread.interrupt();
    
            boolean interrupted2 = thread.isInterrupted();
            // interrupted2=true
            System.out.println("interrupted2=" + interrupted2);
    
            boolean interrupted3 = Thread.interrupted();
            // interrupted3=false
            System.out.println("interrupted3=" + interrupted3);
        }
    }
    
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            synchronized (this) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    // InterruptedException	false
                    System.out.println("InterruptedException\t" + Thread.currentThread().isInterrupted());
                }
            }
        }
    }
    
    

    这里写图片描述

    ② object.wait(long timeout): 会释放锁
    public class SleepWaitTest {
        public static void main(String[] args) throws InterruptedException {
            SleepWaitTest object = new SleepWaitTest();
    
            new Thread(() -> {
                synchronized (object) {
                    System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t等待打印文件...");
                    try {
                        object.wait(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t打印结束。。。");
                }
            }).start();
    
    		 // 先上面的线程先执行
            Thread.sleep(1000);
    
            new Thread(() -> {
                synchronized (object) {
                    for (int i = 0; i < 5; i++) {
                        System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "\t" + i);
                    }
                }
            }).start();
        }
    }
    

    因main方法中有Thread.sleep(1000)所以上面的线程Thread-0肯定会被先执行,当Thread-0被执行时就拿到了object对象锁,然后进入wait(5000)5秒钟等待,此时wait释放了锁,然后Thread-1就拿到了锁就执行线程体,Thread-1执行完后就释放了锁,当等待5秒后Thread-0就能再次获取object锁,这样就继续执行后面的代码。wait方法是释放锁的,如果wait方法不释放锁那么Thread-1是拿不到锁也就没有执行的机会的,事实是Thread-1得到了执行,所以说wait方法会释放锁

    这里写图片描述

    ③ sleep与wait的区别
    • sleep在Thread类中,wait在Object类中
    • sleep不会释放锁,wait会释放锁
    • sleep使用interrupt()来唤醒,wait需要notify或者notifyAll来通知
    5.join()

    让当前线程加入父线程,加入后父线程会一直wait,直到子线程执行完毕后父线程才能执行。当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。

    将某个线程加入到当前线程中来,一般某个线程和当前线程依赖关系比较强,必须先等待某个线程执行完毕才能执行当前线程。一般在run()方法内使用

    join() 方法:

    public final void join() throws InterruptedException {
            join(0);
    }
    
    
    public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
    
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
    
        if (millis == 0) {
        	 // 循环检查线程的状态是否还活着,如果死了就结束了,如果活着继续等到死
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
    
    
    public final synchronized void join(long millis, int nanos) throws InterruptedException {
    
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
    
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException("nanosecond timeout value out of range");
        }
    
        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }
    
        join(millis);
    }
    
    

    JoinTest

    public class JoinTest {
        public static void main(String[] args) {
            new Thread(new ParentRunnable()).start();
        }
    }
    
    class ParentRunnable implements Runnable {
        @Override
        public void run() {
            // 线程处于new状态
            Thread childThread = new Thread(new ChildRunable());
            // 线程处于runnable就绪状态
            childThread.start();
            try {
                // 当调用join时,parent会等待child执行完毕后再继续运行
                // 将某个线程加入到当前线程
                childThread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            for (int i = 0; i < 5; i++) {
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "父线程 running");
            }
        }
    }
    
    class ChildRunable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                try { Thread.sleep(1000); } catch (InterruptedException e) {}
                System.out.println(new Date() + "\t" + Thread.currentThread().getName() + "子线程 running");
            }
        }
    }
    
    

    程序进入主线程,运行Parent对应的线程,Parent的线程代码分两段,一段是启动一个子线程,一段是Parent线程的线程体代码,首先会将Child线程加入到Parent线程,join()方法会调用join(0)方法(join()方法是普通方法并没有加锁,join(0)会加锁),join(0)会执行while(isAlive()) { wait(0);} 循环判断线程是否处于活动状态,如果是继续wait(0)知道isAlive=false结束掉join(0), 从而结束掉join(), 最后回到Parent线程体中继续执行其它代码。

    在Parent调用child.join()后,child子线程正常运行,Parent父线程会等待child子线程结束后再继续运行。
    这里写图片描述

    • join() 和 join(long millis, int nanos) 最后都调用了 join(long millis)。

    • join(long millis, int nanos)和join(long millis)方法 都是synchronized。

    • join() 调用了join(0),从源码可以看到join(0)不断检查当前线程是否处于Active状态。

    • join() 和 sleep() 一样,都可以被中断(被中断时,会抛出 InterrupptedException 异常);不同的是,join() 内部调用了wait(),会出让锁,而 sleep() 会一直保持锁。

    6. yield()

    交出CPU的执行时间,不会释放锁,让线程进入就绪状态,等待重新获取CPU执行时间,yield就像一个好人似的,当CPU轮到它了,它却说我先不急,先给其他线程执行吧, 此方法很少被使用到,

    /**
     * A hint to the scheduler that the current thread is willing to yield
     * its current use of a processor. The scheduler is free to ignore this
     * hint.
     *
     * <p> Yield is a heuristic attempt to improve relative progression
     * between threads that would otherwise over-utilise a CPU. Its use
     * should be combined with detailed profiling and benchmarking to
     * ensure that it actually has the desired effect.
     *
     * <p> It is rarely appropriate to use this method. It may be useful
     * for debugging or testing purposes, where it may help to reproduce
     * bugs due to race conditions. It may also be useful when designing
     * concurrency control constructs such as the ones in the
     * {@link java.util.concurrent.locks} package.
     */
    public static native void yield();
    

    这里写图片描述

    public static void main(String[] args) {
        new Thread(new Runnable() {
            int sum = 0;
            @Override
            public void run() {
                long beginTime=System.currentTimeMillis();
                for (int i = 0; i < 99999; i++) {
                    sum += 1;
                    // 去掉该行执行用2毫秒,加上271毫秒
                    Thread.yield();
                }
                long endTime=System.currentTimeMillis();
                System.out.println("用时:"+ (endTime - beginTime) + " 毫秒!");
            }
        }).start();
    }
    

    sleep(long millis) 与 yeid()

    • sleep(long millis): 需要指定具体睡眠的时间,不会释放锁,睡眠期间CPU会执行其它线程,睡眠时间到会立刻执行
    • yeid(): 交出CPU的执行权,不会释放锁,和sleep不同的时当再次获取到CPU的执行,不能确定是什么时候,而sleep是能确定什么时候再次执行。两者的区别就是sleep后再次执行的时间能确定,而yeid是不能确定的
    • yield会把CPU的执行权交出去,所以可以用yield来控制线程的执行速度,当一个线程执行的比较快,此时想让它执行的稍微慢一些可以使用该方法,想让线程变慢可以使用sleep和wait,但是这两个方法都需要指定具体时间,而yield不需要指定具体时间,让CPU决定什么时候能再次被执行,当放弃到下次再次被执行的中间时间就是间歇等待的时间
    7. setDaemon(boolean on)

    线程分两种:

    • 用户线程:如果主线程main停止掉,不会影响用户线程,用户线程可以继续运行。
    • 守护线程:如果主线程死亡,守护线程如果没有执行完毕也要跟着一块死(就像皇上死了,带刀侍卫也要一块死),GC垃圾回收线程就是守护线程
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                IntStream.range(0, 5).forEach(i -> {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "\ti=" + i);
                });
            }
        };
        thread.start();
    
    
        for (int i = 0; i < 2; i++) {
            System.out.println(Thread.currentThread().getName() + "\ti=" + i);
        }
        System.out.println("主线程执行结束,子线程仍然继续执行,主线程和用户线程的生命周期各自独立。");
    }
    

    这里写图片描述

    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                IntStream.range(0, 5).forEach(i -> {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "\ti=" + i);
                });
            }
        };
        thread.setDaemon(true);
        thread.start();
    
    
        for (int i = 0; i < 2; i++) {
            System.out.println(Thread.currentThread().getName() + "\ti=" + i);
        }
        System.out.println("主线程死亡,子线程也要陪着一块死!");
    }
    

    这里写图片描述

    六 线程组

    可以对线程分组,分组后可以统一管理某个组下的所有线程,例如统一中断所有线程

    public class ThreadGroup implements Thread.UncaughtExceptionHandler {
        private final ThreadGroup parent;
        String name;
        int maxPriority;
        
        Thread threads[];
        
        private ThreadGroup() {
            this.name = "system";
            this.maxPriority = Thread.MAX_PRIORITY;
            this.parent = null;
        }
        
        public ThreadGroup(String name) {
            this(Thread.currentThread().getThreadGroup(), name);
        }
        
        public ThreadGroup(ThreadGroup parent, String name) {
            this(checkParentAccess(parent), parent, name);
        }
        
        // 返回此线程组中活动线程的估计数。 
        public int activeGroupCount();
        
        // 中断此线程组中的所有线程。
        public final void interrupt();
    }
    
    public static void main(String[] args) {
        String mainThreadGroupName = Thread.currentThread().getThreadGroup().getName();
        System.out.println(mainThreadGroupName);
        // 如果一个线程没有指定线程组,默认为当前线程所在的线程组
        new Thread(() -> { }, "my thread1").start();
    
        ThreadGroup myGroup = new ThreadGroup("MyGroup");
        myGroup.setMaxPriority(5);
    
        Runnable runnable = () -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
            String groupName = threadGroup.getName();
            ThreadGroup parentGroup = threadGroup.getParent();
            String parentGroupName = parentGroup.getName();
            ThreadGroup grandpaThreadGroup = parentGroup.getParent();
            String grandpaThreadGroupName = grandpaThreadGroup.getName();
            int maxPriority = threadGroup.getMaxPriority();
            int activeCount = myGroup.activeCount();
    
            // system <- main <- MyGroup(1) <- my thread2
            System.out.println(MessageFormat.format("{0} <- {1} <- {2}({3}) <- {4}",
                    grandpaThreadGroupName,
                    parentGroupName,
                    groupName,
                    activeCount,
                    Thread.currentThread().getName()));
        };
    
        new Thread(myGroup, runnable, "my thread2").start();
    }
    

    线程组与线程组之间是有父子关系的,自定义线程组的父线程组是main线程组,main线程组的父线程组是system线程组。
    这里写图片描述

    展开全文
  • 多线程快速入门

    千次阅读 2020-04-17 22:07:12
    每个进程包含一到线程线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行任务。通常由操作系统...
  • 下面是Python 2.7.9手册中对GIL的...去释放GIL锁,并查看其他线程是否可以执行,由于任务被加锁,在第二个cpu时间片继续把时间片分给第一个线程,这让cpu调度时间白白浪费,反而导致多线程比单线程耗时久。
  • 多线程一定比单线程

    千次阅读 2018-10-24 19:22:21
    不一定。因为,存在线程创建和上下文切换的时间开销。
  • 多线程就一定比单线程吗?

    千次阅读 2019-06-24 16:55:00
    我相信很多人在遇到这个问题时不假思索的回答出答案,因为并发,那为什么并发呢? 对于多核 CPU 来说,每个物理核心都能独立执行任务,这种条件下,使用并发技术(准确的来说这里可以称之为并行,因为多核)...
  • 快速理解Java单线程和多线程比较

    千次阅读 2019-07-10 16:43:00
    单线程是一个一个线程顺序执行,一个线程只有当前一个线程执行完之后才能执行。这种方法可以导致当某个线程等待响应的时间特别长时,下一个线程...所以当比较单线程和多线程哪个更快时,就是对上面两者之间开销的...
  • Java多线程

    万次阅读 多人点赞 2021-06-11 16:28:49
    Java多线程Java多线程线程的创建线程常见方法线程的状态线程的优先级守护线程线程组Java线程池线程池的创建线程池的参数线程池的使用线程不安全问题Java中的锁synchronized同步方法synchronized同步语句块...
  • Java多线程超详解

    万次阅读 多人点赞 2019-06-11 01:00:30
    随着计算机的配置越来越高,我们需要将进程进一步优化,细分为线程,充分提高图形化界面的多线程的开发。这就要求对线程的掌握很彻底。 那么话不多说,今天本帅将记录自己线程的学习。 线程的相关API //获取当前...
  • 多线程快速解压FastZipArchive介绍

    千次阅读 2013-10-23 18:36:55
    在iOS项目中用到解压缩,用的是ZipArchive,下载点击这里,ZipArchive是基于minizip的,但不支持多线程,封装的也不方便,然后就有了FastZipArchive,现在有一个粗糙的...3 拥有更快的解压缩速度(FAST_MODE下做了代
  • 多线程

    千次阅读 2019-05-30 22:50:07
    多线程机制可以同时运行多个程序块,使程序的运行的效率更快。 进程:一个由操作系统分配的内存空间,包含一个或多个线程。 理解线程与进程: 打开一个微博客户端就是打开了一个进程,打开微博以后除了可以直接看见...
  • 多线程一定比单线程效率高吗?

    千次阅读 2015-02-03 15:19:51
    比如一些搜索代理或者群发email的多线程软件,由于操作一次需要网络的返回信息 花的时间比较长,而对cpu来说却是空闲的,如果是一个一个顺序执行,那么搜索几千个IP就花上好久好久。 而如果用多线
  • WPF中如何创建多线程,响应速度更快

    千次阅读 2010-09-10 09:58:00
    不管相关的长时间运行进程的类型为何(不管是从数据库获取大量结果,进行异步 Web 服务调用,还是任何数量的其他潜在密集型操作),简单的事实就是,响应更快的应用程序是让用户更满意的长期保证。但是,开始在 WPF ...
  • 多进程与多线程区别

    万次阅读 多人点赞 2016-04-21 10:01:16
    在Unix上编程采用多线程还是多进程的争执由来已久,这种争执最常见到在C/S通讯中服务端并发技术 的选型上,比如WEB服务器技术中,Apache是采用多进程的(perfork模式,每客户连接对应一个进程,每进程中只存在唯一一...
  • 昨天晚上做了个实验,用线程复制7个文件和单个线程复制7个文件(总共约1.7G)比较,代码如下:import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io....
  • 多线程面试题(值得收藏)

    万次阅读 多人点赞 2019-08-16 09:41:18
    金九银十到了,即将进入找工作的高峰期,最新整理的最全多线程并发面试47题和答案总结,希望对想进BAT的同学有帮助,由于篇幅较长,建议收藏后细看~ 1、并发编程三要素? 1)原子性 原子性指的是一个或者多个操作,...
  • 前几天遇到一个很有意思的问题,提炼出来就是使用多线程去读取文件的话加快读取速度吗? 在往下看之前先自己想一想,同样如果在面试中问你这个问题该怎么回答呢? 假设有一个文件大小为10G,我们需要将其读取到...
  • 多线程为什么比单线程呢?

    万次阅读 2016-06-05 12:30:03
    多线程为什么比单线程呢?
  • 对于单核CPU下多线程程序在同一时间点都只能有一个线程运行,对于多核CPU可以实现真正的并发同步运行,这种说法正确吗? 另外在多线程的情况下使用互斥对象来实现线程同步,这样的话多线程程序的运行效率受影响吗?
  • 线程、进程、多线程、多进程 和 多任务 小结

    千次阅读 多人点赞 2019-04-20 11:59:56
    4 多线程 5 线程与进程的关系 6 线程和进程的区别 7 进程的优缺点 7.1 进程的优点 7.2 进程的缺点 8 线程的优缺点 8.1 线程的优点 8.2 线程的缺点 9 多线程的优缺点 9.1 多线程的优点 9.2 多线程的缺点 ...
  • Arduino的多线程

    万次阅读 多人点赞 2019-05-08 15:43:59
    前言 本章节是如何使用Arduino 的多线程技术。截止目前,Arduino 的性能很棒,但是... 为了正确理解多线程技术和Arduino 之间的关系,我们需要理解为什么它不是真正的多线程。 在计算机CPU上,多线程应用程序...
  • 异步多线程

    千次阅读 2017-11-23 21:11:32
    1.首先讲解一下进程、线程与多线程的概念 进程-线程 计算机概念 进程是一个程序在电脑上运行时,全部计算资源的合集叫进程。 线程是程序的最小执行单位,包含计算资源,任何一个操作的响应都是线程。 多线程:多个...
  • Java多线程面试题

    万次阅读 2020-10-25 15:56:40
    wait 方法: 是 Object 的方法,必须与 synchronized 关键字一起使用,线程进入阻塞状态,当 notify 或者 notifyall 被调用后,解除阻塞。但是,只有重新占用互斥锁之后才进入可运行状态。睡眠时,释放互斥锁。 ...
  • Android多线程:你必须要了解的多线程基础知识汇总

    千次阅读 多人点赞 2019-05-13 08:59:23
    多线程的应用在开发中非常常见 今天,我将献上一份全面 & 详细的多线程基础知识汇总,希望你们喜欢。 目录 1. 线程 - 介绍 1.1 定义 一个基本的CPU执行单元 & 程序执行流的最小单元 比进程小的可...
  • 基于SpringBoot的多线程实现快速导入EXCEL

    千次阅读 热门讨论 2019-04-19 18:57:31
    此次出一版基于SpringBoot的多线程实现快速导入EXCEL,效率之高体现在速度之,单次解析的数据量之大。对比如下: 第一版:导入5万条数据,花费近一个时 第二版:导入10万条数据,花费23分钟,优化后达到了15分钟...
  • 多线程技术

    千次阅读 2016-11-07 15:29:41
    多任务、多线程和多处理这些术语经常被交替地使用,但是它们在本质上是...在多线程程序中,操作系统让一个线程的代码执行一段时间(被称为时间片)后,切换到另外的线程继续运行。暂停某个线程的运行而开始执行另一个

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 509,046
精华内容 203,618
关键字:

多线程会更快吗