深入了解多线程(Lock锁,AQS,CAS等)


开局先来两张图:
在这里插入图片描述
在这里插入图片描述

1.乐观锁与悲观锁

不是指什么具体类型的锁,而是指在并发同步的角度。
乐观锁:认为对于共享资源的并发操作是不会发生修改的,在更新数据的时候,会采用尝试更新,不断重试的方式更新数据。乐观的认为,不加锁的并发操作共享资源是没问题的.乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是 否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

悲观锁:认为对于共享资源的并发操作,一定是发生xi修改的,哪怕没有发生修改,也会认为是修改的,因此对于共享资源的操作,悲观锁采取加锁的方式,认为,不加锁的并发操作一定会出现问题。总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样 别人想拿这个数据就会阻塞直到它拿到锁.

悲观锁的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高。

乐观锁的问题:并不总是能处理所有问题,所以会引入一定的系统复杂度。

2.互斥锁与读写锁

互斥锁与读写锁就是具体的实现,互斥锁在java 中的体现就是synchronized关键字以及Lock接口实现类ReentrantLock,读写锁在java中的具体实现就是ReentrantReadWriteLock。
多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互 斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。
读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,读者之间并不互斥,而写者则要求与任何人互斥。

3.自旋锁

实际中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个事实,自旋锁诞生了。你可以简单的认为自旋锁就是下面的代码:

while((lock) == 失败) {}

底层采用CAS来保证原子性,自旋锁获取锁的时候不会阻塞,而是通过不断的while循环的方式尝试获取锁。
优点:减少线程上下文切换的消耗.
缺点:是会消耗CPU。如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。

4.可重入锁

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操 作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归 锁)。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized 关键字锁都是可重入的。

public sychrnozied void test() {    
//执行逻辑,调用另一个加锁的方法    
test2();
}
public sychronized void test2() {  

 }

在上面代码中,sychronized关键字加在类方法上,执行test方法获取当前对象作为监视器的对象锁,然后又调用test2同步方法.

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

这三种锁是指锁的状态,并且是针对Synchronized.
无锁: 没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改 失败的线程会不断重试直到修改成功。

偏向锁: 指一段同步代码一直被同一个线程所访问,那么该线程会自动的获取锁。降低获取锁的代价。
轻量级锁: 当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于 被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;如果线程处于活动状态,升级为轻量级锁的状态。
重量级锁: 当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级会让其他申请线程阻塞,性能降低.

6.公平锁与非公平锁

ReentrantLock支持两种锁:非公平锁和公平锁.
非公平锁: 非公平锁指多个线程获取锁的顺序并不是按照申请锁的顺序来获取,有可能后申请锁的线程比先申请锁的线程优先获取到锁,此极大的可能会造成线程饥饿现象,迟迟获取不到锁.
公平锁: 公平锁指多个线程按照申请锁的顺序来依次获取锁。

在这里插入图片描述

7.独享锁与共享锁

独享锁是指该锁一次只能被一个线程所持有。共享锁是指可被多个线程所持有。在java中,对ReentrantLock对象以及synchroized关键字而言,是独享锁的。但是对于ReadWriteLock接口而言,其读是共享锁,其写操作是独享锁。读锁的共享锁是可保证并发读的效率,读写、写写、写读的过程中都是互斥的,独享的。独享锁与共享锁在Lock的实现中是通过 AQS(抽象队列同步器)来实现的。

CAS(Compare And Swap)

CAS(Compare And Swap),即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
在JAVA中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现这个CAS。

当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到 操作失败的信号。可见 CAS 其实是一个乐观锁

自旋锁就是基于CAS实现.

ABA问题:
ABA 的问题,就是一个值从A变成了B又变成了A,而这个期间我们不清楚这个过程。值虽然没变,但实则已经变换过了.

解决方法:加入版本信息,例如携带 AtomicStampedReference 之类的时间戳作为版本信息,保证不会出现老的值。

AQS(AbstractQueuedSynchronizer)

抽象队列同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列构成等待队列。

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者的关系:锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作。锁和同步器很好的隔离了使用者和实现者所需关注的领域。

AQS则实现了对同步状态的管理,以及对阻塞线程进行排队,等待通知等等一些底层的实现处理。AQS的核心也包括了这些方面:同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现.

AQS模板方法:
独占式锁:

  1. void acquire(int arg):独占式获取同步状态,如果获取失败则插入同步队列进行等待。
  2. void acquirelnterruptibly(int arg):与acquire方法相同,但在同步队列中等待时可以响应中断。
  3. boolean tryAcquireNanos(int arg,long nanosTimeout) :在2的基础.上增加了超时等待功能,在超时时间内没有获得同步状态返回false
  4. boolean tryAcquire(int arg) :获取锁成功返回true,否则返回false
  5. boolean release(int arg) :释放同步状态,该方法会唤醒在同步队列中的下一一个节点。

共享式锁:

  1. void acquireShared(int arg) :共享式获取同步状态,与独占锁的区别在于同一时刻有多个线程获取同步状态。
  2. void acquireSharedInterruptibly(int arg) :增加了响应中断的功能
  3. boolean tryAcquireSharedNanos(int arg,lone nanosTimeout) :在2的基础上增加了超时等待功能.
  4. boolean releaseShared(int arg) :共享锁释放同步状态。

主要工作图:
在这里插入图片描述
AQS详解在这里

总结:
1.线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(), 同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试;

2.线程获取锁是一个自旋的过程,当且仅当当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞;

3.释放锁的时候会唤醒后继节点;

总体来说:在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点。

ReentrantLock,

ReentrantLock重入锁,是实现Lock接口的一个类, 也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。

用法类似synchronized

synchronized(object){ 
       // working   
         }  

  
ReentrantLock lock = new ReentrantLock(); 
lock.lock(); 
  try {
 // working
  } finally { 
 lock.unlock() 
 }  
Semaphore

一个计数信号量,主要用于控制多线程对共同资源库访问的限制。

private static final Semaphore avialable = new Semaphore(5);

public static void main(String[] args) {
    ExecutorService pool = Executors.newFixedThreadPool(10);
    Runnable r = new Runnable() {
        public void run() {
            try {
                avialable.acquire(); //此方法阻塞
                Thread.sleep(10 * 1000);
                System.out.println(Thread.currentThread().getName());
                avialable.release();//此方法释放
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    for (int i = 0; i < 10; i++) {
        pool.execute(r);
    }
    pool.shutdown();
}

限制五个线程.

CountDownLatch

倒计时器

 public static void main(String[] args) throws Exception {
        CountDownLatch latch = new CountDownLatch(10);
        Runnable r = new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(Math.round(Math.random() * 10000));
                    latch.countDown();//计数减一
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 11; i++) {
            new Thread(r).start();
        }
// 必须等到计数为0,才会结束,不然就阻塞
        latch.await();//阻塞

        System.out.println("结束");
死锁

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期 地阻塞,因此程序不可能正常终止。
死锁产生的四个必要条件:

1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了 一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。 ① 资源一次性分配(破坏请求与保持条件) ② 可剥夺资源:在线程满足条件时,释放掉已占有的资源 ③ 资源有 序分配:系统为每类资源赋予一个编号,每个线程按照编号递 请求资源,释放则相反 .

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值