多线程从入门到入坟(面试吹牛逼专用)

简介: 提高程序执行效率,比如多线程读取、写入文档等等(摊牌了,我生产中没用过,但这不妨碍我吹牛逼);

为啥子要用多线程

提高程序执行效率,比如多线程读取、写入文档等等(摊牌了,我生产中没用过,但这不妨碍我吹牛逼);

怎么使用

两个方法:

1、继承Thread类,重写run方法;

2、实现Runnable或者Callable接口,重写run方法;(其实Thread底层也是通过实现Runnable接口)

通常使用实现后者,因为java是单继承,万一你的线程类还需要继承其他类的话就搞不了了鸭,用后者相对灵活一些;


继承Thread类demo

20200828101636689.png

可以看到线程执行的顺序是随机的

20200828101938115.png

还可以给你自己的线程取名字

2020082810212140 (1).png


小啊giao运行截图

2020082810221751.png

demo代码

package com.jd.clouddemo.test;
public class MyThreadDemo extends Thread {
    public static void main(String[] args) {
        for(int i = 0;i<10;i++){
            MyThreadDemo myThreadDemo = new MyThreadDemo();
            myThreadDemo.setName("小啊giao"+i+"号");
            myThreadDemo.start();
        }
    }
    @Override
    public void run() {
//        这里写你需要执行的业务逻辑
        System.out.println(Thread.currentThread().getName()+"开始执行");
    }
}

实现Runnable接口demo

20200828102957373.png

20200828103102494.png

我们再看看上面通过继承Thread类的方法,最好亲自敲一下,感受感受

20200828103200587.png

通过实现Runnable搞的线程类只能通过构造方法来命名,传参也是一样

20200828103935162.png

20200828103950144.png


简单说下实现多线程两种方式的区别


实现Runnable接口比继承Thread类所具有的优势:


1):适合多个相同的程序代码的线程去处理同一个资源


2):可以避免java中的单继承的限制


3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立


4):线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类


线程池

线程池的作用

20200828153835152.png

怎么用

1、使用现成的

JUC包下的Executors,打开你的idea随便new个类,轻轻敲出Executors,再加个“.”就能看到它的方法

20200828154202580.png

但是阿里巴巴开发规范不推荐用现成的,因为这几种都可能造成内存溢出,所以最好自己创建,这样也能更好的理解线程池原理;

2、自己创建怎么搞

除了下面这几个参数,还有个参数是线程工厂,我们用默认的就行,不用传

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                5, //核心线程数,可以同时运行的线程数量
                10, //最大线程数,任务队列装满的时候,当前可以同时运行的线程数变为这个数
                1L,//等待时间,当线程池中的线程数量大于核心线程数,如果没有新的任务过来,核心线程数以外的线程不会立刻被回收,而是等待这个时间到了才进行回收,作用的话我想就是避免线程创建销毁带来的内存消耗吧
                TimeUnit.SECONDS,//等待时间的单位
                new ArrayBlockingQueue<>(100),//任务队列,100是设置队列容量
                new ThreadPoolExecutor.CallerRunsPolicy());//饱和策略,任务队列满了并且当前线程数达到最大线程数会触发这个机制,有四种,先别问,下面会讲

四种饱和策略:

20200828155223912.png

任务队列

如果当前运行的线程小于corePoolSize,则新任务会直接运行,不会存入队列

如果当前运行的线程大于等于 corePoolSize,则 Executor始终首选将请求加入队列,而不添加新的线程。

如果无法将任务加入队列,则创建新的线程,除非创建此线程后线程数会超出 maximumPoolSize,在这种情况下,会触发饱和策略。


1、无界队列(慎用)


队列大小无限制,常用的为无界的LinkedBlockingQueue,使用该队列做为阻塞队列时要尤其当心,当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM。阅读代码发现,Executors.newFixedThreadPool 采用就是 LinkedBlockingQueue,当QPS很高,发送数据很大,大量的任务被添加到这个无界LinkedBlockingQueue 中,导致cpu和内存飙升服务器挂掉。


2、有界队列


常用的有两类,一类是遵循FIFO原则的队列如ArrayBlockingQueue,另一类是优先级队列如PriorityBlockingQueue。PriorityBlockingQueue中的优先级由任务的Comparator决定。

