Sychronized和Lock

提到并发大家都会想到锁,提到锁,面试经常被问到Sychronized和Lock,整理了下相关的资料,方便大家参考

锁的分类

悲观锁乐观锁

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

共享锁和排它锁

共享锁

如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁——直到该数据对象上的所有共享锁都被释放。

排它锁

排他锁(Exclusive Locks,简称 X 锁),又称为写锁或独占锁,是一种基本的锁类型。如果事务 T1对数据对象 O1加上了排他锁,那么在整个加锁期间,只允许事务 T1对 O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作——直到T1释放了排他锁。

公平锁非公平锁

Sync两个子类:FairSync和NofairSync

公平锁

多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

非公平锁

多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

可重入锁和不可重入锁

不可重入锁

若当前线程执行中已经获取了锁,如果再次获取该锁时,就会获取不到被阻塞。

可重入锁

若当前线程执行中已经获取了锁,如果再次获取该锁时,依旧可以获取,不被阻塞。

无锁,偏向锁,轻量级锁,重量级锁

在这里插入图片描述

偏向锁

没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。
“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。

自旋锁

首先,内核态与用户态的切换上不容易优化。但通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)。
使用-XX:-UseSpinning参数关闭自旋锁优化;-XX:PreBlockSpin参数修改默认的自旋次数。

自适应自旋锁

如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。
相反的,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。

轻量级锁

顾名思义,轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。

Sychronized

线程运行时拥有自己的栈空间,会在自己的栈空间运行,如果多线程间没有共享的数据也就是说多线程间并没有协作完成一件事情,那么,多线程就不能发挥优势,不能带来巨大的价值。那么共享数据的线程安全问题怎样处理?很自然而然的想法就是每一个线程依次去读写这个共享变量,这样就不会有任何数据安全的问题,因为每个线程所操作的都是当前最新的版本数据。那么,在java关键字synchronized就具有使每个线程依次排队操作共享变量的功能。

作用范围

在这里插入图片描述

实现原理

在这里插入图片描述

在这里插入图片描述
上面用黄色高亮的部分就是需要注意的部分了,这也是添Synchronized关键字之后独有的。执行同步代码块后首先要先执行monitorenter指令,退出的时候monitorexit指令。
使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。

优化

Synchronized,它最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性(排它性)。
1、Java对象头
首先,我们要知道对象在内存中的布局:
已知对象是存放在堆内存中的,对象大致可以分为三个部分,分别是对象头、实例变量和填充字节。Synchronized存在对象头中

2、锁升级
锁的4中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)
2、锁粗化
按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。
锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
3、锁消除
Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间

Lock

Lock锁的意义

与使用synchronized方法和语句相比, Lock实现提供了更广泛的锁操作。 它们允许更灵活的结构,可以具有完全不同的属性,并且可以支持多个关联的Condition对象。
锁是一种用于控制多个线程对共享资源的访问的工具。 通常,锁提供对共享资源的独占访问,一次只能有一个线程可以获取该锁,并且对共享资源的所有访问都需要首先获取该锁。 但是,某些锁可能允许并发访问共享资源,例如ReadWriteLock的读取锁。
使用synchronized方法或语句可访问与每个对象关联的隐式监视器锁,但会强制所有锁的获取和释放以块结构方式进行。当获取多个锁时,它们必须以相反的顺序释放锁。
虽然用于synchronized方法和语句的作用域机制使使用监视器锁的编程变得更加容易,并且有助于避免许多常见的涉及锁的编程错误,但在某些情况下,您需要以更灵活的方式使用锁。 例如,某些用于遍历并发访问的数据结构的算法需要使用“移交”或“链锁”:您获取节点A的锁,然后获取节点B的锁,然后释放A并获取C,然后释放B并获得D等。 Lock接口的实现通过允许在不同范围内获取和释放锁,并允许以任意顺序获取和释放多个锁,从而启用了此类技术。

lock常用方法

1 void lock(); // 获取锁。

最普通的的获取锁,如果锁被其他线程获取则进行等待
lock不会像synchronized一样在异常的时候自动释放锁因此必须在finally中释放锁,以保证发生异常的时候锁一定被释放
注意:lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁、lock()就会陷入永久等待状态

2 获取中断锁

void lockInterruptibly() throws InterruptedException;
除非当前线程被中断,否则获取锁。
获取锁(如果有)并立即返回。如果该锁不可用,则出于线程调度目的,当前线程将被挂起。

