美文网首页
java锁synchronized和lock

java锁synchronized和lock

作者: AlastairYuan | 来源:发表于2020-03-03 18:37 被阅读0次

同步代码块,同步方法,或者是用java提供的锁机制,我们可以实现对共享资源变量的同步控制。

技术点:

1、线程与进程:

在开始之前先把进程与线程进行区分一下,一个程序最少需要一个进程,而一个进程最少需要一个线程。关系是线程–>进程–>程序的大致组成结构。所以线程是程序执行流的最小单位,而进程是系统进行资源分配和调度的一个独立单位。以下我们所有讨论的都是建立在线程基础之上。

2、Thread的几个重要方法:

我们先了解一下Thread的几个重要方法。

a、start()方法,开始执行该线程;
b、stop()方法,强制结束该线程执行;
c、join方法,等待该线程结束。
d、sleep()方法,线程进入等待。
e、run()方法,直接执行线程的run()方法,但是线程调用start()方法时也会运行run()方法,区别就是一个是由线程调度运行run()方法,一个是直接调用了线程中的run()方法!!
看到这里,可能有些人就会问啦,那wait()和notify()呢?

要注意,其实wait()与notify()方法是Object的方法,不是Thread的方法!!

同时,<b>wait()与notify()会配合使用,分别表示线程挂起和线程恢复。</b>

这里还有一个很常见的问题,顺带提一下:<b>wait()与sleep()的区别,简单来说wait()会释放对象锁而sleep()不会释放对象锁。</b>

3、线程状态:

线程总共有<b>5大状态</b>,通过上面第二个知识点的介绍,理解起来就简单了。

<b>新建状态</b>:新建线程对象,并没有调用start()方法之前

<b>就绪状态</b>:调用start()方法之后线程就进入就绪状态,但是并不是说只要调用start()方法线程就马上变为当前线程,在变为当前线程之前都是为就绪状态。值得一提的是,线程在睡眠和挂起中恢复的时候也会进入就绪状态哦。

<b>运行状态</b>:线程被设置为当前线程,开始执行run()方法。就是线程进入运行状态

<b>阻塞状态</b>:线程被暂停,比如说调用sleep()方法后线程就进入阻塞状态

<b>死亡状态</b>:线程执行结束

4、锁类型

<b>可重入锁</b>(synchronized和ReentrantLock):在执行对象中所有同步方法不用再次获得锁

<b>可中断锁</b>(synchronized就不是可中断锁,而Lock是可中断锁):在等待获取锁过程中可中断

<b>公平锁</b>(ReentrantLock和ReentrantReadWriteLock): 按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利

<b>读写锁</b>(ReadWriteLock和ReentrantReadWriteLock):对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写

Synchronized与Lock的区别

类别 synchronized Lock
存在层次 Java的关键字,在jvm层面上 是一个接口
锁的释放 1、以获取锁的线程执行完同步代码,释放锁
2、线程执行发生异常,jvm会让线程释放锁
在finally中必须释放锁,不然容易造成线程死锁
锁的获取 假设A线程获得锁,B线程等待。
如果A线程阻塞,B线程会一直等待
分情况而定,Lock有多个锁获取的方式,
大致就是可以尝试获得锁,线程可以不用一直等待
(可以通过tryLock判断有没有锁)
锁状态 无法判断 可以判断
锁类型 可重入
不可中断
非公平
可重入
可判断
可公平(两者皆可)
性能 少量同步 大量同步
<b>1.</b>Lock可以提高多个线程进行读操作的效率。
(可以通过readwritelock实现读写分离)
<b>2.</b>在资源竞争不是很激烈的情况下,
Synchronized的性能要优于ReetrantLock,
但是在资源竞争很激烈的情况下,
Synchronized的性能会下降几十倍,
但是ReetrantLock的性能能维持常态;
<b>3.</b>ReentrantLock提供了多样化的同步,
比如有时间限制的同步,可以被Interrupt的同步
(synchronized的同步是不能Interrupt的)等。
在资源竞争不激烈的情形下,
性能稍微比synchronized差点点。
但是当同步非常激烈的时候,
synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。

Synchronized与Static Synchronized

每个类有一个锁,它可以用来控制对static数据成员的并发访问。
访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁。

static synchronized控制类的所有实例(对象)的访问(相应代码块)。
synchronized相当于 this.synchronized,static synchronized相当于Something.synchronized

Lock接口

//Lock是一个接口
public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。
unLock()方法是用来释放锁的。
在Lock中声明了四个方法来获取锁,那么这四个方法有何区别呢?

首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    //释放锁
    lock.unlock();   
}

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

所以,一般情况下通过tryLock来获取锁时是这样使用的:

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         //
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。

因此lockInterruptibly()一般的使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。

而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

Lock类型

一、公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

二、可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
说的有点抽象,下面会有一个代码的示例。
对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是ReentrantLock重新进入锁。
对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

synchronized void setA() throws Exception{
    Thread.sleep(1000);
    setB();
}

synchronized void setB() throws Exception{
  Thread.sleep(1000);
}
三、独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。

四、互斥锁/读写锁

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是<b>具体的实现</b>。
互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock

五、乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的<b>角度</b>。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

六、分段锁

分段锁其实是一种锁的<b>设计</b>,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

七、偏向锁/轻量级锁/重量级锁

这三种锁是指锁的<b>状态</b>,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
<b>偏向锁</b>是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
<b>轻量级锁</b>是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
<b>重量级锁</b>是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

八、自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
线程自旋和适应性自旋

我们知道,java线程其实是映射在内核之上的,线程的挂起和恢复会极大的影响开销。
并且jdk官方人员发现,很多线程在等待锁的时候,在很短的一段时间就获得了锁,所以它们在线程等待的时候,并不需要把线程挂起,而是让他无目的的循环,一般设置10次。
这样就避免了线程切换的开销,极大的提升了性能。

而<b>适应性自旋</b>,是赋予了自旋一种学习能力,它并不固定自旋10次一下。
他可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。

相关文章

  • Lock锁

    synchronized 锁和 Lock 锁的区别 Lock 锁,Java类,synchronized,关键字。s...

  • Java中的锁——Lock和synchronized

    一、Lock接口 1、Lock接口和synchronized内置锁 a)synchronized:Java提供的内...

  • 笔记

    java中有哪几种锁? 1.有synchronized和lock两种锁,synchronized是java的...

  • 高并发篇

    高并发篇 java锁有那些 synchronized和lock的区别 synchronized的类锁和对象锁的区别...

  • 【Java并发007】原理层面:ReentrantLock中lo

    一、前言 Java线程同步两种方式,synchronized关键字和Lock锁机制,其中,AQS队列就是Lock锁...

  • 死磕Java——Lock锁

    一、死磕Java——Lock锁 首先,知道的是Java中加锁的方式有两种,分别是synchronized和Lock...

  • Java并发编程显式锁

    显式锁 有了 synchronized 为什么还要 Lock? Java 程序是靠 synchronized 关键...

  • Java性能 -- CAS乐观锁

    synchronized / Lock / CAS synchronized和Lock实现的同步锁机制,都属于悲观...

  • Android面试知识点(三)*

    1、java里的锁总结(synchronized隐式锁、Lock显式锁、volatile、CAS)[https:/...

  • java锁synchronized和lock

    同步代码块,同步方法,或者是用java提供的锁机制,我们可以实现对共享资源变量的同步控制。 技术点: 1、线程与进...

网友评论

      本文标题:java锁synchronized和lock

      本文链接:https://www.haomeiwen.com/subject/msdklhtx.html