使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。

这个队列会导致部分任务丢失


3、同步移交队列


如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。


再分享个小知识

如果想让多线程的操作完了再执行主线程,在.shutDown后调用threadPoolExecutor的isTerminated方法来判断线程池的任务是否执行完毕;(也可以使用CountDownLatch实现,这个后面单独写一篇博客来讲;)

        threadPoolExecutor.shutdown();
        while (!threadPoolExecutor.isTerminated()) System.out.println("执行完毕");


引发的血案

多线程虽然牛逼,但是会引起并发问题(线程安全的典故也由此而来),比如有个变量a=1,现在有A、B两个线程去操作它,对它进行+1操作,那讲道理这两个线程执行完后a的值应该为3,但是如果不对变量或方法进行处理,a往往会等于2,线程要对变量值进行修改,就必须先读取他的当前值,问题就出在这儿,A线程先过来读取a=1,兴冲冲的准备进行+1操作,但是在+1操作执行之前,B线程也过来了,A线程的操作对B是不可见的,它也读取到a=1,哦豁,它又不知道A也在进行操作,所以在它看来只进行+1操作就万事大吉了,于是AB都对值为1的a进行+1操作,最终结果也就等于2了;当然也有可能等于3(线程的调度具有随机性,B在A操作完了之后再读取a,此时a=2,+1=3听懂掌声);

如何避免并发问题

对于如何保证线程安全这个话题,完全可以写一本书了,这里面牵涉到java内存模型、jvm内存模型、c++等等可怕的东西,但是好在java为我们提供了一些大宝贝用来保证线程安全,对于初学者,不用太在意底成实现,搞理科一定到先知其然后知其所以然,知道怎么用这些大宝贝即可;


1、synchronized


不墨迹,先看看不加synchronized的运行效果

package com.jd.clouddemo.test;
public class MyRunnableDemo implements Runnable {
    private static int count;
    public MyRunnableDemo() {
        count = 0;
    }
    public static void main(String[] args) {
        MyRunnableDemo syncThread = new MyRunnableDemo();
        Thread thread1 = new Thread(syncThread, "小啊giao");
        Thread thread2 = new Thread(syncThread, "大啊giao");
        thread1.start();
        thread2.start();
    }
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + (count++));
        }
        /**
         虽然不明显,但是可以看出是乱序的
         小啊giao:1
         大啊giao:0
         小啊giao:2
         大啊giao:3
         大啊giao:5
         大啊giao:6
         大啊giao:7
         小啊giao:4
         小啊giao:8
         小啊giao:9
         */
    }
}


下面说说synchronized三种用法(还有其它使用场景,这里就不一一列出)

1.1 修饰类

    public void run() {
        for (int i = 0; i < 5; i++) {
            synchronized (MyRunnableDemo.class) {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
            }
        }
        /**
         不管执行多少次,永远保持有序,永远年轻,永远热泪盈眶,嘤嘤嘤
         小啊giao:0
         小啊giao:1
         小啊giao:2
         小啊giao:3
         小啊giao:4
         大啊giao:5
         大啊giao:6
         大啊giao:7
         大啊giao:8
         大啊giao:9
         */
    }

1.2 修饰代码块

    public void run() {
        for (int i = 0; i < 5; i++) {
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
            }
        }
        /**
         不管执行多少次,永远保持有序,永远年轻,永远热泪盈眶,嘤嘤嘤
         小啊giao:0
         小啊giao:1
         小啊giao:2
         小啊giao:3
         小啊giao:4
         大啊giao:5
         大啊giao:6
         大啊giao:7
         大啊giao:8
         大啊giao:9
         */
    }

1.3 修饰方法

    //这里加上synchronized 
    public synchronized void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + (count++));
        }
        /**
         不管执行多少次,永远保持有序
         小啊giao:0
         小啊giao:1
         小啊giao:2
         小啊giao:3
         小啊giao:4
         大啊giao:5
         大啊giao:6
         大啊giao:7
         大啊giao:8
         大啊giao:9
         */
    }

2、lock

