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

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

为啥子要用多线程

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

怎么使用

两个方法:

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月前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
195 60
【Java并发】【线程池】带你从0-1入门线程池
|
3月前
|
监控 Kubernetes Java
阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
|
2月前
|
数据采集 Java Linux
面试大神教你:如何巧妙回答线程优先级这个经典考题?
大家好,我是小米。本文通过故事讲解Java面试中常见的线程优先级问题。小明和小华的故事帮助理解线程优先级:高优先级线程更可能被调度执行,但并非越高越好。实际开发需权衡业务需求,合理设置优先级。掌握线程优先级不仅能写出高效代码,还能在面试中脱颖而出。最后,小张因深入分析成功拿下Offer。希望这篇文章能助你在面试中游刃有余!
53 4
面试大神教你:如何巧妙回答线程优先级这个经典考题?
|
2月前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
168 14
|
2月前
|
缓存 安全 Java
面试中的难题:线程异步执行后如何共享数据?
本文通过一个面试故事,详细讲解了Java中线程内部开启异步操作后如何安全地共享数据。介绍了异步操作的基本概念及常见实现方式(如CompletableFuture、ExecutorService),并重点探讨了volatile关键字、CountDownLatch和CompletableFuture等工具在线程间数据共享中的应用,帮助读者理解线程安全和内存可见性问题。通过这些方法,可以有效解决多线程环境下的数据共享挑战,提升编程效率和代码健壮性。
115 6
|
3月前
|
算法 安全 Java
Java线程调度揭秘:从算法到策略,让你面试稳赢!
在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
130 16
|
3月前
|
安全 Java 程序员
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
121 11
|
3月前
|
Java Linux 调度
硬核揭秘:线程与进程的底层原理,面试高分必备!
嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
66 6
|
3月前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
4月前
|
并行计算 算法 安全
面试必问的多线程优化技巧与实战
多线程编程是现代软件开发中不可或缺的一部分,特别是在处理高并发场景和优化程序性能时。作为Java开发者,掌握多线程优化技巧不仅能够提升程序的执行效率,还能在面试中脱颖而出。本文将从多线程基础、线程与进程的区别、多线程的优势出发,深入探讨如何避免死锁与竞态条件、线程间的通信机制、线程池的使用优势、线程优化算法与数据结构的选择,以及硬件加速技术。通过多个Java示例,我们将揭示这些技术的底层原理与实现方法。
198 3

热门文章

最新文章