Java 多线程与并发【原理第一部分笔记】
Synchronized
synchronized的基本含义以及使用方式
在Java中线程安全问题的主要诱因就是存在共享数据(也称为临界资源)以及存在多条线程共同操作这些共享数据
解决问题的根本方法是,在同一时刻有且只有一个线程在操作共享数据,其他数据必须等到该线程处理完数据后再对共享数据进行操作
此时就引入了互斥锁,互斥锁的特性有互斥性(即同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问,互斥性也称为原子性)以及可见性(必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致)
需要明确的一点是synchronized锁的不是代码,锁的都是对象
根据获取的锁的分类,可以分为获取对象锁以及获取类锁
对于获取对象锁来说,主要有两个用法,第一个是同步代码块(synchronized(this),synchronized(类实例对象)),锁是小括号中的实例对象,第二个就是同步非静态方法(synchronized method),锁是当前对象的实例对象
对于获取类锁来说,主要有两个用法,第一个是同步代码块(synchronized(类.class)),锁是小括号中的类对象(class对象),第二个就是同步静态方法(synchronized static method),锁是当前对象的类对象(class对象)
对象锁和类锁的总结
1.有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块
2.若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程就会被阻塞
3.若锁住的是用一个对象,那么一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程就会被阻塞
4.若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然
5.同一个类的不同对象的对象锁互不干扰
6.类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的
7.类锁和对象锁互不干扰
synchronized底层实现的原理
实现synchronized的基础是Java对象头和monitor
对象在内存中的布局有三块区域,分别是对象头,实例数据以及对齐填充,主要说一下对象头
对象头的结构,主要由Mark word和class metadata address组成
Mark Word是一个非固定的数据结构,以便于存储更多的有效数据,会根据对象的状态来调整自己
在Java的设计中,每个对象自出生就带了一把看不见的锁,其称为内部锁,或者是monitor锁,可以将monitor理解为是一个同步工具,通常其被描述为一个对象
monitor锁的竞争,获取以及释放
自旋锁
许多情况下,共享数据的锁定状态持续时间比较短,切换线程不太值得,自旋锁是怎么实现的呢,即通过让线程执行忙循环等待锁的释放,不让出CPU,Java6以后默认开启,缺点就是,如果锁被其他的线程长时间的占用,会带来许多性能上的开销,因此,应该进行限定,但是比较困难去判定这个限定的界限,因此就需要一个更好的来代替
自适应自旋锁
自适应自旋锁自旋的次数不再固定,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定
锁消除
锁消除是虚拟机另一种锁优化,这种锁优化更加的彻底,在JIT编译时,通过对运行上下文进行扫描,去除不可能存在竞争的锁,通过这种方式来消除没有必要的锁,可以节省毫无意义的时间
锁粗化
这是另一个极端,这是在存在有一连串操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中,那即便是没有线程竞争,频繁的锁操作也会导致不必要的性能操作,这是就可以通过扩大加锁的范围,避免反复的加锁和解锁
synchronized的四种状态:
这四种状态分别是,无锁,偏向锁,轻量级锁,重量级锁,会随着竞争情况逐渐升级,锁降级也是会发生的,在jvm进入前一个安全点的时候会检查是否有闲置的monitor,然后看一下是不是要降级,但是主要还是发生锁升级(锁膨胀)
锁膨胀的方向是,无锁->偏向锁->轻量级锁->重量级锁,主要说一下偏向锁和轻量级锁
偏向锁
在大多数情况下,锁不存在多线程竞争,总是由同一个线程多次获得,因此为了减少同一个线程获取锁的代价,就引入了偏向锁,偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入了偏向模式,此时Mark Word的结构也变为偏向锁结构,当线程再次请求锁的时候,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记为偏向锁以及当前线程id等于Mark Word的threadid即可,这样就省去了大量有关锁申请的操作,偏向锁在失败以后并不会立刻膨胀为重量级锁
这种不适合用于锁竞争比较激烈的多线程场合
轻量级锁
轻量级锁是由偏向锁升级得到的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁,轻量级锁适合用于存在线程交替执行同步块的场景,如果存在同一时间访问同一个锁的情况,那么就会导致轻量级锁膨胀为重量级锁
锁的内存语义
当线程释放锁的时候,Java内存模型会把该线程对应的本地内存中的共享变量刷新到主内存中,当线程获取锁的时候,Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
轻量级锁的加锁过程
1.在代码进入同步块的时候,如果同步对象锁状态为无锁状态,那么虚拟机首先将当前线程的栈帧中建立一个名为锁记录的空间,用于存储对象目前的Mark Word的拷贝
2.将拷贝对象头中的Mark Word复制到锁记录中
3.拷贝成功以后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向lock record的指针,并将lock record里的owner指针指向object Mark Word,如果成功了就执行第四步,否则执行第五步
4.如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为00,即表示此对象处于轻量级锁定状态
5.如果失败了,虚拟机首先检查对象的Mark Word是不是指向当前的线程的栈帧,如果是就说明当前的栈帧已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则就说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁的标志的状态值就为10,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态,而当前线程便尝试使用自旋来获取锁,
轻量级锁的解锁过程
1.通过CAS操作尝试把线程中复制的displaced Mark Word对象替换为当前的Mark Word
2.如果替换成功了,整个同步过程就完成了
3.如果替换失败了,说明还有其他的线程尝试获取过该锁(此时锁已经膨胀了),那么就要在释放锁的同时,唤醒挂起来的线程
偏向锁,轻量级锁以及重量级锁的汇总
synchronized和reentrantlock的区别
reentrantlock(再入锁)
其位于java.util.concurrent.locks包,和countdownlatch,futuretask以及semaphore一样基于AQS实现的,而且reentrantlock可以实现比起synchronized更细粒度的控制,比如控制fairness,但是在编码中要注意必须要明确unlock()方法的释放,在调用lock()之后,必须调用unlock()释放锁,需要注意的是,reentrantlock的性能并不一定就比synchronized高,需要看场景,而且其也是可重入的
reentrantlock公平性的设置
使用reentrantlock fairlock = new reentrantlock(true)就可以设置,在参数为true的时候,更倾向于将锁赋予等待时间最久的线程,这就是所谓的公平锁,即获取锁的顺序按先后调用lock方法的顺序来进行,不过慎用,只有在程序必须要使用这种情况的时候才使用是最好的,而所谓的非公平锁就是抢占的顺序不确定,一切看运气,运气好就抢得到,其中synchronized就是非公平锁
reentrantlock将锁对象化
可以判断是否有线程,或者某个特定线程,在排队等待获取锁,并且可以进行带超时的获取锁的尝试,而且还可以感知到有没有成功获取到锁
reentrantlock能不能将wait,notify以及notifyall对象化呢?
是可以的,可以使用Java.util.concurrent.locks.condition
对synchronized和reentrantlock的区别的总结
1.synchronized是关键字,而reentrantlock是类
2.reentrantlock可以对获取锁的等待时间进行设置,避免死锁,而且reentrantlock可以获取各种锁的信息,此外,reentrantlock还可以灵活的实现多路通知
3.二者的锁机制不一样,synchronized是操作对象头中的Mark Word,而reentrantlock是调用UNsafe类中的park()方法