之前两篇文章介绍了线程的基本概念和锁的基本知识,本文主要是学习同步机制,包括使用synchronized关键字、ReentrantLock等,了解锁的种类,死锁、竞争条件等并发编程中常见的问题。
常用来保证代码的原子性,主要有三种使用方法
synchronized void method() {
//业务代码
}
synchronized void staic method() {
//业务代码
}
synchronized(this) {
//业务代码
}
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 中,等待被唤醒。
-同步是锁住的
可从锁的实现、功能特点、性能维度等分析
锁的实现:synchronized是通过jvm实现,是java的关键字;而reentrantlock是通过jdk层面的api实现的的(一般是lock()和unlock()方法配合try/catch/finally语句实现)
性能:jdk1.6前synchronized性能比较差,应该都是要通过底层调用,但是1.6以后增加了适应性自旋,锁消除等,两者性能差不多。
功能特点:-
锁可以分为
以上是锁的名词,有的是指锁的状态,有的是锁特性或者设计。
乐观锁和悲观锁并不是两种特定类型锁,是人们定义的概念或者思想。主要是指人们看待同步的角度。
①. 乐观锁:乐观锁适合读多的场景,不加锁会代理大量的性能提升 ,在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)。如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。期间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。
独享锁是指该锁只能被一个线程获取,共享锁是指该锁可以被多个线程持有。
对于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队列性能较好。
4.可重入锁:可重入锁又名递归锁,是指在同一个线程在外层获锁的时候,在进入内存自动获取锁。也就是在执行对象中所有的同步方法不用再次获取锁。 对于Java ReetrantLock而言,从名字就可以看出是一个重入锁,其名字是Reentrant Lock 重新进入锁。对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
上面的代码就是一个可重入锁的一个特点。如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。
分段锁是一种设计,并不是具体的一种锁,对于ConcurrentHashMap而言是最好的例子,其并发就是通过分段式锁来实现的
java每个对象都可以作为锁,锁有四种基本:无锁、偏向锁、轻量级锁、重量级锁,并且锁可以进行升级不能下降,这三种是指锁的状态,并且是针对synchronized的,是在java5,jdk1.6后引入实现高效升级synchronized,这三种锁通过对象监视器在对象头中的字段表明。
自旋锁是一种技术,是为了让线程等待,我们只需要让线程执行一个忙循环。自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式获取锁,这样好处是减少线程上下文切换的消耗,缺点是循环会消耗cpu。
自旋锁是一种非阻塞锁,核心就是自旋两个字,即用自旋代替阻塞操作,某一个线程尝试获取锁的时候,如果该锁已经被另一个线程占用,那么这个这个线程将不断循环进行检查该锁是否被释放((默认次数是10,可以使用-XX:PreBlockSpinsh参数设置该值)),而不是让此线程挂起或者睡眠,一旦另一个线程释放锁那么此线程就会立即获得锁。自旋是一种忙等待状态,过程会一直消耗cpu的时间片。
在等待锁的过程中可以中断。
死锁是一种现象,程A持有资源x,线程B持有资源y,线程A等待线程B释放资源y,线程B等待线程A释放资源x,两个线程都不释放自己持有的资源,则两个线程都获取不到对方的资源,就会造成死锁。
死锁不能自行打破,所以线程死锁后,线程不能进行响应,所以要注意线程的使用并发场景。
死锁形成条件
如何破坏避免形成死锁呢:
如何排查
可以使用jdk自带的工具排查: