volatile的原理和内存屏障参考《Java并发编程的艺术》原子类源码基于JDK8
一丶volatile 与内存屏障
volatile修饰的字段,Java线程模型保证所有线程看到这个变量值是一致的。
1.volatile是如何保证可见性
volatile修饰的变量执行写操作的时候多出lock前缀指令的代码,lock前缀的指令会导致
将当前这个处理器缓存行的数据写回到系统内存这个写回内存的操作将导致其他CPU里缓存了该地址内存的数据无效
为了提高处理速度,处理器不直接和内存通信,而是先把系统内存的数据读到内部缓存后继续操作,但是操作完不知道何时写回内存。如果对volatile修饰的变量执行写操作,将会让数据写回到系统内存,但是其他线程还是使用缓存中的旧值,还是会存在问题。所以在多处理器下为了保证每一个处理器缓存时一致的,就会实现缓存一致性协议,每个处理器通过嗅探总线上传播的数据来检查自己缓存的数据是否过期,如果发现自己缓存行中对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效,当前处理器对这个数据进行修改操作时,会重新从主内存拉取最新的数据到缓存。
2 指令重排序
在程序执行时,为了提高性能,处理器和编译器通常会对指令进行重排序。
编译器优化重排序,编译器在不改变语义的情况下,重新安排语句执行顺序。指令级别并行重排序,如果不存在数据依赖性,处理器改变语句对应机器指令的执行顺序。内存系统重排序,由于处理器使用缓存和读/写缓冲区,这使得加载和存储的操作看起来是乱序执行
为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障来禁止处理器级别的(指令级别并行重排序,内存系统重排序)指令重排序
3 JMM中内存屏障的类型
不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。
Load Barrier: 在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据Store Barrier:在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存
实际使用中,又分为以下四种:
4.volatile的内存语义
可见性:对一个volatile变量的读一定能看到(任何线程)对这个volatile变量最后的写原子性:对任意单个volatile变量的读和写具有原子性,但是自增这种复合操作不具备原子性
5.volatile内存语义的实现
JMM为了实现volatile的内存语义限制了编译器重排序和处理器重排序
当第一个操作是普通变量都或者写且第二个操作是volatile写时,编译器不能重排序这两个操作当第二个操作是volatile写时,无论第一个操作是什么都不可以重排序,保证了volatile写操作前的指令不会重排序到volatile写之后当第一个操作是volatile读时,不管第二个操作是什么,都不可重排序,保证了volatile读之后的指令不会重排序到volatile读之前当第一个操作是volatile写,第二个操作是volatile读,不能重排序
为了实现volatile的内存语义,JMM在volatile读和写的时候会插入内存屏障
volatile写的内存屏障
这里的store store 屏障可以保证前面所有普通写对所有处理器可见,实现了在volatile写之前写入缓存的最新数据写回到主内存volatile写之后的内存屏障,避免与后续的volatile读写出现重排序,由于虚拟机无法判断volatile写之后是否需要一个store load屏障,比如在volatile写之后立即return,为了保证volatile的内存语义,JMM十分保守的插入一个store load屏障。volatile 读的内存屏障
这里的loadload保证了下面普通读不可以在volatile读之前,loadstore保证普通写不可在volatile之前
二丶CAS
1.什么是CAS
即比较并替换,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
2.CAS的缺点
ABA问题是指在CAS操作时,其他线程将变量值A改为了B,但是又被改回了A,等到本线程使用期望值A与当前变量进行比较时,发现变量A没有变,于是CAS就将A值进行了交换操作,但是实际上该值已经被其他线程改变过,这与乐观锁的设计思想不符合。ABA问题的解决思路是,每次变量更新的时候把变量的版本号加1,那么A-B-A就会变成A1-B2-A3,只要变量被某一线程修改过,改变量对应的版本号就会发生递增变化,从而解决了ABA问题。热点数据更新问题如果一个数据同时被1000个线程更新,那么存在一个倒霉蛋线程自旋1000次才能成功修改,第一个成功的线程会导致999个线程失败,999个线程必须自旋,依次类推,自旋是消耗CPU资源的,如果一直不成功,那么会占用CPU资源。解决方法:破坏掉for死循环,当超过一定时间或者一定次数时,return退出。或者把热点数据拆分开,最后再汇总
none
这些问题在后面的原子类代码中都有具体的实践
三丶原子类
Java8在java.util.atomic具有16个类,大致可以分为
原子更新基本类型AtomicBooleanAtomicIntegerAtomicLong原子更新数组AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray 原子更新引用数组原子更新引用类型AtomicReference 原子更新引用类型AtomicReferenceFieldUpdater 原子更新引用类型的字段AtomicMarkableReference 原子更新代标记位的引用类型,可以更新一个布尔类型的标记位和引用类型。AtomicStampedReference 原子更新带有版本号,引用类型,该类把版本和引用类型关联起来,可以用于原子更新数据和数据的版本号,可以解决CAS出现的ABA问题累加器DoubleAccumulator Doule类型累加器,支持函数是表达式描述值要如何变化DoubleAdder Doule类型累加器,支持增大减小多少LongAccumulator Long类型累加器,支持函数是表达式描述值要如何变化LongAdder Long类型累加器,支持增大减小多少
四丶原子类源码解析
1.原子更新基本类型
AtomicBoolean ,AtomicInteger,AtomicLong的原理类似,选择AtomicInteger看下。
1.1AtomicInteger 源码解析
1.1.1 字段和偏移量
使用 volatile修饰内部int 类型的value 字段javaprivate volatile int value;//value字段就是用于存储整形变量的,后续操作也是对这个字段的CAS操作volatile修饰保证了value字段对所有线程的可见性,也保证了对value的修改可以立即刷新会主存,以及对value的读取操作也会从主存读取。静态代码块获取value对于 AtomicInteger对象的偏移量javaprivate static final Unsafe unsafe = Unsafe.getUnsafe();//value字段偏移量private static final long valueOffset;static { try { //调用Unsafe中的objectFieldOffset 方法获取value字段相对的偏移量 //cas操作需要需要知道当前value字段的地址, //这个地址是相对AtomicInteger的偏移量, //知道AtomicInteger的地址再加上偏移就可以直接操作value地址的值了 valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); }}
1.1.2 构造方法
java
public AtomicInteger(int initialValue) { value = initialValue;}public AtomicInteger() { //无参构造value 为int 基本类型}
1.1.3 获取和设置value值
java
public final int get() { return value;}public final void set(int newValue) { value = newValue;}
这里没有进行任何线程安全的控制,因为JMM保证了从主存读取volatile修饰的变量,和写入volatile修饰的变量是原子性的操作
1.1.4 获取并赋值 getAndSet
java
public final int getAndSet(int newValue) { return unsafe.getAndSetInt(this, valueOffset, newValue);}
这个方法获取后赋值value为入参newValue,直接调用了Unsafe的getAndSetInt方法
java
public final int getAndSetInt(Object o, long offset, int newValue) { //记录CAS修改前的值 int v; do { //这里和unsafe中的普通读取是存在区别的 //获取旧值,并赋值给v v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, newValue)); //如果CAS修改失败,说明存在多个线程正在进行修改 那么一直进行CAS return v;}
注意这里的getIntVolatile 是带有内存屏障的读取volatile变量,如果这里使用getInt也许会导致重排序出现
java
public final int getAndSetInt(Object o, long offset, int newValue) { //记录CAS修改前的值 int v; //获取旧值,并赋值给v v = getInt(o, offset); do { //导致这里的CAS永远不会成功 因为这里读取v 是没有理解从主存刷新的 } while (!compareAndSwapInt(o, offset, v, newValue)); //如果CAS修改失败,说明存在多个线程正在进行修改 那么一直进行CAS return v;}
1.1.5 比较并设置 compareAndSet
java
public final boolean compareAndSet(int expect, int update) { //入参依次是当前对象,value偏移量,期望值,更新目标 //当前对象,value偏移量可以定位到value字段的地址 //执行CAS操作的时候,将内存位置的值与预期原值(expect)比较, //如果相匹配,那么处理器会自动将该位置值更新为新值(update), //否则,处理器不做任何操作 return unsafe.compareAndSwapInt(this, valueOffset, expect, update); //如果CAS更新成功返回true 否则返回false //这个方法不会尝试自旋到更新成功位置}
1.1.6 获取并自增1或自减1
java
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1);}
java
public final int getAndDecrement() { return unsafe.getAndAddInt(this, valueOffset, -1);}
还是调用的Unsafe的getAndAddInt方法
1.1.7 自增1,自增1并获取,增加并获取
java
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1;}
java
public final int decrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, -1) - 1;}
javapublic final int addAndGet(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta) + delta;}
1.1.8 支持函数式接口的几个方法
这几个方法式JDK8支持函数式接口后新增的方法
getAndAccumulatejavapublic final int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction) { int prev, next; do { //旧值 prev = get(); //CAS将设置成的值 调用IntBinaryOperator获取 next = accumulatorFunction.applyAsInt(prev, x); } while (!compareAndSet(prev, next)); return prev;}比如说我要实现增大到旧值的x倍,并且返回旧值,那么就可以使用java//这里的2 就是增大两倍,int doubleReturnPre = ai.getAndAccumulate(2, (pre, x) -> pre * x);accumulateAndGetjavapublic final int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) { int prev, next; do { prev = get(); next = accumulatorFunction.applyAsInt(prev, x); } while (!compareAndSet(prev, next)); return next;}和getAndAccumulate 不同在于返回是CAS更新成功的值,意味着下面这行代码返回的是增大后的值,而不是增大前的值java//这里的2 就是增大两倍,int doubleReturnNew = ai.accumulateAndGet(2, (pre, x) -> pre * x);updateAndGetjavapublic final int updateAndGet(IntUnaryOperator updateFunction) { int prev, next; do { prev = get(); next = updateFunction.applyAsInt(prev); } while (!compareAndSet(prev, next)); return next;}IntUnaryOperator的applyAsInt只接受一个参数,这里传入了当前值,可以在applyAsInt中定义如何更新。updateAndGet返回新值getAndUpdatejavapublic final int getAndUpdate(IntUnaryOperator updateFunction) { int prev, next; do { prev = get(); next = updateFunction.applyAsInt(prev); } while (!compareAndSet(prev, next)); return prev;}和updateAndGet类似,返回的是旧值
1.1.9lazySet
java
public final void lazySet(int newValue) { unsafe.putOrderedInt(this, valueOffset, newValue);}
lazySet提供一个store store屏障(在当代系统中是很低成本的操作,或者说没有操作),但是没有store load屏障,我的理解是把volatile的写后store load替换成了store store,Store load屏障可以让后续的load指令对其他处理器可见,但是需要将其他处理器的缓存设置成无效让它们重新从主层读取,store store,是保证后续处理器在写volatile变量的时候可以看见lazyset方法改变的值,但是后续的读不保证一定可见,但是对于volatile变量的读自然是会读到最新值的,从而减少了开销。lazySet的lazy 意味着最终数据的一致性,但是当前是进行了偷懒的(指store store替代了storeload)
2.AtomicBoolean
源码基本上和AtomicInteger类似,但是并不是底层存的布尔类型,而是使用int类型,0代表false,1代表true
3.AtomicLong
和AtomicInteger类似
2.原子更新数组
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray的原理类似,数组类型更新的问题在于,我要更新下标为i的元素,我怎么知道i这个元素的地址。如果我们知道第一个元素相对于对象的偏移base,和每个元素的偏移s,那么第i个元素就是base+i*s
java
private static final int base = unsafe.arrayBaseOffset(int[].class);private static final int shift; static { //每个元素的大小 int scale = unsafe.arrayIndexScale(int[].class); //必须是2的n次幂 if ((scale & (scale - 1)) != 0) throw new Error("data type scale not a power of two"); //每个元素大小为4 那么第n个大小偏移就是n*4 也就是n<<2 //shift 是31 - scale的前导0 方便后续进行位移操作获取第n个元素相对于第一个的偏移量 shift = 31 - Integer.numberOfLeadingZeros(scale); }
2.1计算第i个元素的位置
java
//前置检查private long checkedByteOffset(int i) { if (i < 0 || i >= array.length) throw new IndexOutOfBoundsException("index " + i); return byteOffset(i);}//第n个元素的位置//i*4+base==> i<<2 + baseprivate static long byteOffset(int i) { return ((long) i << shift) + base;}
2.2 获取和设置
java
public final int get(int i) { return getRaw(checkedByteOffset(i));}private int getRaw(long offset) { //调用getIntVolatile 保证了可见性 return unsafe.getIntVolatile(array, offset);}//同样设置也是调用putIntVolatilepublic final void set(int i, int newValue) { unsafe.putIntVolatile(array, checkedByteOffset(i), newValue);}
其他方法和AtomicInteger中大差不大都是调用Unsafe中的相关方法
3.原子更新引用
AtomicReference还是老套路,不多赘述
3.1 原子更新引用的一个字段
AtomicReferenceFieldUpdater 是一个抽象类,使用的时候必须调用newUpdater(持有字段类的class,字段类型,字段名称)来获取它的实现AtomicReferenceFieldUpdaterImpl(调用了AtomicReferenceFieldUpdaterImpl的构造方法涉及一些类加载器知识)后续的更新也是调用unsafe的cas相关操作
3.2 原子的更新引用和布尔标记
AtomicMarkableReference可以同时更新引用和引用的标记,上面我们提到CAS的一个缺点——ABA问题,比如说,当前商店存在一个活动,如果账户办理冲一百送50,每个账户依次机会,A用户充值后获得150元立马消费成0元接着充值100,如果用普通的原子类AtomicInteger程序还会再次送50元给用户A(ABA问题,程序不知道是否赠送过了),我们可以使用锁充值后获取锁往集合里面记录当前用户赠送了,也可以使用AtomicMarkableReference通过更新mark来记录用户赠送过了
AtomicMarkableReference内部维护了一个Pair,并且private volatile Pair
3.2.1compareAndSet(旧引用,新引用,旧标记,新标记)
java
public boolean compareAndSet(V expectedReference, V newReference, boolean expectedMark, boolean newMark) { //当前AtomicMarkableReference 中的 Pair 记录引用和 标记 Pair
也就是说,首先要求旧值是和当前pair相同的,如果修改之前被其他线程修改了那么短路返回false,如果引用从始至终都没改变,那么都不需要CAS操作,否则CAS pair属性,下面是casPair的源码——还是老套路
java
private boolean casPair(Pair
3.2.2 attemptMark(旧引用,新标记)
java
public boolean attemptMark(V expectedReference, boolean newMark) { Pair
和compareAndSet的区别在于,其只要求引用相同,如果mark相同那么什么都不做,反之CAS修改pair
3.3 原子的更新引用和版本号
AtomicStampedReference也是用来解决ABA问题的,不同的是其标记不只是true和false,可以是1,2,3等等等版本号,我们把AtomicMarkableReference中活动改下,每一个账户可以参与3次活动,那么在充值的时候我们把版本号加1,最后版本号来到3 表示这个账户参与了3次,后续充值就不赠送了。
AtomicStampedReference 实现和AtomicMarkableReference简直一模一样,区别在于AtomicStampedReference中Pair类是引用和版本号
4.累加器
上面我们提到CAS的缺点说到存在热点数据更新导致多数线程失败自旋的问题,其中一个解决办法是自旋次数,失败就返回活动太火爆这种劝退消息,另外一种解决办法是——热点数据拆分开,最后再汇总。这个思路和ConcurrentHashMap的分段锁思路类似,既然我如同HashTable导致性能低下(修改key A和B都受一把锁的影响)那么我把数据,不同的数据使用不同的锁,就可以提高吞吐量了。在累加器中的体现就是,在最初无竞争时,只更新base的值,当有多线程竞争时通过分段的思想,让不同的线程更新不同的段,最后把这些段相加就得到了完整存储的值。
none
累加器的思路都类似,我们选择LongAdder 和 LongAccumulator来看下
4.1 LongAdder
4.1.1 LongAdder的内部结构
LongAdder 内部有base用于在没有竞争的情况下,进行CAS更新,其中还有Cell数组在冲突的时候根据线程唯一标识对Cell数组长度进行取模,让线程去更新Cell数组中的内容。这样最后的值就是 base+Cell数组之和,LongAdder自然只能保证最终一致性,如果边更新边获取总和不能保证总和正确。
4.1.2 LongAdder的继承关系
这里比较迷惑的就是Striped64这个类,此类是一个内部类,用于实现 Adder 和 Accumulator,我们上面所说的base,Cell数组其实就是在此类中的。
4.1.3 LongAdder 源码解析
4.1.3.1Cell 类
此类位于Striped64中,就是我们上面说的Cell数组进行热点数据分离的Cell
java
@sun.misc.Contended static final class Cell { volatile long value; Cell(long x) { value = x; } final boolean cas(long cmp, long val) { return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val); } //老套路 unsafe private static final sun.misc.Unsafe UNSAFE; //value 字段的偏移量 private static final long valueOffset; static { //初始化 获取unsafe 实例 以及获取value 偏移量 try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class> ak = Cell.class; valueOffset = UNSAFE.objectFieldOffset (ak.getDeclaredField("value")); } catch (Exception e) { throw new Error(e); } }}
Cell 这个类还是老套路,唯一不同的是它类上面具有一个注解 @sun.misc.Contended此注解会进行缓存填充,避免缓存伪共享 (这部分内容在文末);
4.1.3.2 Striped64中的属性
NCPU 记录了系统 CPU 的核数,因为真正的并发数最多只能是 CPU 核数,因此 cells 数组一般要大于这个数。cells 数组,大小是 2 的次方,这样将线程映射到 cells 元素时方便计算。base,基本数值,一般在无竞争能用上,同时在 cells 初始化时也会用到。cellsBusy,自旋锁,在创建或扩充 cells 时使用
4.1.3.3 LongAdder #void add(long x)
java
public void increment() { add(1L);}public void decrement() { add(-1L);}//LongAdder中增大和减小都是直接调用的add(long x) 方法
java
public void add(long x) { //as ——Cells数组的引用 //b 获取到的base值 //v 期望值 //m 标识Cells数组的长度-1 //a 标识当前线程命中的Cell单元格 Cell[] as; long b, v; int m; Cell a; //如果 cells数组初始化了(Striped64是懒惰的初始化,没有线程竞争的时候cells数组不会被初始化) // 或者 cas的修改base值 失败了(说明多个线程都在尝试cas修改,出现了竞争) if ((as = cells) != null || !casBase(b = base, b + x)) { //没有发生冲突的标识 boolean uncontended = true; //as == null || (m = as.length - 1) < 0 表示如果cell数组为空 if (as == null || (m = as.length - 1) < 0 || //或者当前线程的cell单元没有初始化 (a = as[getProbe() & m]) == null || //或者cas修改base失败了 !(uncontended = a.cas(v = a.value, v + x))) longAccumulate(x, null, uncontended); }}
A.对于第一个if
从来没有发生过竞争并发量很低的时候Cell数组就是空,这个时候第一个if中的 (as = cells) != null就是false 会继续执行后续的!casBase(b = base, b + x)这一步会cas的更新bese 值
如果cas更新base成功了,那么皆大欢喜,直接结束了,说明当前并发还是很低如果cas 更新失败,说明这一瞬间有多个线程都在更新base值,并发比较高,当前线程是一个倒霉蛋,cas更新没有抢过别人。这个时候会进入到 if代码块中
之前发生过竞争这个时候第一个if的 (as = cells) != null就成立了 ,不会走第一个if中的cas操作,直接进入第二个ifB.对于第二个if进入第二个if,当前线程需要把值更新到对应的cell中
as == null || (m = as.length - 1) < 0 这意味着cell数组没有初始化,也就是说这是第一次存在高并发竞争的情况,那么调用longAccumulate 这个方法会帮我们初始化cell数组的(a = as[getProbe() & m]) == null 这意味着,cell数组初始化了,但是当前线程标识取模数组长度得到当前线程应该更新的cell为空如果当前线程所属的Cell为空,那么也会调用longAccumulatenone这里我们要关注一点getProbe 方法初始的时候都是0,0取模任何数都是0那么每一个线程最开始都会分配第一个Cell,那么第一个Cell为空意味着什么昵,这个问题需要我们看完longAccumulate 方法才能揭晓其实probe=0在longAccumulate方法中意味着当前线程没有和其他线程发生冲突更新在longAccumulate 会初始化probe 设置冲突更新表示为false
getProbe方法是获取线程的一个标识,获取的是当前线程中的threadLocalRandomProbe字段,字段没有初始化的时候默认是0m是 cell数组的长度-1,cell数组的长度为2的n此幂,m的二进制全1,getProbe() & m就是对cell数组长度进行取模,这点在HashMap源码中也使用到了
!(uncontended = a.cas(v = a.value, v + x)) 这里是调用Cell的cas方法,就是更新Cell对象中的value字段,如果这里cas失败了,说明当前存在一个线程也在更新当前cell对象的value,两个线程要更新一个cell,说明出现了冲突,也会调用longAccumulate进行自旋更新cell单元格中的值。
4.1.3.4 Striped64#longAccumulate
none
longAccumulate 方法非常长,我们拆看慢慢看
初始化threadLocalRandomProbejava//如果是0 表示没有是没有初始化的//这里会为当前线程生成一个probeif ((h = getProbe()) == 0) { ThreadLocalRandom.current(); // force initialization h = getProbe(); //设置为没有竞争, wasUncontended = true;}自旋保证当前线程能把值写入A.如果Cell数组已经成功初始化,下面都是A的子情况B.当前Cell数组没有初始化,当前线程进行初始化javaelse if (cellsBusy == 0 && cells == as && casCellsBusy()) { //是否完成了初始化 boolean init = false; try { //==========代码点1(后续解析中会使用到)============== //确认没有其他老六抢先初始化 if (cells == as) { // 初始化 Cell[] rs = new Cell[2]; //选择一个格子 设置为x,probe奇数那么选择rs[1] 反之rs[0] rs[h & 1] = new Cell(x); cells = rs; init = true; } } finally { //释放锁 cellsBusy = 0; } //如果成功初始化 那么结束 if (init) break;}
在判断(a = as[(n - 1) & h]) == null即当前线程对应的cell为空(代码点1),首先在代码点2是判断了cellsBusy == 0说明当前无线程在创建Cell单元格的,然后new了一个Cell,继续在代码点3 还是会判断cellsBusy == 0,是由于我们在new一个cell的过程中可能存在消耗完时间片的情况,然后其他线程恰好可能已经获得到了cellsBusy这把锁,这里再次判断cellsBusy反之无脑获取锁执行casCellsBusy,可以说doug lea真的是性能狂魔在代码点4,来到代码点4其实已经在代码点1处已经判断了当前线程对应的Cell单元格为空啊,为什么这里还要判断一次昵,因为可能在当前new 一个cell的这段时间有另外一个线程也设置了这个位置的Cell,或者改变了cell数组,并且释放了cellsBusy锁,为了保证此位置的Cell单元格的值不被当前线程无脑覆盖,所有再次进行了判断。什么时候会结束自旋,这段代码其实给出了一个答案——created为true这里的created只会在当前线程成功设置其对应的单元格为new Cell(增加的值)时为true,也就代表着当前线程已经成功进行了一个增加操作什么时候会继续自旋
(n >= NCPU || cells != as) 如果cell数组长度已经大于等于jvm可以使用的cpu核心数了,或者cells引用指向改变了,那么扩容意向设置为false,然后执行advanceProbe对当前线程的probe进行rehashjava线程模型的学习笔记中,我们指出,java线程和操作系统是一对一模型,我理解这里一个cpu核心执行在一个时刻运行一个线程,所以cells数组太大也没什么用。那么为什么cells != as成立也是进行rehash probe然后继续自旋昵,这里可以理解为当前线程尝试对它对应的cell单元格进行cas操作,但是失败了,这个时候发现cells != as说明有其他线程对当前cell数组进行了扩容,从而改变了cells数组的引用指向(as是就的cells数组)为了防止多次扩容,这个时候就设置以下扩容意向为false 然后让当前线程“从卷java,转变为卷Go”换一个并发不那么高的Cell数组单元格进行cas操作。代码点2进入到这里的情况有 (a = as[(n - 1) & h]) == null但是被其他线程初始化了对应位置的Cell,cas设置对应cell失败,cell数组已经达到jvm可用cpu,当前线程执行的途中没有其他线程完成扩容。但是当前还是无法在自己对应的cell上成功进行cas,说明和其他线程发生了冲突,这个时候让当前线程rehash以下probe然后再次自旋一次,如果还是无法在自己对应的cell进行cas操作,且没有发生扩容的话会来到下面的3拿到锁,对cell数组进行扩容,进入这里,说明没有其他线程进行扩容,当前线程对应的cell不为null,但是对cell进行CAS操作还是失败。这时候为了提高性能只能牺牲一点空间了,进行扩容。有意思的点在于代码点3,为什么在这里还是需要进行一次判断昵,因为cellsBusy == 0 && casCellsBusy()这两个操作不是原子的,可能cellsBusy == 0执行完失去了时间片,这时候有一个老六进行了扩容,改变了cells数组引用指向,并且释放了锁,这时候如果不做这个判断,可能导致cell数组元素的丢失。后续就是对cells进行扩容,然后释放锁,设置扩容意向为false,然后continue,注意这个continue,这会导致当前线程不会执行advanceProbe,为什么昵,哥们都扩容了,你现在让我换个格子,那我为啥要扩容,属于是“Java太卷,但是我命由我不由天,为自己创造岗位”,后续这个线程进行自旋的时候随机到的Cell数组可能还是原来的,可能是原来位置加上当前cell数组长度的一半,但是还是可以把一些“竞争者”分散开了代码点2处的if (cellsBusy == 0)不成立这意味着,当前线程对应的Cell为空,但是存在其他线程正在调整Cell数组大小or或创建单元格,为了保证Cell数组中的值不被覆盖,这个时候会执行到代码点5调用advanceProbe重新为当前线程生成一个probejava//使用位操作,把当前线程的probe随机打散,为啥这里这样进行位操作//我只能说,可能时doug lea研究后的,或者他喜欢这个几个数字//但是这几个数组都是质数,大概率后面是存在理论支撑的,static final int advanceProbe(int probe) { probe ^= probe << 13; // xorshift probe ^= probe >>> 17; probe ^= probe << 5; UNSAFE.putInt(Thread.currentThread(), PROBE, probe); return probe;}代码点3处的if(cellsBusy == 0 && casCellsBusy())不成立和上面的1差不多if (created)不成立这意味,代码点4处的判断不成立,说明存在线程A已经完成了cell数组的扩容,导致当前线程对应的Cell改变了(数组长度扩大2,probe%长度=n,可能是原来的位置n,也可以是n+当前长度/2 )也可能是线程A给当前线程对应Cell单元格赋值了(线程A的probe对数组长度取模后和当前线程相同,但是线程A抢先一步设置了单元格)但是这时候不会调用到advanceProbe因为可以沿用之前的probe找到对应的位置进行设置值,这个坑位还是可以设置值的只是有人抢先一步了,不能直接new Cell(x),需要让这个Cell值增加x,但是1和2调用advanceProbe的原因是,为了提升性能,让他随便找个其他坑位做增加的操作。none再次给看跪了,doug lea真性能狂魔情况1:如果当前线程threadLocalRandomProbe取模后对应的cell为空,那么我们需要在当前线程对应的位置new一个cell赋值上去java//as——cells数组引用//a 当前线程对于的cell//n 当前数组长度//v 期望值//h 当前线程的threadLocalRandomProbe//x是当前线程要增加的值Cell[] as; Cell a; int n; long v;//如果cells初始化了if ((as = cells) != null && (n = as.length) > 0) { //如果当前线程threadLocalRandomProbe取模后对于的cell为空 //==========代码点1(后续解析中会使用到)============== if ((a = as[(n - 1) & h]) == null) { //cellsBusy是一个自旋锁保证Cell数组的线程安全 //0代表无线程调整Cell数组大小or或创建单元格 //1 则反之 //==========代码点2(后续解析中会使用到)============== if (cellsBusy == 0) { //为当前线程创建一个cell, //x直接赋值给其中的value 后续求和会加上这个x,从而实现增加 Cell r = new Cell(x); //0代表无线程调整Cell数组大小or或创建单元格 //casCellsBusy 是从0设置成1 表示当前线程尝试获取这把锁 //==========代码点3(后续解析中会使用到)============== if (cellsBusy == 0 && casCellsBusy()) { boolean created = false; try { Cell[] rs; int m, j; //重新判断cell数组初始化了,且当前cell是空 //看下方解析为何需要重新 //==========代码点4(后续解析中会使用到)============== if ((rs = cells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null) { //设置到cell数组上 rs[j] = r; created = true; } } finally { //释放锁 cellsBusy = 0; } //如果这里成功创建了cell,说明成功把值加上去了 //那么退出自旋 if (created) break; continue; // Slot is now non-empty } } collide = false; //....省略部分代码,这部分也会在后续解析 //重新刷新当前线程的Probe //==========代码点5(后续解析中会使用到)============== h = advanceProbe(h); }这里比较有意思有情况2:如果在LongAdder#add方法中对应Cell进行CAS失败,那么rehash后继续自旋javaif ((as = cells) != null && (n = as.length) > 0) { if ((a = as[(n - 1) & h]) == null) { //省略了情况1的代码 } else if (!wasUncontended) // CAS already known to fail wasUncontended = true; //....省略部分代码,这部分也会在后续解析 //重新刷新当前线程的Probe h = advanceProbe(h);wasUncontended这个变量位false只可能是调用longAccumulate这个方法入参就为false,让我们回到LongAdder#addjavapublic void add(long x) { Cell[] as; long b, v; int m; Cell a; if ((as = cells) != null || !casBase(b = base, b + x)) { //初始为true boolean uncontended = true; if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || //注意这里 !(uncontended = a.cas(v = a.value, v + x))) //要让uncontended为false //那么说明上面的a.cas(v = a.value, v + x)失败了 longAccumulate(x, null, uncontended) }}这里我们可以看到,必须是当前线程对其cell进行cas操作失败才可能为false,这里的false意味着,当前并发很高,有几个老六线程在对这个一个cell进行cas,那么这个时候会执行到else if (!wasUncontended) wasUncontended = true然后执行advanceProbe,这意味着,只能说当前线程命不好执行重新rehash下probe换一个Cell单元格进行操作,可以理解为Java就业太卷了,换Go语言了。这样做的好处是提高了其他cell单元格的利用率,性能up,这里把wasUncontended随后设置为true,可以理解为,当前线程都要rehash了,后续发生还不行那就是“岗位不够了”得扩容Cell数组了,后续也就用不着wasUncontended情况3:成功把值增加到对应的Celljavaif ((as = cells) != null && (n = as.length) > 0) { //省略讲过的代码。。。 // 成功把值增加到对应的cell else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) //自旋结束 break;这里出现了自旋退出的另外一个情况,那就是当前线程成功把增加的值设置到其对应的cell单元格,这时候结束自旋,很合理。这里出现了一个fn指的是调用当前方法传入的LongBinaryOperator 是一个函数式接口。LongAdder的add方法默认传入的是空,会执行v + x也就是增加cell单元格的值,这个LongBinaryOperator在LongAccumulator使用到,后续我们看下java@FunctionalInterfacepublic interface LongBinaryOperator { long applyAsLong(long left, long right);}情况四:对Cell数组进行扩容如果并发实在是太大了,Cell数组单元格的数量已经容纳不下这么多线程一起执行了,那么为了避免想AtomicLong一样无脑自旋,浪费CPU,这时候会选择对Cell数组进行扩容。javaif ((as = cells) != null && (n = as.length) > 0) { //省略讲过的代码。。。 //==========代码点1(后续解析中会使用到)============== //collide 表示扩容的意向,为true并不代表一定会扩容 //如果cell数组的长度大于了jvm可以使用的核心数 或者cells数组引用改变了 else if (n >= NCPU || cells != as) collide = false; //==========代码点2(后续解析中会使用到)============== else if (!collide) collide = true; //拿到cellsBusy这把锁 else if (cellsBusy == 0 && casCellsBusy()) { try { //判断下cells引用没有改变 //==========代码点3(后续解析中会使用到)============== if (cells == as) { //扩容 扩大1倍 Cell[] rs = new Cell[n << 1]; for (int i = 0; i < n; ++i) rs[i] = as[i]; //改变cells应用指向 cells = rs; } } finally { //释放锁 cellsBusy = 0; } //扩容意向为false collide = false; continue; } //rehash probe h = advanceProbe(h);}这里有意思的点有
这里有意思的点,在代码点1还是会进行cells == as的判断,这是由于cellsBusy == 0 && cells == as && casCellsBusy() 并不是一个原子操作,可能存在其他线程,抢先初始化cell数组,所以需要再次判断以下。这里我们可以看到初始化的cell数组大小为2,后续都是扩大一倍
C.Cell没有初始化,但是当前线程尝试初始化失败,尝试操作base值
java
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break;
来到这里,说明A和B都是不成立的,也就意味着,当前线程进来的时候发现cell没有初始化,然后来到B,但是cellsBusy == 0 && cells == as && casCellsBusy()发现不成立,不成立的情况有
cellsBusy == 0不成立,说明之前有线程已经拿到锁了,正在初始化cells == as 不成立,有一个线程已经完成了初始化,导致cell引用指向改变casCellsBusy()不成立,竞争锁的过程中失败了
这个时候会让当前线程尝试更新下base值,说不定很多线程都在尝试更新cell元素,这个时候更新下base 可能也许会成功。
4.1.3.5 sum
java
public long sum() { Cell[] as = cells; Cell a; long sum = base; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum;}
没什么好说的,强于doug lea也只能保证最终一致性,显然如果存在其他线程并发add的时候,这个方法只能拿到快照数据
4.1.3.6 reset
java
public void reset() { Cell[] as = cells; Cell a; base = 0L; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) a.value = 0L; } }}
没什么好说的,线程不安全,如果存在其他线程add,这时候调用reset,可能导致并没有reset成功,或者说如果其他线程扩容到一般,调用reset,那么reset也会不成功。还有一点是reset并不会改变cell数组大小
4.1.3.7 sumThenReset
java
public long sumThenReset() { Cell[] as = cells; Cell a; long sum = base; base = 0L; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) { sum += a.value; a.value = 0L; } } } return sum; }
求和并设置为0并不会改变cell数组大小。
4.2 LongAccumulator
大致和LongAdder类似,LongAccumulator需要指定如何如果操作 ——LongBinaryOperator(旧值没有冲突时时base,冲突时是cell,accumulate传入的值)
java
public LongAccumulator(LongBinaryOperator accumulatorFunction, long identity) { this.function = accumulatorFunction; base = this.identity = identity;}
4.2.1 accumulate
java
public void accumulate(long x) { Cell[] as; long b, v, r; int m; Cell a; //cell数组没有初始化 if ((as = cells) != null || (r = function.applyAsLong(b = base, x)) != b && !casBase(b, r)) { boolean uncontended = true; //cell数组没有初始化 if (as == null || (m = as.length - 1) < 0 || (a = as[getProbe() & m]) == null || !(uncontended = (r = function.applyAsLong(v = a.value, x)) == v || //或cas失败 a.cas(v, r))) //这里传入了 function 在 longAccumulate中就不是简单的自增了 longAccumulate(x, function, uncontended); }}
4.2.3 get
java
public long get() { Cell[] as = cells; Cell a; long result = base; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) result = function.applyAsLong(result, a.value); } } return result; }
这一这里返回的值,不是进行累加而是function.applyAsLong(result, a.value)取决于你定义的操作——LongBinaryOperator
4.3 DoubleAdder 和DoubleAccumulator
和LongAdder 与LongAccumulator 类似,但是是通过把Double转换成Long调用doubleAccumulate来完成的
五.缓存伪共享
我们在解析LongAdder源码的时候看到 Striped64中的Cell类上面存在一个@sun.misc.Contended的注解,我们说这是为了反正缓存伪共享,下面我们聊下啥是缓存伪共享
java
@sun.misc.Contended static final class Cell { volatile long value; //省略部分代码}
1.什么是伪共享
缓存是由缓存行组成的,通常一个缓存行是 64 字节,在程序运行的过程中,缓存每次更新都从主内存中加载连续的 64 个字节。因此,如果访问一个 long 类型的数组时,当数组中的一个值被加载到缓存中时,另外 7 个连续的元素也会被加载到缓存中,地址上不连续的就不会加载到同一个缓存行了。这种免费加载也有一个坏处。设想如果我们有个 long 类型的变量 a,它不是数组的一部分,而是一个单独的变量,并且还有另外一个 long 类型的变量 b 紧挨着它,那么当加载 a 的时候将免费加载 b。如果一个 CPU 核心的线程在对 a 进行修改,另一个 CPU 核心的线程却在对 b 进行读取,当前者修改 a 时,会把 a 和 b 同时加载到前者核心的缓存行中,更新完 a 后其它所有包含 a 的缓存行都将失效,因为其它缓存中的 a 不是最新值了,而当后者读取 b 时,发现这个缓存行已经失效了,需要从主内存中重新加载。这就很坑爹了,我只是想更新a,但是却让有效的b无效了。
2.解决伪共享的办法
2.1填充无用字节
只要我填一些无用的字节,在a和b之间,让a和b不在一个缓存行中就解决了这个问题,但是现在虚拟机很聪明,会对我们手动填充的无用字节进行忽视
2.2 使用@sun.misc.Contended
这也是一种填充无用字节的做法,但是是jvm帮我填充。
如下Long1这个类标注了@sun.misc.Contended我们在启动的jvm的时候加上 -XX:-RestrictContended 对比不加 @sun.misc.Contended注解的时候,其实有很大的差别(几个数量级的差距)
java
public static void main(String[] args) { test2();}private static void test2() { Long1 long1 = new Long1(); CountDownLatch latch = new CountDownLatch(2); long start = System.currentTimeMillis(); new Thread(() -> { for (int i = 0; i < 1000000000; i++) { long1.l1++; } latch.countDown(); }).start(); new Thread(() -> { for (int i = 0; i < 1000000000; i++) { long1.l2++; } latch.countDown(); }).start(); try { latch.await(); long end = System.currentTimeMillis(); System.out.println(end - start); } catch (InterruptedException e) { }} @Contended static class Long1 { private volatile long l1; private volatile long l2; }
3.为什么Cell要加@sun.misc.Contended
如果使用@sun.misc.Contended那么ACell 和BCell不在一个缓冲行,就不会发生这样的情况了,从主内存加载数据到缓存还是需要消耗一定时间的。本文作者: Cuzzz本文链接: https://www.cnblogs.com/cuzzz/p/16558333.html
举报/反馈
在世界地图上看,地处印度洋的斯里兰卡就像亚欧大陆滴落的一滴眼泪。马可·波罗曾称它为“世界上最美丽的岛屿”。而如今,这个印度洋上的美丽岛国,却深陷经济和政治危机,可能即将陷入人道主义危机,亟待国际援助。
经济危机社会动荡
斯里兰卡正陷入自1948年脱离英国独立以来最为严重的经济危机。物价飙升,食品、药品和能源短缺,外汇储备枯竭,政局陷入动荡。
2022年5月19日,斯里兰卡出现独立以来首次主权债务违约。7月5日,时任总理拉尼尔·维克拉马辛哈在国会上表示,国家破产了。此前,6月9日,联合国呼吁援助斯里坎卡受经济危机影响最严重的170万人,并警告晚些时候有可能发生人道主义危机。
7月6日,联合国世界粮食计划署(WFP)发布的报告显示,在人口约2100万的斯里兰卡,每10户家庭就有3户面临着粮食不安全状况,涉及人口达到626万,其中6.56万人面临严重的粮食不安全。7月9日,持续数月的经济危机升级,数千名抗议群众攻入总统官邸和办公厅。随后,又放火焚烧了总理的私人住宅。7月13日,总统秘密出逃。
7月18日,维克拉马辛哈曾以代理总统的身份透露消息称,斯里兰卡政府与国际货币基金组织(IMF)的谈判已近尾声,还称其他争取外国援助的讨论也在取得进展。
7月20日,斯里兰卡议会通过投票,维克拉马辛哈正式被选为新任总统。但维克拉马辛哈并不是一位受民众欢迎的政治人物,现年也已经73岁,其政治生涯将如何落幕,目前尚存很大变数。
斯里兰卡央行的统计数据显示,2022年以来,首都科伦坡的通胀水平已经超过了60%,食品通胀率高达81%。
过去几个月里,为了遏止抗议浪潮,斯里兰卡政府已经不止一次地宣布过国家进入紧急状态和实施宵禁措施,但最终都收效甚微。
经济灾难始于恐袭
从近处说,这一轮经济危机始于2019年。
2019年4月21日,复活节当日,在首都科伦坡的三家五星级酒店、两处教堂、科伦坡南方代西瓦里动物园附近及东部城市拜蒂克洛一处教堂等地,连续发生八起爆炸事件,共造成253人死亡(包括38名外籍),超过500人受伤——这是斯里兰卡自2009年结束长达26年的内战以来,造成最多人员伤亡的恐怖袭击事件。
据媒体报道,当年6月,斯里兰卡央行在一份遭到泄露的内部资料中表示,由于这次袭击,政府因增值税等间接税而损失的年度收入约为260亿卢比(合1.19亿美元),而当年末斯里兰卡的外汇储备总额才为76.42亿美元。
根据Wind数据,笔者估算,由于这次袭击,斯里兰卡2019年的旅游业收入同比下降至少24.32%,外商直接投资净流入下降约46.29%。
政治家族垄断权力
从远处说,这一轮经济危机与权力垄断脱不开关系。
2009年恐怖袭击发生后,社会普遍担心社会秩序被扰乱,斯里兰卡再次被拖回内战状态。在这种背景下,以强硬姿态终结了斯里兰卡持续26年内战的国防部前部长戈塔巴雅·拉贾帕克萨以压倒性优势赢得了同年11月的总统选举。
而前一任总统是谁呢?正是现任总统戈塔巴雅的哥哥马欣达·拉贾帕克萨。后者在2005年胜选上台,2010年连任成功。
经过马欣达连任两届的铺垫,2019年戈塔巴雅上台后,其领导的执政联盟斯里兰卡人民阵线(SLPP)压倒性赢得了议会选举,拿下225个议席中的145个,取得绝对控制权。
斯里兰卡2015年的第19号宪法修正案规定,总统、总理和议会共同享有行政权力。但戈塔巴雅控制议会后,立刻推动废除限制总统权力的第19号宪法修正案。
但其实无论是否废除修正案,拉贾帕克萨家族都已经事实上掌控了这个国家。比如,上述议会选举中,已经六次获任斯里兰卡总理,却从未干完任何一个任期的时任总理维克拉马辛哈领导的统一国民党(UNP)在议会选举中仅获得1个席位。
不仅如此,维克拉马辛哈实际上也是拉贾帕克萨家族的附庸。侧面证据如下:
-2015年5月,时任总统马欣达在抗议中下台,辞职前委任维克拉马辛哈作为总理。紧接着,维克拉马辛哈阻拦了检方对拉贾帕克萨家族启动的多项调查。
-2019年11月,戈塔巴雅任命自己的哥哥、前总统马欣达·拉贾帕克萨为总理,维克拉马辛哈下台。
-2022年5月9日,经济危机持续一段时间后,面对广泛的社会抗议,马欣达被迫下野,维克拉马辛哈再次被委任为总理。
-2022年7月13日,局势几乎彻底失控,总统戈塔巴雅秘密出逃,在缺席的情况下仍能将维克拉马辛哈委任为代理总统。
-2022年7月20日,由议会选出新总统的投票当中,投票支持维克拉马辛哈的,主要仍是拉贾帕克萨家族所主导的原执政党人民阵线(SLPP)。
无论是戈塔巴雅还是马欣达,上台后都大量任用家族成员担任政府要职:马欣达的长兄查马尔获任农业部长;马欣达的弟弟巴兹尔获任财政部长(他同时持有美国国籍);马欣达的长子纳马尔获任体育部长,并被视为下一任政治领袖的接班人;马欣达的次子约西塔担任马欣达的总理办公厅主任。
据统计,戈塔巴雅和马欣达统治斯里兰卡期间,拉贾帕克萨家族总共出了11位部级领导人,还有40多名拉贾帕克萨家族成员担任政府职务。
拉贾帕克萨家族实际上像管理家族企业一样管理着这个国家。政治高度集权最具破坏性的后果就是:缺失纠错机制。
政策失误叠加疫情
2009年,内战结束后的斯里兰卡开始快速重建。Wind数据显示,2009~2015年间,斯里兰卡的年均经济增速为6.49%,人均GDP为3244.97美元;同期印度的经济增速为8.63%,但人均GDP只有1426.57美元。
但是,自2015年起斯里兰卡GDP增长率呈现出逐年递减的趋势,2015~2019年,分别为4.49%,3.58%,3.27%和2.33%。原因在于,斯里兰卡当局从20世纪60年代就开始从国际金融组织借钱维持经济运转和发展。2009~2015年,马欣达执政期间虽然经济发生了较快的增长,但连年内战和战后经济重建已经给斯里兰卡留下了巨额债务。
据Wind显示,自2000~2009年,斯里兰卡的外债总额分别从90.31亿增长到186.62亿美元(106.64%);2009~2015年,从186.62亿继续增长到448.39亿美元(140.27%)。两个阶段外债的年均增速分别为8.75%和17.57%,远高于同期实际GDP的增长率。
有评论认为,戈塔巴雅是“无端出台”减税计划。尽管拉贾帕克萨家族控制斯里兰卡、戈塔巴雅推出减税计划是经济危机的重要原因,但戈塔巴雅却绝非无能之辈,减税计划也并非“无端”——或许是因为他看到了过去近十年经济增长带来的社会问题,以及债务融资型增长模式对斯里兰卡的困扰。
2019年斯里兰卡大选,戈塔巴雅的承诺就是推行减税和绿色转型,当时颇受欢迎。
减税很实惠,上台伊始推行减税政策确实也短期提振了斯里兰卡的经济,吸引了更多的海外投资。绿色转型很时髦,符合世界主流议题。
但问题在于,历史进程并不会按剧本走,好心可能办坏事。当权力失去制约,领导听不进建议,出问题就绝不是偶然。
戈塔巴雅上台后的第一件事是履行其竞选承诺,推行减税:下调包括增值税和企业所得税在内的多项关键税税率,取消居民预扣税、国家建设税等7项征税科目。据测算,减税政策使斯里兰卡的增值税降幅达到50%,政府的税收总额减少了1/4(仅占GDP的8.4%),为斯里兰卡独立以来的最低水平。
最了解一个国家家底的是该国财长。而早在戈塔巴雅竞选期间,时任财长曼加拉·萨马拉维拉就高调反对戈塔巴雅的减税计划,甚至直言:这样的计划将把整个国家“变成下一个委内瑞拉,或者下一个希腊”。他在戈塔巴雅上台几天后就辞职了,后来财长被换成了马欣达的弟弟巴兹尔。据称巴兹尔既不懂得经济和金融,也容不下来自同事的反对意见。
所谓流年不利莫过于,在减税计划导致财政收入大幅锐减的情况下,再正面遭遇新冠疫情。
2020年,为应对新冠大流行,斯里兰卡当年支出近1000亿卢比(约合5.40亿美元),这直接导致斯里兰卡的政府赤字水平激增1倍,达到了2.09万亿卢比(112.86亿美元)。在新冠危机等新变数下,戈塔巴雅始终拒绝推翻自己的减税计划,还承诺相关税率将保持至少五年不变。
旅游业是斯里兰卡第三大外汇收入来源,超过13万人直接从事酒店业,相关就业近40万人。恐怖袭击叠加疫情冲击,2020年,斯里兰卡游客数仅50.8万,同比下降了73.5%,旅游收入约6.82亿美元,同比下降81.1%,使得2018年贡献该国GDP占比近5%的旅游业锐减到2020年的0.8%,超过4万个工作岗位流失。
服务贸易顺差收窄导致斯里兰卡当年的贸易顺差由28.49亿美元降至8.19亿美元。
除此之外,侨汇也是斯里兰卡外汇的一个重要来源。
根据世界银行调查报告,斯里兰卡劳动力资源丰富,是对外劳务输出较多的国家,但受疫情影响,海外务工人员由2019年的20.3万人减少至2020年的5.37万人,侨汇收入也被腰斩。
一波未平一波又起
在翘首以盼中,转机并未到来。反而是在邻国印度首先爆发的德尔塔变异株传入斯里兰卡,使得经济和民生雪上加霜。
斯里兰卡的工业基础羸弱。第二产业以劳动密集型工业为主,如纺织业,但也没有什么大的制造业;资金和技术密集型工业发展相对缓慢。除了粮食可以自给自足外,斯里兰卡的大量原材料、能源和生活必需品依赖进口,这就导致斯里兰卡的商品贸易逆差大于服务贸易顺差,对外贸易常年赤字,国际储备不断被消耗。
Wind数据显示,在戈塔巴雅2019年底就任时,斯里兰卡的外汇储备还有76.42亿美元;但到2022年6月底,外汇储备仅剩下18.54亿美元,还不够维持1个月的正常开支。
同期,斯里兰卡的外债规模开始飙升,负债率(外债余额/国内生产总值)、偿债率(当年外债还本付息额/当年商品和劳务出口收入额)、债务率(当年债务余额/当年商品和劳务出口收入额)均超过警戒水平。2019年的负债率水平还保持在42%,至2021年已经达到119%。
在公共政策讨论中,日本往往被视为现代货币理论(MMT)的经典案例。日本是特殊的,也是复杂的。无论是MMT的拥趸,还是批判者,都能从日本经验中找到想要的依据。但日本本国人士却大多是MMT的批判者。
为了配合总统的经济政策并降低本国的债务负担,斯里兰卡央行选择了超发货币,进行财政赤字货币化(MMT的实践)。Wind数据显示,从2019年12月到2022年5月,斯里兰卡的货币供应量(M1)增加了85.32%。叠加疫情的持续冲击,斯里兰卡实践MMT的直接后果,是通胀开始飙升,2022年7月,通胀率飙升至60.8%,创历史新高。
斯里兰卡农业立国,但是化肥依赖进口。在疫情冲击下,由于供应链危机、原材料上涨等原因,化肥价格上涨,使得本就很紧张的外汇收支,更加捉襟见肘。
2021年4月,名义上是履行竞选承诺,推行绿色转型,实际上是为了降低化肥进口以节约外汇,斯里兰卡在全国范围内禁止使用化肥和杀虫剂,强迫农民使用有机肥料。
一方面,有机农业本身产量就较化肥农业低,另一方面,斯里兰卡国内也没有供全国使用的有机肥料。一刀切的绿色革命,导致了大面积的农作物歉收(有研究称,改革后的农业成本上涨到了此前的10倍,而产量仅为此前的一半,稻米产量下降了20%,胡椒、肉桂和蔬菜的产量下降了30%)。导致本来可以自足的粮食也需要国际进口,不仅没有节约外汇,反而增加了外汇。
国际货币基金组织(IMF)2022年3月的一份报告还称,化肥禁令还损害了茶叶和橡胶的生产,导致“潜在的重大”出口损失——这原本是斯里兰卡首屈一指的出口支柱行业。
2021年11月,政府政策不得不180度转变,取消化肥禁令。但问题在于,化肥价格居高不下,政府没有外汇补贴,农民自身已经无法支付高昂的化肥价格,很多人因此拒绝继续农耕。
斯里兰卡经济已经出现崩溃迹象。
国际环境再生剧变
2022年2月24日,俄乌冲突爆发并在当日即升级为全面军事对抗,并迅速发展为二战以来欧洲最大规模的武装战争。
远在欧洲的战争,却成了压垮斯里兰卡的最后一根稻草。
第一,再次冲击旅游业。俄乌双方都是斯里兰卡的重要游客来源国,2022年1月,来自两国的旅游人数占同期旅游总人数的四分之一;
第二,斯里兰卡粮食进口的40%以上来自乌克兰,俄罗斯是斯里兰卡的第二大茶叶出口市场。俄乌战争的爆发同时冲击了斯里兰卡的出口和进口。
第三,也最为严重的是,俄乌冲突还导致全球范围内的粮食、能源等大宗商品价格暴涨,谷物、燃油、食用油等商品价格居高不下。
斯里兰卡当地的电力供应主要为火电。俄乌战争爆发后,由于石油短缺,自2021年底就开始的全国限电开始延长。2022年3月底,部分地区的每日限电时长达到创纪录的13个小时,原本散见于各地的抗议浪潮开始出现全国蔓延的趋势。
与此同时,燃油价格暴涨约242%后依然非常短缺,人们开始在加油站前排起超过10个小时的长队,这一时长在不久后就发展到了数十个小时甚至数天。
2022年3月21日,两名70岁的老人在排队等待加油时意外身亡。据不完全统计,因排队加油猝死的人数已经达到了19个。目前,黑市的92号汽油价格已经高达55元/升,加满一辆油箱40升的车需要的钱比绝大部分当地人一个月工资还高,且价格仍在上涨。
曾经,服装制造业是斯里兰卡为数不多的发展较好的第二产业,且是斯里兰卡的第二大外汇收入来源,每年向全球市场的出口价值约54.2亿美元。但由于油电中断以及美元短缺,当地的很多工厂也已经无法进行正常生产。
此次危机根源深远
斯里兰卡总面积65610平方千米(大概相当于两个海南岛),总人口为2192万人,位于南亚次大陆以南,与印度隔保克海峡相望。
热带季风气候使得斯里兰卡的农业基础非常好,吸纳了近30%的就业人口,粮食产量自给有余,是大米净出口国之一;锡兰红茶、马黛茶、咖啡和香料等出口农产品在国际市场地位稳固,为斯里兰卡赚得了大量外汇收入。
但是,斯里兰卡资源贫瘠、经济体量小、工业基础薄弱。原材料、能源,甚至日用品,高度依赖进口。2009年斯里兰卡内战结束时,政府选择将重点放在开拓国内市场,导致进口支出持续增长,外汇收入则高度依赖服务业,导致常年贸易赤字。
斯里兰卡最主要的优势在于其地处印度洋、紧邻亚欧国际主航线的地理位置,这使其有望成为印度洋上的航空、物流和商业枢纽,但要实现这一愿景,必须要有完善的港口、物流、电力等基建设施。所以,无论是战后经济重建,还是未来的经济规划,都导致斯里兰卡必须大规模举借外债。
外债驱动式发展导致斯里兰卡受国际环境影响巨大。Wind数据显示,2021年末,斯里兰卡外债总额达到507.24亿美元,相当于GDP的比重为60.02%,较上年上涨3.43%,债务结构缺陷使其易受汇率波动影响。而2022年6月的外汇储备已经锐降到18.54亿美元。
没有一片雪花是无辜的。
总结来说,经济体量小基础差且结构不合理、外汇储备不足、长期贸易赤字、债务率高且结构不合理、外汇收入高度依赖服装制造业和旅游业、恐怖袭击重创旅游业、疫情爆发影响旅游业和全球供应链、俄乌战争、美联储加息等等原因,再加上家族式统治集团导致权力高度集中,一系列错误的经济政策得不到纠正,共同导致了斯里兰卡如今的惨状。
总结
迄今为止,联合国、中国、印度、IMF等国际组织和国家已经提供了一定援助,但斯里兰卡的经济和政治危机仍在加重,政治变革难以取得实质性的推进,几乎所有人都认同改革的重要性、必要性和紧迫性,但没有人能够说出具体的改革措施。
斯里兰卡也仍在与IMF进行持续的救助谈判,但按照惯例,斯里兰卡必须先完成政府改组,并满足IMF提出的一揽子改革计划,才有可能获取贷款。
危机终将过去,生活也会继续,但苦难由每一个人承担,不知道多少家庭的命运会因此而改写。过去几年来,国际局势风云变幻。真是,心有戚戚。
值得警惕的是,斯里兰卡正在发生的事情可能是其他低收入和中等收入国家即将发生的事情的不祥预演,因为通胀飙升、债务高企、货币超发以及外汇储备缩水是几乎所有新兴经济体正经历的“逆风局”,债务风险在全球范围内正在持续上升。
举报/反馈
成立 8 年,从生死边缘到第一次冠军。
文丨张家豪
编辑丨程曼祺
刚刚公布的 7 月新势力销量排行中,哪吒汽车以超 1.4 万辆第一次登顶月度冠军。今年前 7 个月,哪吒卖出了 77000 多台车,冲上造车新势力本年总销量榜第二。
即使如此,哪吒品牌和它的掌舵人在大众层面尚缺乏存在感。哪吒主销车型价格在 10 万元上下,它不够高端、不够科技,缺乏让消费者记忆深刻的标签。
在商业世界提起张勇,人们也会想是哪个张勇:阿里的张勇?海底捞的张勇?“哦,哪吒的张勇”,他不是第一个跳出来的选项。
在以销量增长引起汽车业关注前,2014 年即成立的哪吒多年默默无闻。
哪吒核心团队全部来自传统车企。哪吒母公司合众新能源由奇瑞新能源前副总经理方运舟创立,CEO 张勇在 2018 年加入,他是方运舟在奇瑞的老同事,来哪吒前在北汽新能源总领销售。哪吒负责生产的联席总裁、CTO、设计中心总经理等中高层也多来自奇瑞、北汽、一汽。
投资机构不看好传统车企背景的公司做智能电动汽车,哪吒早期融资困难,2021 年之前,哪吒一共拿了不到 70 亿元人民币。在 2020 年 4 月的一个饭局上,张勇对李斌半开玩笑:蔚来很惨,好歹也有名气,我们一路惨过来没人知道。
如今哪吒的销量增长,被视为逆袭。哪吒团队却不这么认为。他们觉得哪吒只是 “活下来了”,在第一轮淘汰赛中没有出局,这还不是成功。
活下来的方式不存在戏剧性:哪吒的转折发生在 2020 年哪吒 U 和哪吒 V 陆续上市后,尤以 V 卖得好。V 售价在 8 万左右,同价位的车多数像升级版老头乐,V 则是一辆小型 SUV,用 “大车打小车” 策略抓住了这个价格区间消费者最关心的尺寸性价比。
哪吒团队正确复用了传统车企的经验,做到了该做的事,在自己熟悉的 10 万区间市场,把车卖给熟悉的人群。随着新能源车渗透率提升,其局面会趋近汽车消费大盘,更便宜、更有性价比的车本就会卖得更多。
今年 6 月,哪吒发布了基于新汽车平台推出的轿跑哪吒 S,拥有剪刀门的限量耀世版先亮相,起售价 33.88 万元。上周日,哪吒发布了 S 的非限量版,其中起售价最低的纯电续航 310 公里的增程式电动车 19.98 万起售,最贵的 715 公里续航激光雷达版售价 31.98 万。
小鹏从 G3 到 P7 的转变是一个向上走的榜样,但跳跃小于哪吒。P7 起售价比 G3 上涨近 41%。而哪吒 S 对比哪吒 V,起售价涨幅达 150%。
与同样还在牌桌的蔚小理等公司相比,哪吒和张勇一直在用极有限的资源,在极小的缝隙里生存。更难的是,现在他们还试图往中高端走、长得更大,不仅要在缝隙里活下来,还要连续转身。
能否卖好 20 万价位的车,将决定哪吒故事的下半段:一个从传统汽车行业走出来的新公司,在活下来之后,是否还有更大可能?
不被看好
哪吒是中国的第一批造车新势力,其母公司合众新能源创立于 2014 年 10 月,在销量增长引起关注前,哪吒的前 6 年几乎默默无闻。(为方便阅读,下文以哪吒统称合众公司和哪吒品牌。)
哪吒诞生的那一年发生了很多事:特斯拉正式进入中国,蔚来、小鹏成立,贾跃亭宣布乐视要做汽车。
热闹是别人的,哪吒一直在聚光灯外,早期难以获得资本支持。
相信智能电动汽车是新物种的人会投蔚来、理想、小鹏,其中比较不幸的投了乐视。这几位创始人是互联网连续创业者,他们帮风投赚过钱,更容易获得投资。
希望对冲互联网造车的钱也有更好的去向。比如威马和后来的拜腾、高合,这些公司的创始人来自沃尔沃、宝马等更高端的品牌。
哪吒母公司合众创始人方运舟和最初的核心团队都来自奇瑞。方运舟在奇瑞工作了 15 年,曾是奇瑞 1999 年成立的 “清洁能源汽车专项组” 成员,这是中国最早新能源车项目之一。奇瑞是中国新能源早鸟,但常年生产 10 万左右的中低端车型,总销量在 2010 年之后连续下滑,不再是自主品牌第一梯队。
VC 的钱不好拿,哪吒选择拿地方政府的钱,出让了不小的份额。哪吒的第一轮融资中,浙江嘉兴县级市桐乡政府的投资占了 75% 的股份,创始团队股权只剩 15%。张勇今年 6 月曾回应称,哪吒管理团队总体持股约 20%。对比看,李斌在蔚来持股 10.6%,何小鹏和李想持股均超 20%。但后面三家融到的钱要比哪吒多很多。
股份稀释多、创始团队份额少。很多投资人不喜欢这种股权结构,认为公司核心管理层与公司利益绑定不深。这带来了更多融资波折。
2017 年底,擅长做园区地产项目的华夏幸福创始人王文学以个人资金获得了 53.4% 的哪吒母公司股份,成为控股股东,担任董事长。
地产背景的董事长,未能给哪吒加分。整个 2018 年,哪吒见了 200 多个投资机构,没有获得新融资。即使它是那么便宜——王文学收购过半股份只花了 3.3 亿元人民币,可计算出,哪吒 2017 年底的总估值约 6 亿人民币。此时蔚来、小鹏、理想、威马的估值已达到 50 亿美元上下。
牌面并不好,张勇还是在 2018 年 2 月加入了哪吒。他告诉《晚点 LatePost》,这是 2017 年做好的决定。方运舟创业以来,不时会和张勇见面,“一起喝酒、聊聊”。
张勇身材精瘦,细长的眼睛排列成微微下向的八字形,有点儿像中年版的李荣浩。他在 1996 年大学毕业后进入汽车业,前两年在山东一家公司做研发,1998 年加入北汽福田转做销售,此后在奇瑞、北汽新能源负责销售工作多年。
2017 年,张勇任职的北汽新能源一年卖了 10 万台车,中国第一。他觉得继续做职业经理人是重复性劳动。
“我也干不到一把手。” 张勇说,创业,让一件事从零到一,会更有成就感。
方运舟能给张勇足够空间。二人是老同事,彼此信任,张勇说如果到一个新地方,还要花时间磨合,揣测对方的想法,“那事情没法干了”。从加入起,张勇便全权负责日常经营。
张勇加入 4 个月后,哪吒第一款车 N01 在 2018 年 6 月 发布,哪吒品牌正式诞生。张勇起的 “哪吒” 一名褒贬不一,有人认为新颖、有人认为这不像汽车品牌,大部分人同意它好记。
老兵创业的哪吒并没有比新手更快,N01 在 2018 年 10 月开始交付。同一年交付的品牌还有成立时间与哪吒相当的蔚来、小鹏、威马。
N01 销量并不亮眼。从开始交付到当年底,2 个月卖了 1000 台。2018 年 6 月开始交付的蔚来,在当年卖了 1.1 万台车,和哪吒同期交付的威马卖了近 4000 台。排在哪吒前面的还有云度、新特、电咖。
放到 10 万价格区间的汽车汪洋中,N01 的销量更是沧海一粟。那一年中国一共卖了 2800 多万台车,其中 33.5%,也就是近 940 万台在 5 到 10 万之间。
N01 的销售结构也不好,一半的车由出租车公司、租车公司和地方政府采购。外界把此事与张勇的北汽新能源经历联想,一种声音是,他陷入了用 B 端冲销量的销售惯性。B 端占比高是北汽新能源销量一度领先,但后劲不足的重要原因。
这年底,因为地产主业陷入危机,王文学从哪吒撤走,股份转手给了江西宜春政府。又一个 “不被看好”、不够市场化的证明。
张勇曾回忆当时市场对哪吒的看法:“你从人、资金到过往的经验、背后的股东,哪一个都不够有成功迹象。”
并非逆袭
融资不畅,哪吒必须用有限的资源活下来。
在开发 N01 的过程中,哪吒分别于 2017 年和 2018 年获得了发改委、工信部的汽车生产资质,自己掌握了生产资格,这样花钱更少但速度相对慢。其它钱更多的车企多选择收购已有产能获得资质,用钱换时间。
依靠桐乡政府、王文学等投资人的资金和支持,哪吒在桐乡自建了第一个工厂,2016 动工、2018 年投产。完成这些工作时,哪吒累计只融到了略多于 30 亿元人民币的钱。
N01 未能一炮打响,它只是卖出去了一些,没有给哪吒直接判死刑。但 N01 的质量表现相对稳定,少了困扰同行的风波,这给了哪吒活下去的潜力。
蔚来 2019 年因电池起火召回了 4803 台蔚来 ES8。理想 2020 年因 “断轴” 事件召回了 10469 台理想 One。同年 10 月,威马 EX5 发生四起自燃事件,威马随后召回了 1282 台 EX5。哪吒没有出现类似的大规模召回。
N01 的短板也很明显:卖点模糊,根源是定位问题。这是多年主管销售、了解 10 万元低线市场购车者需求的张勇的强项。
张勇加入哪吒后的重点工作是定义后续车型。2018 年下半年到 2019 年,哪吒完成了哪吒 U 和哪吒 V 的开发。两款车分别在 2020 年 3 月和 2020 年 11 月发布。
两款新车,尤其是哪吒 V 的上市,是哪吒发展的转折点。
在这之前,哪吒勉力求生,靠不算理想的 N01 支撑。整个 2019 年,N01 一共卖出 10000 多台,排在新势力第 4,但与前一名的小鹏差了 6000 多辆,排名第一的蔚来则是哪吒的两倍不止。这期间哪吒还需投资新车开发,资金的捉襟见肘一直持续,到 U 和 V 两款新车上市前,哪吒一共融了不到 70 亿元。
中国造车新势力也正经历行业性艰难时刻。行业龙头、最早 IPO 的蔚来股价在 2019 年 9 月跌至 1.32 美元的冰点,资金链几近断裂,负面舆论缠身。哪吒同样惨,不过无人问津。
2020 年 4 月的一个饭局上,张勇对李斌半开玩笑:蔚来很惨,好歹也有名气,我们一路惨过来没人知道。
在 V 于 2020 年底上市之后,哪吒自身和环境都变了。
V 是哪吒销量起飞的关键车型。表面看,V 和 N01 定位相似,都是 A0 级小型 SUV,起售价也相当,销量却差别很大。
变化在于对 “性价比” 更锋利的洞察和表达。
V 抓住的性价比核心是 “大车打小车”、错位竞争。受方运舟邀请,于 2018 年加入哪吒的设计中心总经理常冰来自一汽红旗,曾设计过检阅车。他认为设计出经典的经济车型有比做豪华车更难。常冰告诉《晚点 LatePost》,张勇不会参与产品具体细节,他负责给产品大方向与定义。在做 U 和 V 时,张勇最关注的指标是级别与尺寸。
在哪吒 V 所处的 7-8 万价格区间,大多数电动车是类似长安奔奔、奇瑞小蚂蚁这样的微型车,看起来像 “老头乐” 升级版。而哪吒 V 是一辆长度达 4 米的小型 SUV,比同价位其他车明显大一圈,比 N01 长 20cm,乘坐和储物空间都更大。
图:哪吒 V(左)与奇瑞小蚂蚁(右)
要实现大车打小车,路径是管理好供应链和控制成本,哪吒没有资源烧钱卖车,成本来自取舍。
11.78 万起售的哪吒 U 全系配备了电动后备箱,这一配置在 U 的主要竞品如广汽埃安 Y、比亚迪元 Plus 等车上都是选配或高配功能。无框车门、隐藏式门把手等” 电动车” 普遍采用的设计,也没有出现在 U 和 V 上,这两款车追求实用,而不是 “像电动车”。
V 和 U 的车机屏幕分辨率都只有 720 P,也没有放置太多智能化能力,这不是该价位段消费者最迫切的需求。
在销售策略上,张勇选择了不常见的 “垂直切换”:即一旦 V 上市,N01 就要立马退市,不留过渡时间。
张勇提出这个想法是在 V 发布前的一次总裁例会上。不少管理层认为这很难办:老的供应合同怎么办?已采购的零部件怎么办?怎么和销售渠道解释?怎么保障已购车客户的售后备件?一堆问题要处理。公司也得承担买多了零部件的损失。
主管生产的哪吒联席总裁孔繁龙曾是一汽轿车公司涂装技术负责人,之后在奇瑞担任生产管理部部长。孔繁龙回忆当时的情形 “很紧迫、很麻烦”。
于张勇而言,这却不是艰难决定,代价和收益是明确的:N01 相对过时,不再适合代表哪吒。对当时缺钱、缺声量的哪吒来说,生产更少的车,也有利于供应链管理和降低成本。
“如果只卖 V 能卖 5000,V 和 N01 一起卖能卖 6000,我也会只卖 V。” 张勇告诉《晚点 LatePost》。
V 从 2020 年 11 月开始交付,第一个月卖出了 1337 辆,第 6 个月达到 3846 辆。哪吒的月度总销量也在当月突破 4500 辆,首次进入新势力前三。
在 10 万左右的主力市场建渠道,是张勇擅长的工作。在低线市场走 4S 店这类传统的代理渠道,快速铺开;到一二线城市走直营模式,开进商场,提升曝光。泾渭分明,渠道之间不打架。张勇接受采访时曾说,如果一座城市既有直营又有代理会产生冲突,很难跑通。
截至 2021 年底,哪吒汽车线下渠道达 331 家,包括直营店 70 家,城市合伙人加盟店 261 家。其中的 57 家直营店和 125 家加盟店是在 2021 年新开的。
随着销量扩大,哪吒过去不被看好的 B 端销量占比被稀释。去年 11 月,张勇曾披露哪吒 C 端用户已达 91%,直接销售给出租车公司等 B 端客户的占比为 9%,略低于乘联会统计的 12% 的行业平均水平。
在 V 于 2020 年底上市的同时,行业环境也发生了巨大变化。
随着蔚小理走出低谷、股价创新高和新能源汽车渗透率在 2020 年快速上升、涨了近 3 倍,更多人和钱加速入局。
有人选择自己做,如小米、牛创;有人选择合作,如百度与吉利的集度、华为与小康的塞力斯;有人考虑投资,曾折戟于造手机的 360 创始人周鸿祎是其中之一。
2020 年底至 2021 年初,周鸿祎开始物色具体投资对象,360 投资团队梳理了除蔚小理外已量产的几家新造车企业,包括高合、零跑、哪吒、威马等。2021 年春节后,周鸿祎密集拜访了这几家企业。
接近此次交易的人士告诉《晚点 LatePost》,威马因库存高被排除;周鸿祎 2018 年就接触过零跑,对零跑的第一款产品不满意,而且零跑背后已有安防巨头大华这个重要股东。
360 和哪吒相互需要。随着汽车软件功能增多、车联网普及,汽车成为网络安全的重要场景。哪吒需要钱、也希望获得互联网能力背书。
周鸿祎认可哪吒团队的做事理念和产品定位。在最近接受《中国企业家》采访时,他说自己和哪吒团队一见如故,“哪吒花钱最少,我信奉少花钱多办事。”
周鸿祎告诉《晚点 LatePost》,2021 年考虑投资时,他试乘了哪吒的两款车,坐进 U 后排的第一感觉是,空间和他自己的丰田霸道 SUV 差不多大;V 的性价比让周鸿祎印象深刻:“六七万的国产油车连气囊都没有,更别说给你装个大屏幕了。”
到 2021 年 4 月上海车展前,周鸿祎已比较倾向哪吒。哪吒力邀 360 成为第二大股东,去年 6 月公布的 29 亿元投资金额即是为保障第二大股东地位。接近此次交易的人士称,这超出了周鸿祎起初的投资预期,但他还是同意了。
360 之后,哪吒陆续获得了宁德时代、平安等知名产业方和机构的投资,去年总融资额近 100 亿人民币,资金情况大为改善。
哪吒也开始着手解决其股权结构中,核心团队股份偏少的不足。360 在 6 月发布公告,放弃了 29 亿投资中仍未实际投入的 10 亿元额度,将 3.532% 股权转给了其他两家基金,市场解读 360 对哪吒不够看好。周鸿祎说,这些基金背后是哪吒团队,哪吒创始人团队股权太少,要让创始人团队主导公司发展。
曾毓群出席了哪吒和宁德时代的投资与合作签约仪式。一位现场的人士回忆,曾毓群签约时对方运舟和张勇说,宁德时代刚起步做动力电池时,是奇瑞、北汽给予了他们进入行业的契机:“感谢再次给我们机会。”
“哪吒的人是有资历、被认可的一群老兵。” 一位哪吒投资人告诉《晚点 LatePost》,很多故事里把哪吒描述成逆袭,周鸿祎接受采访时曾夸张地说第一次见张勇,以为他是产线工人;其实张勇和哪吒团队不是 “不知从哪儿冒出来的 Nobody,而是 Proven Guy”。
这位投资人评价,在中国汽车 10 万元上下的低线市场,张勇是能数出来的最好的 4、5 个销售负责人之一,他洞悉需求,也知道怎么把需求和产品联系。
哪吒 2021 年之后的快速增长,在于过去没有生产、质量、渠道上的大纰漏,U 和 V 两款车又找到了性价比的长板。他们复用了过去在奇瑞、北汽销售便宜汽车的经验,用 “为人民造车” 的定位在销量最多的 10 万元区间挣到了生存空间。
资源长期有限的哪吒只能做最重要、最基本,也贴合造车、卖车商业常识的事。
张勇自知,便宜的车本应该卖得更多,这也是常识。去年底接受《财经》采访时,他说自己做的是 6、70 分,李斌能得 8、90 分,因为哪吒的市场比蔚来大好几倍,只有销量到它的 2-3 倍才能算一样优秀。张勇告诉《晚点 LatePost》,哪吒现在只是 “活下来了”。
从过程和现状来说,这都不是一个戏剧性的逆袭故事。
仰攻宜早
把 10 万的车卖得还不错,是哪吒团队和张勇过去做成过的事。在今年推出价格翻番的哪吒 S 则是这家公司的关键一跃。
这是一个比想象中更冒险的决策,因为它的起点是哪吒销量尚无起色,行业也哀鸿遍野的 2019 年。
张勇告诉《晚点 LatePost》,在 2019 年下半年,V 和 U 快接近开发完成时,哪吒启动了第二代平台山海,其意义就是支撑哪吒的产品往上走。
当时哪吒资金很少,累计融资额不到 70 亿,这些钱要被用来建厂和生产、开发三款车型。新平台的研发需要 20 亿,会占用哪吒近一半的资金。内部质疑这是 “还没学会走就想跑”。
张勇觉得一直卖 10 万的车,哪吒会发展受限,老东家奇瑞就有这个教训。要做更高端、性能更好的车,必须要新平台,这才能放下更大的电池,配备更新的电子电气架构和悬架设计。
新平台的投入看起来大,其实长远看更省钱。张勇说山海平台一开始就考虑了电池、电机和底盘的通用化,模具不需要重新开发,越往后的车型开发成本越低。
资金紧张,张勇说服团队和股东分批投入,一期需要 10 亿,先投 5 亿 “做做看”。
很多公司都在做平台,张勇解释平台有真有伪,区别是通用化程度,如果最初没有定义好,开发某个车时,模具就要临时改,这看起来能减少平台开发时间,抢某代产品的上市点,但来回的物流、供应链管理都是成本,长远没有规模效应,最终费钱也费时间。
曾在 2017 年面临相似抉择的威马选了另一条路。2019 年年底,威马推出的中高端 SUV EX6 Plus 沿用了 EX5 的老平台,造型相似、车身略长、内饰和智能化有提升,官方指导起售价约 28 万元。交付后的第一年,EX6 共卖出不到 500 台。
新平台开发半年后,2020 年 3 月,哪吒 U 发布不久后的一次总裁例会上,张勇提出,要开始具体规划山海平台的第一款车型。他已有一个大致方向:做运动型轿车。这在当时是一个小众决定。哪吒联席总裁孔繁龙告诉《晚点 LatePost》,有不少管理层都觉得第一款中高端车型应该做 SUV。
张勇认为在中国市场做 SUV 是最正确的事,决策上没有压力,但正因为做的人多,市场竞争已饱和;自主品牌做中高端轿跑的更少。
开发过程中,出现了一些不可控情况。在张勇最初的设想里,新平台第一款车的主销车型价格可控制在 20 万以内,所以 2020 年接受采访时,他说哪吒 “不做 20 万以上的车”。
但随着电池、芯片等零部件陆续涨价或缺货,哪吒发现把新车做到 20 万以下很难。张勇同时感知到,在 20 万上下价格区间,消费者对智能化的需求超出了他的预期。一些本想放到新平台后续车型上的配置,如激光雷达、高算力芯片,被提前到了 S 上。整车成本进一步攀升。
张勇随即调整了 S 的开发思路,在 “又好又便宜” 和 “又便宜又好” 之间明确选前者,好是第一位,然后才是便宜。这是因为在 20 万区间,哪吒品牌不够强,需要用产品力打动消费者。张勇在内部多次说,如果哪吒 S 的产品只比友商好 20%,那就不用做了,因为不会有人买,“至少要好 50%”。
产品定义上,哪吒 S 延续 “大车打小车” 策略,车身长 4.98 米,宽 1.98 米,轴距 2.98 米,达到了 C 级车标准,是这个价位上的大尺寸车。经典的 C 级轿车有宝马 5 系,奥迪 A6 等。
图:哪吒 S 耀世版(左)与激光雷达版(右)
哪吒副总裁兼设计中心总经理常冰分析了哪吒 S 与竞品的差异:和尺寸接近的比亚迪汉比,哪吒 S 更运动、年轻,和智己 L7、蔚来 ET7 比,它更便宜;和价格接近的特斯拉 Model 3、小鹏 P7 比,哪吒 S 轴距更长、因而后排空间更大,内饰也更精致。
为了更好,张勇可以容忍多花钱。常冰告诉《晚点 LatePost》,哪吒 S 的第一版造型,在临近冻结前的一个半月有了一次大幅修改,前脸被调整得更 “凶狠”、更有攻击性。哪吒 S 最初的姿态不够低趴,到样车环节,重新修改了前悬架结构。这些修改付出了几千万的额外成本。
但如果修改会拖慢上市节奏,张勇会慎重考虑。拖 2 个月可以接受,拖半年的功能改善大概率就会被放到下一个车型。
这背后的逻辑是,新品牌向高端走,宜早不宜迟。
张勇认为,现在消费升级很快,哪吒也得跟着变,没法等完全准备好再动,要在跑动中瞄准。
哪吒品牌中心副总经理张洪汉告诉《晚点 LatePost》,哪吒最近一两年销量有上升,但在一、二线城市卖得不多,这反而是机会:消费者对哪吒品牌的认知尚未固化,要在这之前抓紧推出高端车。
蔚小理已证明,在面对智能电动汽车时,如果产品和服务过关,消费者愿意接受一个新品牌。高端市场的这种趋势更明显。传统意义上的豪华品牌,长于文化积淀、豪华感的营造。现在新势力四、五十万的车主打服务、自动驾驶、智能座舱,也被市场接受。
在 S 发布前后,哪吒的营销、渠道和生产制造都在为之调整。
周鸿祎告诉《晚点 LatePost》,哪吒团队之前 “笨嘴拙舌”,只知道干活。张勇过去在社交网络只例行转发一些官博内容,现在也开始 “吆喝”。受李想启发,他在 6 月 S 耀世版发布后,在微博上说哪吒 S 是 “100 万内最好的轿跑”。他罕见地说了一些豪言壮语,评价哪吒 S 销量目标时,他说 1 万及格,2 万优秀。
先于 S 主销车型发布耀世版也是一种营销方式,耀世版有造型酷炫的剪刀门,限量 999 台,这一想法是市场部在开发初期提的,目的是引起关注和讨论。
为配合更高价位、进入一二线城市,哪吒扩大了直营渠道比例。截至 2022 年 7 月,哪吒汽车已在 30 个城市开设了 105 家直营店,覆盖所有一线和新一线城市。北上广均有 6 家及以上直营店,基本都开在人流密集的商圈。
张勇告诉《晚点 LatePost》,他现在并不特别担心 S 的销售,但供应链是一道关卡,是哪吒自己不能完全掌控的部分。零部件短缺是行业共同困境,为保供哪吒 S,哪吒已派员工到供应商工厂里一起工作,针对一些紧缺的零部件也在开发第二供应商。
哪吒已在桐乡等地有三座工厂,今年为 S 新建了一条新产线,它能支持过去哪吒几款车型没用过的工艺,比如全景天幕。
哪吒 S 是这家公司第二阶段起点,从外部视角观察,这款车声势浩大,不容有失。
如果 S 失败了,代价是什么?张勇的回答是 S 月销没到 1 万及格线,也不意味着哪吒高端化本身的终止。高端化的基石不是 S,而是 S 基于的山海平台。平台能力已形成,山海平台的两款新车已在开发中,今年内,哪吒会发布该平台继 S 后的第二款车。
“反对无效”
公司成立 4 年后才加入的张勇把自己视作创业者,他拥有足够的话语权,负责几乎所有前台运营。
一位接近 360 投资哪吒的人士总结称,方运舟主要解决 0 到 1,“把孩子生下来”,张勇是接棒从 1 到 10。
一般的想象中,“半路空降” 的 CEO 更像职业经理人,不敢拍板,实际上张勇的内部管理角色更像典型的创业公司 CEO,他是一锤定音的决策者。
哪吒 V 的垂直切换,山海平台第一款车做轿跑而非 SUV,内部有讨论乃至反对。面对这类重要决定,张勇惯常的做法是先抛出议题,想好答案,不说,让大家分头调查、判断,持续讨论,塑造共识,到最终决定时,还反对的,“反对无效”。
“创业公司需要效率,我没时间说服所有人。” 张勇告诉《晚点 LatePost》,“如果(其他高管)不理解就去执行,做着做着你就明白了。”
孔繁龙等业务线负责人信任张勇。他告诉《晚点 LatePost》:“如果老板决定了方向,那我们就帮他把关、解决问题、把代价降到最低,迟迟决策不了,不是哪吒的特点。”
张勇也可以被说服,会改变想法,如果信息和环境发生了变化。2021 年底张勇曾想做一款 5 米以上的大型 SUV,团队前后忙了半年,造型都有了雏形。到今年 4 月,竞争对手类似车型陆续发布,张勇觉得哪吒再做这款车已无法做出差异化,项目被砍。
在一号位对业务足够熟悉、有判断力,且团队磨合充分、相互信任时,这种组织方式很高效。哪吒很多人都是汽车行业老兵,不少人是老同事,他们过去也长于打造性价比车型。
而随着哪吒进入 20 万中高端市场,它需要补充新的人和能力,以满足这个区间的消费者对智能化、软件功能的需求。这是哪吒尚未被证明的部分。
据《晚点 LatePost》了解,哪吒已吸纳了一些来自百度、斑马智行(阿里与上汽合资)等科技公司的人才。不过他们中,尚没有人成为核心管理层。一位哪吒投资人评价,张勇在用人上比较挑剔,跨界人才需要在公司干一段时间、做出成绩、证明自己能够融入。
张勇告诉《晚点 LatePost》,哪吒已建立了一支七八百人的自动驾驶加智能座舱的研发团队。对比看,小鹏自动驾驶研发团队就有 1500 多人。哪吒对自动驾驶的追求是,“一定要处在头部”,但不必追第一,“这不是我的战略方向。” 张勇说。
哪吒 S 将是哪吒智能化表现的一个试炼,仅看配置——华为的激光雷达和 MDC 计算平台、高通 8155 座舱芯片,“很有性价比”。用起来怎样,产品最后会说话。
一位哪吒投资人描述他眼中的张勇:不得罪人、不踩人,他偶尔调侃,但从不让自己陷入语言争论,他和蔚小理当家人的关系都不错,他非常在意自己披露的信息、说过的话是不是算数。
在奇瑞、北汽和 2021 年之前的哪吒,张勇证明自己能在资源受限时带队打仗。哪吒现在的情况比之前好得多,资金更充裕、团队更齐整,一路惨过来的哪吒,终于拿到了好牌。他们要去打资源更充裕的仗,挑战经验之外的事。
哪吒眼前的目标是要在 2025 年达到 50 万年销量并实现盈利,张勇认为这是资格赛的门票。50 万之后,张勇更大的野心是,哪吒能成为新势力中第一个年销过百万的公司,这是一个在内部也尚未提过的目标。
7 月末,张勇在社交媒体提了一连串问题:“最近一直检讨审视自己,是不是膨胀了?想要干的事是不是太多了?是继续聚焦于今天明天做减法,还是可以适当做一些后天的事?”
这几天,他对自己的检讨可能有了阶段性答案。本周一,哪吒第一次获得新势力月销第一,张勇更新了社交媒体:“我们的目标是星辰和大海,在这之前,先解决温饱和生存。”
举报/反馈
唐山警方深夜通报:
两起实名举报案件,已抓获6人
↓↓↓
蛋糕店被敲诈勒索案
2名嫌疑人被抓获
唐山市丰南公安局发布警情通报:近日,唐山市路北区孟某在网上实名举报其被敲诈勒索、威胁恐吓,蛋糕店被暴力打砸。
根据唐山市公安局指定管辖,唐山市丰南区公安局迅速成立专班,组织力量对该案进行侦办,主要犯罪嫌疑人史某、李某某已被抓获归案。
据媒体此前报道,6月10日晚,河北唐山一名蛋糕店老板通过视频,实名曝光一个以刑满释放人员组成的涉嫌黑社会团伙。他在视频中表示自2021年7月起,该团伙一直对其进行敲诈勒索、暴力打砸,并致其蛋糕店闭店停业。
酒吧非法拘禁案
4名嫌疑人被抓获
滦州市公安局发布警情通报:近日,张某在网上实名举报朱某斌、杨某等人在唐山市路北区某酒吧对其及同事进行殴打并非法拘禁。
根据唐山市公安局指定管辖,滦州市公安局立即成立专案组,迅速开展工作,已将朱某斌、杨某等4名犯罪嫌疑人抓获。
据悉,6月11日晚,张女士通过视频实名举报唐山市路北区黑社会团伙。
新黄河记者就此事联系到张女士,她表示,5月23日下午4点,唐山市路北区长宁道星际酒吧(星际liveshow)老板以到单位领工资为由将作为驻唱歌手的她和5名同事叫到酒吧,组织他人对他们非法拘禁16小时,并殴打逼迫他们下跪、打“白条”。
其间张女士趁乱逃出酒吧,约凌晨两点前往唐山市路北区祥丰道派出所报警,张女士称,接受约数小时问询后,派出所民警直到早上七点才出警,但首次出警仅在酒吧拿回被扣留的私人物品,其他被拘禁同事并未找到。
张女士说:“后来自己大闹派出所,酒吧另一个老板才把其他同事带了过来,但民警并未对酒吧相关人员进行处理,直接让他们走了。”
根据张女士提供的一段录音显示,6月11日晚上,唐山市路北区祥丰道派出所一名工作人员通过电话联系她,要求她删掉网上的实名举报视频。
12日上午,唐山市委政法委员会办公室一名工作人员告诉澎湃新闻,“我们已经关注此事”。随后,唐山市路北区委宣传部一名工作人员称,针对此事,相关部门正在调查。若有结果,将发布通报回应社会关切。
再现唐山打人事件?
警方通报:系摆拍
↓↓↓
据四川省射洪市公安局官方微信消息,6月12日,射洪市公安局在工作中发现,一条疑似“射洪发生一起夜宵店打人事件”的视频在部分网络社交平台传播。射洪市公安局高度重视,针对视频反映情况展开调查,现将查证情况通报如下:
6月11日,李某峻(射洪某跆拳道馆股东)与董某雷(该跆拳道馆馆长)为蹭热点从而达到宣传跆拳道馆的目的,组织谭某、钟某莹(女)、张某彤(女)、王某、何某庆、成某同、杨某等7人,于当晚在射洪城区某烧烤店外摆拍女子被调戏,男子见义勇为打跑骚扰者的视频,并剪辑添加“再现唐山打人事件”“续唐山事件后”等文字内容上传网络造成不良影响,严重扰乱公共秩序。
目前,相关涉案人员已被抓获归案,案件正在进一步办理中。
射洪公安郑重提醒,网络不是法外之地,公安机关将依法严肃查处制造传播谣言的违法行为,构成犯罪的将依法追究其刑事责任。
唐山开展“雷霆风暴”:
将严打寻衅滋事、侮辱妇女等违法行为
↓↓↓
6月12日下午,唐山市召开夏季社会治安整治“雷霆风暴”专项行动动员部署大会,唐山市委、市政府决定,从即日起开展为期半个月的夏季社会治安整治“雷霆风暴”专项行动,把打击锋芒对准群众反映强烈、社会影响恶劣的犯罪活动,出重拳,用重典,惩恶务尽,绝不手软,以雷霆万钧之势尽锐出战、决战决胜。
专项行动将全面整治社会治安领域突出问题,坚决依法打击各类违法犯罪活动,进一步净化社会治安环境,全力保障人民生命财产安全,切实提高人民群众安全感、满意度。整治内容为:
打架斗殴、寻衅滋事、故意伤害、侮辱妇女等违法犯罪行为。
敲诈勒索、欺行霸市、强揽工程、强迫交易等违法犯罪行为。
聚众赌博、组织容留卖淫、吸毒贩毒、盗窃、抢劫、诈骗、网络犯罪等违法犯罪行为。
对违法犯罪进行包庇纵容,失职失责的违纪违法行为。
扫黑除恶!严惩不贷!
来源:综合自北京日报、四川省射洪市公安局官方微信、中国青年报、澎湃新闻、北京青年报、法制日报等
编辑:依依
举报/反馈
封面新闻记者 于婷
一群蒙面人拿着砍刀闯入正在营业的歌厅、酒吧,寻衅滋事、肆意打砸,对受害人进行砍杀。这一系列案件引起了四川西昌警方的重视。此后2年间,专案组辗转重庆、广东多地,走访询问证人160余人,固定证据材料1100余份,抓获犯罪嫌疑人16名,破获案件21件,形成证据材料42卷。
今年7月29日,马某某等人犯组织、领导、参加黑社会性质组织罪被一审宣判,涉案的16名犯罪嫌疑人分别判处2年至25年有期徒刑不等的刑罚。至此,一个从2013年以来就在凉山州西昌市、木里县等多地活动的黑社会性质的犯罪组织宣告覆灭。
10月27日,全省公安机关扫黑除恶专项斗争凉山专场新闻通气会对这一案件进行了通报。
2017年8月,西昌市公安局在对九类案件进行分析研判时,发现在2017年发生的多起个案之间有着相当类似的情节:2017年年初,在西昌市海河边某歌城发生的一起寻衅滋事案件中,几个年轻人蒙着面、拿着砍刀在歌城内进行打砸,给歌城造成了巨大损失。2017年5月,在西昌市某菜市场内一家鲜面加工坊内,两个年轻人买面条时声称手机丢失,并迅速召集了一伙年轻人前往滋事。2017年6月,在西昌市海河边某酒吧又发生一起寻衅滋事案,3名犯罪嫌疑人蒙着面、拿着砍刀对受害人进行砍杀。
“都是蒙着面的年轻人,都是用长砍刀砍杀或是打砸。”如果这些案件是一伙人所为,那背后应该就有黑恶势力的影子。西昌公安开始着手对这系列作案手法类似的案件进行有意识的串并。2018年5月,西昌市公安局在侦查“赵某组织、领导黑社会性质组织案”时,发现之前就进入侦查视线的犯罪嫌疑人马某某为了获取经济利益,于2013年7月至12月参与了赵某的黑社会性质犯罪组织,对西昌市西客站周边的非法客运市场的垄断,并实施了多起暴力案件。
随着案件调查的进一步深入、线索的进一步详实,马某某及其身后的黑社会性质组织轮廓逐渐清晰。2018年6月,报经凉山州公安局、省公安厅同意后,西昌市公安局抽调了两名省扫黑除恶专家库成员苏绍平和唐仕勇分别担任该专案组组长和副组长,以及13名侦查员组成专案组。
经查,马某某及其兄弟系凉山州木里县人,2010年10月之后靠逞凶好斗逐渐打出“名气”,纠集了一帮有前科的“兄弟”,一个黑恶势力犯罪团伙开始初见雏形。在团伙形成之初,多充当打手的角色,收钱替人办事。但随着团伙的逐渐发展,2013年以后,为了获取非法利益、扩大其犯罪集团非法影响,以马某某为首的犯罪集团在西昌市、木里县等地有组织的进行非法经营、聚众斗殴、寻衅滋事、敲诈勒索、开设赌场、故意伤害、非法持有枪支、故意毁坏公私财物等恶性暴力案件,严重破坏了西昌市、木里县的经济、社会、生活秩序,造成了极其恶劣的社会影响。
2年多时间,专案组辗转重庆、广东多地,走访询问证人160余人,固定证据材料1100余份,抓获犯罪嫌疑人16名,破获案件21件,形成证据材料42卷。
7月29日,马某某等人犯组织、领导、参加黑社会性质组织罪被一审宣判,涉案的16名犯罪嫌疑人分别判处2年至25年不等的有期徒刑。至此,这个黑社会性质的犯罪组织宣告覆灭。
举报/反馈