当前位置: 澳门新濠3559 > 编程 > 正文

使用原子类更精巧轻量,CAS 在现实中的应用

时间:2019-10-07 13:17来源:编程
什么是悲观锁、乐观锁?在java语言里,总有一些名词看语义跟本不明白是啥玩意儿,也就总有部分面试官拿着这样的词来忽悠面试者,以此来找优越感,其实理解清楚了,这些词也就唬

什么是悲观锁、乐观锁?在java语言里,总有一些名词看语义跟本不明白是啥玩意儿,也就总有部分面试官拿着这样的词来忽悠面试者,以此来找优越感,其实理解清楚了,这些词也就唬不住人了。

本文讲解CAS机制,主要是因为最近准备面试题,发现这个问题在面试中出现的频率非常的高,因此把自己学习过程中的一些理解记录下来,希望能对大家也有帮助。

图片 1

  在谈谈java中的volatile一文中,我们提到过并发包中的原子类可以解决类似num 这样的复合类操作的原子性问题,相比锁机制,使用原子类更精巧轻量,性能开销更小,本章就一起来分析下原子类的实现机理。

  • synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
  • CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

什么是悲观锁、乐观锁?在java语言里,总有一些名词看语义跟本不明白是啥玩意儿,也就总有部分面试官拿着这样的词来忽悠面试者,以此来找优越感,其实理解清楚了,这些词也就唬不住人了。

CAS 是现代操作系统,解决并发问题的一个重要手段,最近在看eureka的源码的时候。遇到了很多 CAS 的操作。今天就系统的回顾一下 Java 中的CAS。

悲观的解决方案(阻塞同步)

  我们知道,num 看似简单的一个操作,实际上是由1.读取 2.加一 3.写入 三步组成的,这是个复合类的操作(所以我们之前提到过的volatile是无法解决num 的原子性问题的),在并发环境下,如果不做任何同步处理,就会有线程安全问题。最直接的处理方式就是加锁

synchronized(this){
    num  ;
 }

  使用独占锁机制来解决,是一种悲观的并发策略,抱着一副“总有刁民想害朕”的态势,每次操作数据的时候都认为别的线程会参与竞争修改,所以直接加锁。同一刻只能有一个线程持有锁,那其他线程就会阻塞。线程的挂起恢复会带来很大的性能开销,尽管jvm对于非竞争性的锁的获取和释放做了很多优化,但是一旦有多个线程竞争锁,频繁的阻塞唤醒,还是会有很大的性能开销的。所以,使用synchronized或其他重量级锁来处理显然不够合理。

那么问题来了,什么是CAS操作?

CAS是Compare-and-swap的简写,是一种有名的无锁算法,在java中,我们主要分析Unsafe类,因为所有的CAS操作都是它来实现的,而在Unsafe类中这些方法也都是native方法

 public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

看到上面的解释是不是索然无味,查找了很多资料也没完全弄明白,通过几次验证后,终于明白,最终可以理解成一个无阻塞多线程争抢资源的模型。先上代码

