多线程

并发与并行

  • 并发:同一时间只能处理一个任务,但是可以每个任务轮着做(时间片轮转)
  • 并行:同一时间可以做多个任务

锁机制

  • synchronized关键字:用于java对象、方法、代码块提供线程安全的操作。java每个对象都有个monitor对象,加锁就是在竞争monitor对象。 对代码块加锁是通过在前后分别加上monitorenter 以及 monitorexit(有两个) 指令,正常情况下只会执行第一个monitorexit释放锁,在释放锁后就接着同步代码块之后的内容继续向下执行。第二个是用来处理异常的,如果程序发生异常,就会执行第二个monitorexiit,并且会继续向下通过athrow 指令抛出异常。对方法是否加锁通过ACC_SYNCHRONIZED标记位来判断。
    作用于成员变量和静态方法是锁住的this对象实例;作用于静态方法,锁住的是class实例;作用于代码块时候,锁住的所有代码块中的对象。
    实际上 synchronized使用锁是就是存储在java对象头中,对象是存储在堆内存中的,每个对象内部都有一部分空间用于存储对象头信息,对象头信息中包含了mark work用于存放hashcode 和对象的锁信息,不同状态下存储的数据结构有一些不同。

在JDK6以前,synchronized一直被称为重量级锁,monitor依赖于底层操作系统的lock实现,java的线程是映射到操作系统的原生线程上的

轻量级锁:

在即将开始执行同步代码块中的内容时候,会首先检查对象的mark word,查看锁对象是否被其它线程占用,如果没有任何线程占用,那么会在当前的线程中所处的栈帧中建立一个名为锁记录(lock record)的空间,用于复制并存储对象目前的mark word信息。接着虚拟机使用CAS操作将对象的mark word更新为轻量级锁状态(数据结构变为指向lock record的指针,指向当前的栈帧),CAS操作基于cmpxchg指令,如果CAS操作失败的话,那么说明有线程已经进入了这个同步代码块中,这时候虚拟机会再检查对象的mark word 是否指向当前线程的栈帧,如果是说明不是其他线程,而是当前线程已经有了这个对象的锁,如果不是,说明已经被其它线程占用,只能将锁膨胀为重量级锁,按照重量级锁操作执行。

重量级锁:

在java虚拟机中,monitor是由Objectmonitor实现的,每个等待锁的线程都会被封装成ObjectWaiter对象 ,

  • 首先会进入entry set 等着,当线程获取到对象的monitor之后会进入the owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加一,
  • 若线程调用wait()方法,将释放当前持有的monitor对象,owner变量恢复为null,count-1,同时该线程进入waitset集合中等待被唤醒,
  • 若当前线程执行完毕后也将释放monitor并复位变量的值,以便其它的线程进入获取对象的monitor。

自旋锁

入自旋锁之后,不会将处于等待状态的线程挂起,而是通过无线循环的方式,不断检测是否能够获取锁,由于单个线程占用锁的时间非常短,所以说循环的次数不会太多。如果等待时间太长,也只会浪费处理器资源,因此自旋锁等待时间是有限制的,如果失败就会采用重量级锁机制。JDK1.6之后,自旋锁得到了优化,次数限制不在是固定的,而是自适用变化的,如果某个锁经常都自自旋失败,有可能不在采用自旋策略,直接使用重量级锁。
解锁过程同样采用CAS算法,如果对象的mark word仍然指向线程的锁记录,那么就用CAS操作把对象的mark word 和复制到栈帧中的displaced mark word 进行交换,如果替换失败,说明其它线程尝试获取该锁,在释放锁的同时,需要唤醒被挂起的线程。

偏向锁

偏向锁实际上专门为单个线程而生的,当某个线程第一次获得锁的时候,如果接下来都没有其它线程来获取此锁,那么持有锁的线程不在需要同步操作,偏向锁也会通过CAS操作记录线程id,值得注意的是如果对象通过调用hashcode方法计算过对象的一致性hash值,那么它不支持偏向锁,会直接进入到轻量级锁状态,因为hash需要被保存,而偏向锁的mark word 无法保存hash值,如果对象已经是偏向锁状态再调用hashcode方法,直接回将锁升级为重量级锁。

JMM内存模型

