java锁机制

来源:互联网 发布:多媒体数据特点 编辑:程序博客网 时间:2024/06/06 16:54

java的锁,相信大家都不会陌生,在前面讲集合类家族的时候提到了一个线性安全与不安全的概念,而锁这个机制,原本就是一个线性安全的保证。

 

在众多的锁机制中,大家最熟悉的莫过于synchronize关键字,这个关键字修饰的方法、类、代码块在被某处调用时候会加上锁,除非锁解开,否则其他地方完全不能调用,这种机制我们称为悲观锁:无论不加锁存不存在线性安全的问题,都给加上锁。这样的机制无疑会产生两个问题,第一是当有两处这样的锁AB,然后两个线程XYX占用了AY占用了BX在等待Y释放BY在等X释放A,这样就会产生死锁的问题;第二是一旦这种机制被多个线程频繁使用,效率将会受到非常大的影响。例子:

 设计一个类

public class CASTest {private Integer number = 0;public void addNumber() {number ++;}public Integer getNumber() {return number;}}

测试:

                CASTest test = new CASTest();Runnable runnable = new Runnable() {@Overridepublic void run() {for (int i=0; i<10000; i++) {test.addNumber();}}};long from = new Date().getTime();  new Thread(runnable).start();runnable.run();System.out.println(test.getNumber());System.out.println(new Date().getTime() - from);

结果:

12359
3

可以看到,结果中number并不是想象中的20000,现在加上synchronize:

public class CASTest {private Integer number = 0;public synchronized void addNumber() {number ++;}public Integer getNumber() {return number;}}
结果:

20000
6

可以看到,虽然结果完美的变成了20000,但是效率降低了一倍,两个线程都有如此大的效率问题,可想而知悲观锁的性能问题有多大。

JDK1.5以后有一种新的机制叫做CAS,这种机制会保存代码的原值,在代码做修改的时候,它会比较代码的当前值和原值,当且仅仅当二者相等的时候才修改其值。这种锁被称为乐观锁。例子:

public class CASTest {private AtomicInteger number = new AtomicInteger(0);public void addNumber() {number.getAndAdd(1);}public Integer getNumber() {return number.get();}}

测试的调用同上

20000
2

结果可以看出,在误差范围内时间几乎和普通变量无二,而且最终值也是20000.,这充分说明了乐观锁不仅基本可以达到悲观锁的效果,而且在效率上大大提高。


来看下用到了乐观锁的AtomicInteger的源代码:

/** * An {@code int} value that may be updated atomically.  See the * {@link java.util.concurrent.atomic} package specification for * description of the properties of atomic variables. An * {@code AtomicInteger} is used in applications such as atomically * incremented counters, and cannot be used as a replacement for an * {@link java.lang.Integer}. However, this class does extend * {@code Number} to allow uniform access by tools and utilities that * deal with numerically-based classes. * * @since 1.5 * @author Doug Lea*/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;

    /**     * Atomically adds the given value to the current value.     *     * @param delta the value to add     * @return the previous value     */    public final int getAndAdd(int delta) {        return unsafe.getAndAddInt(this, valueOffset, delta);    }
对于所有的原子类而言,最终都是调用了UnSafe类。

Unsafe类中有大量的native方法,可以下载OpenJDK的源码,在openjdk/openjdk/hotspot/src/share/vm/prims/unsafe.cpp中有源代码

随便看一个compareAndSwapInt方法:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))  UnsafeWrapper("Unsafe_CompareAndSwapInt");  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
native方法的源码似乎不太容易读懂,但是我们可以看到这里是将用Atomic::cmpxchg方法获取到的值来和e做比较。

jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest, jbyte compare_value) {  assert(sizeof(jbyte) == 1, "assumption.");  uintptr_t dest_addr = (uintptr_t)dest;  uintptr_t offset = dest_addr % sizeof(jint);  volatile jint* dest_int = (volatile jint*)(dest_addr - offset);  jint cur = *dest_int;  jbyte* cur_as_bytes = (jbyte*)(&cur);  jint new_val = cur;  jbyte* new_val_as_bytes = (jbyte*)(&new_val);  new_val_as_bytes[offset] = exchange_value;  while (cur_as_bytes[offset] == compare_value) {    jint res = cmpxchg(new_val, dest_int, cur);    if (res == cur) break;    cur = res;    new_val = cur;    new_val_as_bytes[offset] = exchange_value;  }  return cur_as_bytes[offset];}

从这个方法的源码中,我们大致看出其利用了存储值的地方取出了现有值并返回。


源码姑且一看(我也没有完全看懂),有兴趣的自己接着研究。


这里还有个问题,就是我们看到在AtomicInteger类中用volatile修饰了value变量,这个volatile修饰符有什么用处呢。


据说这个修饰符是为了不同线程的可见性,可以认为有了这个修饰符修饰,线程间可见性比没有这个更强:


public class VolatileTest extends Thread{private Integer number = 0;private Boolean flag = true;public void run() {while (flag) {try {Thread.sleep(1);System.out.println("xxxxxxxxxxx");number ++;} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}public Integer getNumber() {return number;}public void stops() {flag = false;}public static void main(String[] args) {VolatileTest test = new VolatileTest();new Thread(test).start();try {Thread.sleep(1000);test.stops();System.out.println(test.getNumber());} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}
。。。。
xxxxxxxxxxx
xxxxxxxxxxx
xxxxxxxxxxx
974
xxxxxxxxxxx
可以看见,在stop之后线程并没有马上停下。

加上private volatile Boolean flag = true;

结果:

xxxxxxxxxxx
xxxxxxxxxxx
xxxxxxxxxxx
985

这个结果并不是每次都生效,只是大多数时候是这样,因为本来就是一个机制问题。



除了上述锁机制以外,还有一种reentrantlock机制,reentrantlock原本也属于CAS内容的一部分,在使用的时候它更好地替代了synchronize,例子:

public class LockTest {Lock lock = new ReentrantLock();public void doSomething() {lock.lock();System.out.println("xxxxxxx");try {Thread.sleep(1000);System.out.println("yyyyy");} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}public static void main(String[] args) {LockTest test = new LockTest();new Thread(new Runnable() {@Overridepublic void run() {test.doSomething();}}).start();test.doSomething();}}

结果:

xxxxxxx
yyyyy
xxxxxxx
yyyyy

从这里可以看到,由于lock的存在,dosomething方法就被锁定不能被其他线程调用了,这看起来有点像synchronize,而且由于采用了类对象的方式实现,在使用的时候比synchronize更可控和灵活。

此外JDK官方还提出了ReentrantLock是一种不会死锁的锁机制,原因是当线程await的时候,ReentrantLock将其视为释放。

而且ReentrantLock可以用传入的参数将其定位公平锁,虽然公平锁的性能较低,但是在有些业务逻辑下锁的公平性还是有必要的。


ReentrantLock机制采用的是阻塞队列实现锁机制的,他有一个内参state,在调用lock方法的时候会先看这个state是否为0,如果不是则阻塞在队列中循环等待state变化,state是一个volatile修饰的变量,因此只要锁一被释放就会被其他线程见到并使用。此外查看和改变这个state的值就是采用了unsafe类,即采用了CAS机制。


具体的源码我不做详解了,可以看这篇博客:https://www.cnblogs.com/xrq730/p/4979021.html


总结下java的锁机制,首先比较 普通写法、变量加volatile修饰、用CAS、synchronize修饰 这四种,从左至右效率依次降低,线性安全依次增加。


综合来讲个人比较推荐volatile和CAS一起使用来实现线性安全,高效且极少出错,在用到锁的时候可以尝试使用ReentrantLock。


原创粉丝点击