Skip to content

Java多线程

更新: 5/9/2025 字数: 0 字 时长: 0 分钟

预备知识

美团技术团队——基本功 | 一文讲清多线程和多线程同步

首先我们要知道我们的一个Java应用就是一个进程,而一个进程中可以存在多个线程,线程之间公用进程的部分内容(堆,常量区等)。

进程的运行会带给我们一种多个线程在同时运行的错觉,实际上后台是使用时间片轮转法来进行的多线程运行,即将一段时间切分为多个细小的事件片,然后为每个线程公平的分配时间片,然后线程依次运行。

如何创建一个线程

本质上Java只能通过new Thread().start()的形式创建线程。

Thread函数接受一个实现了Runable接口的方法,亦或我们可以直接通过lambda表达式传入一个方法。

生命周期

Java存在六种生命周期

  • NEW:初始状态,尚未调用start方法。
  • RUNNABLE:运行中,调用了start等待运行的状态。
  • BLOCKED:阻塞状态,有锁占用。
  • WAITTING:等待状态,等待另一个线程结束(Object.wait()/Object.notify())或通知停止等待(Thread.join())。
  • TIME_WAITTING:超时等待状态,在等待状态的基础上设定了一个最高等待时间(在上面的方法里传入一个long型参数)。
  • TERMINATED:终止状态,即线程结束

操作系统的线程中有RUNNING和READY两个过程,即JAVA中的RUNNABLE,Java之所以将两个分类合并可能是因为在时间片轮转法中每个时间片的时间过短,没有必要进行区分

为什么Object是wait而Thread是sleep

因为wait这一操作由对象锁实现,而Thread的sleep并未有这一操作。

为什么不能直接调用Thread的run方法而是要用start?

run会将Thread要执行的内容在当前线程执行。

并发VS并行

并发:时间片轮转,要求同一段时间内同时进行

并行:严格的同一时刻进行

同步VS异步

同步:严格的上下顺序

异步:部分内容只发出调用,不考虑后续结果

为什么要使用多线程

多线程可以使得将一部分用户不可见且不需要严格按顺序执行的内容单独放出来执行(比如同步缓存),这一过程可以加速用户得到反馈,提高用户体验。

单核CPU上多线程并不一定能够效率

搜先我们将一个线程的工作分为CPU密集型和IO密集型

  • CPU密集型:主要为计算与逻辑操作,大量消耗CPU资源,CPU密集型在时间片轮转的过程中线程频繁的切换会导致系统开销增加,效率降低
  • IO密集型:主要为读写文件,网络通信,不太暂用CPU资源,IO密集型多个线程同时运行可以利用 CPU 在等待 IO 时的空闲时间,提高了效率

死锁

通俗的讲就是A调用B并等待B执行完,而B若想执行完必须A执行完,进而导致互相锁死。

比如A拿到一个锁,然后调用B,等待B执行完成,但B若想执行,必须拿到A持有的锁。

volatile关键字

volatile关键字主要的作用就两个

  1. 可见性:保证每一次的读取都从主存中读取,不经过线程的高速缓冲区,进而保证每个线程都对该变量可见。
  2. 禁止重排:JVM有时为了效率会重排我们的代码,在单线程情况下这种行为往往是安全的,但涉及到多线程,这种情况的就可能不安全了

很多小伙伴初学多线程的时候肯定会被这个名词吓到,我们慢慢来看。

synchronized

synchronized关键字是我们最常见的加锁形式,他会先去检查一个方法/类有没有上锁

  • 如果没有,则会上锁,然后执行后续指令。
  • 如果有,则等待别人释放锁,然后执行后续指令

那我们一般选择什么来作为被锁的东西呢(选什么作为锁),选择具有唯一性的东西作为锁。

即锁对象为地址不可变的东西,因为锁本身是去比对的地址。

后面会深入的去聊synchronized

常见的具有唯一性的

  • Object.class:Object的类锁
  • this:很少会有操作能该边只身所在的对象的地址

悲观锁VS乐观锁

这两个名词看着也很唬人,别慌。

悲观锁

其实就是synchronized的实现方式(ReentrantLock也是,就是一般提到的锁),会让其他请求锁的线程阻塞,但高并发场景下会产生大量的阻塞线程会增加系统开销,且悲观锁存在死锁的可能性。

乐观锁

为了解决上面的问题,所以有了乐观锁,乐观锁不去阻塞线程,而是去查看要修改的资源是否被别的线程修改过了。

版本号和CAS是两种实现乐观锁的方式。

版本号

比如在设计数据库字段的时候设计一个version作为版本号,每次修改的时候先select一次,然后update的时候带上version作为where的判断条件

sql
select id,name,version from data_base where id=1;
update data_base set name=#{newName},version=#{newVersion} where id=1 and version=#{oldVersion};

CAS

CAS则是希望在进行操作时除了传入一个该边后的值(N:NEW)外,还希望传入一个预期中这个值现在理应是的值(E:Expected)用于与被该边的值(V:VAR)进行比较。

然后操作时进行下列原子操作

  • V与E比较,不相等说明被修改过,放弃更新,CAS失败
  • V与E比较,相等说明未被修改过,将V修改为E

ABA问题

CAS存在ABA问题,即V可能由a转变为b再转变为a,这时就不能保证我们的数据始终未被修改过了。

解决方案就是在CAS的基础上加上版本号或时间戳,然后在比对的过程中针对版本号再进行一次比对。

synchronized关键字

历史故事

synchronized是Java用来声明悲观锁的方式,在Java6之前,synchronized属于重量级锁,效率低下,知道Java6时,引入了轻量级锁,自旋锁等技术,才优化了他的效率

因此不要有种synchronized不能用的想法

锁啥?

  • 类锁(实例和class类)
  • 方法锁(静态方法获取实例锁,实例方法获取实例的锁)

上锁时建议注意唯一性,即锁的类地址不应该发生变化(因此String类的实例化不建议作为锁对象,因为缓冲池的原因)

构造方法不可以被synchronized修饰

底层原理(又到了我们最喜欢的JVM时间)

synchronized本身是基于一个montior(监视器)实现的,监视器由c++实现(ObjectMonitor),每一个对象中都放有一个ObjectMonitor对象。

类锁

在字节码中,锁的起始代码部分使用了monitorenter指令,该指令对monitor的计数器进行检查,若为0则置为1表示代表锁已被获取。

而锁的结尾部分使用了monitorexit指令,该指令将monitor的计数器置为0表示代表锁已被释放。

方法锁

方法锁使用的是flag:ACC_SYNCHRONIZED,JVM通过访问这个flag里是否存在ACC_SYNCHRONIZED来判断是否为同步方法,然后再去获取对应对象的monitor

synchronized的四种状态

无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态

他们是一次升级的关系,竞争越激烈升级越高,且为单向升级,不可降级

volatile vs synchronized

  • volatile 仅能保证可见性,只能针对变量修饰,而synchronized通过锁的行为可以同时保证原子性与可见性
  • volatile 更加轻量,故而性能更好

简单的讲,我们完全可以通过使用synchronized的形式来实现volatile的功能,但是没有必要

本站访客数 人次      本站总访问量