java并发编程

java并发编程

基础概念

并发:是在多个实体上的多个事件,是在一台处理器上同时处理多个任务,同一时刻是只有一件事情发生

并行:在多个实体上的多个事件,是多台处理器在同时处理多个任务,同一时刻大家都在做事情

进程:系统中的每一个应用程序就是一个进程,每个进程都有自己的内存空间和系统资源

线程:轻量级进程,每个进程内会拥有一或多个线程。是大多数操作系统进行时序调度的基本单元

管程:monitor(锁)是一种同步机制,保证同一时间只有一个线程可以访问被保护的对象,jvm中同步是基于进入和退出 监视器对象来实现的,每个对象都会有一个monitor

用户线程:默认都是用户线程,它是系统的工作线程,会完成这个程序需要的业务操作

守护线程:是一种特殊的线程,为其他的线程服务,在后台默默的完成一些系统性服务,比如垃圾回收线程就是最典型的例子。守护线程作为一个服务线程,没有服务对象就没有存在的必要了。加入系统只剩下守护线程,则jvm会自动退出//isDaemon

Callable:有返回值,能抛异常。

Thread:继承

Runnable:实现

CompletableFuture

future接口定义了操作异步任务执行的一些方法,如获取异步任务执行结果,取消任务的执行,判断任务是否被取消,判断任务是否执行完毕等futureTask

get阻塞

isDone导致轮训,cpu空转

CompletableFuture提供了一种观察者模式的机制,可以让任务执完成后通知监听的一方

CompletableStage接口代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另一个阶段

CompletableFuture类提供了Future的扩展

runAsync无返回值

supplyAsync有返回值

whenCompletable方法可以取得响应结果和异常

join==get:获取结果
thenapply:有参有返回
thenaccept:有参无返回
thenrun:无参无返回
now:没完成则返回value
complate:和now类似
handle:处理异常
applyToEither:那个快返回哪个结果,阻塞
thenCombine:合并结果

悲观锁

认为自己在使用数据时别的线程一定会来修改,因此会在使用前先加锁,防止别的线程修改。如synchronized,lock的实现类

适合多写操作的场景

乐观锁

认为自己在使用数据时别的线程不回来修改,所以不会加锁。只是在更新数据时判断一下数据有没有被更新,没更新则写入,更新了则根据需求,放弃修改或者重试抢锁。判断规则:版本号,cas

适合读操作多的场景

对象锁

普通同步方法

类锁

静态同步方法

synchronized

重量级锁

非公平锁

悲观锁

隐式锁

自动锁:

​ 所以不用担心死锁。在锁代码块是会在源码中释放两次,一次普通释放,一次异常释放;

​ 在锁对象时,会添加ACC_SYNCHRONIZED标志,在调用时有标志则先获取锁在调用同步方法。然后在完成时释放,无论正常还是异常。

​ 在锁类时,会根据static添加ACC_STATIC标志,判断为类锁

可重入锁:当线程已经获取到资源的时,再次获取资源不需要继续获取锁就可使用

每个锁对象拥有一个锁 计数器和一个指向持有该锁的线程的指针

管程:监视器,是一种程序结构结构内的多个子程序形成多个工作线程互斥访问共享资源

公平锁与非公平锁:在多线程强一份资源时,为了避免线程乱序和挤压,保证数据的一致性和安全性,在使资源前必须先获得锁,各个县城获有锁的几率是不同的

非公平锁能够更充分的利用cpu的时间片。线程的上下文切换次数会比较少,也就浪费的开销少,性能快

排查死锁:jconsole

LockSupport与线程中断

一个线程不应该有其他的线程来中断或停止,

interrupt一种中断协商机制

interrupt()设置中断状态为true,不会立即停止线程

阻塞状态设置中断状态会导致状态清空,并且抛出一个异常

不活动的线程状态为false

Thread.interrupted()判断线程是否被中断并且清除中断状态

isInterrupted判断线程是否中断

通过volatile或atomic修饰属性来实现

locksupport用来创建锁和其他同步类的基本县城阻塞源语

