范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

深入AQS原理我画了35张图就是为了让你深入AQS

  前言
  谈到并发,我们不得不说AQS(AbstractQueuedSynchronizer),所谓的AQS即是抽象的队列式的同步器,内部定义了很多锁相关的方法,我们熟知的ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore等都是基于AQS来实现的。
  我们先看下AQS相关的UML图:
  思维导图:
  AQS实现原理
  AQS中 维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
  这里volatile能够保证多线程下的可见性,当state=1则代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个FIFO的等待队列中,比列会被UNSAFE.park()操作挂起,等待其他获取锁的线程释放锁才能够被唤醒。
  另外state的操作都是通过CAS来保证其并发修改的安全性。
  具体原理我们可以用一张图来简单概括:
  AQS 中提供了很多关于锁的实现方法,getState():获取锁的标志state值setState():设置锁的标志state值tryAcquire(int):独占方式获取锁。尝试获取资源,成功则返回true,失败则返回false。tryRelease(int):独占方式释放锁。尝试释放资源,成功则返回true,失败则返回false。
  这里还有一些方法并没有列出来,接下来我们以ReentrantLock作为突破点通过源码和画图的形式一步步了解AQS内部实现原理。目录结构
  文章准备模拟多线程竞争锁、释放锁的场景来进行分析AQS源码:
  三个线程(线程一、线程二、线程三)同时来加锁/释放锁
  目录如下:线程一加锁成功时AQS内部实现线程二/三加锁失败时AQS中等待队列的数据模型线程一释放锁及线程二获取锁实现原理通过线程场景来讲解公平锁具体实现原理通过线程场景来讲解Condition中await()和signal()实现原理
  这里会通过画图来分析每个线程加锁、释放锁后AQS内部的数据结构和实现原理场景分析线程一加锁成功
  如果同时有三个线程并发抢占锁,此时线程一抢占锁成功,线程二和线程三抢占锁失败,具体执行流程如下:
  此时AQS内部数据为:
  线程二、线程三加锁失败:
  有图可以看出,等待队列中的节点Node是一个双向链表,这里SIGNAL是Node中waitStatus属性,Node中还有一个nextWaiter属性,这个并未在图中画出来,这个到后面Condition会具体讲解的。
  具体看下抢占锁代码实现:
  java.util.concurrent.locks.ReentrantLock .NonfairSync:static final class NonfairSync extends Sync {      final void lock() {         if (compareAndSetState(0, 1))             setExclusiveOwnerThread(Thread.currentThread());         else             acquire(1);     }      protected final boolean tryAcquire(int acquires) {         return nonfairTryAcquire(acquires);     } }
  这里使用的ReentrantLock非公平锁,线程进来直接利用CAS尝试抢占锁,如果抢占成功state值回被改为1,且设置对象独占锁线程为当前线程。如下所示:protected final boolean compareAndSetState(int expect, int update) {     return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }  protected final void setExclusiveOwnerThread(Thread thread) {     exclusiveOwnerThread = thread; }线程二抢占锁失败
  我们按照真实场景来分析,线程一抢占锁成功后,state变为1,线程二通过CAS修改state变量必然会失败。此时AQS中FIFO(First In First Out 先进先出)队列中数据如图所示:
  我们将线程二执行的逻辑一步步拆解来看:
  java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire():public final void acquire(int arg) {     if (!tryAcquire(arg) &&         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))         selfInterrupt(); }
  先看看tryAcquire()的具体实现:java.util.concurrent.locks.ReentrantLock .nonfairTryAcquire():final boolean nonfairTryAcquire(int acquires) {     final Thread current = Thread.currentThread();     int c = getState();     if (c == 0) {         if (compareAndSetState(0, acquires)) {             setExclusiveOwnerThread(current);             return true;         }     }     else if (current == getExclusiveOwnerThread()) {         int nextc = c + acquires;         if (nextc < 0)             throw new Error("Maximum lock count exceeded");         setState(nextc);         return true;     }     return false; }
  nonfairTryAcquire()方法中首先会获取state的值,如果不为0则说明当前对象的锁已经被其他线程所占有,接着判断占有锁的线程是否为当前线程,如果是则累加state值,这就是可重入锁的具体实现,累加state值,释放锁的时候也要依次递减state值。
  如果state为0,则执行CAS操作,尝试更新state值为1,如果更新成功则代表当前线程加锁成功。
  以线程二为例,因为线程一已经将state修改为1,所以线程二通过CAS修改state的值不会成功。加锁失败。
  线程二执行tryAcquire()后会返回false,接着执行addWaiter(Node.EXCLUSIVE)逻辑,将自己加入到一个FIFO等待队列中,代码实现如下:
  java.util.concurrent.locks.AbstractQueuedSynchronizer.addWaiter():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; }
  这段代码首先会创建一个和当前线程绑定的Node节点,Node为双向链表。此时等待对内中的tail指针为空,直接调用enq(node)方法将当前线程加入等待队列尾部:private Node enq(final Node node) {     for (;;) {         Node t = tail;         if (t == null) {             if (compareAndSetHead(new Node()))                 tail = head;         } else {             node.prev = t;             if (compareAndSetTail(t, node)) {                 t.next = node;                 return t;             }         }     } }
  第一遍循环时tail指针为空,进入if逻辑,使用CAS操作设置head指针,将head指向一个新创建的Node节点。此时AQS中数据:
  执行完成之后,head、tail、t都指向第一个Node元素。
  接着执行第二遍循环,进入else逻辑,此时已经有了head节点,这里要操作的就是将线程二对应的Node节点挂到head节点后面。此时队列中就有了两个Node节点:
  addWaiter()方法执行完后,会返回当前线程创建的节点信息。继续往后执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)逻辑,此时传入的参数为线程二对应的Node节点信息:
  java.util.concurrent.locks.AbstractQueuedSynchronizer.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(node);                 p.next = null;                failed = false;                 return interrupted;             }             if (shouldParkAfterFailedAcquire(p, node) &&                 parkAndChecknIterrupt())                 interrupted = true;         }     } finally {         if (failed)             cancelAcquire(node);     } }  private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {     int ws = pred.waitStatus;     if (ws == Node.SIGNAL)         return true;     if (ws > 0) {         do {             node.prev = pred = pred.prev;         } while (pred.waitStatus > 0);         pred.next = node;     } else {         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);     }     return false; }  private final boolean parkAndCheckInterrupt() {     LockSupport.park(this);     return Thread.interrupted(); }
  acquireQueued()这个方法会先判断当前传入的Node对应的前置节点是否为head,如果是则尝试加锁。加锁成功过则将当前节点设置为head节点,然后空置之前的head节点,方便后续被垃圾回收掉。
  如果加锁失败或者Node的前置节点不是head节点,就会通过shouldParkAfterFailedAcquire方法将head节点的waitStatus变为了SIGNAL=-1,最后执行parkAndChecknIterrupt方法,调用LockSupport.park()挂起当前线程。
  此时AQS中的数据如下图:
  此时线程二就静静的待在AQS的等待队列里面了,等着其他线程释放锁来唤醒它。线程三抢占锁失败
  看完了线程二抢占锁失败的分析,那么再来分析线程三抢占锁失败就很简单了,先看看addWaiter(Node mode)方法: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; }
  此时等待队列的tail节点指向线程二,进入if逻辑后,通过CAS指令将tail节点重新指向线程三。接着线程三调用enq()方法执行入队操作,和上面线程二执行方式是一致的,入队后会修改线程二对应的Node中的waitStatus=SIGNAL。最后线程三也会被挂起。此时等待队列的数据如图:
  线程一释放锁
  现在来分析下释放锁的过程,首先是线程一释放锁,释放锁后会唤醒head节点的后置节点,也就是我们现在的线程二,具体操作流程如下:
  执行完后等待队列数据如下:
  此时线程二已经被唤醒,继续尝试获取锁,如果获取锁失败,则会继续被挂起。如果获取锁成功,则AQS中数据如图:
  接着还是一步步拆解来看,先看看线程一释放锁的代码:
  java.util.concurrent.locks.AbstractQueuedSynchronizer.release()public final boolean release(int arg) {     if (tryRelease(arg)) {         Node h = head;         if (h != null && h.waitStatus != 0)             unparkSuccessor(h);         return true;     }     return false; }
  这里首先会执行tryRelease()方法,这个方法具体实现在ReentrantLock中,如果tryRelease执行成功,则继续判断head节点的waitStatus是否为0,前面我们已经看到过,head的waitStatue为SIGNAL(-1),这里就会执行unparkSuccessor()方法来唤醒head的后置节点,也就是我们上面图中线程二对应的Node节点。
  此时看ReentrantLock.tryRelease()中的具体实现:protected final boolean tryRelease(int releases) {     int c = getState() - releases;     if (Thread.currentThread() != getExclusiveOwnerThread())         throw new IllegalMonitorStateException();     boolean free = false;     if (c == 0) {         free = true;         setExclusiveOwnerThread(null);     }     setState(c);     return free; }
  执行完ReentrantLock.tryRelease()后,state被设置成0,Lock对象的独占锁被设置为null。此时看下AQS中的数据:
  接着执行java.util.concurrent.locks.AbstractQueuedSynchronizer.unparkSuccessor()方法,唤醒head的后置节点:private void unparkSuccessor(Node node) {     int ws = node.waitStatus;     if (ws < 0)         compareAndSetWaitStatus(node, ws, 0);     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); }
  这里主要是将head节点的waitStatus设置为0,然后解除head节点next的指向,使head节点空置,等待着被垃圾回收。
  此时重新将head指针指向线程二对应的Node节点,且使用LockSupport.unpark方法来唤醒线程二。
  被唤醒的线程二会接着尝试获取锁,用CAS指令修改state数据。执行完成后可以查看AQS中数据:
  此时线程二被唤醒,线程二接着之前被park的地方继续执行,继续执行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(node);                 p.next = null;                failed = false;                 return interrupted;             }             if (shouldParkAfterFailedAcquire(p, node) &&                 parkAndCheckInterrupt())                 interrupted = true;         }     } finally {         if (failed)             cancelAcquire(node);     } }
  此时线程二被唤醒,继续执行for循环,判断线程二的前置节点是否为head,如果是则继续使用tryAcquire()方法来尝试获取锁,其实就是使用CAS操作来修改state值,如果修改成功则代表获取锁成功。接着将线程二设置为head节点,然后空置之前的head节点数据,被空置的节点数据等着被垃圾回收。
  此时线程三获取锁成功,AQS中队列数据如下:
  等待队列中的数据都等待着被垃圾回收。线程二释放锁/线程三加锁
  当线程二释放锁时,会唤醒被挂起的线程三,流程和上面大致相同,被唤醒的线程三会再次尝试加锁,具体代码可以参考上面内容。具体流程图如下:
  此时AQS中队列数据如图:
  公平锁实现原理
  上面所有的加锁场景都是基于非公平锁来实现的,非公平锁是ReentrantLock的默认实现,那我们接着来看一下公平锁的实现原理,这里先用一张图来解释公平锁和非公平锁的区别:
  非公平锁执行流程:
  这里我们还是用之前的线程模型来举例子,当线程二释放锁的时候,唤醒被挂起的线程三,线程三执行tryAcquire()方法使用CAS操作来尝试修改state值,如果此时又来了一个线程四也来执行加锁操作,同样会执行tryAcquire()方法。
  这种情况就会出现竞争,线程四如果获取锁成功,线程三仍然需要待在等待队列中被挂起。这就是所谓的非公平锁,线程三辛辛苦苦排队等到自己获取锁,却眼巴巴的看到线程四插队获取到了锁。
  公平锁执行流程:
  公平锁在加锁的时候,会先判断AQS等待队列中是存在节点,如果存在节点则会直接入队等待,具体代码如下.
  公平锁在获取锁是也是首先会执行acquire()方法,只不过公平锁单独实现了tryAcquire()方法:
  #java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire():public final void acquire(int arg) {     if (!tryAcquire(arg) &&         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))         selfInterrupt(); }
  这里会执行ReentrantLock中公平锁的tryAcquire()方法
  #java.util.concurrent.locks.ReentrantLock.FairSync.tryAcquire():static final class FairSync extends Sync {     protected final boolean tryAcquire(int acquires) {         final Thread current = Thread.currentThread();         int c = getState();         if (c == 0) {             if (!hasQueuedPredecessors() &&                 compareAndSetState(0, acquires)) {                 setExclusiveOwnerThread(current);                 return true;             }         }         else if (current == getExclusiveOwnerThread()) {             int nextc = c + acquires;             if (nextc < 0)                 throw new Error("Maximum lock count exceeded");             setState(nextc);             return true;         }         return false;     } }
  这里会先判断state值,如果不为0且获取锁的线程不是当前线程,直接返回false代表获取锁失败,被加入等待队列。如果是当前线程则可重入获取锁。
  如果state=0则代表此时没有线程持有锁,执行hasQueuedPredecessors()判断AQS等待队列中是否有元素存在,如果存在其他等待线程,那么自己也会加入到等待队列尾部,做到真正的先来后到,有序加锁。具体代码如下:
  #java.util.concurrent.locks.AbstractQueuedSynchronizer.hasQueuedPredecessors():public final boolean hasQueuedPredecessors() {     Node t = tail;     Node h = head;     Node s;     return h != t &&         ((s = h.next) == null || s.thread != Thread.currentThread()); }
  这段代码很有意思,返回false代表队列中没有节点或者仅有一个节点是当前线程创建的节点。返回true则代表队列中存在等待节点,当前线程需要入队等待。
  先判断head是否等于tail,如果队列中只有一个Node节点,那么head会等于tail,接着判断head的后置节点,这里肯定会是null,如果此Node节点对应的线程和当前的线程是同一个线程,那么则会返回false,代表没有等待节点或者等待节点就是当前线程创建的Node节点。此时当前线程会尝试获取锁。
  如果head和tail不相等,说明队列中有等待线程创建的节点,此时直接返回true,如果只有一个节点,而此节点的线程和当前线程不一致,也会返回true
  非公平锁和公平锁的区别:非公平锁性能高于公平锁性能。非公平锁可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量
  非公平锁性能虽然优于公平锁,但是会存在导致线程饥饿的情况。在最坏的情况下,可能存在某个线程一直获取不到锁。不过相比性能而言,饥饿问题可以暂时忽略,这可能就是ReentrantLock默认创建非公平锁的原因之一了。Condition实现原理Condition简介
  上面已经介绍了AQS所提供的核心功能,当然它还有很多其他的特性,这里我们来继续说下Condition这个组件。
  Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition中的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition
  其中AbstractQueueSynchronizer中实现了Condition中的方法,主要对外提供awaite(Object.wait())和signal(Object.notify())调用。Condition Demo示例
  使用示例代码:/**  * ReentrantLock 实现源码学习  * @author 一枝花算不算浪漫  * @date 2020/4/28 7:20  */ public class ReentrantLockDemo {     static ReentrantLock lock = new ReentrantLock();      public static void main(String[] args) {         Condition condition = lock.newCondition();          new Thread(() -> {             lock.lock();             try {                 System.out.println("线程一加锁成功");                 System.out.println("线程一执行await被挂起");                 condition.await();                 System.out.println("线程一被唤醒成功");             } catch (Exception e) {                 e.printStackTrace();             } finally {                 lock.unlock();                 System.out.println("线程一释放锁成功");             }         }).start();          new Thread(() -> {             lock.lock();             try {                 System.out.println("线程二加锁成功");                 condition.signal();                 System.out.println("线程二唤醒线程一");             } finally {                 lock.unlock();                 System.out.println("线程二释放锁成功");             }         }).start();     } }
  执行结果如下图:
  这里线程一先获取锁,然后使用await()方法挂起当前线程并释放锁,线程二获取锁后使用signal唤醒线程一。Condition实现原理图解
  我们还是用上面的demo作为实例,执行的流程如下:
  线程一执行await()方法:
  先看下具体的代码实现,#java.util.concurrent.locks.AbstractQueuedSynchronizer.ConditionObject.await(): public final void await() throws InterruptedException {     if (Thread.interrupted())         throw new InterruptedException();     Node node = addConditionWaiter();     int savedState = fullyRelease(node);     int interruptMode = 0;     while (!isOnSyncQueue(node)) {         LockSupport.park(this);         if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)             break;     }     if (acquireQueued(node, savedState) && interruptMode != THROW_IE)         interruptMode = REINTERRUPT;     if (node.nextWaiter != null)        unlinkCancelledWaiters();     if (interruptMode != 0)         reportInterruptAfterWait(interruptMode); }
  await()方法中首先调用addConditionWaiter()将当前线程加入到Condition队列中。
  执行完后我们可以看下Condition队列中的数据:
  具体实现代码为:private Node addConditionWaiter() {     Node t = lastWaiter;     if (t != null && t.waitStatus != Node.CONDITION) {         unlinkCancelledWaiters();         t = lastWaiter;     }     Node node = new Node(Thread.currentThread(), Node.CONDITION);     if (t == null)         firstWaiter = node;     else         t.nextWaiter = node;     lastWaiter = node;     return node; }
  这里会用当前线程创建一个Node节点,waitStatus为CONDITION。接着会释放该节点的锁,调用之前解析过的release()方法,释放锁后此时会唤醒被挂起的线程二,线程二会继续尝试获取锁。
  接着调用isOnSyncQueue()方法判断当前节点是否为Condition队列中的头部节点,如果是则调用LockSupport.park(this)挂起Condition中当前线程。此时线程一被挂起,线程二获取锁成功。
  具体流程如下图:
  线程二执行signal()方法:
  首先我们考虑下线程二已经获取到锁,此时AQS等待队列中已经没有了数据。
  接着就来看看线程二唤醒线程一的具体执行流程:public final void signal() {     if (!isHeldExclusively())         throw new IllegalMonitorStateException();     Node first = firstWaiter;     if (first != null)         doSignal(first); }
  先判断当前线程是否为获取锁的线程,如果不是则直接抛出异常。接着调用doSignal()方法来唤醒线程。private void doSignal(Node first) {     do {         if ( (firstWaiter = first.nextWaiter) == null)             lastWaiter = null;         first.nextWaiter = null;     } while (!transferForSignal(first) &&              (first = firstWaiter) != null); }  final boolean transferForSignal(Node node) {     if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))         return false;      Node p = enq(node);     int ws = p.waitStatus;     if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))         LockSupport.unpark(node.thread);     return true; }  /**  * Inserts node into queue, initializing if necessary. See picture above.  * @param node the node to insert  * @return node"s predecessor  */ private Node enq(final Node node) {     for (;;) {         Node t = tail;         if (t == null) {            if (compareAndSetHead(new Node()))                 tail = head;         } else {             node.prev = t;             if (compareAndSetTail(t, node)) {                 t.next = node;                 return t;             }         }     } }
  这里先从transferForSignal()方法来看,通过上面的分析我们知道Condition队列中只有线程一创建的一个Node节点,且waitStatue为CONDITION,先通过CAS修改当前节点waitStatus为0,然后执行enq()方法将当前线程加入到等待队列中,并返回当前线程的前置节点。
  加入等待队列的代码在上面也已经分析过,此时等待队列中数据如下图:
  接着开始通过CAS修改当前节点的前置节点waitStatus为SIGNAL,并且唤醒当前线程。此时AQS中等待队列数据为:
  线程一被唤醒后,继续执行await()方法中的while循环。public final void await() throws InterruptedException {     if (Thread.interrupted())         throw new InterruptedException();     Node node = addConditionWaiter();     int savedState = fullyRelease(node);     int interruptMode = 0;     while (!isOnSyncQueue(node)) {         LockSupport.park(this);         if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)             break;     }     if (acquireQueued(node, savedState) && interruptMode != THROW_IE)         interruptMode = REINTERRUPT;     if (node.nextWaiter != null)        unlinkCancelledWaiters();     if (interruptMode != 0)         reportInterruptAfterWait(interruptMode); }
  因为此时线程一的waitStatus已经被修改为0,所以执行isOnSyncQueue()方法会返回false。跳出while循环。
  接着执行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(node);                 p.next = null;                failed = false;                 return interrupted;             }             if (shouldParkAfterFailedAcquire(p, node) &&                 parkAndCheckInterrupt())                 interrupted = true;         }     } finally {         if (failed)             cancelAcquire(node);     } }
  此时线程一的流程都已经分析完了,等线程二释放锁后,线程一会继续重试获取锁,流程到此终结。Condition总结
  我们总结下Condition和wait/notify的比较:Condition可以精准的对多个不同条件进行控制,wait/notify只能和synchronized关键字一起使用,并且只能唤醒一个或者全部的等待队列;Condition需要使用Lock进行控制,使用的时候要注意lock()后及时的unlock(),Condition有类似于await的机制,因此不会产生加锁方式而产生的死锁出现,同时底层实现的是park/unpark的机制,因此也不会产生先唤醒再挂起的死锁,一句话就是不会产生死锁,但是wait/notify会产生先唤醒再挂起的死锁。总结
  这里用了一步一图的方式结合三个线程依次加锁/释放锁来展示了ReentrantLock的实现方式和实现原理,而ReentrantLock底层就是基于AQS实现的,所以我们也对AQS有了深刻的理解。
  另外还介绍了公平锁与非公平锁的实现原理,Condition的实现原理,基本上都是使用源码+绘图的讲解方式,尽量让大家更容易去理解。
  一枝花算不算浪漫
  https://www.cnblogs.com/wang-meng/p/12816829.html

