多线程的学习目标
上天:
-锻炼解决问题技能
-高并发 缓存 大流量 大数据量
入地:
-面试
-JVM OS算法 线程 IO
概念:
什么是线程?
-每一个进程中有不同的执行路径(简单来说,线程就是进程的最小执行单元)
如何创建一个线程?有几种创建的手段?
【1】, 一个类继承Thread(extends Thread)并重写其中的 run 方法。
使用的时候,直接new出该类,调用类中的 run,这个就是一个线程了,但并未启动!
【2】,一个类实现了Runnable接口(implement Runnable),并重写了 run 方法
使用的时候,就需要new出一个线程出来,并且把 new 该类为对象作为参数,就可 start 启动了。
【3】,使用Lambda表达式创建:
new Thread(()-> {
System.out.println("");
}).start();
【注意】:创建对象了直接调用了 run 方法,在这个时候,已经有一个线程了。
如果还需要调用start启动线程的话,就有两个线程同时交替执行了。
线程的常用方法:
1. 睡眠:sleep
当前线程暂停一会儿,让别的线程运行一段时间
2. 谦让:yield
当前线程退出一下,离开CPU一段时间,回到等待队列里,先让别的线程执行。
3. 加入:join
当前线程 A 调用该方法,请别的线程 B 加入到该线程中执行,B 执行完了,才到 A 线程执行!
线程的状态:
-new 线程的创建
-start 启动,就会被线程调度器执行,之后就进入了整个是Runnable的状态(就绪状态)
Blocked,阻塞状态,等待进入同步代码块的锁:
-synchronize,没有获得锁的状态。获得锁的状态就进入 了就绪状态Ready
TimedWaiting,等待状态:
调用Thread.sleep(time),o.wait(time),t.join(time),LockSupport.parkNanos(),LockSupport.parkUntil() 方法进入
时间一旦结束,就自动关闭。
Waiting,等待状态:
调用o.wait(),t.join(),LockSupport.park(),方法进入等待状态
调用o.notify(),o.notifyAll(),LockSupport.unpark() 方法回到了、Runnable状态
注意:stop,suspend和resume等方法容易引起死锁问题,故而启用!
线程同步:
-synchronized
-锁的是对象,不是代码
锁的对象,对象的某两位来指定什么样的类型的锁,还记录了当前线程
-this.XX.class
-锁定方法,非锁定方法 同时执行
锁的竞争:
假设有两个线程在竞争,竞争时线程内部的线程栈的空间生成一个 LR(lockrecord) 的空间,两个线程以自旋的方式竞争,想发设法的获取到锁。
如果线程 A 获得锁了,会在该线程的内存栈上标记这把锁!B 线程继续以自旋的方式继续等待 A 线程释放锁!
锁升级:
-偏向锁(一般指的是第一次拿到这把锁的线程,这个对象是这个线程独有):线程来了之后,不记录线程。
该线程下次来了之后。就不用考虑加锁了,直接就可以访问 了,因此效率高一点。前提是没有别的线程来竞争!!!
-自旋锁(无锁):在有新的线程来了之后,发现有线程在执行,锁被占用了,就会陷入等待时间。
自旋锁和轻量级锁的区别:
先说自旋锁,自旋锁不是真正的锁,他只是一种概念,也就是再竞争锁资源时,如果没拿到, 那我就再次尝试获取这个锁资源,你可以理解为while循环下,频繁的尝试获取锁。
而synchronized的轻量级锁就是基于自旋锁实现的,但是他用了优化过的自旋锁,也就是自适应自旋锁,每次自旋多久,是根据上次获取锁资源的情况来决定的,如果上次成功,这次就多自旋一会,如果上次失败,这次就少自旋一会,这个意思。
简单可以理解为,轻量级锁是基于自旋锁实现的,而自旋锁不是锁,是乐观锁的一种体现,底层就是CAS操作。
-重量级锁(这个是交给CPU执行的):如果在在自旋状态下超过了一定的时间或者线程数(一般是10次)。一般都是线程太多了,
那它们就不会占用CPU的资源了,就会进入重量级锁,进入等待队列(不消耗CPU)。
轻度竞争,只会在用户态内部运行,偏向锁 ~ 轻量级锁 的过程不需要跟操作系统打交道。
一旦升级为重量级锁,那么这个就交给操作系统来处理了。
锁重入:
如果A方法调用 对象锁O,方法B调用A方法,那么方法B就是可重复锁。重入的次数必须记录,因为要对应解锁几次
偏向锁 自旋锁 ~ 线程栈 ~ LR+ 1
重量级锁 ~ ObjectMonitor字段上
Volatile关键字
易失行性的,不稳定的
保证线程可见性:
堆内存是所有线程的公共部分(共享变量),线程中也有相应的内存,有两个线程在访问同一个共享变量,当线程 A 在对共享变量的值进行改变的时候,
B 线程也在做修改,当 B 修改成功了。A 线程是不知道的,这个就是线程的不可见。简单一句话:线程 A 什么时候修改值,B 线程是不知道的。
MESI:缓存一致性协议。在Java中线程之间的可见性是需要靠缓存一致性协议来执行的。归根结底是需要靠硬件来实现的
禁止指令重排序:
这个是在超高的并发情况下才会有可能出现的
指令重排序:这个是跟现代的 cup 有关系。一般的,现在的 cup 指令都是并排的执行的。简单的说,就是代码的执行顺序可能不一样(不一定的按照从上到下执行)
Compare And Swap(无锁优化,自旋,也叫乐观锁)
一,什么是原子操作?如何实现原子操作
1,synchronized可以完成原子操作,他是给予阻塞的锁的机制,但是有问题:
如果被阻塞的线程优先级很高怎么办?
拿到锁的线程一直不释放锁怎么办?
有大量线程进行竞争,消耗cpu。还容易出现死锁
锁的粒度比较大,影响性能。
二,CAS的原理(Compare And Swap:比较交换)
从指令级别保证这是一个原子操作。
每个CAS都包含三个运算符:
一个内存地址V
一个期望的值A
一个新值B
基本思路:
如果在内存地址V上进行操作,如果说这个地址上存放的值就是我期望的值A。就给地址V赋给新值B.
如果内存地址上不是我期待的A值,那就什么都不做。
在循环(这里用的是死循环,不带条件的一直在那里循环。又叫:自旋)里不断的进行CAS操作
利用了现代操作系统都支持CAS的指令,循环这个指令,直到成功为止。
其实是不再语言层面进行处理,而是把它交给了cpu和内存去实现,利用cpu的多处理能力,在硬件层面实现多线程安全。
三,CAS的问题
1,ABA问题
问题描述:
第一个线程拿到内存地址上的值。此时第二个线程也拿到内存地址上的值,然后修改成B,然后又改回去。
在第二个线程完成前面一顿操作之后,第一个线程才开始比较内存地址上的是否和A相等。那么比较的结果
肯定是相同的啦。第二个线程,修改过值,又修改回去。但是对于第二个线程的那些操作,第一个线程却不知道,这是有风险的。
怎么解决:引入版本号,每次变化,版本号都变化。这样每次数据变化都可以感知到
2,开销问题
在死循环里不断的进行操作,如果这个操作长期不成功,浪费cpu资源
3,只能保证一个共享变量的原子操作
一个内存地址只能指向一个变量,当有多个共享变量时,就无法保证操作的原子性。
那么如果有多个共享变量呢?我们可以把多个共享变量变成一个共享变量,比如把多个变量封装到一个类中。
四,JDK中相关原子操作类的使用
1,更新基本类型:
AtomicBoolean,AtomicInteger,AtomicLong
2,更新数组类:
AtomicInterArray,AtomicLongArray,AtomicReferenceArray
3,更新引用类型:
AtomicReference,AtomicMarkableReference,AtomicStampedReference
4,原子更新字段类:
AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
ABA问题
原理:
如果是基础类型的,其实也就无所谓了。如果是引用类型,简单的理解:你的女朋友和你分手,在和你复合之前,你经历了别的女人。
解决:
其实这个ABA问题,问得不多,加个version版本就可以解决了。 、
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统