前言
之前两篇文章介绍了线程的基本概念和锁的基本知识,本文主要是学习同步机制,包括使用synchronized关键字、ReentrantLock等,了解锁的种类,死锁、竞争条件等并发编程中常见的问题。
一、关键字synchronized
- synchronied关键字可以把任意一个非null的对象当做锁,属于独占式的悲观锁。同时属于可重入锁
- 早期的的synchronized属于重量级的锁,效率低下,因为监视器是依赖底层的操作系统Lock实现的,从6之后java对sychronized进行了优化,jdk1.6以后还引入了 大量的优化,比如自旋锁,适应性锁,锁消除,锁粗化,偏向锁,轻量级锁等。
1.synchronized用法
常用来保证代码的原子性,主要有三种使用方法
- 修饰实例:作用于当前的对象实例加锁,进入同步代码前获得,当前对象实例的锁。
synchronized void method() {
//业务代码
}
- 修饰静态方法: 也就是给当前类加锁,会作用于该类所有的对象实例。如果线程A调用一个实例对象的非静态synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥现象,因为静态synchronized方法是占用的锁是当前类的锁,而访问非静态synchronized方法占用的锁是当前实例对象的锁。
synchronized void staic method() {
//业务代码
}
- 修饰代码块: 指定加锁对象,对给定的对象/类加锁,synchronized(this object)表示进入同步前要获得给定对象的锁,synchronized(类.class)表示进入同步前要获得给定类class的锁
synchronized(this) {
//业务代码
}
2. synchronized实现原理
- 使用synchronized是不用我们去加锁和释放lock,unlock,是jvm已经代替去做了
- synchronized修饰代码块的时候,jvm是使用monitorenter和monitorexit两个指令实现的(监视器)
- 当修饰同步方法,jvm采用ACC_SYNCHRONIZED标记符来实现的同步, 这个标识表面了这是一个同步方法
3.synchronized锁住的原理
monitorenter,monitorexit,ACC_SYNCHRONIZED都是基于monitor(监视器)
所谓的Monitor其实是一种同步工具,也可以说是一种同步机制。在Java虚拟机(HotSpot)中,Monitor是由
ObjectMonitor实现的,可以叫做内部锁,或者Monitor锁。
ObjectMonitor的工作原理:
ObjectMonitor有两个队列:WaitSet、EntryList,用来保存ObjectWaiter 对象列表。
_owner,获取 Monitor 对象的线程进入 _owner 区时, _count + 1。如果线程调用了wait() 方法,此时会释放Monitor 对象, _owner 恢复为空, _count - 1。同时该等待线程进入 _WaitSet 中,等待被唤醒。
-同步是锁住的
- monitorenter,在判断拥有同步标识 ACC_SYNCHRONIZED 抢先进入此方法的线程会优先拥有 Monitor 的owner ,此时计数器+1。
- monitorexit,当执行完退出后,计数器-1,归 0 后被其他进入的线程获得
4.除了原子性,synchronized的可见性和有序性,可重入性怎么实现
- 可见性:线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量的时候,需要从主内存中重新读取最新的值。线程加锁后,其他线程无法获得主内存中的共享变量的值,线程解锁前必须把共享变量的最新值刷新到主内存中。
- 有序性:synchronized同步的代码块具有排他性,一次只能被一个线程拥有,所以可以保证同一个时刻,代码是单线程执行的,因为as-if-serial存在,单线程语句是能够保证最终结果是有序的,但是不保证不会进行指令重排,所以synchronized是保证有序是执行结果的有序而不是防止指令重排的有序性。
- 可重入性:synchronized是可重入锁,也就说允许一个线程二次请求自己持有的锁的临界资源,这种情况就是可重入锁,锁对象有个计数器,会记录线程获取锁的次数,当执行完对应的代码后,计数器就会减去1,只有归零就会释放锁。之所以可以重入就是因为这个计数器。
5.synchronized和ReentrantLock的区别
可从锁的实现、功能特点、性能维度等分析
-
锁的实现:synchronized是通过jvm实现,是java的关键字;而reentrantlock是通过jdk层面的api实现的的(一般是lock()和unlock()方法配合try/catch/finally语句实现)
-
性能:jdk1.6前synchronized性能比较差,应该都是要通过底层调用,但是1.6以后增加了适应性自旋,锁消除等,两者性能差不多。
-
功能特点:-
- ReentrantLock比synchronized增加了一些高级功能,如等待中断,可实现公平锁,可实现选择性通知;
- synchronized只能是非公平锁(内部锁),- ReentrantLock可以指定是公平还是非公平(公平锁就是先等待的线程先获得锁);
- synchronized与wait()和notify()/notifyAll()方法结合实现等待/通知机制,ReentrantLock类借助Condition接口与newCondition()方法实现。
- ReentrantLock需要手工声明来加锁和释放锁,一般跟finally配合释放锁。而synchronized不用手动释放锁
二、锁类型
锁可以分为
- 悲观、乐观锁
- 独享、共享锁
- 互斥锁、读写锁
- 可重入锁
- 公平锁、非公平锁
- 分段锁
- 偏向锁,轻量级锁、重量级锁
- 自旋锁
以上是锁的名词,有的是指锁的状态,有的是锁特性或者设计。
1.乐观锁、悲观锁
乐观锁和悲观锁并不是两种特定类型锁,是人们定义的概念或者思想。主要是指人们看待同步的角度。
- 乐观锁:顾名思义就是乐观的认为每次取数据,别人都不会修改,所以不上锁,但是在更新的时候会去判断在此期间别人有没有取更新这个数据,可以使用版本号等机制,乐观锁适用于多读的应用程序,这样可以提高吞吐量,在java中原子变量类就是使用了乐观锁的一种实现方式CAS(compare and swap 比较并交换)来实现的
- 悲观锁:总是假设每次去获取数据,都认为别人会修改,所以每次拿取数据都会进行上锁,这样别人拿取数据就会阻塞,直到拿到锁才行,比如Java里面的关键字synchronized实现就是悲观锁,悲观锁适合写操作多的场景。
①. 乐观锁:乐观锁适合读多的场景,不加锁会代理大量的性能提升 ,在java编程中是无锁编程,常常采用的是CAS算法。典型的例子就是原子类,通过CAS自旋实现原子的更新操作。
乐观锁更新判断其他线程有没有更新共享变量 一般采用数据版本机制或者CAS操作实现
(1):数据版本机制:一般两种方式,一种是使用版本号,另一个是使用时间戳方式。
版本号方式:一般是在数据表上加上一个数据版本号version字段,表示更新的次数,当数据被更新的时候计数加一,当线程A更新数据时候,会在读取数据的同时也会读取version字段,在更新提交的时候,若刚才的读取的version和数据库中的version相等才会更新,否则会重新进行更新操作。直到更新成功。
update table set xxx=#{xxx}, version=version+1 where id=#{id} and version=#{version};
(2):CAS操作:当多个线程尝试使用CAS同时更新一个变量时候,只有一个线程能够更新变量,其他线程并不会被挂起,会收到通知失败,并可以再次尝试。
CAS需要三个字段值:1.需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B),如果内存位置的V值和预期原值A想匹配,那么就会更新B,否则不做变动。
②:悲观锁:悲观锁认为对于同一个数据的并发操作,一定会发生修改的,哪怕没有修改,也会认为修改。因此对于同一份数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁并发操作一定会出问题。在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。期间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。
2.独享锁、共享锁
独享锁是指该锁只能被一个线程获取,共享锁是指该锁可以被多个线程持有。
对于java而言ReentrantLock是独享锁,但是对于另一个lock的实现类ReadWriterLock来说,其读是共享锁,其写是独占锁。独享锁和共享锁是通过AQS来实现的,通过不同的方法,来实现独享或者共享(synchronized是独占锁)
AQS:AbstractQueueSynchronized抽象同步队列,简称AQS;它是java并发包的基础,并发的锁就是基于Aqs实现的。
-
AQS是基于一个FIFO的双向队列,其内部定义了一个node节点类,node节点内部的SHARED用来标记该线程是获取共享变量时被阻挂起后放入AQS队列的,EXCLUSIVE用来标记线程是独占资源时被挂起放入AQS队列。
-
AQS使用一个volatile修饰的int类型的成员变量state来表示同步状态,修改同步状态成功表示获得锁,volatile保证了变量在线程之间的可见性,修改state通过CAS机制来保证修改的原子性。
-
获取state方式有两种,独占和共享。一个线程使用了独占的方式,那么其他线程就失败会被阻塞;一个线程使用共享时获取资源,另一个线程还可以通过CAS的方式进行获取。
-
如果共享资源被占用,需要一定的阻塞等待唤醒机制来保证锁的分配,AQS会将获取共享资源失败的线程添加到一个变体的CLH中。
-
AQS中的ClH变体等待队列特性
- AQs中队列是个双链表,也是符合FIFO先进先出的特性。
- 通过head、tail两个头尾节点来组成队列结构,通过volatile来保证可见性。
- Head指向的节点本身已经获得了锁,是一个虚拟节点,节点本身不具备具体线程
- 获取不到同步状态,会将节点进行自旋获取锁,自旋一定次数失败后,会将线程阻塞,相对于CLH队列性能较好。