首页 文章 精选 留言 我的

精选列表

搜索[并发编程],共10000篇文章
优秀的个人博客,低调大师

详解java并发包源码之AQS独占方法源码分析

AQS 的实现原理 学完用 AQS 自定义一个锁以后,我们可以来看一下刚刚使用过的方法的实现。 分析源码的时候会省略一些不重要的代码。 AQS 的实现是基于一个 FIFO 队列的,每一个等待的线程被封装成Node存放在等待队列中,头结点是空的,不存储信息,等待队列中的节点都是阻塞的,并且在每次被唤醒后都会检测自己的前一个节点是否为头结点,如果是头节点证明在这个线程之前没有在等待的线程,就尝试着去获取共享资源。 AQS 的继承关系 AQS 继承了AbstractOwnableSynchronizer,我们先分析一下这个父类。 public abstract class AbstractOwnableSynchronizer implements java.io.Serializable { protected AbstractOwnableSynchronizer() { } /** * 独占模式下的线程 */ private transient Thread exclusiveOwnerThread; /** * 设置线程,只是对线程的 set 方法 */ protected final void setExclusiveOwnerThread(Thread thread) { exclusiveOwnerThread = thread; } /** * 设置线程,对线程的 get 方法 */ protected final Thread getExclusiveOwnerThread() { return exclusiveOwnerThread; } } 父类非常简单,持有一个独占模式下的线程,然后就只剩下对这个线程的 get 和 set 方法。 AQS的内部类 AQS 是用链表队列来实现线程等待的,那么队列肯定要有节点,我们先从节点讲起。 Node 类,每一个等待的线程都会被封装成 Node 类 Node 的域 public class Node { int waitStatus; Node prev; Node next; Thread thread; Node nextWaiter; } waitStatus:等待状态 prev:前驱节点 next:后继节点 thread:持有的线程 nextWaiter:condiction 队列中的后继节点 Node 的 status: Node 的状态有四种: CANCELLED,值为 1,表示当前的线程被取消,被打断或者获取超时了 SIGNAL,值为 -1,表示当前节点的后继节点包含的线程需要运行,也就是 unpark; CONDITION,值为 -2,表示当前节点在等待 condition,也就是在 condition 队列中; PROPAGATE,值为 -3,表示当前场景下后续的 acquireShared 能够得以执行; 取消状态的值是唯一的正数,也是唯一当排队排到它了也不要资源而是直接轮到下个线程来获取资源的 AQS 中的方法源码分析 acquire 这个方法执行了: tryAcquire public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } 看到上面的 tryAcquire 返回 false 后就会调用addWaiter新建节点加入等待队列中。参数 EXCLUSIVE 是独占模式。 private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // 拿到尾节点,如果尾节点是空则说明是第一个节点,就直接入队就好了 Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 如果尾节点不是空的,则需要特殊方法入队 enq(node); return node; } 在addWaiter方法创建完节点后,调用 enq 方法,在循环中用 CAS 操作将新的节点入队。 因为可能会有多个线程同时设置尾节点,所以需要放在循环中不断的设置尾节点。 private Node enq(final Node node) { for (;;) { Node t = tail; // 查看尾节点 if (t == null) { // Must initialize // 尾节点为空则设置为刚刚创建的节点 if (compareAndSetHead(new Node())) tail = head; } else { // 尾节点 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } 在这里,节点入队就结束了。 那么我们回来前面分析的方法, public final void acquire(long arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } 刚刚分析完了addWaiter方法,这个方法返回了刚刚创建并且加入的队列。现在开始分析acquireQueued方法。 final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; // 在循环中去获取锁 for (;;) { // 拿前一个节点 final Node p = node.predecessor(); // 如果前一个节点是头结点则尝试着去获取锁 if (p == head && tryAcquire(arg)) { // 拿到锁后把这个节点设为头结点,这里 setHead 会把除了 next 以外的数据清除 setHead(node); p.next = null; // help GC failed = false; return interrupted; } // 这个方法查看在获取锁失败以后是否中断,如果否的话就调用 // parkAndCheckInterrupt 阻塞方法线程,等待被唤醒 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } acquireInterruptibly 因为很像所以顺便来看一下acquireInterruptibly所调用的方法:在此我向大家推荐一个架构学习交流裙。交流学习裙号:821169538,里面会分享一些资深架构师录制的视频录像 private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return; } // 只有这一句有差别,获取失败了并且检测到中断位被设为 true 直接抛出异常 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } } acquireNanos 再来看一下有限时间的,当获取超时以后会将节点 Node 的状态设为 cancel,设置为取消的用处在后面的 release 方法中会有体现。 private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout <= 0L) return false; final long deadline = System.nanoTime() + nanosTimeout; final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; failed = false; return true; } nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) return false; if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); if (Thread.interrupted()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } } 总结一下过程 release 这个方法首先去调用了我们实现的 tryRelease,当结果返回成功的时候,拿到头结点,调用 unparkSuccessor 方法来唤醒头结点的下一个节点。在此我向大家推荐一个架构学习交流裙。交流学习裙号:821169538,里面会分享一些资深架构师录制的视频录像 public final boolean release(long arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } private void unparkSuccessor(Node node) { int ws = node.waitSatus; // 因为已经获取过锁,所以将状态设设为 0。失败也没所谓,说明有其他的线程把它设为0了 if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * 一般来说头结点的下一个节点是在等待着被唤醒的,但是如果是取消的或者意外的是空的, * 则向后遍历直到找到没有被取消的节点 * */ Node s = node.next; // 为空或者大于 0,只有 cancel 状态是大于 0 的 if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }

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