线程等待和唤醒的三种方法:

  1. Object包中的wait和notify//必须加锁才能使用,通知方法提前执行,则无法被唤醒
  2. Condition的await和signal//同上
  3. Locksupport类的阻塞和唤醒//没有锁块要求,通过许可证来实现唤醒:调用unpart获取当前线程的许可证,有且只能拥有一个,不能累加。哪怕是提前唤醒了,被唤醒的线程在等待前就拥有了许可证,在等待时直接被唤醒

JMM

java内存模型

cpu的计算速度远远高于内存,所以cpu是先把内存中的数据读到缓存里,而内存的读和写操作的事就不会造成不一致的问题。

java定义了一种内=内存模型,来屏蔽调各种硬件和操作系统的内存访问差异,已实现让java程序在各种平台下都能达到一直的内存访问效果

  • 可见性:当一个线程修改了共享变量的值,其它线程能够立即知道//线程把共享变量写入主内存//线程拥有私有内存,内次修改从主内存中取数据,修改完后写入到主内存。

  • 原子性:脏读

  • 有序性:禁止重排序。为什么重排序?jvm能根据处理器特性适当的对机器指令进行重排序,使机器指令更符合cpu的之行特性,最大限度的发挥cpu。但是他只能保证串行语义一致,并没有保证多线程中语义一致。处理器在进行重排序是考虑的是数据依赖性

Volatile

保证可见性当写操作时,立即将本地内存中的共享变量刷新到主存中。读的时候jmm会将本地的内存设为无效,重新回到内存中读最新共享变量

有序性:内存屏障

内存屏障:在指令序列中就像一堵墙一样使两侧的代码无法穿过,

内存屏障在锁中的作用:

  1. 获取锁
  2. 加载屏障
  3. 获取屏障
  4. 临界区
  5. 释放屏障
  6. 储存屏障

内存屏障分类:

​ 读屏障:在读命令之前插入读屏障,让工作内存或cpu高速缓存当中的缓存数据失效,重新回到主内存中获取新数据

​ 写屏障:在写指令之后插入写屏障,强制吧写缓冲区的数据同步到主内存中

许进不许出策略

CAS

比较并交换。它包含三个操作数,内存值,预期原值与更新值,匹配则更新,不匹配不做任何操作,多个线程同时执行cas操作只有一个会成功。拿内存值与预期原值比较,相等则修改,不等重试的行为被称为自旋。

CAS是jdk提供的非阻塞原子性操作,他通过硬件保证了比较-更新的原子性,整个cas是一条cpu的原子指令,不会造成数据不一致的问题

再执行cas指令时,会判断当前系统是否为多核线程,如果是就给总线加锁,只有一个线程对总监加锁成功,实际上是通过cpu独占实现的

unsafe是cas的核心,由于java无法访问底层系统,需要通过本地方法来访问,unsafe相当于一个后门

缺点:

如果修改失败,会一直尝试,时间过长会给cpu带来过大开销

会有aba的问题:公司的钱被挪用例子,添加版本号解决

AtomicStampedReference,版本号

AtomicMarkableReference一次性

AtomicInteger基本类型

AtomicLong

AtomicBoolean

AtomicIntegerArray

AtomicLongArray

AtomicReference

AtomicReferenceArray

AtomicIntegerFieldUpdate

AtomicLongFieldUpdate

AtomicReferenceFieldUpdate

DoubleAdder

DoubleAccumulator

LongAdder减少乐观锁的重试次数,只能用来计算加法,且从0计算

LongAccumulator提供了自定义的函数操作

striped64

分散热点,将value值分散到一个cell数组中,不同的线程会命中到不同的下标,这样就相当于卖票时开了多个窗口,将访问分散开来。在获取真正的总值时在将cell数组求和返回

ThreadLocal线程局部变量

ThreadLocal提供了线程局部变量,属于当前线程的独立空间,存放自己专属的本地变量, 从而避免了线程抢夺资源导致的安全问题。

initialValue()返回当前线程的初始值

对象的内存分布

在HotSpot虚拟机中,对象在堆内存中的存储布局可以划分为三个部分

  • 对象头
    • 对象标记:哈希码、GC标记、GC次数、同步锁标记、偏向锁持有者
    • 类元信息(又叫类型指针)实例对象的模板
    • 对象长度(数组对象独有)

实例数据:存放类属性的信息,包括父类的属性信息

对齐填充(保证8个字节的倍数)

