多线程能够提升程序性能,也属于高薪必能核心技术栈,本篇会全面详解Java多线程。@mikechen

主要包含如下几点:

基本概念

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

进程

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

线程

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

多线程

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

同步

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()必须在同步代码块中调用。

2) 关于是否释放同步监视器:如果两个方法都使用在同步代码块呵呵同步方法中,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();
    } }

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();
    }
}

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");
                    }
                });
            }         } }

核心参数

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

线程池任务执行流程

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

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

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

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();
        } }

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.普通同步方法

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

/**
* 用在普通方法
*/
private synchronized void synchronizedMethod() {
System.out.println("--synchronizedMethod start--");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("--synchronizedMethod end--");
}

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");
}

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");
}
}

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

在单线程环境中这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的锁
}

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

3.上下文切换

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


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

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

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

以上

作者简介

陈睿|mikechen,10年+大厂架构经验,《BAT架构技术500期》系列文章作者,分享十余年BAT架构经验以及面试心得!

阅读mikechen的互联网架构更多技术文章合集

Java并发|JVM|MySQL|Spring|Redis|分布式|高并发|架构师

Java多线程超级详解(只看这篇就够了)的更多相关文章

  1. Java线程池详解,看这篇就够了!

    构造一个线程池为什么需要几个参数?如果避免线程池出现OOM?Runnable和Callable的区别是什么?本文将对这些问题一一解答,同时还将给出使用线程池的常见场景和代码片段. 基础知识 Execu ...

  2. Java多线程编程详解

    转自:http://programming.iteye.com/blog/158568 线程的同步 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题.Ja ...

  3. JAVA多线程synchronized详解

    Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 当两个并发线程访问同一个对象object中的这个synchronized(this)同 ...

  4. Java多线程基础详解

    基础概念进程进程是操作系统结构的基础:是一次程序的执行:是一个程序及其数据在处理机上顺序执行时所发生的活动.操作系统中,几乎所有运行中的任务对应一条进程(Process).一个程序进入内存运行,即变成 ...

  5. Java的LinkedList详解,看源码之后的总结

    1. LinkedList实现了一个带表头的双向循环链表: 2. LinkedList是线程不同步的: 3. LinkedList中实现了push.pop.peek.empty等方法,因此Linked ...

  6. 【多线程】java多线程Completablefuture 详解【在spring cloud微服务之间调用,防止接口超时的应用】【未完成】

    参考地址:https://www.jianshu.com/p/6f3ee90ab7d3 示例: public static void main(String[] args) throws Interr ...

  7. 手把手教Linux驱动3-之字符设备架构详解,有这篇就够了

    一.Linux设备分类 Linux系统为了管理方便,将设备分成三种基本类型: 字符设备 块设备 网络设备 字符设备: 字符(char)设备是个能够像字节流(类似文件)一样被访问的设备,由字符设备驱动程 ...

  8. 【Java面试必备JVM】JVM看这篇就够了

    链接–>JVM

  9. Java多线程——多线程方法详解

    本系列文章是Java多线程的详解介绍,对多线程还不熟悉的同学可以先去看一下我的这篇博客Java基础系列3:多线程超详细总结,这篇博客从宏观层面介绍了多线程的整体概况,接下来的几篇文章是对多线程的深入剖 ...

随机推荐

  1. HttpUploadFile

    public static void HttpUploadFile(string url, string file, string paramName, string contentType, Nam ...

  2. [python][flask] Flask 图片上传与下载例子(支持漂亮的拖拽上传)

    目录 1.效果预览 2.新增逻辑概览 3.tuchuang.py 逻辑介绍 3.1 图片上传 3.2 图片合法检查 3.3 图片下载 4.__init__.py 逻辑介绍 5.upload.html ...

  3. MATLAB调用NASTRAN的连续计算解决方案

    使用MATLAB调用NASTRAN提交bdf文件计算的方法笔者试过几种,分别做一下介绍 MATLAB直接调用操作系统命令 该方法对单次计算可能方便些,定义一个字符串,并使用eval命令调用,具体格式为 ...

  4. STM32内存知识

    在了解STM32内存之前需要了解 MCU 的型号和MDK 中的.map 文件,很多刚学习 stm32 时都不会过多的去了解 MCU 的选型,是在太枯燥了.这里在从新了解一下,久了就熟悉了. 一.STM ...

  5. NODE.JS exports require理解

    node.js exports 的作用是什么? 因为A.js文件想访问B.js文件中的类或函数,是不能直接访问的.为了解决这个问题 node.js 产生了 exports ,exports 实际可以理 ...

  6. 1.1 操作系统的第一个功能——虚拟化硬件资源 -《zobolの操作系统学习札记》

    1.1 操作系统的第一个功能--虚拟化硬件资源 目录 1.1 操作系统的第一个功能--虚拟化硬件资源 问1:操作系统一般处于计算机系统的哪一个位置? 问2:管理硬件资源为什么要单独交给操作系统? 问3 ...

  7. 用QT制作3D点云显示器——QtDataVisualization

    因为QT的三维显示模块QtDataVisualization已经对个人开发免费开放了,所以在制作点云,地图,表格之类的东西的时候,其实我们都不需要使用QtCharts或者QOpenGL模块了.直接使用 ...

  8. [LINUX] 像电影里的黑客一样用 terminal 作为日常开发

    目录 1.效果预览 2.具体实现 2.1 定位鼠标位置 2.2 获取屏幕位置 2.3 计算鼠标在哪个窗口 2.4 1920x1080 平铺效果设计 2.5 1280x1024 平铺效果设计 3 代码 ...

  9. 使用Docker搭建自己的Bitwarden密码管理服务

    相信身为开发者,总会在各种网站中注册账号,为了方面记忆可以使用同一套账号密码进行注册,自从前段时间学习通时间撞库后有些人已经开始疯狂改密码了,可是密码一多就很难记忆,最好找个地方存储账户信息 我曾经使 ...

  10. 从Python到水一篇AI论文(核心 or Sci三区+)

    博客配套视频链接: https://space.bilibili.com/383551518?spm_id_from=333.1007.0.0 b 站直接看 配套 github 链接:https:// ...