java中的各种锁详细介绍,java锁的用法

  java中的各种锁详细介绍,java锁的用法

  本文给大家带来了一些关于java的知识,包括乐观锁、悲观锁、排他锁、共享锁等等。下面就来看看吧,希望对你有帮助。

  

乐观锁和悲观锁

  

悲观锁

  悲观锁对应生活中的悲观主义者。悲观主义者总是想到事情会变坏。

  举个生活中的例子,假设厕所只有一个坑。悲观锁厕所的时候会第一时间把门锁上,让别人上厕所只能在门外等着。这种状态是“阻塞的”。

  回到代码世界,共享数据有一个悲观锁。每次线程要操作这个数据的时候,都会假设其他线程也可能操作这个数据,所以每次操作之前都会被锁死,所以其他线程只有拿不到锁才能操作这个数据。

  在Java中,synchronized和ReentrantLock是典型的悲观锁,一些使用synchronized关键字的容器类,比如HashTable,也是悲观锁的应用。

  乐观锁

  乐观锁定对应的是生活中一个乐观的人,一个乐观的人总是认为事情会往好的方向发展。

  举个生活中的例子,假设厕所只有一个坑。乐观锁定认为:前不着村后不着店人少,没人会抢我的坑。每次锁门都很浪费时间,不如不锁。看,看好锁定自然看好!

  回到代码世界,乐观锁在操作时不会锁定数据,在更新时会判断其他线程是否会更新数据。

  乐观锁可以通过使用版本号机制和CAS算法来实现。在Java语言中,java.util.concurrent.atomic包下的原子类是通过使用CAS乐观锁定实现的。

  两种锁的使用场景

  悲观锁和乐观锁没有优劣之分。他们有自己的适应场景。

  乐观锁适用于写得少(冲突少)的场景,因为不需要加锁和释放锁,节省了锁的开销,从而提高了吞吐量。

  如果是写的多读的少的场景,也就是冲突严重,线程之间的竞争有动机,使用乐观锁会导致线程不断重试,也可能降低性能,所以在这种场景下使用悲观锁更合适。

  

独占锁和共享锁

  独占锁

  排他锁意味着一个锁一次只能被一个线程持有。如果一个线程向数据添加了一个排他锁,那么其他线程可以向数据添加任何类型的锁。获得排他锁的线程可以读取和修改数据。

  JDK中的Synchronized和java.util.concurrent(JUC)包中的锁的实现类都是排他锁。

  共享锁

  共享锁意味着一个锁可以被多个线程持有。如果一个线程将共享锁添加到数据中,那么其他线程只能将共享锁添加到数据中,并且可以添加独占锁。获取共享锁的线程只能读取数据,并且可以修改数据。

  ReentrantReadWriteLock是JDK的一种共享锁。

  

互斥锁和读写锁

  互斥锁

  互斥锁是互斥锁的常规实现,即同一时刻只允许一个访问者访问一个资源,并且是唯一的、排他的。

  互斥锁一次只能有一个线程有互斥锁,其他线程只能等待。

  读写锁

  读写锁是共享锁的具体实现。读写锁管理一组锁,一个是只读锁,一个是写锁。

  当没有写锁时,读锁可以由多个线程同时持有,而写锁是独占的。写锁的优先级高于读锁,获得读锁的线程必须能够看到由之前释放的写锁更新的内容。

  与互斥锁相比,读写锁具有更高程度的并发性。一次只有一个写线程,但是多个线程可以同时读。

  JDK定义了一个读写锁的接口:读写锁。

  公共接口读写锁{

  /**

  *获取读锁

  */

  lock read lock();

  /**

  *获取写锁

  */

  lock writeLock();

  }ReentrantReadWriteLock实现读写锁接口。具体实现这里就不展开了,以后再分析源代码。

  

公平锁和非公平锁

  公平锁

  公平锁意味着多个线程按照申请锁的顺序获取锁。在这里,类似于排队买票。先来的人先买,后面的人排在队尾。这是公平的。

  java中的构造函数可以初始化公平锁。

  /**

  *创建一个可重入锁,true表示公平锁,false表示不公平锁。默认不公平锁定

  */

  Lock lock=new ReentrantLock(真);非公平锁

  不公平锁是指多线程获取锁的顺序与申请锁的顺序不一致。稍后应用的线程可能比最先应用的线程具有获取锁的优先权。在高并发环境下,可能会造成优先级反转或饥饿(某个线程始终无法获得锁)。

  在java中,synchronized关键字是一个不公平锁,ReentrantLock默认也是一个不公平锁。

  /**

  *创建一个可重入锁,true表示公平锁,false表示不公平锁。默认不公平锁定

  */

  lock lock=new reentrant lock(false);

可重入锁

  重入锁(recursive lock)又称递归锁,是指同一线程在外层方法中获取锁,内层方法自动获取锁。

  对于Java ReentrantLock,它的名字可以看作是一个可重入锁。对于Synchronized,它也是一个可重入锁。

  敲黑板:重入锁的一个好处是可以在一定程度上避免死锁。

  以synchronized为例,看看下面的代码:

  public synchronized void mehtodA()引发异常{

  //做一些魔术

  method b();

  }

  公共同步void mehtodB()引发异常{

  //做一些魔术

  }在上面的代码中,methodA调用methodB。如果一个线程调用methodA,然后调用methodB,它就不需要再次获取锁。这就是可重入锁的特点。如果不是重入锁,mehtodB可能不会被当前线程执行,这可能会导致死锁。

  