lock是一个接口,它有很多实现类,我们这里使用ReentrantLock;

    public void run() {
//        这里写你需要执行的业务逻辑
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();
        System.out.println(Thread.currentThread().getName()+"读取到的值为:"+param);
        param = param + 1;
        System.out.println(Thread.currentThread().getName()+"进行+1操作后的值为:"+param);
        reentrantLock.unlock();
    }
        /**
     * Thread-0读取到的值为:1
     * Thread-1读取到的值为:1
     * Thread-0进行+1操作后的值为:2
     * Thread-1进行+1操作后的值为:3
     */

3、volatile关键字


这个就牛逼了,很多同步框架的底层都是这玩意儿实现的,因为它能保证变量在内存中的可见性,用人话说就是线程能及时拿到这个变量最新的值,就拿上面那个例子,如果变量a用volatile修饰,对于a来讲就不存在线程不安全的问题了,因为volatile能及时刷新a在内存中的值,A线程执行+1操作后B线程能及时发现;


这玩意儿平时不怎么用,想了一上午也没想到一个很好的例子来证明演示效果,后续想到了再来补充,我觉得这个懂原理即可,嘤嘤嘤;


关于volatile有一道很不错的面试题,就是为什么volatile只能保证可见性,不能保证原子性,可能有些叼毛连题都读不懂,简单举个例:

就拿上面的例子来说,a用volatile修饰,如果你对a进行a++操作,他是保证同步的,因为a++实际上不是一个原子操作,是两个,两个原子操作加起来他就不是原子操作了!!!如果面试问到就这么说,面试官不会再细问,因为他连自己都不知道;


web开发中能用到多线程的场景很少(一般搞app用的多一点),即使跟面试官说没用过,他也能理解,但是这玩意儿就像汽车的安全气囊,你可以不用,但是用的时候你不能没有,你要是说不会,那面试官不得不怀疑你的学习能力和实际开发经验是否作假;



相关文章
|
2月前
|
安全 数据处理 开发者
Python中的多线程编程:从入门到精通
本文将深入探讨Python中的多线程编程,包括其基本原理、应用场景、实现方法以及常见问题和解决方案。通过本文的学习,读者将对Python多线程编程有一个全面的认识,能够在实际项目中灵活运用。
|
1月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
1月前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
2月前
|
算法 NoSQL Java
Springboot3新特性:GraalVM Native Image Support和虚拟线程(从入门到精通)
这篇文章介绍了Spring Boot 3中GraalVM Native Image Support的新特性,提供了将Spring Boot Web项目转换为可执行文件的步骤,并探讨了虚拟线程在Spring Boot中的使用,包括如何配置和启动虚拟线程支持。
131 9
Springboot3新特性:GraalVM Native Image Support和虚拟线程(从入门到精通)
|
1月前
|
安全 Java 调度
Java中的多线程编程入门
【10月更文挑战第29天】在Java的世界中,多线程就像是一场精心编排的交响乐。每个线程都是乐团中的一个乐手,他们各自演奏着自己的部分,却又和谐地共同完成整场演出。本文将带你走进Java多线程的世界,让你从零基础到能够编写基本的多线程程序。
36 1
|
1月前
|
Java 数据处理 开发者
Java多线程编程的艺术:从入门到精通####
【10月更文挑战第21天】 本文将深入探讨Java多线程编程的核心概念,通过生动实例和实用技巧,引导读者从基础认知迈向高效并发编程的殿堂。我们将一起揭开线程管理的神秘面纱,掌握同步机制的精髓,并学习如何在实际项目中灵活运用这些知识,以提升应用性能与响应速度。 ####
49 3
|
2月前
|
Java
Java中的多线程编程:从入门到精通
本文将带你深入了解Java中的多线程编程。我们将从基础概念开始,逐步深入探讨线程的创建、启动、同步和通信等关键知识点。通过阅读本文,你将能够掌握Java多线程编程的基本技能,为进一步学习和应用打下坚实的基础。
|
3月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
3月前
|
消息中间件 前端开发 NoSQL
面试官:线程池遇到未处理的异常会崩溃吗?
面试官:线程池遇到未处理的异常会崩溃吗?
83 3
面试官:线程池遇到未处理的异常会崩溃吗?
|
3月前
|
消息中间件 前端开发 NoSQL
面试官:如何实现线程池任务编排?
面试官:如何实现线程池任务编排?
43 1
面试官:如何实现线程池任务编排?