3 尝试获取锁

boolean tryLock();
非阻塞获取锁(如果有)并立即返回true值。 如果锁不可用,则此方法将立即返回false值。相比于Lock这样的方法显然功能更加强大,我们可以根据是否能获取到锁来决定后续程序的行为
注意:该方法会立即返回,即便在拿不到锁的时候也不会在一只在那里等待
该方法的典型用法是:
Lock lock = new ReentrantLock();
if(lock.tryLock()){
try{
// TODO
}finally {
lock.unlock();
}
}
else{ // TODO}

4 在一定时间内获取锁

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
如果线程在给定的等待时间内获取到锁,并且当前线程尚未中断,则获取该锁。
如果锁可用,则此方法立即返回true值。 如果该锁不可用,则出于线程调度目的,当前线程将被挂起,并处于休眠状态,直到发生以下三种情况之一:
该锁是由当前线程获取的。
其他一些线程会中断当前线程,并支持锁定获取的中断。
经过指定的等待时间如果获得了锁,则返回值true 。
如果经过了指定的等待时间,则返回值false 。 如果时间小于或等于零,则该方法将根本不等待。
注意事项
在某些实现中,中断锁获取的能力可能是不可能的,并且如果可能的话可能是昂贵的操作。 程序员应意识到可能是这种情况。 在这种情况下,实现应记录在案。与正常方法返回或报告超时相比,实现可能更喜欢对中断做出响应。Lock实现可能能够检测到锁的错误使用,例如源码交易可能导致死锁的调用,并且在这种情况下可能引发(未经检查的)异常。

5 解锁

void unlock(); //释放锁。
Lock实现通常会限制哪些线程可以释放锁(通常只有锁的持有者才能释放锁),并且如果违反该限制,则可能引发(未经检查的)异常。

多线程方法调用

wait()释放锁

该方法用来将当前线程置入休眠状态,直到在其他线程调用此对象的notify()方法或notifyAll()方法将其唤醒。

在调用wait()之前,线程必须要获得该对象的对象级别锁,因此只能在同步方法或同步块中调用wait()方法。进入wait()方法后,当前线程释放锁。在从wait()返回前,线程与其他线程竞争重新获得锁。如果调用wait()时,没有持有适当的锁,则抛出IllegalMonitorStateException,它是RuntimeException的一个子类,因此,不需要try-catch结构。

notify()不释放锁

该方法唤醒在此对象监视器上等待的单个线程。如果有多个线程都在此对象上等待,则会随机选择唤醒其中一个线程,对其发出通知notify(),并使它等待获取该对象的对象锁。注意“等待获取该对象的对象锁”,这意味着,即使收到了通知,wait的线程也不会马上获取对象锁,必须等待notify()方法的线程释放锁才可以。和wait()一样,notify()也要在同步方法/同步代码块中调用。

notifyAll()不释放锁

notifyAll也是来自于Object类的方法,其作用是唤醒在此对象监视器上等待的所有线程。其用法与notify()基本一致,只不过它会唤醒一个对象监视器上等待的全部线程,被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。

sleep()不释放锁

sleep方法的作用是让当前线程暂停指定的时间(毫秒),sleep方法是最简单的方法,比较容易理解。唯一需要注意的是其与wait方法的区别。

最简单的区别是,wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。

Lock和synchronized区别

构成方面

synchronized是关键字属于JVM层面,
底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象,只有在同步块或方法中才能调wait/notify等方法
monitorenter 进入
monitorexit 退出
Lock是具体类(java.util.concurrent.locks.Lock)是api层面的锁

使用方法方面

synchronized不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放锁的占用,(synchronized底层中一次monitorenter会有两次monitorexit,一次是正常释放锁,一次是在报错的情况下释放锁,保证锁的顺利释放)
ReentrantLock则需要用户去手动释放锁,若没有主动释放锁,就有可能出现死锁现象。
需要lock()和unlock()方法配合try/finally语句块来完成。

等待是否可中断

synchronized不可中断,除非抛出异常或者正常运行完成
ReentrantLock可中断
设置超时方法tryLock(long timeout ,TimeUnit unit)
lockInterruptibly()放代码块中,调用interrupt()方法可中断

加锁是否公平

synchronized非公平锁
ReentrantLock两者都可以,默认非公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁

锁绑定多个条件Condition

synchronized没有
ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精准唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
在这里插入图片描述


版权声明:本文为weixin_43960071原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>