自旋锁

  自旋锁是指线程在没有得到锁的情况下不直接挂起,而是执行一个繁忙的循环,这种循环称为自旋。

  自旋锁的目的是降低线程被挂起的概率,因为线程的挂起和唤醒也是消耗资源的操作。

  如果锁被另一个线程长时间占用,即使在自旋后,当前线程仍然会被挂起,忙循环就会变成浪费系统资源的操作,降低整体性能。因此,自旋锁不适合锁需要很长时间的并发情况。

  在Java中,AtomicInteger类有spin的操作。让我们看一下代码:

  public final int getAndAddInt(Object o,long offset,int delta) {

  int v;

  做{

  v=getIntVolatile(o,offset);

  } while(!compareAndSwapInt(o,offset,v,v delta));

  回归v;

  }如果}CAS操作失败,它将始终循环获取当前值并重试。

  另外,自适应自旋锁也需要了解。

  JDK1.6中引入了自适应spin,更加智能。旋转时间不再是固定的,而是由同一把锁上的最后一次旋转时间和锁的所有者的状态决定的。如果虚拟机认为这种旋转有可能再次成功,则需要更多的时间。如果旋转很少成功,则可以直接省略旋转过程,以避免浪费处理器资源。

  

分段锁

  分段锁是一种锁的设计,不是特定的锁。

  分段锁设计的目的是进一步细化锁的粒度。当操作不需要更新整个数组时,它只锁定数组中的一项。

  在Java语言中,CurrentHashMap的底层使用段锁。与segment一起,它可以同时使用。

  

锁升级(无锁偏向锁轻量级锁重量级锁)

   JDK1.6为了提高性能,减少获取和释放锁带来的消耗,引入了四种锁状态:无锁、偏锁、轻量锁和重量级锁。会随着多线程的竞争逐步升级,但不能降级。

  无锁

  无锁状态其实就是上面说的乐观锁定,这里就不赘述了。

  偏向锁

  Java偏向锁意味着它会偏向第一个访问锁的线程。如果在运行的进程中只有一个线程访问锁定的资源,并且没有多线程竞争,那么这个线程就不需要重复获取锁。在这种情况下,一个偏置锁将被添加到线程中。

  偏置锁的实现是通过控制目标标志字的标志位来实现的。如果对象处于偏向状态,则需要进一步判断对象头中存储的线程ID是否与当前线程ID一致,如果一致,则直接输入。

  轻量级锁

  当线程竞争变得激烈时,偏向锁就会升级为轻量锁。轻量级锁认为虽然存在竞争,但竞争程度理想情况下是低的,它通过旋转来等待前一个线程释放锁。

  重量级锁

  如果线程并发进一步加剧,线程自旋超过一定次数,或者一个线程持有一个锁,一个线程自旋,第三个线程来访问(反正竞争继续增加),轻量级锁就会扩展成重量级锁,重量级锁会阻塞除此时拥有该锁的线程以外的所有线程。

  升级到重量级锁其实就是互斥锁。当一个线程获得锁时,其余的线程将被阻塞并等待。

  在Java中,synchronized关键字的内部实现原理是锁升级的过程:无锁-偏锁-轻量级锁-重量级锁。这个过程将在下面对同步关键词原理的解释中详细描述。

  

锁优化技术(锁粗化、锁消除)

  锁粗化

  锁定粗化是为了减少多个同步块的数量,扩大单个同步块的范围。本质上就是把多个加锁和解锁请求合并成一个同步请求。

  比如一个循环体中有一个代码同步块,每个循环都会执行加锁和解锁操作。

  私有静态最终对象锁=新对象();

  for(int I=0;i 100i ) {

  同步(锁定){

  //做一些神奇的事情

  }

  }锁粗化后变成这样:

  同步(锁定){

  for(int I=0;i 100i ) {

  //做一些神奇的事情

  }

  }锁消除

  锁消除意味着虚拟机编译器在运行时检测到没有共享数据的竞争锁,然后消除这些锁。

  下面举个例子让大家更好的理解。

  公共字符串测试(字符串s1,字符串s2){

  string buffer string buffer=new string buffer();

  string buffer . append(S1);

  string buffer . append(S2);

  返回string buffer . tostring();

  }上面的代码中有一个测试方法,主要用于串接字符串s1和字符串s2。

  测试方法中有三个变量S1、S2和字符串缓冲区。都是局部变量。局部变量在堆栈上,堆栈是线程私有的,所以即使多个线程访问测试方法,它也是线程安全的。

  我们都知道StringBuffer是线程安全的类,append方法是同步方法,但是test方法本来就是线程安全的。为了提高效率,虚拟机帮我们消除了这些同步锁,这个过程叫做锁消除。

  StringBuffer.class

  //append是同步方法。

  公共同步StringBuffer append(String str) {

  toStringCache=null

  super . append(str);

  还这个;

  }:

一张图总结:

  关于Java并发编程的知识很多,也是Java面试的高频考点。面试官必问,需要学习Java并发编程其他知识的朋友可以下载“阿里师兄总结的Java知识笔记共283页,超级详细”。

  在Java语言的各种锁面前,最后用六个问题来概括:

  推荐:以上《java视频教程》是图文详解!关于java锁的整理和总结的更多细节,请关注我们的其他相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: