科百科
当前位置: 首页 范文大全

java最新多线程技术(Java多线程超级详解)

时间:2023-07-29 作者: 小编 阅读量: 1 栏目名: 范文大全

同步Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。并行多个CPU实例或者多台机器同时执行一段处理逻辑,是真正的同时。此外,如果线程执行了interrupt()或stop()方法,那么它也会以异常退出的方式进入死亡状态。如果有多个线程被wait,就唤醒优先度最高的。

多线程能够提升程序性能,也属于高薪必能核心技术栈,本篇会全面详解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;@Overridepublic 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;@Overridepublic 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(){@Overridepublic void run() {System.out.println(Thread.currentThread().getName() "mikchen的互联网架构创建新线程1");}}.start();//使用Lambda表达式,实现多线程new Thread(()->{System.out.println(Thread.currentThread().getName() "mikchen的互联网架构创建新线程2");}).start();//优化Lambdanew 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;// 操作2i = j; // 操作3i = i1; // 操作4

xxxxxxxxxxbri;// 操作2bri = j; // 操作3bri = i1; // 操作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的锁}

xxxxxxxxxxbrpublic void add(int m) {brsynchronized(lockA) { // 获得lockA的锁brthis.value= m;brsynchronized(lockB) { // 获得lockB的锁brthis.another= m;br} // 释放lockB的锁br} // 释放lockA的锁br}brbrpublic void dec(int m) {brsynchronized(lockB) { // 获得lockB的锁brthis.another -= m;brsynchronized(lockA) { // 获得lockA的锁brthis.value -= m;br} // 释放lockA的锁br} // 释放lockB的锁br}

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

3.上下文切换

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

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

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

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

以上!

更多架构技术干货,私信【架构】即可查看我原创的300期 BAT架构技术系列文章与1000 大厂面试题答案合集。

    推荐阅读
  • 宋濂是什么时期人(宋濂是元末明初人)

    跟着小编一起来看一看吧!宋濂是什么时期人宋濂是元末明初人。宋濂,初名寿,字景濂,号潜溪,别号龙门子、玄真遁叟等,汉族。祖籍金华潜溪,后迁居金华浦江。元末明初著名政治家、文学家、史学家、思想家,与高启、刘基并称为“明初诗文三大家”,又与章溢、刘基、叶琛并称为“浙东四先生”。被明太祖朱元璋誉为“开国文臣之首”,学者称其为太史公、宋龙门。

  • 怎么处理榕树扦插枝(榕树扦插枝方法)

    接下来我们就一起去研究一下吧!怎么处理榕树扦插枝首先我们要从榕树的枝丫上选择一段健壮的枝条,来作为扦插用的插枝,不过在选择扦插枝条的时候,一定要注意不可在树干腐烂的榕树上选择枝条,因为存活的几率会大大减少。接着将原先选好的枝条拿出来,将末端破开切成四个等份,然后让这四个面的枝条分别向外伸展开来。于是再将破开的四个部分分别粘上一些生根粉,这样有助于提高植苗的存活率,主要是促进插枝在土壤中萌发的过程。

  • 有关高端大气上档次的网名(有关高端大气上档次的网名有哪些)

    柔泉梦雾生如夏花,接下来我们就来聊聊关于有关高端大气上档次的网名?以下内容大家不妨参考一二希望能帮到您!有关高端大气上档次的网名柔泉梦雾生如夏花星空素月一枕清风梦绿萝曦夏温柔养猫人荒年花前月下蝉舞热铁皮屋顶上的猫青色发尾仟陌尘缘海岛落日有了月亮悠云霓裳雪蝶冰花凉拌莲花白缱绻草莓浩瀚星辰月亮邮递员深蓝冷雨太阳花暖兮滚烫的冷雨木森一枕庭前雪人间樱桃月影疏桐薄荷撞可乐北澜

  • 腰椎间盘突出最佳锻炼操(腰椎间盘突出症康复锻炼操)

    确实,每个患者的康复都要靠自己维持保护。腰突患者拱桥练习方法有两种,一种是“三点”式拱桥,一种是“五点”式拱桥,前者的练习难度较大,锻炼的肌肉也较多。“三点”式拱桥的练习方法是仰卧床上,双脚收缩与床面呈90度,然后双脚掌及头为支点身体向上拱起如拱桥状。要坚持做10到30个为一轮,做3轮以上,且要不断增加锻炼强度,至少坚持一个月。如年龄大或体虚者,可适当减少!

  • 君子兰抽箭时如何养护(君子兰年年养夹箭)

    在给君子兰增大温差之前,可以将君子兰放在5-10℃的低温环境下冻上10天左右,注意晚上温度不能低于5℃,否则君子兰很容易就被冻伤了!保证10℃以上的温差,才能让君子兰开花。君子兰播种比较慢,一般1个月左右才出根冒芽。

  • 2022年车辆保险费率是怎样的(2022年车险保费新规)

    车险费改今年有了新的改革方案,根据这方案各保险公司可依自身风险识别能力、风险成本情况和风险定价能力,对不同风险水平的机动车和驾驶人规定不同的商业车险费率。接下来由的小编为大家整理了一些关于车辆保险费率方面的知识,欢迎大家阅读!

  • 如何跟老板谈部门团建(老板问你怎么组织公司活动)

    交易卡材料:索引卡、记号笔时间:10-15分钟团体人数:无限制说明:还记得棒球或口袋妖怪交易卡吗?对于这个有趣的团队建设活动,你就是被“交易”的人。向每个团队成员分发大型索引卡和标记。让他们制作一张自己的个人交易卡,上面写上他们的名字、自画像、昵称,以及一个每个人都不太可能知道的关于他们的事。这些可能是团队、徽标、产品或价值声明的照片。将所有牌面朝下,然后将小组分成几组。

  • 杭州地铁早上多久运行(杭州地铁运营时间调整了)

    杭州地铁早上多久运行杭州市地铁集团刚发布《关于杭州地铁运营调整的通告》。请广大乘客朋友合理安排出行,乘车全程请佩戴口罩。如后续行车计划调整,我们将及时在杭州地铁官方微博、车站等平台进行通知。

  • 黄金柚子与红柚区别 红心柚和黄金柚的区别

    红柚的果肉则是红色的,富含番茄红素,糖分较足。红柚果肉颗粒饱满,口感酸甜可口。红柚原产南非、以色列、台湾。关于黄金柚子黄金蜜柚别名黄金柚子。黄金蜜柚生长快,种植2年后即可挂果,5年后即可丰产,盛果期单株产量达200斤以上,柚果每年7月份成熟。

  • ubuntu系统好处(让老电脑焕发青春)

    为了体验UbuntuKylin,我的老本子ACERASPIRE4710G经历了各种折腾。先是升级CPU在无电池情况下冒死刷bios;终于用上UbuntuKylin了,又体验了一把用gparted扩大根目录空间;又遇到了新问题:麒麟移动运行环境一直无法连接软件源,无法使用,AndroidAPP还是体验不了!既然是折腾,那就再安装一个Android操作系统!但成功安装是目的。PhoenixOS既然与RemixOS都是基于Android的系统,镜像文件结构也相同,借鉴前文的思路,用直接复制文件的方法进行安装。