首页 文章 精选 留言 我的

精选列表

搜索[指标分析],共10000篇文章
优秀的个人博客,低调大师

CVE-2018-15664漏洞分析报告

近日来自SUSE的安全专家Aleksa Sarai公布了编号为CVE-2018-15664的docker相关高危安全漏洞,该漏洞的CVSS评分为8.7,影响面涵盖所有docker发行版本,攻击者可利用该漏洞逃逸出容器读取或篡改host或其他任意容器内的文件,当前docker官方已经给出了临时方案以降低攻击面并将于下个release版本发布。 漏洞描述 该漏洞的根因是使用FollowSymlinkInScope函数时触发TOCTTOU(time-of-check-to-time-of-use) 文件系统的竞争条件缺陷引起的。这段函数代码在docker源码中使用较多,最为直接的就是docker cp命令。根据Sarai的描述:FollowSymlinkInScope的作用是解析容器中运行进程的文件路径,而攻击者可以利用解析校验完成

优秀的个人博客,低调大师

分析redis的各种使用情景

使用docker-compose演示redis的各种使用情景,最后介绍了codis和kubernetes方案 总结 模式 特点 单机版-RDB 备份快,但数据可能不完整 单机版-AOF 备份慢,但是数据比较完整 1主N从 读写分离的典范 树状主从(N级缓存) 为了规避主重启导致的大规模全量复制,但是需要维持每一个中间master的健康 主从自动切换(Sentinel) 在主从的基础上加了Sentinel角色,通过Sentinel实现主从的自动切换 集群(N主N从) 基于slot的key分片,客户端支持得不是很多,所以用的人不多 单机版 RDB模式(默认模式) 定期快照模式 version: '3' services: redis-master: image: redis ports: - "12660:6379" expose: - "6379" networks: - default entrypoint: - redis-server volumes: - ./data:/data AOF模式 逐一写入,数据比较完整,文件较大,但恢复较慢 version: '3' services: redis-master: image: redis ports: - "12660:6379" expose: - "6379" networks: - default entrypoint: - redis-server - --appendonly yes volumes: - ./data:/data 主从复制版 1主N从 这种模式简单粗暴,但是master一旦重启,多从节点全量复制,IO将会比较繁重 version: '3' services: redis-master: image: redis ports: - "12660:6379" expose: - "6379" networks: - default entrypoint: - redis-server - --save 1 1 volumes: - ./data:/data redis-slave1: image: redis ports: - "12661:6379" expose: - "6379" networks: - default entrypoint: - redis-server - --slaveof redis-master 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 redis-slave2: image: redis ports: - "12662:6379" expose: - "6379" entrypoint: - redis-server - --slaveof redis-master 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 树状主从(N级缓存) 从节点作为主节点. 这种模式规避了单master version: '3' services: redis-master: image: redis ports: - "12660:6379" expose: - "6379" networks: - default entrypoint: - redis-server - --save 1 1 volumes: - ./data:/data redis-slave1: image: redis ports: - "12661:6379" expose: - "6379" networks: - default entrypoint: - redis-server - --slaveof redis-master 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 # - --masterauth xxx redis-slave2: image: redis ports: - "12662:6379" expose: - "6379" entrypoint: - redis-server - --slaveof redis-slave1 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 # - --masterauth xxx 主从自动切换(Sentinel模式) 这时需要引入 Sentinel 的概念 Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务: 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。 默认的Sentinel配置去掉注释后长这样 port 26379 daemonize no pidfile /var/run/redis-sentinel.pid logfile "" dir /tmp # 监视主服务器,下线master需要2个Sentinel同意 sentinel monitor mymaster redis-master 6379 2 # 30秒内有效回复视为master健康,否则下线 sentinel down-after-milliseconds mymaster 30000 # 故障转移时,从服务器同部数最大值 sentinel parallel-syncs mymaster 1 # 1. 同一个sentinel对同一个master两次failover之间的间隔时间。 # 2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。 # 3.当想要取消一个正在进行的failover所需要的时间。 # 4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了。 sentinel failover-timeout mymaster 90000 基本格式是sentinel <选项的名字> <主服务器的名字> <选项的值> 原本我想通过docker-compose up --scale redis-sentinel=3直接启动3个容器,结果发现这容器竟然会修改配置文件,所以只能分开写了 初版 # docker-compose up --scale redis-sentinel=3 虽然可以启动,但是3个容器同时写同一个文件,感觉不是很好 version: '3' services: redis-master: image: redis ports: - "12660:6379" expose: - "6379" entrypoint: - redis-server - --save 1 1 volumes: - ./data:/data # 重启策略改为no手动让他宕机模拟主从切换 restart: "no" redis-slave1: image: redis ports: - "12661:6379" expose: - "6379" entrypoint: - redis-server - --slaveof redis-master 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 # - --masterauth xxx redis-slave2: image: redis ports: - "12662:6379" expose: - "6379" entrypoint: - redis-server - --slaveof redis-slave1 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 # - --masterauth xxx redis-sentinel: image: redis expose: - "26379" entrypoint: - redis-server - /usr/local/etc/redis/redis.conf - --sentinel volumes: - ./redis-sentinel.conf:/usr/local/etc/redis/redis.conf 最终版: version: '3' services: redis-master: image: redis ports: - "12660:6379" expose: - "6379" entrypoint: - redis-server - --save 1 1 volumes: - ./data:/data # 重启策略改为no手动让他宕机模拟主从切换 restart: "no" redis-slave1: image: redis ports: - "12661:6379" expose: - "6379" entrypoint: - redis-server - --slaveof redis-master 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 # - --masterauth xxx redis-slave2: image: redis ports: - "12662:6379" expose: - "6379" entrypoint: - redis-server - --slaveof redis-slave1 6379 - --slave-serve-stale-data yes #当从机与主机断开连接时,或者当复制仍在进行时,slave仍然会回复client请求, 尽管数据可能会出现过期或者如果这是第一次同步,数据集可能为空。 - --slave-read-only yes #0作为一个特殊的优先级,标识这个slave不能作为master - --slave-priority 100 # - --masterauth xxx redis-sentinel1: image: redis expose: - "26379" entrypoint: - redis-server - /usr/local/etc/redis/redis.conf - --sentinel volumes: - ./redis-sentinel1.conf:/usr/local/etc/redis/redis.conf redis-sentinel2: image: redis expose: - "26379" entrypoint: - redis-server - /usr/local/etc/redis/redis.conf - --sentinel volumes: - ./redis-sentinel2.conf:/usr/local/etc/redis/redis.conf redis-sentinel3: image: redis expose: - "26379" entrypoint: - redis-server - /usr/local/etc/redis/redis.conf - --sentinel volumes: - ./redis-sentinel3.conf:/usr/local/etc/redis/redis.conf sentinel启动之后,配置发生了变化 这些内容没了 sentinel monitor mymaster redis-master 6379 2 sentinel down-after-milliseconds mymaster 30000 sentinel parallel-syncs mymaster 1 变成 sentinel myid 3ae98d6815c1a9b941f8283b7e48bfeef7435905 sentinel deny-scripts-reconfig yes # Generated by CONFIG REWRITE sentinel config-epoch mymaster 0 sentinel leader-epoch mymaster 0 sentinel known-replica mymaster 172.24.0.4 6379 sentinel known-sentinel mymaster 172.24.0.7 26379 19ac7e0519e3c30a75e23bac34a7033594256c54 sentinel known-sentinel mymaster 172.24.0.5 26379 c596734a7f55ba2b6c7e3e81562aa6687e45fdeb sentinel current-epoch 0 之后通过docker ps和docker stop手动停掉了master那个容器,sentinel发觉了,并重新选主. 此时通过redis-cli输入info,发现它已经成功变成了可以写入数据的master # Generated by CONFIG REWRITE sentinel config-epoch mymaster 1 sentinel leader-epoch mymaster 1 sentinel known-replica mymaster 172.24.0.3 6379 sentinel known-replica mymaster 172.24.0.2 6379 sentinel known-sentinel mymaster 172.24.0.7 26379 19ac7e0519e3c30a75e23bac34a7033594256c54 sentinel known-sentinel mymaster 172.24.0.5 26379 c596734a7f55ba2b6c7e3e81562aa6687e45fdeb sentinel current-epoch 1 此时重启master,虽然他以server形式启动,但是角色已经自动被贬为slave. 此时master没有变化,所以sentinel的配置内容没有变 集群版(N主N从) 集群基于16384个slot做分片.目前各语言客户端实现比较少.所以用的人不是很多. 在docker中运行时,需要使用host网络模式(--net=host) redis-trib.rb redis版本<5时,可以用redis-trib.rb建集群 ./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 \ 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 # 引入新节点 ./redis-trib.rb add-node 127.0.0.1:7006 <任意节点IP>:<节点端口> # 重新分片 ./redis-trib.rb reshard <任意节点IP>:<节点端口> redis-cli =5直接用redis-cli即可. redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \ 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \ --cluster-replicas 1 redis-cli --cluster reshard 127.0.0.1:7000 redis-cli reshard <host>:<port> --cluster-from <node-id> --cluster-to <node-id> --cluster-slots <number of slots> --cluster-yes redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 # Adding a new node as a replica redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e redis-cli --cluster del-node 127.0.0.1:7000 `<node-id>` 集群指令 CLUSTER REPLICATE <master-node-id> cluster nodes Kubernetes 个人觉得吧,redis跟Kubernetes不是特别契合.kubernetes本身有网络瓶颈的问题,通过svc去访问,频繁DNS解析也不好对吧.这对于高频访问redis的场景来说是致命的. 而且pod这种易失架构注定了在Kubernetes上面用redis要么用数据卷挂载,要么用主从自动切换模式(纯内存的话至少要1主2从,并且用反亲和度错开彼此的运行节点). 主从和单机版倒好解决,单机的话挂载好数据卷,主从的话,主和从分开2个deploy/statefulset部署即可. 但是集群版就比较麻烦.官方的设计还是偏向于传统二进制人工运维,没有做到云原生 看了一下官方的helm chart,也是用的主从模式. codis codis是redis集群没出来之前,豌豆荚团队做的一个方案,通过proxy,隔离了后端的redis集群 优点 支持Kubernetes 有web图形界面,方便运维 Redis获得动态扩容/缩容的能力,增减redis实例对client完全透明、不需要重启服务 缺点 稳定性堪忧 依赖于国内的豌豆荚团队开发,迭代速度较慢 原版的docker镜像较大,没有根据组件进行分开 基于redis 3.x,而且很多原生的redis指令被阉割了 强依赖注册中心(Zookeeper、Etcd、Fs) 我们用了几个月吧,到后期频繁出现 ERR handle response, backend conn reset 此外,日常观察发现pod退出/重启困难.如果某个group节点全部挂掉的话,整个集群将不可读写. 综上,codis已经影响到了严重影响到了我们程序的正确性,决定弃用codis.改为普通的1主N从的模式. 主挂了之后导致服务不可用 #1356 参考链接 【Redis笔记】 第4篇: redis.conf中Replication配置项说明 Redis 配置文件详解 redis命令参考 redis主从复制常见的一些坑 Redis 的各项功能解决了哪些问题? 集群教程

