Java/JUC进阶/Java 并发 - 03 十五种锁

锁分类

1. 公平锁/ 非公平锁

2. 可重入锁/ 不可重入锁

3. 独享锁/ 共享锁

4. 互斥锁/ 读写锁

5. 乐观锁/ 悲观锁

6. 分段锁

7. 偏向锁/ 轻量级锁/  重量级锁

8. 自旋锁

名词解释

公平锁

公平锁是指多个线程按照申请的顺序来获取锁

非公平锁

非公平锁是指多线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象

对于 Java ReentrantLock 来说, 同构构造函数指定该锁是否是公平锁、默认是非公平锁, 非公平锁的优点在于吞吐量比公平锁大。

对于Synchronized 来说, 也是一种非公平锁。 由于其并不像 ReentrantLock 是通过 AQS 来实现线程的调度,所以并没有任何办法使其变成公平锁。

可重入锁

广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提是一个对象或者class), 这样的锁叫做可重入锁。ReentrantLock 和 synchronized 都是可重入锁。

ReentrantLock 可重入锁实现,AQS中维护了一个线程可见的 stat 来计算重入次数,避免频繁的持有和释放操作。

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

不可重入锁
不可重入锁,与可重入锁相反, 不可递归调用, 递归调用就发生死锁,使用自旋锁来模拟一下不可重入锁

package com.test;

import java.util.concurrent.atomic.AtomicReference;

/**
 * @Descrintion:
 * @Date : Created in 10:48 2019/5/14
 * @
 */
public class UnreentrantLock {

    private AtomicReference<Thread> owner = new  AtomicReference<Thread>();

    public void lock(){
        Thread current = Thread.currentThread();

        for(;;){
            System.out.println("start");
            if(!owner.compareAndSet(null, current)){
                System.out.println("running");
                return ;
            }
        }
    }

    public void unlock(){
        Thread current = Thread.currentThread();
        owner.compareAndSet(current,null);
    }

    public static void main(String[] args) {

        UnreentrantLock unreentrantLock = new UnreentrantLock();
        unreentrantLock.lock();
        //unreentrantLock.unlock();
        System.out.println("=========");
        unreentrantLock.lock();
    }
}

未调用unlock 输出

 

调用unlock() 输出

使用原子引用来存放线程, 同一线程两次调用lock() 方法,如果不执行unlock() 释放锁的话,第二次调用自旋的时候就会产生死锁

把他变成一个可重入锁

package com.test;

import com.design.filter.Criteria;

import java.util.concurrent.atomic.AtomicReference;

/**
 * @Descrintion: 可重入锁
 * @Date : Created in 11:10 2019/5/14
 * @
 */
public class CreentrantLock {

    private AtomicReference<Thread> owner = new AtomicReference<>();
    private int state = 0 ;

    public void lock(){
        Thread current = Thread.currentThread();
        if(current == owner.get()){
            state++ ;
            System.out.println("++");
            return ;
        }
        for(;;){
            System.out.println("start");
            if(!owner.compareAndSet(null, current)){
                System.out.println("running");
                return ;
            }
        }
    }

    public void unlock(){
        Thread current = Thread.currentThread();
        if(current == owner.get()){
            if(state != 0){
                state-- ;
            }else{
                owner.compareAndSet(current, null);
            }
        }
    }

    public static void main(String[] args) {
        CreentrantLock creentrantLock = new CreentrantLock();
        creentrantLock.lock();
        //creentrantLock.unlock();
        creentrantLock.lock();
    }

}

未调用unlock

调用 unlock

每次执行操作之前,判断当前锁持有者是否是当前对象,采用state 计数,不用每次去释放锁

独享锁和共享锁在JUC的ReentrantLock 和 ReentrantReadWriteLock 它两一个是独享一个是共享

独享锁

该锁每一次只能被一个线程所持有  synchronized 是独享锁

共享锁

该锁可以被多个线程共有,典型的就是ReentrantReadWriteLock 里的读写锁,他的读锁是可以被共享的,但是他的写锁确每次只能被独占,另外读锁的共享可保证并发读是非常高效的,但是读写和写写、写读都是互斥的, 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

互斥锁

在访问共享资源之前进行加锁,在访问完成之后进行解锁操作,加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进行解锁,如果解锁时有一个以上的线程阻塞,那么所有的该锁上的线程都被编成就绪状态,第一个变为就绪状态的线程有执行加锁操作,那么其他的线程又会进入等待,在这种方式下,只有一个线程能够访问被互斥锁保护的资源。

读写锁

读写锁既是互斥锁,又是共享锁, read 模式是共享, write 是互斥(排它锁)的

读写锁有三种状态:读加锁状态,写加锁状态和不加锁状态

读写锁在Java 中的具体实现就是 ReentrantLock

一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。只有一个线程可以占有写状态的锁,但可以有多个线程同时占有写状态锁,这也是它可以实现高并发的原因,当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放; 如果处于读状态锁下,允许其他线程获得它的读状态锁,但是不允许获得它写状态锁,直到所有线程的读状态锁被释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态的锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远大于写操作的情况

乐观锁

顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。JUC 包下面的原子变量类都是适用了乐观锁的一种实现方式 CAS 实现

悲观锁

顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

分段锁

分段锁是一种锁 的设计, 并不是具体的一种锁,对于 ConcurrentHashMap 而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

并发容器类的加锁机制是基于粒度更小的分段锁,分段锁是提升多并发程序性能的重要手段之一。

并发程序中,串行操作是会降低可伸缩性,并且上下文切换也会减低性能,在锁上发生竞争时将导致这两种情况问题,使用独占锁时保护受限资源的时候,基本上是采用串行方式每次只能有一个线程访问它。所以对于可伸缩性来说最大的威胁就是独占锁。

我们一般有三种方式降低锁的竞争程度

1.减少锁的持有时间

2.降低锁的请求频率

3.使用带有协调机制的独占锁

在某些情况下,我们可以将锁分解技术进一步扩展为一组独立对象上的锁进行分解,这称为分段锁。

偏向锁/轻量级锁/重要级锁

锁状态

1. 无锁状态

2.偏向锁状态

3.轻量级锁状态

4.重量级锁状态

锁状态是通过对象监视器在对象头中的字段来表明的。四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。 这四种状态都不是Java 语言中的锁,而是JVM为了提高锁的获取与释放效率而做的优化(synchronzied)

偏向锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降级获取锁的代价。

轻量级锁

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能低下。

自旋锁

CAS 算法是乐观锁的一种实现方式,CAS 算法中有涉及到自旋锁。自旋锁(spinlock):它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名

猜你喜欢

转载自blog.csdn.net/wszhongguolujun/article/details/90200883