浙江的民营企业家有多优秀?来讨论一下和数据比对一下中国最新93家千亿市值的公司,有26家是浙江人创立的,你知道么?浙商确实牛掰啊,长三角三省一市市值最高企业都是浙商创立的。上海拼多多,江苏药明生物,安徽阳光电源,浙江阿里巴巴,浙商Python数据类型第2篇字典和集合的原理及应用目录一集合1。定义个有元素的集合2。自动去重3。集合常用的五个方法二集合和字典都是无序的三字典和集合都是无序的,在内存中是怎么存储?1。为什么说字典和集合是无序的?2。字典查找值的8月比亚迪新能源卖疯了要不,干脆停了燃油车让让产能?当前国内车市新能源车市场热度很高,无论混动还是纯电动车型,都有一票粉丝等着入手。市场需求摆在这儿,而比亚迪多年努力也没有白费,凭借超级DMi系统和EV车型,走上销量快车道。8月份比格力的汽车项目会和格力手机一样无疾而终吗?在2021年8月31日,格力拿到了银隆新能源30。47的股权,再加上董明珠将其个人持有的银隆新能源17。46股权的表决权委托给格力电器。至此银隆新能源将成为格力电器的控股子公司。有华为的鸿蒙系统正式量产,向安卓开了一枪?编者按昨晚,华为终于带来了量产版的鸿蒙操作系统,这也被看做是真正的国产操作系统,未来还有可能贯穿到所有领域,绝不仅限于移动终端和PC端。而鸿蒙操作系统从一开始的设计就站在了很高的维目标3亿台设备!华为鸿蒙系统正式亮相,库克妥协了?6月2日晚,华为鸿蒙系统(HarmonyOS2。0)正式亮相,同时还发布了多款搭载鸿蒙系统的产品,其中包括了手机手表和平板等。除此之外,华为方面还表示,自己旗下的设备将陆续升级为鸿每日精选好机天玑1100性能强手机到手1569元一千多元的价格能够买到什么样的手机呢?如果说能够买到一个能够胜任主流的游戏的产品你信么?如今手机品牌都在对产品的游戏体验做出提升,而一款能够流畅游玩主流游戏的手机,也无需花费4千5用户口碑汇总卖到5000的旗舰机型,用户上手后都满意吗?每到618双11这种电商大促的节日,很多厂商都会对自己旗下的产品进行降价促销。但我们不要被利益冲疯了头脑,因为,在618期间基本所有的厂商都会进行降价,促销。尤其是那些口碑不好销量腾讯借鉴罗永浩7年前实现的功能,转手自己还申请了专利据天眼查APP显示,近日腾讯公开了一项名为会话消息的定时发送方法装置计算机设备及存储介质的专利,公开号为CN112887189A,申请日期是2019年11月29日。关于该专利的作用布加迪发布三款新型智能手表,售价7K起!土豪会买吗?布加迪的跑车设计方面中确立了质量卓越和精致的地位。它现在正与钟表制造商VIITA合作开发三款出色的智能手表,这些智能手表将反映尖端技术和材料,以制作两家公司认为的杰作。VIITA已年度旗舰!华为鸿蒙手表正式发布,三大亮点赢在了起跑线备受瞩目的鸿蒙系统终于问世了,在6月2日的鸿蒙操作系统及华为全场景新品发布会上,华为正式发布了鸿蒙OS手机版,后续华为手机用户将会陆续收到系统升级推送。在这场发布会上,华为还发布了
翻倍基头牌下一步棋落何处?韩广哲透露后续操作思路降低股票数量,将优势公司集中财联社(北京,记者陈俊岭)讯,三季报发布后,我会结合11月12月美联储taper,以及经济的后续定位,观察市场是否存在贝塔方面的风险,看是否对组合配置进行调整在金鹰民族新兴以104新基发行遇寒流,10月成立新基97只仅为上月一半,30只延募,5只募集失败,公募仍握超7000亿资金未入场财联社(北京,记者黎旅嘉)讯,10月新基发行遇冷,到底有多冷?数据说话。Wind数据显示,截至10月31日,年内新成立基金1484只,总发行份额达24714。06亿份,较2020年最吝啬亿万富豪排行榜出炉!贝佐斯马斯克入围捐不到1在福布斯所公布的400位富豪榜中,身为美国前两大亿万富豪的亚马逊创始人贝佐斯(JeffBezos)特斯拉创始人兼董事长马斯克(ElonMusk)双双入列,两人长期以来在谁更富有谁更学前班第31课如何避免收到追加保证金通知?驾驭保证金交易的能力是决定每个交易者水平的重要组成部分。保证金交易使交易者在盈利时扩大利润,然而,保证金交易也伴随着一些非常严重的风险和责任,是每个交易者都需要意识到的问题。大家是五种震荡指标对比研究,及其交易实战策略,你了解多少?基于技术面的交易在新手和专业交易者中颇受欢迎。有无数的技术方法可以在市场上实施,其中一种方法包括使用技术震荡指标。本文将重点对几种不同的震荡指标做比较研究,以期能够给交易者带来一定加密货币比黄金更能抗通胀,比特币年底有望挑战10万美元在比特币创新历史新高66,974美元后掉头走低,盘中一度探至60,034美元,目前在61,000美元附近交投。BitMEX首席执行官AlexanderHoptner指出,比特币年底比特币再创历史新高,投资大佬直言看不懂,不玩了美国首只比特币期货ETF周二正式上市,并盘中大涨近5,显示投资者反应热烈,促使加密货币投资热潮再度袭来。隔夜盘中,加密货币龙头比特币盘中突破66,000美元关口,最高探至66,97ACY证券连续两年作为ATC官方赞助商于Everest赛事中再次大放异彩以最奢华的赛马比赛闻名全球的Everest赛事,于2021年10月16日(星期六)在澳大利亚悉尼皇家Randwick赛马场举办。由于疫情人数的限制,新南威尔士州政府决定这场盛事最多学前班第30课如何计算外汇保证金水平?对外汇交易者来说,保证金水平是需要时刻留意的重要数据。它可以及时提醒交易者账户过度亏损以及被追加保证金的可能。不过,我们不需要自己去计算保证金水平,大多数交易平台都会自动显示这个数交易圣经新金融怪杰20句至理名言纪念那些已经淡出或者即将淡出人们视野,曾经在华尔街市场上叱咤风云的交易大师。感谢大师们的无私奉献。在此记录下比较经典的名言警句,作为纪念。如果细细品起来,你会发现这20句,句句经典油价明年飙至100美元?4步助你交易原油盈利近日,受益于全球经济活动回暖且通胀预期升温,布兰特原油价格重拾上涨动能,突破每桶80美元,也比年初高出近50纽约期货原油升穿每桶85美元,创下七年来新高。正值原油市场欣欣向荣之时,