乐观锁
操作共享资源时,总是很乐观,认为自己可以成功。在操作失败时(资源被其他线程占用),并不会挂起阻塞,而仅仅是返回,并且失败的线程可以重试。
优点:
分析下面的转账代码,考虑是否线程安全
interface Account {// 获取余额Integer getBalance();// 取款void withdraw(Integer amount);/*** 方法内会启动 1000 个线程,每个线程做 -10 元 的操作* 如果初始余额为 10000 那么正确的结果应当是 0*/static void demo(Account account) {List ts = new ArrayList<>();long start = System.nanoTime();for (int i = 0; i < 1000; i++) {ts.add(new Thread(() -> {account.withdraw(10);}));}ts.forEach(Thread::start);ts.forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});long end = System.nanoTime();System.out.println(account.getBalance()+ " cost: " + (end-start)/1000_000 + " ms");}
}class AccountUnsafe implements Account {private Integer balance;public AccountUnsafe(Integer balance) {this.balance = balance;}@Overridepublic Integer getBalance() {return balance;}@Overridepublic void withdraw(Integer amount) {balance -= amount;}
}
测试
Account.demo(new AccountUnsafe(10000));
结果发现余额不为0
180 cost: 105 ms
@Override
public Integer getBalance() {synchronized (this){return this.balance;}
}
@Override
public void withdraw(Integer amount) {synchronized (this){balance -= amount;}
}
结果
0 cost: 96 ms
class AccountCas implements Account{private AtomicInteger balance;public AccountCas(int balance){this.balance = new AtomicInteger(balance);}@Overridepublic Integer getBalance() {return balance.get();}@Overridepublic void withdraw(Integer amount) {while(true){int prev = balance.get(); //获取余额的最新值int next = prev - amount;//同步到主存中去if(balance.compareAndSet(prev, next)){break;//同步成功,结束循环}}}
}
结果
0 cost: 86 ms
关键:compareAndSet
,简称CAS
,它虽然未使用锁,但却是原子操作
CAS保障原子性的原理
参考:http://www.manongjc.com/detail/29-djgcfcqoczepwyv.html
CPU通过以下方式实现原子性
总线锁定
总线(BUS)是计算机组件间数据传输方式,也就是说通过总线,CPU与其他组件连接传输数据,就是靠总线完成的,比如CPU对内存的读写。
当处理器要操作共享变量时,会在总线上发出 Lock 信号,其他处理器就不能操作这个共享变量了
缓存锁定
总线锁定方式虽然保持了原子性,但是在锁定期间,总线锁定阻止了被阻塞处理器和所有内存之间的通信,而输出LOCK#信号的CPU可能只需要锁住特定的一块内存区域,因此总线锁定开销较大。
所以现代CPU为了提升性能,通过锁定范围缩小的思想设计出缓存行锁定(缓存行是CPU高速缓存存储的最小单位)
当缓存行中的共享变量回写到内存时,其他 CPU 会通过总线嗅探机制感知该共享变量是否发生变化,如果发生变化,让自己对应的共享变量缓存行失效,重新从内存读取最新的数据
乐观锁会消耗CPU资源,悲观锁节省CPU资源但效率慢(需要加锁,甚至阻塞)
CAS和volatie
public class AtomicInteger extends Number implements java.io.Serializable {...private volatile int value;...
}
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
为什么无锁效率高
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。一旦发生上下文切换,性能将下降
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,如果没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换
CAS更适合多核CPU,线程较少的情况
CAS特点
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
乐观锁
的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,再重试即可J.U.C 并发包提供了:
以AtomicInteger为例
AtomicInteger i = new AtomicInteger(0); //无参则初始值为0
//下列函数均有返回值
i.compareAndSet(int expect, int update); //借助while(true),更新i的值
i.getAndIncrement(); //类似i++
i.incrementAndGet(); //类似++i
i.decrementAndGet(); //类似--i
i.getAndDecrement(); //类似i--
i.getAndAdd(5); //先获取,再增加,返回原值
i.addAndGet(-5); //先增加,再获取,返回修改后的值
i.getAndUpdate(p -> p - 2); //获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
i.updateAndGet(p -> p + 2); //更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
// getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
// getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
i.getAndAccumulate(10, (p, x) -> p + x); //获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
i.accumulateAndGet(-10, (p, x) -> p + x); //计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
模拟updateAndGet
public static void updateAndGet(AtomicInteger i){//模拟updateAndSetwhile(true){int prev = i.get();int next = prev * 10;if(i.compareAndSet(prev, next)){break;}}
}
上面的写法缺乏通用性,可以把操作抽象出来
public static int updateAndGet(AtomicInteger i, IntUnaryOperator operator){//模拟updateAndSetwhile(true){int prev = i.get();int next = operator.applyAsInt(prev);if(i.compareAndSet(prev, next)){return next;}}
}
//使用
System.out.println(updateAndGet(i, p -> p/2));
查看源码,可以看到也是这样的思想
//DecimalAccount和之前的相似
class DecimalAccountCas implements DecimalAccount{private AtomicReference balance;public DecimalAccountCas(BigDecimal balance){this.balance = new AtomicReference<>(balance);}@Overridepublic BigDecimal getBalance() {return balance.get();}@Overridepublic void withdraw(BigDecimal amount) {while(true){BigDecimal prev = balance.get();BigDecimal next = prev.subtract(amount);if(balance.compareAndSet(prev, next)){break;}}}
}
ABA问题:一个线程把数据A变成了B,然后又重新变成了A,此时另一个线程使用compareAndSet时发现A是对的,但实际上中间已经经过了变化
需求:线程1想要感知变量 ref 的中间变化
解决方案:版本号
@Slf4j(topic = "c.test")
public class ConcurrentApplication {static AtomicStampedReference ref = new AtomicStampedReference<>("A", 0);public static void main(String[] args) throws InterruptedException {//获取修改前的参数String prev = ref.getReference();int stamp = ref.getStamp();//修改时要传入旧值进行比对:依次传入 旧值,修改值,旧的版本号,以及版本号+1boolean flag = ref.compareAndSet(prev, "C", stamp,stamp+1);log.debug("flag = {}, version = {}", flag, ref.getStamp());}
}
AtomicStampedReference是通过版本号,甚至可以知道中间被修改过多少次
如果仅仅需要知道有无修改过,只需要一个 boolean 变量即可
AtomicReference 修改的引用本身,现在想要修改引用的对象内容
预备知识,几个函数式接口
//Supplier:() -> 结果
public interface Supplier {T get();
}
//Function:(参数) -> 结果
public interface Function {R apply(T t);
}
//BiConsumer:(参数1, 参数2) -> void
public interface BiConsumer {void accept(T t, U u);
}
//Consumer:(参数) -> void
public interface Consumer {void accept(T t);
}
demo函数
private static void demo(Supplier arraySupplier, Function lengthFun, BiConsumer putConsumer, Consumer printConsumer ) {List ts = new ArrayList<>();//获取调用者传来的数组T array = arraySupplier.get();//得到数组长度int length = lengthFun.apply(array);for (int i = 0; i < length; i++) {// 每个线程对数组作 10000 次操作ts.add(new Thread(() -> {for (int j = 0; j < 10000; j++) {putConsumer.accept(array, j%length);}}));}ts.forEach(t -> t.start()); // 启动所有线程ts.forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}}); // 等所有线程结束//打印数据结果printConsumer.accept(array);
}
demo(()->new int[10],(array)->array.length,(array, index) -> array[index]++,array-> System.out.println(Arrays.toString(array))
);
结果
[5487, 5525, 5502, 5501, 5565, 5565, 5523, 5527, 5524, 5484]
可以看到,元素并没有达到 10000,说明这是多线程下数组是线程不r安全的
原子数组
demo(()->new AtomicIntegerArray(10),(array)->array.length(),(array, index) -> array.getAndIncrement(index),array-> System.out.println(array)
);
结果
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
保障了线程安全
利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常
public class ConcurrentApplication {public static void main(String[] args) throws InterruptedException {Student stu = new Student();AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");//这里的三个参数分别代表,object, expect, update//如果name属性为null,则更新成功(更改为”张三“),否则更新失败updater.compareAndSet(stu, null, "张三");}
}class Student{volatile String name;
}
关于AtomicIntegerFieldUpdater和AtomicInteger的区别
AtomicIntegerFieldUpdater
是staic final
类型,即类变量,并不会占用当前对象的内存
当字段所属的类会被创建大量的实例时,如果用AtomicInteger
每个实例里面都要创建AtomicInteger对象,占用更多内存
private static void demo(Supplier addrSupplier, Consumer action){T addr = addrSupplier.get();List ts = new ArrayList<>();for(int i=0; i<4; ++i){ts.add(new Thread(() -> {for(int j=0; j<500000; j++){action.accept(addr);}}));}long start = System.nanoTime();ts.forEach(t->t.start());ts.forEach(t->{try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});long end = System.nanoTime();//纳秒System.out.println(addr + " cost:" + (end-start)/1000_000);
}
使用AtomicLong做累加
demo(()->new AtomicLong(0),(addr) -> addr.getAndIncrement()
);
//输出:2000000 cost:35
使用LongAdder做累加
demo(()->new LongAdder(),//只能无参,从0开始(addr) -> addr.increment()
);
//输出:2000000 cost:18
LongAdder和AtomicLong都能保障原子性,但前者性能高出很多
性能提升的原因
有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的Cell 变量,因此减少了 CAS 重试失败,从而提高性能
LongAdder 类有几个关键域
transient volatile Cell[] cells;// 累加单元数组, 懒惰初始化transient volatile Cell[] cells;// 基础值, 如果没有竞争, 则用 cas 累加这个域transient volatile long base;// 在 cells 创建或扩容时, 置为 1, 表示加锁transient volatile int cellsBusy;
}
在cells创建或扩容时,仍然需要锁机制来保障安全
CAS锁模拟cellsBusy
public class LockCas {private AtomicInteger state = new AtomicInteger(0);public void lock() {while (true) {if (state.compareAndSet(0, 1)) {break;}}}public void unlock() {log.debug("unlock...");state.set(0);}
}
测试
LockCas lock = new LockCas();
new Thread(() -> {log.debug("begin...");lock.lock();try {log.debug("lock...");sleep(1);} finally {lock.unlock();}
}).start();
new Thread(() -> {log.debug("begin...");lock.lock();try {log.debug("lock...");} finally {lock.unlock();}
}).start();
缓存行伪共享
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
CPU 要保证数据的一致性
,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
缓存行伪共享
因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象,此时
无论谁修改成功,都会导致对方 Core 的缓存行失效
@sun.misc.Contended解决伪共享
原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效
@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);}
}
add()
public void increment() {add(1L);
}public void add(long x) {Cell[] as; long b, v; int m; Cell a;//cells数组是懒惰创建的,一开始为null,表明无竞争//casBase对基础域进行累加,累加失败说明有竞争if ((as = cells) != null || !casBase(b = base, b + x)) {//条件不符合,则说明存在竞争,需要使用多个累加单元boolean uncontended = true;if (as == null || (m = as.length - 1) < 0 ||(a = as[getProbe() & m]) == null ||!(uncontended = a.cas(v = a.value, v + x)))//执行累加单元的caslongAccumulate(x, null, uncontended);//如果累加失败,还是会进入longAccumulate}
}
longAccumulate()
cells未创建,或者cells容量不够等,会调用longAccumulate()
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {int h;if ((h = getProbe()) == 0) {ThreadLocalRandom.current(); // force initializationh = getProbe();wasUncontended = true;}boolean collide = false; // True if last slot nonemptyfor (;;) {Cell[] as; Cell a; int n; long v;//第1个if是 累加单元 是否创建的判断,如果为null,或者累加单元不够了,就执行该块代码//第2个if是 加锁保证创建或扩容cells的原子性//第3个if是 加锁失败,对base累加,累加成功就break,未成功就继续循环进行下一次尝试if ((as = cells) != null && (n = as.length) > 0) {...}else if (cellsBusy == 0 && cells == as && casCellsBusy()) {...}else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))){...}}
}
sum()
对累加单元cells进行合计
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;
}
Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得
package sun.misc;
public final class Unsafe {private static final Unsafe theUnsafe;...
}
Unsafe并不是指线程不安全,而是因为操作底层,因此不建议使用,所有Unsafe
//private static final Unsafe theUnsafe;
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
//如果field的name是一个static的变量,field.get(param),param是任意的都可以,返回类中当前静态变量的值
//如果是非静态变量,field.get(obj),obj必须是当前类的实例对象,返回实例对象obj的变量值
System.out.println(unsafe);
//1. 获取域的偏移地址
long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));//2. 执行CAS操作
Teacher t = new Teacher();
unsafe.compareAndSwapInt(t, idOffset, 0, 1);//给id赋值,且是线程安全的
unsafe.compareAndSwapObject(t, nameOffset, null, "张三");//3. 验证
System.out.println(t);
需求:模拟一个原子整数类,实现前面的转账功能
Account接口
interface Account {// 获取余额Integer getBalance();// 取款void withdraw(Integer amount);/*** 方法内会启动 1000 个线程,每个线程做 -10 元 的操作* 如果初始余额为 10000 那么正确的结果应当是 0*/static void demo(Account account) {List ts = new ArrayList<>();long start = System.nanoTime();for (int i = 0; i < 1000; i++) {ts.add(new Thread(() -> {account.withdraw(10);}));}ts.forEach(Thread::start);ts.forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});long end = System.nanoTime();System.out.println(account.getBalance()+ " cost: " + (end-start)/1000_000 + " ms");}
}
原子整数类 MyAtomicInteger
class MyAtomicInteger implements Account{private volatile int value;private static final long valueOffset;static final Unsafe UNSAFE;static {UNSAFE = UnsafeAccessor.getUnsafe();try {valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));} catch (NoSuchFieldException e) {e.printStackTrace();//这里MyAtomicInteger是自定义的,所以肯定不会是NoSuchFieldException,将它包装为RuntimeExceptionthrow new RuntimeException(e);}}public int getValue(){return value;}public void decrement(int amount){while(true){int prev = this.value;int next = prev - amount;if(UNSAFE.compareAndSwapInt(this, valueOffset, prev, next)){break;}}}public MyAtomicInteger(int value){this.value = value;}@Overridepublic Integer getBalance() {return getValue();}@Overridepublic void withdraw(Integer amount) {decrement(amount);}
}
测试
Account.demo(new MyAtomicInteger(10000));
即便不使用悲观锁、乐观锁机制,不可变类也是安全的
以可变类 SimpleDateFormat 为例
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {new Thread(() -> {try {log.debug("{}", sdf.parse("1951-04-21"));} catch (Exception e) {log.error("{}", e);}}).start();
}
结果容易出现报错 “empty String”
calendar
(详细原理暂略)SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {new Thread(() -> {synchronized (sdf){try {log.debug("{}", sdf.parse("1951-04-21"));} catch (Exception e) {log.error("{}", e);}}}).start();
}
public static void main(String[] args) {//this class is immutable and thread-safeDateTimeFormatter stf = DateTimeFormatter.ofPattern("yyyy-MM-dd");for(int i=0; i<10; i++){new Thread(()->{TemporalAccessor parse = stf.parse("1951-04-21");log.debug("{}", parse);}).start();}
}
以String不可变类示例
public final class Stringimplements java.io.Serializable, Comparable, CharSequence {private final char value[];private int hash; // Default to 0...
}
发现该类、类中所有属性都是 final 的
注意 final 只能保证引用不被修改,不能保证对象内容不被修改,对此,String进行了如下设置
public String(char value[]) {this.value = Arrays.copyOf(value, value.length);
}
//保护性拷贝
//这样设置后,即使外部的char数组被改变了,也不会影响到String对象
以substring为例
public String substring(int beginIndex) {if (beginIndex < 0) {throw new StringIndexOutOfBoundsException(beginIndex);}int subLen = value.length - beginIndex;if (subLen < 0) {throw new StringIndexOutOfBoundsException(subLen);}return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
其内部是调用 String 的构造方法创建了一个新字符串
通过创建副本对象来避免共享的手段称之为保护性拷贝(defensive copy)
保护性拷贝每次需要创建新的对象,享元模式
(Flyweight pattern)可优化这个问题
享元模式:当需要重用数量有限的同一类对象时
包装类是典型的享元模式
valueOf
方法//以Long为里,静态方法LongCache会事先缓存-128到127的对象
private static class LongCache {private LongCache(){}static final Long cache[] = new Long[-(-128) + 127 + 1];static {for(int i = 0; i < cache.length; i++)cache[i] = new Long(i - 128);}
}
//调用valueOf时,可直接返回缓存中的对象
public static Long valueOf(long l) {final int offset = 128;if (l >= -128 && l <= 127) { // will cachereturn LongCache.cache[(int)l + offset];}return new Long(l);
}
-Djava.lang.Integer.IntegerCache.high
来改变除此之外,String串池
和BigDecimal
BigInteger
也是享元模式,不可变类
之前转账操作时使用BigInteger不能保证线程安全,是因为其单个方法是安全的,但不能保证多个操作的组合
是安全的
应用场景:一个线上商城应用,QPS 达到数千,如果每次都重新创建和关闭数据库连接,性能会受到极大影响。 这时预先创建好一批连接,放入连接池。一次请求到达后,从连接池获取连接,使用完毕后再还回连接池,这样既节约了连接的创建和关闭时间,也实现了连接的重用,不至于让庞大的连接数压垮数据库
连接池:Pool
class MockConnection implements Connection{private String name;public MockConnection(String name){this.name = name;}@Overridepublic String toString() {return "MockConnection{" +"name='" + name + '\'' +'}';}//省略一大堆需要implements的方法
}@Slf4j
class Pool{//1. 连接池大小private final int poolSize;//2. 连接对象数组private Connection[] connections;//3. 连接状态数组,0表示空闲,1表示繁忙private AtomicIntegerArray states;//4. 构造方法初始化public Pool(int poolSize){this.poolSize = poolSize;this.connections = new Connection[poolSize];this.states = new AtomicIntegerArray(new int[poolSize]);//将普通数组包装成原子数组for(int i=0; iconnections[i] = new MockConnection("连接"+(i+1));}}//5. 借连接public Connection borrow(){while(true){for(int i=0; iif(states.get(i) == 0){if(states.compareAndSet(i, 0, 1)){log.debug("borrow {}", connections[i]);return connections[i];}}}//如果没有空闲连接,使当前线程等待//加入下面的wait()代码,是为了避免当前线程一直使用CAS操作,导致忙等占用cpusynchronized (this){try{log.debug("waiting...");this.wait();}catch (InterruptedException e){e.printStackTrace();}}}}//6. 归还连接public void free(Connection conn){for(int i=0; iif(connections[i] == conn){states.set(i, 0);log.debug("free {}", conn);synchronized (this){this.notifyAll();}break;}}}
}
测试
public static void main(String[] args) {Pool pool = new Pool(2);for(int i=0; i<5; ++i){new Thread(() -> {Connection conn = pool.borrow();try {Thread.sleep(new Random().nextInt(1000));} catch (InterruptedException e) {e.printStackTrace();}finally {pool.free(conn);}}).start();}
}
final修饰的变量只能被赋值一次,赋值后值不再改变(final要求地址值不能改变)
final字节码
public class TestFinal {final int a = 20;
}
字节码为
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: bipush 20
7: putfield #2 // Field a:I<-- 写屏障:保证这之前的所有操作都被同步到主存,且不会重排序到写屏障之后
10: return
写final
如果是普通变量,int a = 20,则分为2步,首先开辟一块空间,然后赋值20,如果在这直接读取a,将会访问到初始值0
final通过加入一个写屏障,阻止了这种读取
读final
int k = a; 此时是直接将10复制了一份到 k 所在线程空间中,避免了对原有a的修改
static int A = 10;
final static int B = Short.MAX_VALUE+1;
非final的A走的是共享内存,要去另一个类中获取静态变量(GETSTATIC);final修饰的B可以去常量池中找(使用LDC指令)
内存屏障参考:https://blog.csdn.net/qq_23350817/article/details/126525990
因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为无状态