package com.company.reentrantLock;import java.util.concurrent.atomic.AtomicBoolean;/** * Created by wxwall on 2017/6/2. */public class AtomicBooleanTest implements Runnable{ public static AtomicBoolean exits = new AtomicBoolean; public static void main(String[] args) { AtomicBooleanTest abd = new AtomicBooleanTest(); Thread t1 = new Thread; Thread t2 = new Thread; t1.start(); t2.start(); } @Override public void run() { System.out.println("begin run"); System.out.println("real "   exits.get; if(exits.compareAndSet(true,false)){ System.out.println(Thread.currentThread().getName()   " "   exits.get; exits.set; }else{ run(); } }}

输出结果:

begin runreal trueThread-1 falsebegin runreal trueThread-0 false

这里无论怎么运行,Thread-1、Thread-0都会执行if=true条件,而且还不会产生线程脏读脏写,这是如何做到的了,这就用到了我们的compareAndSet(boolean expect,boolean update)方法,先上图简单讲解下程序原理,然后再分析compareAndSet作用。

图片 2Paste_Image.png

这个图中重最要的是compareAndSet(true,false)方法要拆开成compare方法和Set方法理解,是compare是等于true后,就马上设置共享内存为false,这个时候,其它线程无论怎么走都无法走到只有得到共享内存为true时的程序隔离方法区。但是这种得不到状态为true时使用递归算法是很耗cpu资源的,所以一般情况下,都会有线程sleep。

这篇文章并没有展开讲compareAndSet底层调用的是unsafe.compareAndSwapInt方法,因为这是native方法,很多人都会展开找源码,最后也只找到是调用CPU方法,没讲到具体用法,如果只用compareAndSet(true,false)举例则更加简单。这种无阻塞式的多线程操作数据,在大并发情况下,是一笔非常可观的性能提升,所以,如果在大并发或多线程性能要求高的情况下有更加好的技术选型,可以参考这种底层实现。

  • synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
  • CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

阅读这篇文章你将会了解到:

乐观的解决方案(非阻塞同步)

  乐观的解决方案,顾名思义,就是很大度乐观,每次操作数据的时候,都认为别的线程不会参与竞争修改,也不加锁。如果操作成功了那最好;如果失败了,比如中途确有别的线程进入并修改了数据(依赖于冲突检测),也不会阻塞,可以采取一些补偿机制,一般的策略就是反复重试。很显然,这种思想相比简单粗暴利用锁来保证同步要合理的多。

  鉴于并发包中的原子类其实现机理都差不太多,本章我们就通过AtomicInteger这个原子类来进行分析。我们先来看看对于num 这样的操作AtomicInteger是如何保证其原子性的。

 /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current   1;
            if (compareAndSet(current, next))
                return next;
        }
    }

 我们来分析下incrementAndGet的逻辑:

  1.先获取当前的value值

  2.对value加一

  3.第三步是关键步骤,调用compareAndSet方法来来进行原子更新操作,这个方法的语义是:

    先检查当前value是否等于current,如果相等,则意味着value没被其他线程修改过,更新并返回true。如果不相等,compareAndSet则会返回false,然后循环继续尝试更新。

  compareAndSet调用了Unsafe类的compareAndSwapInt方法

