AQS及Java中的多种锁机制

并发编程的优点和缺点

优点:

提升性能

将多核CPU的计算能力发挥到机制,性能得到提升

业务适用

并行计算会比串行计算更适应业务需求,而并发编程更适用于该模型

缺点:

频繁上线文切换

前提是:线程数量 > CPU核数

线程安全

数据的可见性、原子性、有序性

无锁并发编程

Runnable和Callable的区别

  • Runnable从JDK1.1就有了,而Callble是JDK1.5之后增加的
  • Callable执行方法是call(),Runnable执行方法是run()
  • Callable任务执行后可返回值,而Runnable任务是不能返回值(void)
  • call方法可以抛出异常,run方法不可以
  • 运行Callable任务可拿到Future对象,即异步计算结果:
    • 它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果
    • 通过Future对象可以了解任务执行情况,可取消任务的执行,还可以获取执行结果
  • 使用线程池方式运行:
  • Runnable用ExecutorService的execute方法
  • Callable使用submit方法

为什么使用锁

并发操作同一资源

资源竞争、保护

资源胡钗

并发编程的产生

Java中锁的种类

  • synchronized
    • synchronized关键字,语义层面的定义及使用
    • 隐式地获取锁,将锁的获取和释放固化,即先获取在释放当前锁
  • Lock(JDK1.5)
  • 对比:
    • 非阻塞方式获取锁
    • 获取锁喉可被中断
    • 获取锁设置超时机制

synchronize的用法

synchronize的用法.png

Java对象的组成

Java对象的组成.png

synchronize 锁升级

偏向锁即有一个线程A访问带有锁的同步代码块时,会在对象头的mark word记录线程的ID,当有线程访问同步块时,会比较mark word中的线程id,和当前线程是否一致,如果还是线程A则直接获取到锁,当有一个B访问时,会尝试CAS将对象头中的mark wor替换为指向锁记录指针,如果替换成功,则当前线程获得到锁,如果替换失败则说明有其他线程在竞争锁,当前线程使用自旋来获取锁,当自旋次数达到一定次数时,就会升级成为重量级锁,由操作系统Mutexlock实现线程间的切换

轻量锁就是偏向锁+自旋

​ 自旋的次数可以用个jvm参数设置,但是在jdk1.7之后,就不建议修改这个参数了,因为程序进行了优化,锁自动优化,锁弹性升级,它自动判断大部分情况需要多少自旋次数能解决问题,不断升级自旋次数

synchronize 锁升级.png

Lock

  • Lock概念
    • 是一个接口,它定义和规范了"获取和释放锁",Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的
  • Lock接口方法
    • lock() 获取锁
    • lockInterruptibly() 可中断地进行获取锁
    • tryLock() 非阻塞地尝试获取锁
    • tryLock(long time, TimeUnit unit) 有超时时间获取锁
    • unlock() 释放锁
    • Condition newCondition获取等待通知组件,该组件与当前锁绑定,只有当前线程获取锁才可以使用组件await(),await()调用后释放锁
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class TestLock implements Lock {
    // 获取锁
    public void lock() {

    }

    // 可中断地进行获取锁
    public void lockInterruptibly() throws InterruptedException {

    }

    // 非阻塞地尝试获取锁
    public boolean tryLock() {
        return false;
    }

    // 有超时时间获取锁
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    // 释放锁
    public void unlock() {

    }

    // 获取等待通知组件,该组件与当前锁绑定,只有当前线程获取锁才可以使用组件await(),await()调用后释放锁
    public Condition newCondition() {
        return null;
    }
}

Condition

  • **await(): **造成当前线程在接收到信号或被中断之前一直处于等待状态。

  • **await(long time, TimeUnit unit): **造成当前线程在接收到信号、被中断或达到指定等待时间之前一直处于等待状态。

  • **long awaitNanos(long nanosTimeout): **造成当前线程在接收到信号、被中断或达到指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimeout之前唤醒,那么返回值= nanosTimeout - 消耗时间,如果返回值 <= 0,则可以认定它已经超时了。

  • **awaitUninterruptibly(): **造成当前线程在接收到信号之前一直处于等待状态。

    • 注意:该方法对中断不敏感。
  • **boolean awaitUntil(Date deadline): **造成当前线程在接收到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回false。

  • **void signal(): **唤醒一个等待线程。该线程从等待放放返回前必须获得与Condition相关的锁。

  • **void signalAll(): **唤醒所有等待线程,能够从等待放放返回的线程,必须获取与Condition相关的锁

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class TestCondition {

    public static void main(String[] args) {

        final ReentrantLock reentrantLock = new ReentrantLock();
        final Condition condition = reentrantLock.newCondition();

        new Thread(() -> {

            reentrantLock.lock();

            System.out.println(Thread.currentThread().getName() + "拿到了锁");
            System.out.println(Thread.currentThread().getName() + "等待信号");

            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + "拿到信号");

            reentrantLock.unlock();
        }, "线程1").start();

        new Thread(() -> {

            reentrantLock.lock();

            System.out.println(Thread.currentThread().getName() + "拿到了锁");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + "发出信号");
            condition.signalAll();

            reentrantLock.unlock();

        }, "线程2").start();

    }

}

AQS 队列同步器

  • AQS:AbstractQueuedSynchronizer 队列同步器
  • 用来构建锁或者其他同步组件的基础框架
  • 用int成员变量表示同步状态
  • 通过内置的FIFO(先进先出)队列来完成资源获取线程的排队工作
  • 大部分同步需求的基础
  • 同步状态:
    • getState()
    • setSate(int newState)
    • compareAndSetState(int expect, int update)
  • 锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节
  • 同步器(AQS)面向的是锁的创造者OR开发者,它提供了便捷的工具和方法,将同步状态,线程排队,等待及唤醒等底层操作封装起来,让开发者使用更便捷
  • 锁和同步器很多地隔离了使用者和实现者所需关注的范围及各自关注的逻辑细节

volatile

  • 保证不同线程对统一变量操作时的可见性
  • 禁止指令重排序

MESI协议

  • 将当前处理器缓存行的数据写会系统内存
  • 这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效
  • volatile下的MESI:
    • Lock前缀的指令会引起处理器缓存写回内存
    • 一个处理器的缓存回写到内存会导致其他处理器的缓存失效
    • 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,既可以获取当前最新值
MESI协议.png

AQS实现原理

  • 同步器依赖内部的队列完成同步状态的管理,当线程获取同步状态失败时,同步器会将当前线程与等待状态等信息构造成为一个节点(Node)将其放入同步队列,同时会阻塞当前线程。同步状态释放时,唤醒首节点中的线程,使其再次尝试获取同步状态,
  • 主要包括:
    • 同步队列
    • 独占式同步状态获取与释放
    • 共享式同步状态获取与释放
    • 超时获取同步状态等同步器的核心数据结构与模板方法

同步队列

  • 同步队列中的节点(Node):

    • 保存获取同步状态失败线程的引用,等待状态以及前置和后置节点
  • 包含的字段:

    • prev 前置节点
    • next 后置节点
    • thread 获取同步状态的线程
    • waitStatus 等待状态
    • nextWaiter 等待队列中的后置节点
    AQS同步队列.png
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。