• Java多线程超级详解(看这篇就足够了)


    主要会详解以下六大点:

    基本概念

    很多人都对其中的一些概念不够明确,如同步、并发等等,让我们先建立一个数据字典,以免产生误会。

    进程

    在操作系统中运行的程序就是进程,比如你的QQ、播放器、游戏、IDE等等

    线程

    一个进程可以有多个线程,如视频中同时听声音,看图像,看弹幕,等等。

    最近小伙伴,让我帮忙找一些面试题、架构、设计类资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,你也需要的话欢迎下载!点赞收藏+评论转发+关注我之后私信我,注意回复【000】即可白嫖

    多线程

    多线程:多个线程并发执行。

    同步

    Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。

    比如:synchronized关键字,在保证结果准确的同时,提高性能,线程安全的优先级高于性能。

    并行

    多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。

    并发

    通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。

    并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。

    线程的生命周期

    在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态

    • 新建状态:当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值
    • 就绪状态:当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行
    • 运行状态:如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态
    • 阻塞状态:当处于运行状态的线程失去所占用资源之后,便进入阻塞状态
    • 死亡状态:线程在run()方法执行结束后进入死亡状态。此外,如果线程执行了interrupt()或stop()方法,那么它也会以异常退出的方式进入死亡状态。

    线程状态的控制

    可以对照上面的线程状态流转图来看具体的方法,这样更清楚具体作用:

    1.start()

    启动当前线程, 调用当前线程的run()方法

    2.run()

    通常需要重写Thread类中的此方法, 将创建的线程要执行的操作声明在此方法中

    3.yield()

    释放当前CPU的执行权

    4.join()

    在线程a中调用线程b的join(), 此时线程a进入阻塞状态, 知道线程b完全执行完以后, 线程a才结束阻塞状态

    5.sleep(long militime)

    让线程睡眠指定的毫秒数,在指定时间内,线程是阻塞状态

    6.wait()

    一旦执行此方法,当前线程就会进入阻塞,一旦执行wait()会释放同步监视器。

    7.sleep()和wait()的异同

    相同点:两个方法一旦执行,都可以让线程进入阻塞状态。

    不同点:

    1. 两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()

    2. 调用要求不同:sleep()可以在任何需要的场景下调用。wait()必须在同步代码块中调用。

    3. 关于是否释放同步监视器:如果两个方法都使用在同步代码块呵呵同步方法中,sleep不会释放锁,wait会释放锁。

    8.notify()

    一旦执行此方法,将会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先度最高的。

    9.notifyAll()

    一旦执行此方法,就会唤醒所有被wait的线程 。

    10.LockSupport

    LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的。

    多线程的5种创建方式

    1.继承Thread类

    package com.mikechen.java.multithread;
    
    
    
    /**
    * 多线程创建:继承Thread
    *
    * @author mikechen
    */
    class MyThread extends Thread {
    
        private int i = 0;
    
    
        @Override
        public void run() {
            for (i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        }
    
    
        public static void main(String[] args) {
            MyThread myThread=new MyThread();
            myThread.start();
        }
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    2.实现Runnable接口

    package com.mikechen.java.multithread;
    
    
    /**
    * 多线程创建:实现Runnable接口
    *
    * @author mikechen
    */
    public class MyRunnable implements Runnable {
        private int i = 0;
    
        @Override
        public void run() {
            for (i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        }
    
    
        public static void main(String[] args) {
            Runnable myRunnable = new MyRunnable(); // 创建一个Runnable实现类的对象
            Thread thread = new Thread(myRunnable); // 将myRunnable作为Thread target创建新的线程
            thread.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    3.线程池创建

    线程池:其实就是一个可以容纳多个线程的容器,其中的线程可以反复的使用,省去了频繁的创建线程对象的操作,无需反复创建线程而消耗过多的系统资源。

    package com.mikechen.java.multithread;
    
    
    import java.util.concurrent.Executor;
    import java.util.concurrent.Executors;
    
    
    
    /**
    * 多线程创建:线程池
    *
    * @author mikechen
    */
    public class MyThreadPool {
    
    
            public static void main(String[] args) {
                //创建带有5个线程的线程池
                //返回的实际上是ExecutorService,而ExecutorService是Executor的子接口
                Executor threadPool = Executors.newFixedThreadPool(5);
                for(int i = 0 ;i < 10 ; i++) {
                    threadPool.execute(new Runnable() {
                        public void run() {
                            System.out.println(Thread.currentThread().getName()+" is running");
                        }
                    });
                }
    
    
    
            }
    
    
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    核心参数

    public ThreadPoolExecutor(
        int corePoolSize,
        int maximumPoolSize,
        long keepAliveTime,
        TimeUnit unit,
        BlockingQueue workQueue,
        ThreadFactory threadFactory,
        RejectedExecutionHandler handler
    )
    {
        ....
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    从上图可以看出,提交任务之后,首先会尝试着交给核心线程池中的线程来执行,但是必定核心线程池中的线程数有限,所以必须要由任务队列来做一个缓存,先将任务放队列中缓存,然后等待线程去执行。

    最后,由于任务太多,队列也满了,这个时候线程池中剩下的线程就会启动来帮助核心线程池执行任务。

    如果还是没有办法正常处理新到的任务,则线程池只能将新提交的任务交给饱和策略来处理了。

    4.匿名内部类

    适用于创建启动线程次数较少的环境,书写更加简便

    package com.mikechen.java.multithread;
    
    
    
    /**
    * 多线程创建:匿名内部类
    *
    * @author mikechen
    */
    public class MyThreadAnonymous {
    
    
    
    
            public static void main(String[] args) {
                //方式1:相当于继承了Thread类,作为子类重写run()实现
                new Thread() {
                    public void run() {
                        System.out.println("匿名内部类创建线程方式1...");
                    };
                }.start();
    
    
    
                //方式2:实现Runnable,Runnable作为匿名内部类
                new Thread(new Runnable() {
                    public void run() {
                        System.out.println("匿名内部类创建线程方式2...");
                    }
                } ).start();
            }
    
    
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    5.Lambda表达式创建

    package com.mikechen.java.multithread;
    
    
    
    /**
    * 多线程创建:lambda表达式
    *
    * @author mikechen
    */
    public class MyThreadLambda {
        public static void main(String[] args) {
            //匿名内部类创建多线程
            new Thread(){
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"mikchen的互联网架构创建新线程1");
                }
            }.start();
    
    
    
    
            //使用Lambda表达式,实现多线程
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"mikchen的互联网架构创建新线程2");
            }).start();
    
    
    
            //优化Lambda
            new Thread(()-> System.out.println(Thread.currentThread().getName()+"mikchen的互联网架构创建新线程3")).start();
    
    
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    线程的同步

    线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏,线程的同步是保证多线程安全访问竞争资源的一种手段。

    1.普通同步方法

    锁是当前实例对象 ,进入同步代码前要获得当前实例的锁。

    /**
    * 用在普通方法
    */
    private synchronized void synchronizedMethod() {
    System.out.println("--synchronizedMethod start--");
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("--synchronizedMethod end--");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.静态同步方法

    锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁。

    /**
    * 用在静态方法
    */
    private synchronized static void synchronizedStaticMethod() {
    System.out.println("synchronizedStaticMethod start");
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("synchronizedStaticMethod end");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3.同步方法块

    锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

    /**
    * 用在类
    */
    private void synchronizedClass() {
    synchronized (SynchronizedTest.class) {
    System.out.println("synchronizedClass start");
    try {
    Thread.sleep(2000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("synchronizedClass end");
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    4.synchronized底层实现

    synchronized的底层实现是完全依赖JVM虚拟机的,所以谈synchronized的底层实现,就不得不谈数据在JVM内存的存储:Java对象头,以及Monitor对象监视器。

    1.Java对象头

    在JVM虚拟机中,对象在内存中的存储布局,可以分为三个区域:

    • 对象头(Header)
    • 实例数据(Instance Data)
    • 对齐填充(Padding)

    Java对象头主要包括两部分数据:

    1)类型指针(Klass Pointer)

    是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;

    2)标记字段(Mark Word)

    用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等,它是实现轻量级锁和偏向锁的关键.

    所以,很明显synchronized使用的锁对象是存储在Java对象头里的标记字段里。

    2.Monitor

    monitor描述为对象监视器,可以类比为一个特殊的房间,这个房间中有一些被保护的数据,monitor保证每次只能有一个线程能进入这个房间进行访问被保护的数据,进入房间即为持有monitor,退出房间即为释放monitor。

    下图是synchronized同步代码块反编译后的截图,可以很清楚的看见monitor的调用。

    使用syncrhoized加锁的同步代码块在字节码引擎中执行时,主要就是通过锁对象的monitor的取用(monitorenter)与释放(monitorexit)来实现的。

    多线程引入问题

    多线程的优点很明显,但是多线程的缺点也同样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担,并且线程间的共享变量可能造成死锁的出现。

    1.线程安全问题

    1)原子性

    在并发编程中很多的操作都不是原子操作,比如:

    i++;   // 操作2
    i = j; // 操作3
    i = i + 1; // 操作4
    
    xxxxxxxxxxbr?i++;   // 操作2bri = j; // 操作3bri = i + 1; // 操作4
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在单线程环境中这3个操作都不会出现问题,但是在多线程环境中,如果不通过加锁操作,往往很可能会出现意料之外的值。

    在java中可以通过synchronized或者ReentrantLock来保证原子性。

    2)可见性

    可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即得到这个修改的值。

    如上图所示,每个线程都有自己的工作内存,工作内存和主存间要通过store和load进行交互。

    为了解决多线程的可见性问题,java提供了volatile关键字,当一个共享变量被volatile修饰时,他会保证修改的值会立即更新到主存,当有其他线程需要读取时,他会去主存中读取新值,而普通共享变量不能保证其可见性,因为变量被修改后刷回到主存的时间是不确定的。

    2.线程死锁

    线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。

    当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁,如图所示:

    举一个例子:

    public void add(int m) {
        synchronized(lockA) { // 获得lockA的锁
            this.value += m;
            synchronized(lockB) { // 获得lockB的锁
                this.another += m;
            } // 释放lockB的锁
        } // 释放lockA的锁
    }
    
    public void dec(int m) {
        synchronized(lockB) { // 获得lockB的锁
            this.another -= m;
            synchronized(lockA) { // 获得lockA的锁
                this.value -= m;
            } // 释放lockA的锁
        } // 释放lockB的锁
    }
    
    xxxxxxxxxxbr?public void add(int m) {br    synchronized(lockA) { // 获得lockA的锁br        this.value += m;br        synchronized(lockB) { // 获得lockB的锁br            this.another += m;br        } // 释放lockB的锁br    } // 释放lockA的锁br}brbrpublic void dec(int m) {br    synchronized(lockB) { // 获得lockB的锁br        this.another -= m;br        synchronized(lockA) { // 获得lockA的锁br            this.value -= m;br        } // 释放lockA的锁br    } // 释放lockB的锁br}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。

    3.上下文切换

    多线程并发一定会快吗?其实不一定,因为多线程有线程创建和线程上下文切换的开销。

    CPU是很宝贵的资源,速度也非常快,为了保证均衡,通常会给不同的线程分配时间片,当CPU从一个线程切换到另外一个线程的时候,CPU需要保存当前线程的本地数据,程序指针等状态,并加载下一个要执行的线程的本地数据,程序指针等,这个切换称之为上下文切换。

    一般减少上下文切换的方法有:无锁并发编程,CAS算法,使用协程等方式。

    多线程用好了可以成倍的增加效率,用不好可能比单线程还慢。

    以上!

    先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

  • 相关阅读:
    [MRCTF2020]你传你呢
    重修SpringMVC(二)
    使用fvm切换flutter版本
    图文并茂的帮助文档你值得拥有
    Shell编程之免交互
    数据使用要谨慎——不良数据带来严重后果
    Thinkphp漏洞详解合集
    python入门篇08- 函数进阶-参数传递
    物联网的未来:连接的智能世界
    centos安装onlyoffice协作空间报错找不到repositroy
  • 原文地址:https://blog.csdn.net/geejkse_seff/article/details/126106449