/**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return true if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

  Unsafe的compareAndSwapInt是个native方法,也就是平台相关的。它是基于CPU的CAS指令来完成的。

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

在进入正题之前,我们先理解下下面的代码:

什么是 CAS

CAS(Compare-and-Swap)  

  CAS算法是由硬件直接支持来保证原子性的,有三个操作数:内存位置V、旧的预期值A和新值B,当且仅当V符合预期值A时,CAS用新值B原子化地更新V的值,否则,它什么都不做。

  CAS的ABA问题

  当然CAS也并不完美,它存在"ABA"问题,假若一个变量初次读取是A,在compare阶段依然是A,但其实可能在此过程中,它先被改为B,再被改回A,而CAS是无法意识到这个问题的。CAS只关注了比较前后的值是否改变,而无法清楚在此过程中变量的变更明细,这就是所谓的ABA漏洞。 

  

 private static int count = 0; public static void main(String[] args) { for (int i = 0; i < 2; i  ) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep; } catch (Exception e) { e.printStackTrace(); } //每个线程让count自增100次 for (int i = 0; i < 100; i  ) { count  ; } } }).start(); } try{ Thread.sleep; }catch (Exception e){ e.printStackTrace(); } System.out.println; }

CAS 实现原理是什么?

请问cout的输出值是否为200?答案是否定的,因为这个程序是线程不安全的,所以造成的结果count值可能小于200;

CAS 在现实中的应用

那么如何改造成线程安全的呢,其实我们可以使用上Synchronized同步锁,我们只需要在count 的位置添加同步锁,代码如下:

自旋锁

private static int count = 0; public static void main(String[] args) { for (int i = 0; i < 2; i  ) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep; } catch (Exception e) { e.printStackTrace(); } //每个线程让count自增100次 for (int i = 0; i < 100; i  ) { synchronized (ThreadCas.class){ count  ; } } } }).start(); } try{ Thread.sleep; }catch (Exception e){ e.printStackTrace(); } System.out.println; }

原子类型

加了同步锁之后,count自增的操作变成了原子性操作,所以最终的输出一定是count=200,代码实现了线程安全。

限流器

但是Synchronized使用原子类更精巧轻量,CAS 在现实中的应用。虽然确保了线程的安全,但是在性能上却不是最优的,Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。

CAS 的缺点

尽管Java1.6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。

什么是 CAS

所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。例如AtomicBooleanAtomicIntegerAtomicLong。它们分别用于BooleanIntegerLong类型的原子性操作。

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:

 private static AtomicInteger count = new AtomicInteger; public static void main(String[] args) { for (int i = 0; i < 2; i  ) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep; } catch (Exception e) { e.printStackTrace(); } //每个线程让count自增100次 for (int i = 0; i < 100; i  ) { count.incrementAndGet.start(); } try{ Thread.sleep; }catch (Exception e){ e.printStackTrace(); } System.out.println; }

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

使用AtomicInteger之后,最终的输出结果同样可以保证是200。并且在某些情况下,代码的性能会比Synchronized更好。

比较 A 与 V 是否相等。(比较)

而Atomic操作的底层实现正是利用的CAS机制,好的,我们切入到这个博客的正点。

如果比较相等,将 B 写入 V。(交换)

什么是CAS机制

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

这样说或许有些抽象,我们来看一个例子:

1.在内存地址V当中,存储着值为10的变量。

图片 3image

2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

图片 4image

3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

图片 5image

4.线程1开始提交更新,首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败。

图片 64.jpg

5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

图片 7image

6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。

图片 8image

7.线程1进行SWAP,把地址V的值替换为B,也就是12。

图片 9image

从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

看到上面的解释是不是索然无味,查找了很多资料也没完全弄明白,通过几次验证后,终于明白,最终可以理解成一个无阻塞多线程争抢资源的模型。先上代码

import java.util.concurrent.atomic.AtomicBoolean;/** * @author hrabbit * 2018/07/16. */public class AtomicBooleanTest implements Runnable { private static AtomicBoolean flag = new AtomicBoolean; public static void main(String[] args) { AtomicBooleanTest ast = new AtomicBooleanTest(); Thread thread1 = new Thread; Thread thread = new Thread; thread1.start(); thread.start(); } @Override public void run() { System.out.println("thread:" Thread.currentThread().getName() ";flag:" flag.get; if (flag.compareAndSet(true,false)){ System.out.println(Thread.currentThread().getName() "" flag.get; try { Thread.sleep; } catch (InterruptedException e) { e.printStackTrace(); } flag.set; }else{ System.out.println("重试机制thread:" Thread.currentThread().getName() ";flag:" flag.get; try { Thread.sleep; } catch (InterruptedException e) { e.printStackTrace(); } run(); } }}

输出的结果:

thread:Thread-1;flag:truethread:Thread-0;flag:trueThread-1false重试机制thread:Thread-0;flag:falsethread:Thread-0;flag:false重试机制thread:Thread-0;flag:falsethread:Thread-0;flag:false重试机制thread:Thread-0;flag:falsethread:Thread-0;flag:false重试机制thread:Thread-0;flag:falsethread:Thread-0;flag:false重试机制thread:Thread-0;flag:falsethread:Thread-0;flag:false重试机制thread:Thread-0;flag:falsethread:Thread-0;flag:false重试机制thread:Thread-0;flag:falsethread:Thread-0;flag:false重试机制thread:Thread-0;flag:falsethread:Thread-0;flag:false重试机制thread:Thread-0;flag:falsethread:Thread-0;flag:false重试机制thread:Thread-0;flag:falsethread:Thread-0;flag:trueThread-0false

这里无论怎么运行,Thread-1、Thread-0都会执行if=true条件,而且还不会产生线程脏读脏写,这是如何做到的了,这就用到了我们的compareAndSet(boolean expect,boolean update)方法我们看到当Thread-1在进行操作的时候,Thread一直在进行重试机制,程序原理图:

图片 10image

这个图中重最要的是compareAndSet(true,false)方法要拆开成compare方法和Set方法理解,是compare是等于true后,就马上设置共享内存为false,这个时候,其它线程无论怎么走都无法走到只有得到共享内存为true时的程序隔离方法区。

看到这里,这种CAS机制就是完美的吗?这个程序其实存在一个问题,不知道大家注意到没有?

但是这种得不到状态为true时使用递归算法是很耗cpu资源的,所以一般情况下,都会有线程sleep。

CAS的缺点:

1.CPU开销较大在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

返回操作是否成功。

当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。

CAS 是怎么实现的

跟随AtomInteger的代码我们一路往下,就能发现最终调用的是sum.misc.Unsafe这个类。看名称 Unsafe 就是一个不安全的类,这个类是利用了 Java 的类和包在可见性的的规则中的一个恰到好处处的漏洞。Unsafe 这个类为了速度,在Java的安全标准上做出了一定的妥协。

再往下寻找我们发现 Unsafe的compareAndSwapInt是 Native 的方法:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

也就是说,这几个 CAS 的方法应该是使用了本地的方法。所以这几个方法的具体实现需要我们自己去 jdk 的源码中搜索。

于是我下载一个 OpenJdk 的源码继续向下探索,我们发现在/jdk9u/hotspot/src/share/vm/unsafe.cpp中有这样的代码:

{CC "compareAndSetInt",   CC "(" OBJ "J""I""I"")Z",  FN_PTR(Unsafe_CompareAndSetInt)},

这个涉及到,JNI 的调用,感兴趣的同学可以自行学习。我们搜索Unsafe_CompareAndSetInt后发现:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {   oop p = JNIHandles::resolve(obj);   jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);   return (jint)(Atomic::cmpxchg(x, addr, e)) == e; } UNSAFE_END

最终我们终于看到了核心代码Atomic::cmpxchg。

继续向底层探索,在文件java/jdk9u/hotspot/src/os_cpu/linux_x86/vm/atomic_linux_x86.hpp有这样的代码:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value, cmpxchg_memory_order order) {   int mp = os::is_MP();   __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"                     : "=a" (exchange_value)                     : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)                     : "cc", "memory");   return exchange_value; }

我们通过文件名可以知道,针对不同的操作系统,JVM 对于 Atomic::cmpxchg 应该有不同的实现。由于我们服务基本都是使用的是64位linux,所以我们就看看linux_x86 的实现。

我们继续看代码:

__asm__的意思是这个是一段内嵌汇编代码。也就是在 C 语言中使用汇编代码。

这里的volatile和 JAVA 有一点类似,但不是为了内存的可见性,而是告诉编译器对访问该变量的代码就不再进行优化。

LOCK_IF_MP(%4)的意思就比较简单,就是如果操作系统是多线程的,那就增加一个 LOCK。

cmpxchgl就是汇编版的“比较并交换”。但是我们知道比较并交换,有三个步骤,不是原子的。所以在多核情况下加一个 LOCK,由CPU硬件保证他的原子性。

我们再看看 LOCK 是怎么实现的呢?我们去Intel的官网上看看,可以知道LOCK在的早期实现是直接将 cup 的总线阻塞,这样的实现可见效率是很低下的。后来优化为X86 cpu 有锁定一个特定内存地址的能力,当这个特定内存地址被锁定后,它就可以阻止其他的系统总线读取或修改这个内存地址。

关于 CAS 的底层探索我们就到此为止。我们总结一下 JAVA 的 cas 是怎么实现的:

java 的 cas 利用的的是 unsafe 这个类提供的 cas 操作。

unsafe 的cas 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg

Atomic::cmpxchg 的实现使用了汇编的 cas 操作,并使用 cpu 硬件提供的 lock信号保证其原子性

CAS 的应用

了解了 CAS 的原理我们继续就看看 CAS 的应用:

自旋锁

public class SpinLock {   private AtomicReference sign =new AtomicReference<>();   public void lock(){     Thread current = Thread.currentThread();     while(!sign .compareAndSet(null, current)){     }   }   public void unlock (){     Thread current = Thread.currentThread();     sign .compareAndSet(current, null);   } }

所谓自旋锁,我觉得这个名字相当的形象,在lock()的时候,一直while()循环,直到 cas 操作成功为止。

AtomicInteger 的 incrementAndGet()

    public final int getAndAddInt(Object var1, long var2, int var4) {         int var5;         do {             var5 = this.getIntVolatile(var1, var2);         } while(!this.compareAndSwapInt(var1, var2, var5, var5   var4));         return var5;     }

与自旋锁有异曲同工之妙,就是一直while,直到操作成功为止。

令牌桶限流器

所谓令牌桶限流器,就是系统以恒定的速度向桶内增加令牌。每次请求前从令牌桶里面获取令牌。如果获取到令牌就才可以进行访问。当令牌桶内没有令牌的时候,拒绝提供服务。我们来看看eureka的限流器是如何使用 CAS 来维护多线程环境下对 token 的增加和分发的。

public class RateLimiter {     private final long rateToMsConversion;     private final AtomicInteger consumedTokens = new AtomicInteger();     private final AtomicLong lastRefillTime = new AtomicLong(0);     @Deprecated     public RateLimiter() {         this(TimeUnit.SECONDS);     }     public RateLimiter(TimeUnit averageRateUnit) {         switch (averageRateUnit) {             case SECONDS:                 rateToMsConversion = 1000;                 break;             case MINUTES:                 rateToMsConversion = 60 * 1000;                 break;             default:                 throw new IllegalArgumentException("TimeUnit of "   averageRateUnit   " is not supported");         }     }     //提供给外界获取 token 的方法     public boolean acquire(int burstSize, long averageRate) {         return acquire(burstSize, averageRate, System.currentTimeMillis());     }     public boolean acquire(int burstSize, long averageRate, long currentTimeMillis) {         if (burstSize <= 0 || averageRate <= 0) { // Instead of throwing exception, we just let all the traffic go             return true;         }         //添加token         refillToken(burstSize, averageRate, currentTimeMillis);         //消费token         return consumeToken(burstSize);     }     private void refillToken(int burstSize, long averageRate, long currentTimeMillis) {         long refillTime = lastRefillTime.get();         long timeDelta = currentTimeMillis - refillTime;         //根据频率计算需要增加多少 token         long newTokens = timeDelta * averageRate / rateToMsConversion;         if (newTokens > 0) {             long newRefillTime = refillTime == 0                     ? currentTimeMillis                     : refillTime   newTokens * rateToMsConversion / averageRate;             // CAS 保证有且仅有一个线程进入填充             if (lastRefillTime.compareAndSet(refillTime, newRefillTime)) {                 while (true) {                     int currentLevel = consumedTokens.get();                     int adjustedLevel = Math.min(currentLevel, burstSize); // In case burstSize decreased                     int newLevel = (int) Math.max(0, adjustedLevel - newTokens);                     // while true 直到更新成功为止                     if (consumedTokens.compareAndSet(currentLevel, newLevel)) {                         return;                     }                 }             }         }     }     private boolean consumeToken(int burstSize) {         while (true) {             int currentLevel = consumedTokens.get();             if (currentLevel >= burstSize) {                 return false;             }             // while true 直到没有token 或者 获取到为止             if (consumedTokens.compareAndSet(currentLevel, currentLevel   1)) {                 return true;             }         }     }     public void reset() {         consumedTokens.set(0);         lastRefillTime.set(0);     } }

所以梳理一下 CAS 在令牌桶限流器的作用。就是保证在多线程情况下,不阻塞线程的填充token 和消费token。

归纳

通过上面的三个应用我们归纳一下 CAS 的应用场景:

CAS 的使用能够避免线程的阻塞。

多数情况下我们使用的是 while true 直到成功为止。

CAS 缺点

ABA 的问题,就是一个值从A变成了B又变成了A,使用CAS操作不能发现这个值发生变化了,处理方式是可以使用携带类似时间戳的版本AtomicStampedReference

性能问题,我们使用时大部分时间使用的是 while true 方式对数据的修改,直到成功为止。优势就是相应极快,但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。

总结

CAS 是整个编程重要的思想之一。整个计算机的实现中都有CAS的身影。微观上看汇编的 CAS 是实现操作系统级别的原子操作的基石。从编程语言角度来看 CAS 是实现多线程非阻塞操作的基石。宏观上看,在分布式系统中,我们可以使用 CAS 的思想利用类似Redis的外部存储,也能实现一个分布式锁。

从某个角度来说架构就将微观的实现放大,或者底层思想就是将宏观的架构进行微缩。计算机的思想是想通的,所以说了解底层的实现可以提升架构能力,提升架构的能力同样可加深对底层实现的理解。计算机知识浩如烟海,但是套路有限。抓住基础的几个套路突破,从思想和思维的角度学习计算机知识。不要将自己的精力花费在不停的追求新技术的脚步上,跟随‘start guide line’只能写一个demo,所得也就是一个demo而已。

停下脚步,回顾基础和经典或许对于技术的提升更大一些。

希望这篇文章对大家有所帮助。

图片 11

编辑:编程 本文来源:使用原子类更精巧轻量,CAS 在现实中的应用

关键词: 澳门新濠3559