在CPU中,一般会有高速缓存,为了解决内存的速度和处理器的速度,在CPU内部添加一级或者多级高速缓存来提高处理器的数据获取效率,但是现在都是基于多核处理器,每个处理器都有自己的高速缓存,如何保证每个处理器的高速缓存内容一致? 为了解决缓存一致性的问题,需要各个处理器访问缓存时遵循一些协议,在读写时候根据协议来进行操作,java也采用了类似的模型来实现支持多线程的内存模型。 JMM内存模型规定如下:

  • 所有的变量全部存储在主内存,包括成员变量静态变量,不包括线程私有的局部变量。
  • 每条线程都有自己的工作内存,线程对变量的所有操作必须在工作内存中进行, 不能直接操作主内存的数据
  • 不同线程之间的工作内存是相互隔离的,如果需要线程之间传递内容,只能通过主内存完成,无法直接访问对方的工作内存
    每一条线程如果要操作主内存的数据,先得拷贝到自己的工作内存,并对工作内存中的数据的副本进行操作(load save),在将结果拷贝到主内存中。

重排序

在编译或者执行时,为了优化程序的执行效率,编译器或者处理器统称会对指令进行重排序,有以下情况:

  • 编译器重排序: java编译器通过对java代码语义的理解根据优化规则对代码指令进行重排序。
  • 机器指令级别的重排序:现代处理器能够自主的判断和变更机器指令的执行顺序。

volatile 关键字

当写一个volatile变量时候,JMM会把该线程本地内存中的变量强制刷新到主内存中去,并且这个写操作会导致其他线程中的volatile变量缓存无效,这样另一个线程改变了这个值时,当前线程会立即得知,并将工作内存中变量更新为最新的版本。
volatile会禁止指令重拍,也就是说,如果我们操作的是一个volatile变量,将不会出现重排序,在编译时,会在指令序列中插入内存屏障禁止特定类型的处理器重排序。如果在指令间插入一条memory barrier 则会告诉编译器和CPU,不管社什么指令都不能和memory barrier指令重排。所以volatile能够保证之前的指令一定全部执行,之后的指令一定都没有执行,并且前面的语句的结果对后面的语句可见
内存屏障:又称内存栅栏,是一个CPU指令,有两个作用:保证特定的操作顺序;保证某些变量的内存可见性。

happens-before 原则

JMM 提出先行发生原则,禁止编译优化的场景,来向各位程序员做一些保证,

  • 程序次序规则:
  • 监视器锁规则:
  • volatile变量规则:
  • 线程启动规则:
  • 线程加入规则:
  • 传递性规则

锁框架

JDK5 之后 并发包增加了lock接口 以及相关实现类 用来实现锁功能,lock接提供了与synchronized 关键字相似的同步功能,但需要在使用时手动获取释放锁。真正操作一个锁对象,加锁时候,只需要调用lock()方法;释放锁时只需要调用unlock()方法

AQS 队列同步器

AQS是实现锁机制的基础,它的内部包括了锁的获取、释放、以及等待队列,等待队列是由双向链表实现的,每个等待状态下的线程都可以被封装进结点中并放入双向链表中,双向链表基于队列形式操作

head tail status
头结点 尾结点 状态位

公平锁

公平锁只有在等待队列中存在节点时候才能保证不会出现问题。

condition 实现原理

condition类实际上就是用于代替传统对象的wait、notify操作,同样可以实现等待、通知模式,并且一把锁可以创建多个condition对象
我们知道,当一个线程调用await()方法时,会进入等待队列,直到其它线程调用signal()方法将其唤醒,而这里的条件队列正是用于存储这些等待状态的线程。
await: 只有已经持有锁的线程才可以使用此方法,当调用此方法后,会直接释放锁,只有其它线程调用signal()方法或是被中断时才会唤醒等待中的线程,被唤醒后需要等待其它线程释放锁,拿到锁之后才可以继续执行,并且会会恢复到之前的状态;
signal: 只有持有锁的线程才能唤醒锁所属的condition等待的线程,优先唤醒条件队列中的第一个,如果唤醒过程中出现问题,接着往下找,直到找到一个可以唤醒,唤醒操作的本质是将条件队列中的结点丢进AQS等待队列中,参与锁的竞争,拿到锁之后才能恢复运行。

原子类

在JDK8之后,新增了DoubleAdder 和LongAdder 在高并发情况下,LongAdder性能比AtomicLong 性能更好,出现高并发的情况下,atomiclong会进行大量的循环操作保证同步,而longadder会对value值的CAS操作分散为对数组cells中多个元素的CAS操作,内部维护一个cell 数组。

posted @ 2022-03-31 12:47  Henry19  阅读(20)  评论(0编辑  收藏  举报