Java并发机制底层实现原理-原子操作的实现原理

章节目录 原子操作含义 相关术语 保证多处理器操作原子性的两种方式 Java语言层面上实现原子操作 原子操作的含义: 原子本意是"不能被进一步分割的最小粒子",而原子操作意为,不可中断的一个或一系列操作。 相关术语 术语名称 英文 解释 缓存行 Cache line 缓存的最小操作单位 比较并交换 Compare and Swap CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生变化,表示有多线程竞争,则不交换 保证多处理器操作原子性的两种方式 通过总线锁保证操作共享变量是原子性的 如果多个处理器同事对共享变量进行读改写操作(i++是经典的读改写操作),那 么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子性的,操作完之后,共享变量的值会和期望的不一致。 如: public class IncreaceThread implements Runnable { public int i = 1; public void run() { this.i = ++i; } public int getI(){ return this.i; } public static void main(String[] args) { IncreaceThread increaseThread = new IncreaceThread(); Thread thread1 = new Thread(increaseThread); Thread thread2 = new Thread(increaseThread); thread1.start(); thread2.start(); System.out.println(increaseThread.getI()); } } 计算出最终i的值有可能是2,而不是3。 原因可能是多个处理器同时从各自的缓存中读取变量i,分别进行+1操作,然后分别写入到系统内从中。 想要保证改写共享变量的操作是原子的,那就必须保证CPU1读改写共享变量的 时候,CPU2不能操作缓存了该共享变量内存地址的缓存。 处理器使用总线锁来解决这个问题,所谓总线锁,就是处理器提供一个LOCK#信号,当一个处理器在总线上输出此信号时, 其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。 使用缓存锁保证原子性 第二个机制是通过缓存锁定来保证原子性。 同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU 和内存之间的通信锁住,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁的开销比较大。 使用缓存锁定开销会变小,缓存锁定,是指内存区域如果被缓存在处理器缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性来保证操作的原子性。 Java语言层面上实现原子操作 在Java中通过锁和循环CAS的方式实现原子操作 使用循环CAS实现原子操作 JVM中的CAS操作正式利用了处理器提供的cmpxchg指令实现的,自旋基本思路就是循环进行CAS操作知道成功为止,以下代码实现可一个基于CAS线程安全的计数器方法safeCount 和一个非线程安全的计数器count。 package com.imooc.item; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; public class Counter { private AtomicInteger atomicInteger = new AtomicInteger(0); private int i = 0; public void safeCount() { for (; ; ) { int i = atomicInteger.get(); boolean suc = atomicInteger.compareAndSet(i, ++i); if (suc) { break; } } } //线程计数器 public void count() { i++; } public static void main(String[] args) { final Counter cas = new Counter(); List<Thread> ts = new ArrayList<Thread>(600); long start = System.currentTimeMillis(); for (int i = 0; i < 100; i++) { Thread t = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { cas.count(); cas.safeCount(); } } }); ts.add(t); } //开始运行线程 for (Thread t : ts) { t.start(); } //等待所有线程执行完毕 for (Thread t : ts) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(cas.i); System.out.println(cas.atomicInteger.get()); System.out.println(System.currentTimeMillis() - start); } } 运行结果如下所示: 996309 1000000 61 CAS实现原子操作的三大问题 ABA 问题 因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则 更新,但如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会 发现它的值没有发生变化,但实际上却变了,ABA问题的解决思路就是使用版 本号,在变量前面加上版本号,每次变量更新的时候把版本号加1。 那么A->B->A问题就会变成1A-2B-3A. 循环时间开销大 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。常用做法是控制自旋的次数。 只能保证一个共享变量的原子操作 当对一个共享变量执行操作时,我们可以使用循环CAS的方式保证原子操作, 但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候可 以用锁。 使用锁机制实现原子操作 锁机制保证了只有获得锁的线程才能够 操作锁定的线程共享区域(临界区), JVM内部实现了很多的锁机制:偏向锁、轻量级锁和互斥锁。其中,除了偏向 锁,JVM实现锁的方式都使用了循环CAS,即当一个线程想进入同步块的时候使 用循环CAS的方式获取锁,当它退出同步块的时候使用循环CAS释放锁。

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

