java的锁,相信大家都不会陌生,在前面讲集合类家族的时候提到了一个线性安全与不安全的概念,而锁这个机制,原本就是一个线性安全的保证。
在众多的锁机制中,大家最熟悉的莫过于synchronize关键字,这个关键字修饰的方法、类、代码块在被某处调用时候会加上锁,除非锁解开,否则其他地方完全不能调用,这种机制我们称为悲观锁:无论不加锁存不存在线性安全的问题,都给加上锁。这样的机制无疑会产生两个问题,第一是当有两处这样的锁A和B,然后两个线程X和Y,X占用了A,Y占用了B,X在等待Y释放B,Y在等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() {
@Override
public 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 block
e.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 block
e.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() {
@Override
public 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。