(锁) 系列篇
1、synchronized实现 |
(1)synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式:
- 对于普通同步方法,锁是当前实例对象。
public class SynchronizedTest { // ...... public synchronized void test1(){ } }
- 对于静态同步方法,锁是当前类的Class对象。
public class SynchronizedTest { // ...... public static synchronized void test2(){ } }
- 对于同步方法块,锁是Synchonized括号里配置的对象。
public class SynchronizedTest { // ...... Object lock = new Object() ; public void test3(){ synchronized (lock){ } } }
(2)对于SynchronizedTest.java使用javap反解析出汇编指令(open-jdk 1.8.0):
javac SynchronizedTest.java
javap -v SynchronizedTest.class
// ..............
{ //....... public synchronized void test1(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=0, locals=1, args_size=1 0: return LineNumberTable: line 7: 0 public static synchronized void test2(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED Code: stack=0, locals=0, args_size=0 0: return LineNumberTable: line 11: 0 public void test3(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: getfield #3 // Field lock:Ljava/lang/Object; 4: dup 5: astore_1 6: monitorenter 7: aload_1 8: monitorexit 9: goto 17 12: astore_2 13: aload_1 14: monitorexit 15: aload_2 16: athrow 17: return Exception table: from to target type 7 9 12 any 12 15 12 any LineNumberTable: line 15: 0 line 17: 7 line 18: 17 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 12 locals = [ class com/nancy/sync/SynchronizedTest, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 } SourceFile: "SynchronizedTest.java"
由此看出同步方法使用ACC_SYNCHRONIZED标识进入该方法需要同步,同块代码块使用monitorenter / monitorexit来标识同步代码块保护区。虽然实现的形式不一致,实质都是使用moniter对象头Mark Word的标志位实现。
(3)synchronized特点
- 重量锁高并发场景下效率低下(早期依赖于底层的操作系统的Mutex Lock来实现,需要内核态和用户态之间转换)
- 保证线程对变量访问的可见性、排他性、原子性
可见性:修改变量保证其他线程可见
排他性:同一时刻只能一个线程访问
原子性:对变量的操作保证原子执行,例如:i++就是经典的读改写操作
2、cas(Compare And Swap) |
(1)cas实现
cas类似乐观锁,利用操作系统指令原子执行操作,一般结合自旋实现。例如
AtomicInteger#getAndIncrement方法:
public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } // ..... } public final class Unsafe { public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; } /** Volatile version of {@link #getInt(Object, long)} */ public native int getIntVolatile(Object o, long offset); /** * Atomically update Java variable to <tt>x</tt> if it is currently * holding <tt>expected</tt>. * @return <tt>true</tt> if successful */ public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); // ......... }
通过不断自旋实现原子更新操作,可想高并发场景下虽去掉了锁的概念,但过多无效空旋也会占用CPU时间。
(2)cas实现副作用:可能引发典型"A-B-A问题":
cas执行操作时会检查该值与期望值A是否一致(即检查内存中某个地址变量的值),如果一致则更新为C。但如果值从原来是A,变成B,又变成了A,
那么cas进行检查时不会发现它的值发生变化,而实际已经发生了变化(1A-2B-3A),即cas的"A-B-A问题"。
解决方式即使用版本号或者时间戳,发现不一致即更新失败。例如:AtomicStampedReference引入stamp时间戳解决"A-B-A问题":
/** * An {@code AtomicStampedReference} maintains an object reference * along with an integer "stamp", that can be updated atomically. * * <p>Implementation note: This implementation maintains stamped * references by creating internal objects representing "boxed" * [reference, integer] pairs. * * @since 1.5 * @author Doug Lea * @param <V> The type of object referred to by this reference */ public class AtomicStampedReference<V> { private static class Pair<T> { final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference, stamp); } } private volatile Pair<V> pair; // ...... public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair<V> current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); } // Unsafe mechanics private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe(); private static final long pairOffset = objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class); private boolean casPair(Pair<V> cmp, Pair<V> val) { return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); } static long objectFieldOffset(sun.misc.Unsafe UNSAFE, String field, Class<?> klazz) { try { return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field)); } catch (NoSuchFieldException e) { // Convert Exception to corresponding Error NoSuchFieldError error = new NoSuchFieldError(field); error.initCause(e); throw error; } } }
3、synchronized和cas二者区别 |
- synchronized为锁机制隐含着获锁和解锁的操作。利用操作系统提供支持实现访问竞争资源的顺序性,早期JDK版本未对synchronized优化并发效率低。后期引入轻量级锁、偏向锁等概念,大大优化性能。优势在于使用简单,维护成本低。
- cas实现了lock-free的概念,不引入锁也可解决并发场景问题。如果单纯使用cas会引起多重副作用,高级并发包下配合AQS一起可提升并发效率。
- 如果单独使用,二者都偏向并发量不高或者业务简单的场景。例如:单体web应用单调计数场景。