监控服务器ssh登录,并发送报警邮件

最近想监控下云主机的ssh登录情况,所以开始写ssh登录报警监控。实现方式并不难。 一:邮箱申请开启SMTP 在邮箱中选择“设置”----->“账户” 在如下图处开启POP3/SMTP服务,并生成授权码。 二:修改相关参数 登录要进行ssh登录监控的服务器,在/etc/ssh创建"sshrc"文件: #!/bin/bash #获取登录者的用户名 user=$USER #获取登录者的IP地址 ip=${SSH_CLIENT%% *} #获取登录的时间 time=$(date +%F%t%k:%M) #服务器的IP地址 hostname=$(hostname) echo "content=$time,$user,$ip,$hostname" > log python /etc/ssh/testEmail.py "$time" "$user" "$ip" "$hostname" 并在如上自定义路径中创建“testEmail.py”文件。 添加颜色部分,按邮箱,授权码,以及相关术语进行修改。 #!/usr/bin/python # -*- coding: UTF-8 -*- import smtplib from email import encoders from email.header import Header from email.mime.text import MIMEText from email.utils import parseaddr, formataddr import sys def send_mail(dtime,duser,dip,dhostname): #基础信息 # from_addr = input("From:") from_addr = "yaohong@qq.com" password = "授权码" #to_addr = from_addr to_addr = "yaohong@qq.com" # password = raw_input("Password:") # to_addr = input("To:") def _format_addr(s): name, addr = parseaddr(s) return formataddr((Header(name, 'utf-8').encode(), addr)) smtp_server = "smtp.qq.com" mimetex = '您的机器:',dhostname,',于:',dtime,',被IP:',dip,'以账号',duser,'进行登录,请确认是否为公司员工。' #构造邮件 msg = MIMEText(''.join(mimetex), 'plain', 'utf-8') msg['From'] = _format_addr("yaohong") msg['To'] = _format_addr("yaohong@qq.com") msg['Subject'] = Header("来自yaohong", 'utf-8').encode() #发送邮件 server = smtplib.SMTP_SSL(smtp_server, 465) server.set_debuglevel(1) server.login(from_addr, password) server.sendmail(from_addr, [to_addr], msg.as_string()) server.quit() if __name__ == "__main__": send_mail(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]) 三:登录检测 安装完后,再次进行ssh登录该服务器会收到邮件如下图,则表示ssh被监控成功。

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

net core 程序docker打包镜像并发布到官方store

学习一个技术的第一步,总是要先打印或显示一个hello world的。当然,学习docker也不例外。上一篇文章已经简单的介绍了环境的安装和配置。接下来就要打印我们的hello world了。 首先我们来跑一跑官方的hello world程序 #运行官方的hello-world镜像,顺带可以检查一下安装配置是否有问题。 docker run hello-world 出来这个就基本差不多了。 centos: windows: 在接下来跑一个我们自己的。net core 版本的hello world vs2017 preview装起来先,然后创建一个控制台项目.项目右键 add ,选择docker support。添加项目对docker的支持 完事之后大约就这样子 为了发布方便,我们吧项目改一下名字,czd890 是我在docker 注册的账号。所以我所有发布的项目都发布到自己的命名空间下。 然后,我们在main方法里面写上我们的hello world static void Main(string[] args) { Console.WriteLine("Hello World!"); Console.WriteLine("in docker -- private registry"); } 在然后,选择release模式,rebuild我们的项目 就出来 我们来跑一跑我们自己的 hello world。 --rm参数表示run完了之后自动参数容器。这样子,我们docker ps -a 显示所有容器的时候,就不会看到一个超长列表了...... 在在然后,我们吧我们的镜像发布到官方 首先,我们需要登录我们的账号: 在接着,发布:  最后,在我们的centos中来跑一下我们刚才发布的czd890/hello-world镜像

资源下载

更多资源
Mario

Mario

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

腾讯云软件源

腾讯云软件源

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

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册