优秀的个人博客,低调大师

并发编程专题五-AbstractQueuedSynchronizer源码分析

PS:外号鸽子王不是白来的,鸽了好几天,也是因为比较忙,时间太少了,这篇东西有点多,需要慢慢消化。不知不觉居然写了4个多小时.... 一、什么是AQS aqs是AbstractQueuedSynchronizer的简称,是用来构建锁或者其他同步组件(信号量、事件等)的基础框架类。JDK中许多并发工具类的内部实现都依赖于AQS,如ReentrantLock, Semaphore, CountDownLatch等等。 二、AQS的设计模式 2.1模板方法设计模式 在学习原理和源码之前,我们先了解一中设计模式。模板方法设计模式。 模板方法设计模式定义一个操作中算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤。 通俗来说就是完成一件事情,有固定的数个步骤,但是每个步骤根据对象的不同,而实现细节不同;就可以在父类中定义一个完成该事情的总方法,按照完成事件需要的步骤去调用其每个步骤的实现方法。每个步骤的具体实现,由子类完成。 例如 发短信有以下有以下四个步骤: 1,需要发送给的人; 2编写内容; 3,发送日期 4,调用短信网关发送 发邮件也同样有以下四个步骤: 1,需要发送给的人; 2编写内容; ,发送日期 4,调用邮箱服务发送 这时,可以清楚地发现,无论是发短信还是发邮件,它们的步骤几乎是相同的: 1,需要发送给的人; 2发送内容3,发送日期 4,发送。只有具体到发送方法的时候,它们有些步骤才不同。这时我们就可以把相同的步骤提取出来,生成一个模版,进行共用,而到具体的内容时,它们再有具本的实现。具体到代码时,模版就可以用抽象类实现,具体的内容可以到它的子类中实现。 代码如下 import java.util.Date; /** * @Auther: DarkKing * @Date: 2019/4/21 12:09 * @Description: */ public abstract class SendTemplate { //发送给谁,具体发送给谁需要子类去实现 public abstract void toUser(); //发送内容,具体发送内容需要子类去实现 public abstract void content(); //发送日期,因为日期都是一样的,所以父类就可以实现掉 public void date() { System.out.println(new Date()); } //发送方法,不同的发送方式肯定要实现不同的发送方法 public abstract void send(); //发送消息,框架方法-模板方法 public void sendMessage() { toUser(); content(); date(); send(); } } /** * @Auther: DarkKing * @Date: 2019/4/21 12:09 * @Description: 短信发送模板实现类 */ public class SendSms extends SendTemplate { @Override public void toUser() { System.out.println("to Pistachio"); } @Override public void content() { System.out.println(" I LOVE YOU ❤"); } @Override public void send() { System.out.println("set sms"); } public static void main(String[] args) { SendTemplate sendSms = new SendSms(); sendSms.sendMessage(); } } AQS就是采用了模板方法的设计模式,除了AQS,Spring加载配置的过程同样也是使用了这种设计模式。具体以后Spring源码专题在给大家详细说明 2.2AQS中的方法 既然知道AQS所使用的是模板方法设计模式,那具体都有哪些方法,我们现在列举一下。 1、模板方法 独占式(独占锁) 共享式(共享锁) 方法描述 acquire acquireShared 获取锁方法,获取同步状态 acquireInterruptibly acquireSharedInterruptibly 获取锁方法,同acquire,但可以响应中断 tryAcquireNanos tryAcquireSharedNanos 获取锁方法 release releaseShared 释放锁 2、需要子类覆盖的流程方法 独占式获取 tryAcquire 独占式释放 tryRelease 共享式获取 tryAcquireShared 共享式释放 tryReleaseShared 这些方法我们可以看到,AQS中只抛出了一个UnsupportedOperationException异常,所以需要我们在子类中具体去实现。 3、其他方法 isHeldExclusively:判断同步器是否处于独占模式 除此之外,AQS还定义了一个volatile变量state,用于记录锁的状态 getState:获取当前的同步状态 setState:设置当前同步状态 compareAndSetState:使用CAS设置状态,保证状态设置的原子性 为什么Doug Lea大师要这样设计AQS呢?AQS面向的是锁的实现者,而我们在使用Lock锁的时候,只需要调用Lock对应的方法即可,屏蔽了实现细节。而对于锁的实现者来说,简化了锁的实现方式,例如同步状态管理,线程排队等底层操作。隔离了锁的实现者和锁的使用者。从而进行了解耦,当我们在使用ReentrantLock等锁的时候,完全感觉不到AQS的存在。 2.3自定义锁的实现 既然我们了解了AQS中的一些方法,那我们就通过实现父类中的方法,来自己实现一个锁,加深下对模板方法的理解。 自己实现锁的话,首先我们实现Lock接口,Lock共有6个接口,之前我们都已经讲过了,大家如果感兴趣,我的并发编程专题里查看哈~ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.AbstractQueuedSynchronizer; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; /** * @Auther: DarkKing * @Date: 2019/4/21 12:09 * @Description: */ public class SelfLock implements Lock{ //aqs中state 表示获取到锁的状态 state=1 获取到了锁,state=0,表示这个锁当前没有线程拿到 //定义一个内部类,实现AQS模板方法 private static class Sync extends AbstractQueuedSynchronizer{ //是否占用 protected boolean isHeldExclusively() { return getState()==1; } //尝试获取锁 protected boolean tryAcquire(int arg) { //CAS操作,首先对比锁是否被获取到,获取到的话就将锁的状态置为1,否则获取锁失败 if(compareAndSetState(0,1)) { //设置锁的拥有者为当前线程 setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } //尝试释放锁 protected boolean tryRelease(int arg) { //如果锁的状态为0,则不需要释放,抛出异常 if(getState()==0) { throw new UnsupportedOperationException(); } //设置锁的拥有者为null,并且状态设置为0 setExclusiveOwnerThread(null); setState(0); return true; } //Condition对象,用于对锁的对象唤醒和等待 Condition newCondition() { return new ConditionObject(); } } private final Sync sycn = new Sync(); //获取锁。如果锁已被其他线程获取,则进行等待 @Override public void lock() { sycn.acquire(1); } //可以直接调用父类的方法。该方法和lock的区别就是可以响应中断。 @Override public void lockInterruptibly() throws InterruptedException { sycn.acquireInterruptibly(1); } //它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败返回false @Override public boolean tryLock() { return sycn.tryAcquire(1); } //带时间戳的获取锁,如果等待时间超过,则获取失败 @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sycn.tryAcquireNanos(1, unit.toNanos(time)); } //释放锁 @Override public void unlock() { sycn.release(1); } //返回Condition对象 @Override public Condition newCondition() { return sycn.newCondition(); } } 以上方法我们实现了获取锁和释放锁的接口。从实现中我们发现获取锁的时候使用了CAS操作,但释放锁的时候没有进行CAS操作。这样写会不会出现问题呢?接下来写个测试类。测试下锁能否正常使用 import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import com.xiangxue.tools.SleepTools; /** * @Auther: DarkKing * @Date: 2019/4/21 12:09 * @Description: */ public class TestMyLock { public void test() { final Lock lock = new SelfLock(); class Worker extends Thread { public void run() { while (true) { lock.lock(); try { SleepTools.second(1); System.out.println(Thread.currentThread().getName()); SleepTools.second(1); } finally { lock.unlock(); } SleepTools.second(2); } } } // 启动10个子线程 for (int i = 0; i < 10; i++) { Worker w = new Worker(); w.setDaemon(true); w.start(); } // 主线程每隔1秒换行 for (int i = 0; i < 10; i++) { SleepTools.second(1); System.out.println(); } } public static void main(String[] args) { TestMyLock testMyLock = new TestMyLock(); testMyLock.test(); } } 我们定义了10个线程,打印出当前获取锁的线程。从打印我们可以看出来,每次只会打印出一个线程名。说明我们写的锁是起作用的。那为什么释放的时候不需要CAS操作呢。当我们一个线程获取到锁,其他线程又都在做什么呢。接下来我们走进AQS源码中,进行学习。 三、AQS源码解析 我们上面的测试用例中。当我们一个线程获取到锁,其他线程再去获取锁的时候,我们返回了false,线程进入的等待状态,那既然线程进入了等待状态,等待锁被释放的时候了进行唤醒。那么必然要有个地方存储我们进行等待的线程的一个数据结构。 3.1、AQS中的数据结构-节点和同步队列 AQS的中的等待的线程全部存储在一个同步队列里,它是一个先进先出的数据结构,先进来的线程会先被唤醒。同时这个队列还是个双向列表。上一个节点指向下一个节点。还有头指示器指向第一个节点。尾指示器指向最后一个节点。 3.2同步队列节点属性 打开AQS的源码,找到Node类,所有属性如下 字段名 属性值 描述 CANCELLED 1 线程等待超时或者被中断了,需要从队列中移走 SIGNAL -1 后续的节点等待状态,当前节点如果完成则通知后面的节点去运行 CONDITION -2 当前节点处于等待队列 PROPAGATE -3 共享,表示状态要往后面的节点传播 0 初始状态 waitStatus 标识当前节点状态,就是上面的那几个常量字段。 Nodeprev 表示当前节点的前驱节点 Nodenext 表示当前节点的后驱节点 Threadthread 表示当前节点存放的线程 Node nextWaiter 指向等待队列节点 3.3获取锁的过程 当我们执行获取acquire锁的方法时 如果获取失败,则会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,该方法首先会执行addWaiter(Node.EXCLUSIVE), arg), 该方法是将线程包装成Node节点,通过CAS操作,将节点加入到同步队列中。 尾节点变化如图所示 因为多线程的原因,为了保持原子性,所以这里需要使用CAS操作。不然可能会出现尾部节点数据丢失等问题。如果CAS对比操作失败,则执行enq方法,将该操作放入循环中。直到添加成功。(CAS的基本操作) 当Node已经放入了同步队列之后,那么这个线程说明需要进行等待。排队排到我的时候我要去获取锁。所以acquireQueued方法中,会一直循环获取锁。 首先获取前置节点,如果前置节点是头节点,则直接去尝试获取锁。当获取锁成功之后,将该节点设置为头节点,之前的头结点设置为null,脱钩帮助GC。然后returenfalse;说明获取锁成功。不需要在进行等待。 如果获取锁失败,则会执行parkAndCheckInterrupt()方法,将自己阻塞。这是整个获取锁的一个过程。 3.4 释放锁的过程 释放锁的方法为我们定义的unlock();实际调用的AQS中的release方法。 该方法首先执行我们实现的TryRelease尝试释放锁,如果锁释放成功,则获取头结点,然后执行unparkSuccessor(h)方法。 这个方法主要就是获取头结点的下一个节点,如果下个节点为null,则将下个节点的前一个节点设置为尾部,否则的话然后进行唤醒。 首节点的变化如图所示 当一个节点释放锁之后,修改头结点时同时只会有一个线程去操作,所以不需要CAS操作,直接将头节点设为null,然后修改同步器头部指向下一个节点。 整体获取和释放锁的过程如下所示 3.5、AQS中的数据结构-节点和等待队列 每一个锁都有一个Condition对象,该对象主要实现await和signal等方法。用于线程的等待和唤醒。因此每个Condition对象中肯定也存在一个等待队列。 等待队列的数据结构 等待队列和同步队列的区别 1、等待队列里的节点和同步队列节点都是同一个属性。唯一的不同就是等待队列是一个单向链表,而非双向链表。 2、一个同步器(锁)里面,只会有一个同步队列,但可以有多个等待队列。如下图所示。 一个锁可以创建个多个Condition,每一个Condition下都会有一个等待队列。 private Lock lock = new ReentrantLock(); private Condition keCond = lock.newCondition(); private Condition siteCond = lock.newCondition(); 3.6、await方法过程 await表示使当前线程进入等待状态。 首先调用addConditionWaiter方法将该线程包装成Node加入到阻塞队列中去,然后调用fullyRelease方法,将该线程所持有的锁进行释放。在while中判断线程是否被唤醒,如果没有,则进行阻塞。 await方法过程 3.7、single方法过程 single唤醒等待的一个线程。 当执行single的时候,首先需要从等待队列中取出一个节点,如果不为null,则执行doSignal方法。 如果等待队列的下一个节点为null,则把末尾节点设为null 然后调用transferForSignal方法,判断当前节点的状态,如果不能修改值,则取消,如果修改成功,则嗲用enq。将该节点移动到同步队列尾部,并设置waitStatus为SIGNAL。满足条件后唤醒线程。 single流程图如下 因为每个condition对象都会有一个同步机制,而且调用single会指定唤醒对应等待队列的线程,不会丢失信息。所以建议使用single方法唤醒,而不是调用singleAll,而且每次调用singleAll还要将所有等待队列的节点全部移动到同步队列中。 大体上学完AQS,要了解模板方法设计,会自己手动实现锁,了解获取锁和释放锁,以及基于锁的等待和唤醒机制。大家如果还有什么问题,可以加我微信一起讨论哈。 其他阅读 并发编程专题

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册