Java多线程
2025-04-21 11:18:55 0 举报
AI智能生成
自己总结 可能不全
作者其他创作
大纲/内容
Java线程模型
概念
因为Java字节码运行在JVM中,而JVM运行在各个操作系统上,<br>所以当JVM想要进行线程创建和回收的这种操作时,是必须要调用操作系统的相关接口,<br>也就是说JVM线程与操作系统线程之间存在着某种映射关系。<br>这两种不同维度的线程之间的规范和协议呢,就是线程模型<br><br>
模型分类
一对一
用户线程与内核线程建立了一对一的关系<br>缺点:如果用户线程阻塞会直接反映到操作系统上 导致内核状态频繁切换降低性能
多对一
多个用户线程映射到一个内核线程上 用户线程的调度是用户空间来完成的<br>缺点:如果一个用户线程进行了内核的调用并阻塞,其他线程都无法进行内核调用,Java早期就用了这种模型
多对多
用户模型和内核线程是多对对
名词解说
线程主要分两类<br> 内核线程,简称KLT(Kernel Level Thread)<br> 用户线程,简称ULT(User Level Thread)<br><br>
java锁
锁是怎么设计的
了解锁的第一步先了解jvm内存模型
1.红色框为线程私有的 是线程安全的 不用考虑<br>2.蓝色框 Java堆(主要存的对象) 和方法区(主要存类信息,常量和静态变量等)都是线程共享的<br> 所以要考虑数据安全问题
代码层面是如何实现的
每个object都拥有一把锁,这把锁存在对象头中,锁中记录了当前对象被那一个线程所占用<br>也就是这个锁记录在对象头的mark work中
对象头结构
首先看一下对象结构
1.对象头主要存放的是运行时的信息 <br>2.实例数据存放的主要是 对象的属性 方法
对象头包含了两个部分: mark work class point<br> 其中class point就是一个指针指向了当前对象 类型所在方法区中的类型数据<br> mark word存储了运行时的数据 例如:锁标志信息 hashcode 指向锁记录的指针等
synchronized
工作流程
1.entry set中聚集了很多想要进入monitor的线程 他们处于waiting状态<br>2.例如A线程进入了monitor 那他的状态就是active状态 假如A线程经过了一个判断 需要让出执行全 它会被放到wait set中等待<br> 状态标记成waiting 此时B线程成功进入monitor 并且执行完成后 可以手动唤醒 (notify)wait set中的A线程 来继续执行<br>synchronized被编译后会生成monitorenter monitorexit 两个字节码对业务代码进行包裹<br>指令来进行线程同步<br>缺点:可能存在性能问题 java线程实际上是操作系统的映射 所以每次挂起或者唤醒是比较费时间的<br>
使用
修饰静态方法:锁住当前 class,作用于该 class 的所有实例<br>修饰非静态方法:只会锁住当前 class 的实例<br>修饰代码块:该方法接受一个对象作为参数,锁住的即该对象<br>
锁的升级
无锁
1.无竞争 所有线程都能访问到同一个资源<br>2.存在竞争 但是想使用无锁的方式同步线程 就是不通过锁定资源的方式来保证现成安全同步 可使用CAS
偏向锁
一段同步代码,一直被A线程访问没有其他的线程来竞争 <br>这种情况下就会进入偏向锁的状态,让对象认知这个线程 对象直接把所得使用权交给它<br>也就是说偏向锁只有一次CAS操作<br><font color="#f44336">偏向锁是如何实现的:1.</font>查看Mark Word中偏向锁的标识以及锁标志位,若是否为偏向锁为1(倒数第三个数字),并且锁标志位为01(最后倒数两位是0 v 1),则该锁为可偏向状态。<br> 2.若该锁为可偏向状态,判断Mark Word中的线程ID(前23个bit)与当前线程ID是否相等,如果相同,则直接执行同步代码,否则通 过CAS操作竞争锁。<br> 3.如果竞争成功,将Mark Word中线程ID设置为当前线程ID,然后执行同步代码。<br> 4.如果竞争失败,说明有其他线程竞争。持有偏向锁状态的线程在没有字节码正在执行的情况下释放锁,然后恢复到未锁定状态或者膨 胀为轻量级锁。<font color="#f44336"><br><br></font> 释放偏向锁的过程: <font color="#f44336">只有遇到其他线程尝试竞争偏向锁时,持有偏向锁状态的线程才会释放锁。<br> 持有持有偏向锁的线程需要等到所有的同步任务执行完成之后(即没有字节码正在执行),才会暂停持有偏向锁的线程,然后恢复到未锁 定状态或者膨胀为轻量级锁</font>。<font color="#f44336"><br></font>
轻量级锁
当有两个线程开始竞争这个锁对象,情况发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先占有锁对象,锁对象的 Mark Word 就指向哪个线程的栈帧中的锁记录<br><br><br>当一个线程想要获取一个对象的锁时 判断锁标志位如果是00的话 则认为是轻量级锁<br>这时线程会在虚拟机开辟一块叫做 lock record的空间 来存储mark word的副本 和owner指针<br>线程通过CAS尝试获取锁 如果获取成功则复制 对象头中的mark work到lock record中 并且将要owner指针指向该对象<br>对象中的前30个bit 会生成一个指针 指向线程虚拟机中的lock record 这样就实现了对象锁和线程的绑定<br>然后获取了这个线程的对象就会去执行一些任务,如果有其他线程想要获取该对象的话 只能是自旋等待<br><br>也就是自旋超过阈值了就会变成重量级锁
重量级锁(互斥锁)
重量级锁
轻量级锁自旋超过阈值的时候 就升级成了重量级锁 重量级锁主要是就是monitor来控制线程
拓展CAS
什么是栈帧
自旋
在自旋状态下,当一个线程A尝试进入同步代码块,但是当前的锁已经被线程B占有时,线程A不进入阻塞状态,而是不停的空转,等待线程B释放锁。如果锁的线程能在很短时间内释放资源,那么等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,只需自旋,等持有锁的线程释放后即可立即获取锁,避免了用户线程和内核的切换消耗。<br>
适应性自旋
由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:<br>如果在同一个锁对象上,自旋等待之前成功获得过的锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,因此允许自旋等待持续相对更长的时间。<br>相反的,如果对于某个锁,自旋很少成功获得过,那么以后要获取这个锁时将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。<br>自适应自旋解决的是“锁竞争时间不确定”的问题。JVM很难感知确切的锁竞争时间,而交给用户分析就违反了JVM的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定。因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。<br>
CAS
<font color="#f44336">CAS是cpu指令级的原子操作 unsafe提供了CAS方法直接通过native方式调用了CPU指令cmpxchg(所以它没有具体的实现方法 而是底层调用了c) 这一条指令具备原子性 所以不会造成数据不一致的问题</font><br>什么他么的是原子性:不可再分割的最小操作 在执行完毕之前不会受到其他任务或事件影响而中断的操作<br>什么是偏移量(offset):例如 atomicinteger中的value就是偏移量 就是对象地址+value地址就是偏移量<br><br><font color="#4d4d4d">CAS就是Compare and Swap,是项</font><a href="https://so.csdn.net/so/search?q=%E4%B9%90%E8%A7%82%E9%94%81&spm=1001.2101.3001.7020" target="_blank" class="hl hl-1">乐观锁</a><font color="#4d4d4d">技术。他是一种系统</font><font color="#444444">原语 从cpu层面保证他的原子性 具体实现是 Unsafe类+自旋</font><font color="#4d4d4d"><br></font>通过Unsafe类提供硬件级别的原子性操作保证了并发安全,unsafe中的cas方法(<font color="#444444">compareAndSwapObject,</font>compareAndSwapInt等)都是native修饰的 也就是说Java代码不负责具体的去实现他 而是交给jvm或者更底层<font color="#444444"><br></font><font color="#ff0000"><br></font>CAS机制中使用了3个基本操作数:内存地址V(内存变量多线程共享可见),旧的预期值A,要修改的新值B。<br> 举例:1.目前内存变量是v=10 线程A进来后想把值改成11 也就是B=11 A=10旧的值 <br> 2.在线程提交之前线程B抢先一步 将v更新了成了11 v=11 B=12 a=11 现成a提交的时候发现v!=a 所以提交失败<br><font color="#4d4d4d"> 3.线程A提交失败后,知道自己的A不是最新的值,于是把A变成最新的V值11,B变成12,V还是11。再次提交,如果没有别的线 程又修改了V的值,那么就提交成功,V的值变为12,A的值变成12,B的值变成13。否则,重复最开始的步骤。<br></font><br>CAS中会出现的问题:<br>ABA问题:线程A 从内存M中取出v1 B也取出v1 B线程操作后将要v1 变成了v2 但是A线程发现M位置的数据让然是v1 然后A操作成功 这个v1是B线程操作后的 并不是之前的v1 <br> ABA问题解决:1.使用AtomicStampedReference来解决 增加了stamp印戳标记,AtomicStampedReference得compareAndSet()方法先检查当前对象的引 用 值是否等于预期值,并且当前印戳标志是否等于预期标志 如果都相等可以赋 值<br> 2.使用AtomicMarkableReference 不关心修改几次 只关系是否修改过 增加了mark属性(boolean类型)标记是否修改过<br><br>2.只能保证一个共享变量之间的原子性操作 如果想保证多个共享变量那就把这多个共享变量合并成一个共享变量 <br>3.cas自旋空转开销很大 解决办法:1.分散热点 使用loagadder 代替AtomicLong longAdder将单个CAS值 分散到cells数组中<br> 2.使用队列消峰 将cas征用的线程加入队列中排队 降低cas竞争激烈程度 比如aqs<br><br><br>
Java高并发三大特性
原子性
不可中断的一个或者一系列操作,这种操作一旦开始就一直到结束中间不会有任何线程切换
有序性
是指代码执行的先后顺序
可见性
volatile关键字可以实现 这个关键字可以保证可见性 和有序性(因为禁止指令重排序)<br> <font color="#f44336">每一个线程都有自己的工作内存,当线程使用变量的时候 会把主内存的变量复制到工作内存中,读写操作都是在工作内存中的变量副本,操作完成后 将工 作内存的数据刷回主线程,保证线程的可见性 </font><br><br> 有点逼格的原理概述:当一个变量被volatile修饰的时候 汇编后 会在操作变量前多了一个lock addl指令<br> 这个指令有三个意思:1 将当前cpu缓存的数据立即写会系统内存<br> 2.会引起其他cpu缓存该地址数据无效<br> 3.禁止指令重排序(也就是保证了有序性 valatile是如何保证有序性的:在volatile读操作插入一个 loadload屏障 在 写操作前后插入一个storestore屏障 )
指令重排序
cpu为了提高运行效率,编译器和cpu常常会对指令进行重排序 它不保证各个语句执行顺序和代码中的先后顺序一致,但是它最终保证程序的执行结果和代码 结果是一致的 <br> as-if-serial 编译器和cpu指令重排序都要遵守as-if-serial规则 as-if-serial:无论如何重排序,都必须保证代码在单 线程下运行正确,不会对存在数据依赖进行重排序 例如:int a = 1 ; int b = 2; i nt c=a+b; c和a c和b都有数据依赖关系 在指令重 排序c不能排到a和b的前面 可能会改变程序的结构 但是a和b可以重排序<br>Java中禁止指令重排序的有 volatile synchronized final等
JMM(内存模型)
概念
jmm定义了一组规范,一个线程对共享变量写入时 如何确保对另外一个线程是可见的 实际上jmm提供了河里的禁用缓存以及禁止重排序,所以其核心价值在于解决了可见性和有序性<br>拼比各种已硬件和操作系统访问差异保证Java程序在各种平台对内存访问最终一致<br>jmm的两个概念:1.主存 2.工作内存<br>jmm的规定:1.所有变量储存在主存中 2. 每个线程都有自己的工作内存,对变量的操作都是在工作内存中进行的<br> 3.不同线程之间无法访问彼此的工作内存,要想访问只能通过主存来传递<br>jmm的8个操作:
Java显示锁
LOCK(锁)接口
线程
线程状态
<font color="#e65100">1.新建(new)现成已经新建,但是没屌用start()启动线程<br>2.就绪:调用了start()方法就是就绪状态,(sleep(),join yieid等方法调用后也是就绪状态)<br>3.运行(runnable):获得cpu时间片 开始执行run方法中的代码块(就绪状态和运行状态统称为runnable状态)<br>4.阻塞(blocked)阻塞状态不会占用cpu(1)线程等待获取锁 (2)io阻塞 sleep join yieid wait(等都可以阻塞)<br>5.等待(waiting)不会被分配cpu时间片 需要被其他线程显示的唤醒 才会进入就绪状态 object.wait()等待 唤醒方式object.notify() huozhe notifyall()<br>6.限时等待(timetwaiting)例如 sleep(1000) 时间到后自动唤醒<br>7.结束:线程执行完或者发生异常没处理的就是死亡状态</font>
线程操作
1.sleep 休眠: 执行状态变成限时阻塞状态,当线程休眠时间到后 线程不一定会立刻执行 ,因为cpu可能在执行其他线程 线程会进入就绪状态<br>2.interrupt 中断: 如果要中断处于阻塞的线程(object.wait(),Thread.join(),Thread.sleep() 都属于阻塞)则会抛出异常<br> 如果要中断正常运行中的线程则线程不受任何影响 仅仅是线程中断标记设置成了true 通过isinterrupted()可查看线程中断状态<br>3.join 合并: <font color="#7b1fa2">当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行<br> 例如:有 t1 t2 t3 三线程启动 此时有4个线程 main t1 t2 t3 在t1.join的时候 main线程主动让出cpu(阻塞 因为调用了wait方法) 等待t1 线程执行完成后 如果t2也调用了join方法则main线程还是阻塞 等待t2执行完成后在执行 以此类推 等到所有join方法执行完成后在 调用唤醒方法 让main线程进入到就绪状态<br></font>4.yieid让步: 让目前执行的线程放弃cpu的执行权 不会阻塞线程 只是让线程进入就绪状态 然后系统线程调度重启调度<br>5.daemon守护线程:守护线程也称之为后台线程 例如JVM中的GC线程就是守护线程 setDaemon(true)设置守护线程(必须在启动前设置)<br><br>
start()和run()
start()用于启动线程,<br>run()是线程启动后执行的代码入口
thread和runnable
区别:1.使用thread开启线程 可以在子类中直接调用父类的方法(例如线程名称:属性等)<br> 2.(1)使用runnable如果想拿到当前线程的属性方法 则必须先拿到线程的实例(Thread.currentThread())<br> (2)使用runnable创建的类并不是线程类 而是线程类执行的target的目标类 需要将实力传入线程类<br> 也就是(Thread)的构造器才能创建并且执行线程<br> (3)runnable可以更好实现多个线程并发访问的完成同一个任务或资源<br><font color="#f44336">thread和runnable都不能支持返回值</font><br>
callable和futureTask
<font color="#f44336">问题:Callable能否和runnable一样,作为Thread线程的实例的target来执行?<br> 不行 因为Thread中的Target属性为runnable callable接口和runnable并<br> 没有任何继承关系</font>
线程之间的通信
等待-通知模式
<font color="#d32f2f">1.object.wait()(等待),object.notify()唤醒 现成等待和唤醒 需要统一对象锁来开启 要不然不知道是哪个等待或者唤醒哪个<br></font><font color="#f44336"> 使用该方法的时候 必须在拥有对象的同步锁情况下进行</font><br> wati()流程:1.调用改方法后将该线程加入 waitset集中 等待被唤醒<br> 2.释放当前owner权利 让当前线程进入waiting<br> notify() 1.唤醒waitset()中第一条等待的线程 如果是notifyAll()会唤醒所有的等待线程<br> 2.唤醒的线程会从waitSet()移动到EntryList中 让线程具有抢夺owner的权利 线程的状态从 waiting 变成blocked<br> 3.entryList中的线程抢夺到owner的权利后 线程的状态从blocked变成runnable
ThreadPoolExecutor线程池
核心参数
1.int corePoolSize 线程核心数量 也就是最少线程数量<br>2.int maxmumPoolSize 最大线程数量 线程数量大于corepoolSize 小于maximunpoolSize并且阻塞队列已满时才会创建新线程<br>3.long keepAliveTime 空闲线程存活时间 <br>3.阻塞对垒BlockingQUeue<Runnable> <br>4.新线程产生方式ThreadFactory <br>5.拒绝策略 RejectedExecutionHanler<br> (1)<font color="#f44336">AbortPolicy 拒绝策略</font> 线程池队列满了 新任务就会被拒绝 并且抛出异常 也是线程池默认策略<br> (2)<font color="#f44336">DiscardPolicy 抛弃策略</font> 线程池队列满 新任务会被抛弃 不会抛出异常<br> (3)<font color="#f44336">DiscardOldestPolicy 抛弃最老任务</font> 现成队列满了 就会将最早进入队列的任务抛弃<br> (4)<font color="#f44336">CallerRunsPolicy 执行者策略</font> 新任务添加时 如果添加失败 那么提交任务线程会自己去执行该任务 不在线程池中执行<br><br>如何设置核心线程数量<br> 1.IO密集任务 io操作的多 一般是核心cpu核心数的两倍<br> 2.CPU密集任务 主要是大量的计算 等于cpu的核心数 这样才效率高 不频繁切换上下文<br> 3.混合型任务 最佳线程数 = ((线程等待时间+线程CPU时间)/线程cpu时间)+CPU核数<br><br><br>
并发
ThreadLocal
线程本地副本 1,主要作用线程隔离,夸函数传递 能够实现每个线程都有一份变量的本地值,每个线程都有自己的独立ThreadlocaMap空间 <br> key为threadlocal实例 value 为保存的值 也就是threadloca中使用map保存了不同线程的值而已 然后根据不同线程来取不同的值<br><br>会存在内存泄露情况 threadLoca中的key是弱引用 在gc回收的时候key被回收 但是value不会回收 存储的数据多了就容易内存泄露 解决方法就是调用remove()方法<br><br>
ThreadLocal面试问题
我觉得这个文章面试总结的挺他么的到位 nice
面试可能会问的问题
1. <font color="#f44336">i++ 不是线程安全操作</font> <font color="#2196f3">因为它是一个复合操作 其中包含三个jvm指令:内存取值,寄存器增加1,存值到内存(这三个都是原子性操作,但是两个以上的院 原子操作一起进行的就不是原子操作了)</font><br><font color="#2196f3"> 这三个指令都是独立运行的 中间完全可能会出现多个线程并发执行 例如:4个线程同时读取到count=1 并且都进行自增操作</font><br><font color="#2196f3"> 然后存到内存 结果count=2 并不是count=4</font><br><br><font color="#9c27b0">2.object.wait()和notify()</font><font color="#006064">方法为什么要在synchronized同步代码块中使用<br> wait因为jvm释放当前对象锁监视器的owner(使用权) jvm会将当前线程移入到WaitSet()队列 这些操作都是和对象监控器相关的<br> notify jvm对象锁的waitset() 移动到entrylist也都是跟对象锁监视器有关的<br> 什么是对象监视器:例如 : Object obj= new Object() synchronized(obj){} 这个obj就是对象监视器<br></font><font color="#9c27b0"><br></font>3.为什么Java局部变量 方法参数不存在内存可见性问题 在Java中 所有的局部变量和方法参数都不会线程共享 所以也不存在内存可见性问题 所有的object 实例 class实例和数组都存在jvm 堆内存 堆内存存在线程共享 所以有线程可见性问题 <br><br><br><br>
0 条评论
下一页