一个空Object对象占16字节,mark word和类型指针分别占8字节(64bit)

Synchronized与锁升级

synchronized属于重量级锁,在线程竞态激烈的时候效率十分低下,因为监视器锁是依赖于底层的操作系统来实现的。为了减少获得锁和释放锁带来的性能消耗,引入了轻量级锁和偏向锁

jvm中的同步是基于进入和退出管程对象实现的。每个对象实例都会拥有一个管程他可以和对象以其创建和销毁

偏向锁

单线程竞争:当线程持有锁时,会记录偏向线程ID,修改偏向锁位,执行结束不会释放锁,如果没有其他线程来竞争,则一直持有锁,后续在使用锁时,则只需要检查偏向线程ID是不是自己的就可以

持有锁的线程在执行完同步代码块时不会释放锁,当第二个线程尝试获取锁,发现是偏向锁,会判断A线程是否存活,如果A线程仍然存活,将A暂停,偏向锁升级为轻量级锁,A继续执行,B自旋,若A不存在,B持有偏向锁,锁不升级

偏向锁的撤销

偏向锁是一种竞争才会出现释放锁的机制,撤销需要等待全局安全点(该节点没有字节码执行),同时检查偏向锁的线程是否还在执行

轻量级锁

轻量级锁是为了在线程近乎交替执行同步代码块时提高性能的,但是任意时刻最多只有一个线程竞争,等待时间极短,依然使用CAS自旋即可解决

如果线程A拿到锁,B来抢锁,这个时候是偏向锁。B在争抢的时候发现对象头中的Markword中的ID不是自己的,B会尝试CAS操作希望获得锁。如果成功,直接替换线程ID,该所仍为偏向锁。如果失败,偏向锁升级为轻量级锁,轻量级锁有A线程持有,B自旋。

锁消除

即时编译器会无视锁

锁粗化

代码中首尾相接,前后都是同一个锁对象,编译器会将多个synchronized代码块合为一个

AQS

AQS(AbstractQueuedSynchronizer)是Java中用于实现同步器(synchronizer)的抽象基类。它提供了一种框架,使得开发者可以方便地实现各种同步机制,如锁、信号量、倒计时门栓等。

AQS基于一种称为"CLH队列锁"的算法来管理线程的等待和唤醒。它通过一个FIFO(先进先出)的等待队列来管理等待线程,线程会按照一定的顺序排队等待获取锁或资源。

AQS的主要特点包括:

  • 状态管理:AQS内部维护一个状态(state)用于表示同步器的状态。开发者可以使用该状态来实现不同的同步语义。
  • 等待队列:AQS使用一个等待队列来保存正在等待获取锁或资源的线程。这个队列以CLH(Craig, Landin, and Hagersten)队列锁算法作为基础,它使用一种自旋的方式,避免了线程阻塞和切换的开销。
  • 独占模式和共享模式:AQS支持两种同步模式,独占模式和共享模式。在独占模式下,同一时刻只能有一个线程获取到锁或资源;而在共享模式下,多个线程可以同时获取到锁或资源。
  • 获取与释放:AQS提供了acquire(获取)和release(释放)等方法,开发者可以在自定义的同步器中使用这些方法来实现自己的同步逻辑。获取方法负责线程的排队和等待,直到满足获取条件;而释放方法负责释放锁或资源,并唤醒等待队列中的下一个线程。

与AQS相关:ReentrantLock,CountDownLatch,ReentrantReadWriteLock,Semaphore等

ReenTrantLock,ReenTrantReadWriteLock,StampedLock

读写锁:一个资源能被多个线程读,或者被一个线程写,读读共享,读写互斥。适合在大量读场景下使用

缺点:写锁饥饿,写锁长期抢不到。锁降级:写线程在获取了写锁之后能够再次获取写锁,同时也能获得读锁。当线程获取写锁,在获取读锁,释放写锁这样的顺序就会使写锁降级为读锁,

降级保证了数据一致性

邮戳锁

相当于乐观的读写锁。

它的写操作不会被阻塞,读锁获取后,需要对结果进行校验,如果发现被修改过,则转换为悲观模式

他获取锁的时候会获得一个邮戳,为0表示失败,释放锁的时候需要一个邮戳,必须与获取锁的邮戳是同一个。并且是不可重入的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值