- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章Java并发编程之JUC并发核心AQS同步队列原理剖析由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
。
队列同步器AbstractQueuedSynchronizer(简称AQS),AQS定义了一套多线程访问共享资源的同步器框架,是用来构建锁或者其他同步组件的基础框架,是一个依赖状态(state)的同步器。Java并发编程的核心在java.util.concurrent(简称juc)包,而juc包的大部分工具都是以AQS为基础进行构建的,例如Semaphore、ReentranLock、CountDownLatch、CyclicBarrier等,它的作者是鼎鼎大名的Doug Lea.
AQS具备特性 。
它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。state的访问方式有三种
AQS有两种资源共享方式:Exclusive(独占式) 和 Share(共享式)。所谓独占式是指依据AQS中的state控制状态,只有一个线程能够进行工作(其它参与调度的线程会进入阻塞状态,如ReentrantLock);共享式是指,依据AQS中的state控制状态,可以有多个满足条件的线程同时执行(如Semaphore/CountDownLatch)。 。
AQS定义两种队列 。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。一般通过定义内部类Sync继承AQS将同步器所有调用都映射到Sync对应的方法.
自定义同步器实现时主要实现以下几种方法:
实现自定义同步组件时,将会调用同步器提供的模板方法,这些(部分)模板方法与描述如下。同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步状态和查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义.
。
。
AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列,Java中的CLH队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制.
这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 ASQ 队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点线程 .
。
条件等待队列是单向链表实现的,此时Node(下面会介绍)中pre和next都为null。Condition是一个多线程间协调通信的工具类,使得某个、或者某些线程一起等待某个条件(Condition),只有当该条件具备时,这些等待线程才会被唤醒,从而重新争夺锁.
。
同步队列中的节点(Node)用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点,节点的属性类型与名称等,Node类基本属性定义如下所示,它是在AbstractQueuedSynchronizer中的一个内部类.
注意:如果Node在条件队列当中,Node必须是独占模式,不能是共享模式.
static final class Node { static final Node SHARED = new Node(); static final Node EXCLUSIVE = null; static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; volatile int waitStatus; volatile Node prev; volatile Node next; volatile Thread thread; Node nextWaiter;}
Node pre:前驱节点,当前节点加入到同步队列中被设置(尾部添加) 。
Node next:后继节点 。
Thread thread:节点同步状态的线程 。
Node nextWaiter:等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段 。
int waitStatus:等待状态,标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态,使用CAS更改状态,volatile保证线程可见性,高并发场景下,即被一个线程修改后,状态会立马让其他线程可见,五种状态分别为:
。
。
同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部,同步队列的基本结构如图所示.
同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。 涉及两个变化:
head节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点,节点的变化过程如下 。
同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点,该过程如下图所示。涉及两个变化:
设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节 点设置成为原首节点的后继节点并断开原首节点的next引用即可.
。
acquire方法(独占获取)源码分析 。
通过调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出.
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();}
上面方法中首先调用自定义同步器实现的tryAcquire(int arg)方法(重写该方法),该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现.
下面看下addWaiter方法的实现:把当前线程构建为Node节点;判断尾结点是否为空,通过CAS的方式将当前节点放到队列尾部;如果尾结点不为空或者前面CAS插入尾结点失败,调用enq方法,通过自旋的方式插入尾结点.
private Node addWaiter(Node mode) { // 1. 将当前线程构建成Node类型 Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; // 2. 1当前尾节点是否为null? if (pred != null) { // 2.2 将当前节点尾插入的方式 node.prev = pred; // 2.3 CAS将节点插入同步队列的尾部 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node;}
下面看下enq方法的实现:判断尾结点是否为空,如果为空,则通过CAS的方式创建一个空的头结点(Thread为空),并将尾结点也指向头结点;如果尾结点不为空或者上面CAS创建头结点失败,将当前队列的前驱指针指向原来的尾结点,通过CAS的方式将当前节点放到队列尾部,将原来尾结点的后继指针指向当前节点;如果前面都失败了,进行下一次循环。当前线程构造的node节点通过addWaiter方法执行入队之后,其waitStatus为0,头结点的waitStatus也是0,此时是下面这种结构 。
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; //set尾部节点 if (compareAndSetTail(t, node)) {//当前节点置为尾部 t.next = node; //前驱节点的next指针指向当前节点 return t; } } }}
通过addWaiter 方法把线程添加到链表后, 会接着把 Node 作为参数传递给acquireQueued 方法,去竞争锁 。
/** * 已经在队列当中的Thread节点,准备阻塞等待获取锁 */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)) {//如果前驱结点是头结点,才tryAcquire,其他结点是没有机会tryAcquire的。 setHead(node);//获取同步状态成功,将当前结点设置为头结点。 p.next = null; // help GC failed = false; return interrupted; } /** * 如果前驱节点不是Head,通过shouldParkAfterFailedAcquire判断是否应该阻塞 * 前驱节点信号量为-1,当前线程可以安全被parkAndCheckInterrupt用来阻塞线程 */ if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); }}
下面是setHead方法的实现 。
private void setHead(Node node) { head = node; node.thread = null; node.prev = null; }
在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,这是为什么?原因有两个,如下.
如果前驱节点不是Head,通过shouldParkAfterFailedAcquire判断是否应该阻塞:如果前驱节点waitStatus为-1(SIGNAL的状态),当前线程可以安全的被parkAndCheckInterrupt用来阻塞线程;通过循环扫描链表把 CANCELLED 状态的节点移除;如果前驱节点waitStatus不是-1,则通过CAS将前驱节点的waitStatus改为-1.
第一次循环进入shouldParkAfterFailedAcquire方法时head节点为0,会将其改为SIGNAL,此时会返回false,那么外层的方法acquireQueued方法会执行第二次循环进入shouldParkAfterFailedAcquire方法,此时会返回true,当前线程可以被阻塞,则调用parkAndCheckInterrupt()方法阻塞当前线程,其底层调用的是UnSafe类里面的park方法.
shouldParkAfterFailedAcquire方法会将前驱节点的waitStatus改为SIGNAL,因为只有前驱节点的状态是SIGNAL后继节点才可以被阻塞,次数除了tail节点的状态是0,其他的都是-1.
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * 若前驱结点的状态是SIGNAL,意味着当前结点可以被安全地park */ return true; if (ws > 0) { /* * 前驱节点状态如果被取消状态,将被移除出队列 */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * 当前驱节点waitStatus为 0 or PROPAGATE状态时 * 将其设置为SIGNAL状态,然后当前结点才可以可以被安全地park */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false;}
通过分析acquireQueued方法可以得出结论:头结点是获取同步状态成功的节点,头结点的所有有效后继节点线程都会被阻塞,释放锁后需要挨个唤醒头结点的后续线程节点.
独占式同步状态获取流程,也就是acquire(int arg)方法调用流程,如图所示.
release方法(独占释放)源码分析 。
当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法在tryRelease方法释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态).
public final boolean release(int arg) { if (tryRelease(arg)) {//释放一次锁 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h);//唤醒后继结点 return true; } return false;}
该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法底层使用UnSafe的unpark方法来唤醒处于等待状态的线程.
private void unparkSuccessor(Node node) { //获取wait状态 int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0);// 将等待状态waitStatus设置为初始值0 /** * 若后继结点为空,或状态为CANCEL(已失效),则从后尾部往前遍历找到最前的一个处于正常阻塞状态的结点 * 进行唤醒 */ Node s = node.next; 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);//唤醒线程}
上面方法中判断node的后继节点是空或者waitStatus是撤销状态,会从tail往前遍历找到一个离node节点最近的节点,这是为什么呢? 原因在于上面acquire时调用的enq入队方法:先compareAndSetTail(t, node)设置尾结点,然后t.next=node将前驱节点的next指针指向当前节点,如果t.next=node还没有执行的话,链表还没有建立完整,从前向后遍历时会出现遍历到t时找不到t的后继节点,从后往前遍历则不会出现这种情况.
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; //set尾部节点 if (compareAndSetTail(t, node)) {//当前节点置为尾部 t.next = node; //前驱节点的next指针指向当前节点 return t; } } }}
分析了独占式同步状态获取和释放过程后,适当做个总结:在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中(都是头结点的后继节点)并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点.
。
同步队列共享模式与独占模式异同点:
acquireShared方法(共享获取)源码分析 。
acquireShared方法会先调用tryAcquireShared获取同步状态,如果返回值小于0表示获取失败,需要进行排队;如果获取成功,则可以向下执行.
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0)//返回值小于0,获取同步状态失败,排队去;获取同步状态成功,直接返回去干自己的事儿。 doAcquireShared(arg);}
获取失败时会调用doAcquireShared方法进行排队:
先通过addWaiter方法入队,上面已经介绍了,这里就不再重复分析了,唯一不同的就是添加的是一个共享模式的node 。
判断当前节点的前驱节点是否是head,如果是的话再次调用tryAcquireShared获取同步资源,如果获取成功,则将当前node设置为head并且唤醒等待的线程节点 。
如果当前节点的前驱节点不是head或者是head但是获取同步资源失败,则跟上面的共享模式一样调用shouldParkAfterFailedAcquire方法将node的前驱节点设置为SIGNAL(-1)状态,然后阻塞当前线程.
private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED);//入队 boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor();//前驱节点 if (p == head) { int r = tryAcquireShared(arg); //非公平锁实现,再尝试获取锁 //state==0时tryAcquireShared会返回>=0(CountDownLatch中返回的是1)。 // state为0说明共享次数已经到了,可以获取锁了 if (r >= 0) {//r>0表示state==0,前继节点已经释放锁,锁的状态为可被获取 //这一步设置node为head节点设置node.waitStatus->Node.PROPAGATE,然后唤醒node.thread setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } //前继节点非head节点,将前继节点状态设置为SIGNAL,通过park挂起node节点的线程 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); }}
下面看下setHeadAndPropagate方法:
调用setHead方法将当前节点设置head 。
判断如果需要执行唤醒,通过上面的分析这里会调用doReleaseShared执行唤醒 。
/** * 把node节点设置成head节点,且Node.waitStatus->Node.PROPAGATE */private void setHeadAndPropagate(Node node, int propagate) { Node h = head; //h用来保存旧的head节点 setHead(node);//head引用指向node节点 /* 这里意思有两种情况是需要执行唤醒操作 * 1.propagate > 0 表示调用方指明了后继节点需要被唤醒 * 2.头节点后面的节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点 */ if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared())//node是最后一个节点或者 node的后继节点是共享节点 /* 如果head节点状态为SIGNAL,唤醒head节点线程,重置head.waitStatus->0 * head节点状态为0(第一次添加时是0),设置head.waitStatus->Node.PROPAGATE表示状态需要向后继节点传播 */ doReleaseShared(); }}
看下doReleaseShared方法的实现:
判断head!=null && head!=tail,然后判断head的waitStatus如果是SIGNAL则会使用CAS的方式将其改为0,这里没有直接改成PROPAGATE,是因为unparkSuccessor(h)中,如果ws < 0会设置为0,所以ws先设置为0,再设置为PROPAGATE,这里需要控制并发,因为入口有setHeadAndPropagate跟release两个,避免两次unpark。head状态为SIGNAL,且成功设置为0之后唤醒head.next节点线程,此时head、head.next的线程都唤醒了,head.next会去竞争锁,成功后head会指向获取锁的节点,也就是head发生了变化。head发生变化后h==head会不成立了,此时会重新循环,继续唤醒重新获取的新head的下一个节点.
如果本身头节点的waitStatus是0,将其设置为PROPAGATE状态。意味着需要将状态向后一个节点传播.
最后判断如果h==head,即已经没有要唤醒的节点了,跳出循环向下执行。如果h!=head,则说明head指针发生了变化,head已经指向了新唤醒的线程node,继续执行下次循环,获取新的head,唤醒head的后续节点.
private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) {//head是SIGNAL状态 /* head状态是SIGNAL,重置head节点waitStatus为0,这里不直接设为Node.PROPAGATE, * 是因为unparkSuccessor(h)中,如果ws < 0会设置为0,所以ws先设置为0,再设置为PROPAGATE * 这里需要控制并发,因为入口有setHeadAndPropagate跟release两个,避免两次unpark */ if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; //设置失败,重新循环 /* head状态为SIGNAL,且成功设置为0之后,唤醒head.next节点线程 * 此时head、head.next的线程都唤醒了,head.next会去竞争锁,成功后head会指向获取锁的节点, * 也就是head发生了变化。看最底下一行代码可知,head发生变化后会重新循环,继续唤醒head的下一个节点 */ unparkSuccessor(h); /* * 如果本身头节点的waitStatus是出于重置状态(waitStatus==0)的,将其设置为“传播”状态。 * 意味着需要将状态向后一个节点传播 */ } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) //如果head变了,重新循环 break; }}
releaseShared方法(共享释放)源码分析 。
调用tryReleaseShared方法释放资源成功时会调用doReleaseShared方法执行唤醒逻辑,上面已经分析过了.
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false;}
到此这篇关于Java并发编程之JUC并发核心AQS同步队列原理剖析的文章就介绍到这了,更多相关JUC并发核心AQS同步队列内容请搜索我以前的文章或继续浏览下面的相关文章希望大家以后多多支持我! 。
原文链接:https://blog.csdn.net/u012988901/article/details/112251913 。
最后此篇关于Java并发编程之JUC并发核心AQS同步队列原理剖析的文章就讲到这里了,如果你想了解更多关于Java并发编程之JUC并发核心AQS同步队列原理剖析的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
本文全面深入地探讨了Docker容器通信技术,从基础概念、网络模型、核心组件到实战应用。详细介绍了不同网络模式及其实现,提供了容器通信的技术细节和实用案例,旨在为专业从业者提供深入的技术洞见和实
📒博客首页:崇尚学技术的科班人 🍣今天给大家带来的文章是《Dubbo快速上手 -- 带你了解Dubbo使用、原理》🍣 🍣希望各位小伙伴们能够耐心的读完这篇文章🍣 🙏博主也在学习阶段,如若发
一、写在前面 我们经常使用npm install ,但是你是否思考过它内部的原理是什么? 1、执行npm install 它背后帮助我们完成了什么操作? 2、我们会发现还有一个成为package-lo
Base64 Base64 是什么?是将字节流转换成可打印字符、将可打印字符转换为字节流的一种算法。Base64 使用 64 个可打印字符来表示转换后的数据。 准确的来说,Base64 不算
目录 协程定义 生成器和yield语义 Future类 IOLoop类 coroutine函数装饰器 总结 tornado中的
切片,这是一个在go语言中引入的新的理念。它有一些特征如下: 对数组抽象 数组长度不固定 可追加元素 切片容量可增大 容量大小成片增加 我们先把上面的理念整理在这
文章来源:https://sourl.cn/HpZHvy 引 言 本文主要论述的是“RPC 实现原理”,那么首先明确一个问题什么是 RPC 呢?RPC 是 Remote Procedure Call
源码地址(包含所有与springmvc相关的,静态文件路径设置,request请求入参接受,返回值处理converter设置等等): spring-framework/WebMvcConfigurat
请通过简单的java类向我展示一个依赖注入(inject)原理的小例子虽然我已经了解了spring,但是如果我需要用简单的java类术语来解释它,那么你能通过一个简单的例子向我展示一下吗?提前致谢。
1、背景 我们平常使用手机和电脑上网,需要访问公网上的网络资源,如逛淘宝和刷视频,那么手机和电脑是怎么知道去哪里去拿到这个网络资源来下载到本地的呢? 就比如我去食堂拿吃的,我需要
大家好,我是飞哥! 现在 iptables 这个工具的应用似乎是越来越广了。不仅仅是在传统的防火墙、NAT 等功能出现,在今天流行的的 Docker、Kubernets、Istio 项目中也经
本篇涉及到的所有接口在公开文档中均无,需要下载 GitHub 上的源码,自己创建私有类的文档。 npm run generateDocumentation -- --private yarn gene
我最近在很多代码中注意到人们将硬编码的配置(如端口号等)值放在类/方法的深处,使其难以找到,也无法配置。 这是否违反了 SOLID 原则?如果不是,我是否可以向我的团队成员引用另一个“原则”来说明为什
我是 C#、WPF 和 MVVM 模式的新手。很抱歉这篇很长的帖子,我试图设定我所有的理解点(或不理解点)。 在研究了很多关于 WPF 提供的命令机制和 MVVM 模式的文本之后,我在弄清楚如何使用这
可比较的 jQuery 函数 $.post("/example/handler", {foo: 1, bar: 2}); 将创建一个带有 post 参数 foo=1&bar=2 的请求。鉴于 $htt
如果Django不使用“延迟查询执行”原则,主要问题是什么? q = Entry.objects.filter(headline__startswith="What") q = q.filter(
我今天发现.NET框架在做计算时遵循BODMAS操作顺序。即计算按以下顺序进行: 括号 订单 部门 乘法 添加 减法 但是我四处搜索并找不到任何文档确认 .NET 绝对 遵循此原则,是否有此类文档?如
已结束。此问题不符合 Stack Overflow guidelines .它目前不接受答案。 我们不允许提出有关书籍、工具、软件库等方面的建议的问题。您可以编辑问题,以便用事实和引用来回答它。 关闭
API 回顾 在创建 Viewer 时可以直接指定 影像供给器(ImageryProvider),官方提供了一个非常简单的例子,即离屏例子(搜 offline): new Cesium.Viewer(
As it currently stands, this question is not a good fit for our Q&A format. We expect answers to be
我是一名优秀的程序员,十分优秀!