您的位置:首页 > 编程语言 > Java开发

java并发编程——四(synchronized\Lock\volatile) 锁机制原理及关联

2016-02-25 23:10 555 查看

前言

其实标题使用互斥机制更合适,并发中主要两个问题是:线程如何同步以及线程如何通信。

同步主要是通过互斥机制保证的,而互斥机制我们最熟悉的就是锁,当然也有无锁的CAS实现。

多线程共享资源,比如一个对象的内存,怎样保证多个线程不会同时访问(读取或写入)这个对象,这就是并发最大的难题,因此产生了 互斥机制(锁)。

synchronized

When should you synchronize? Apply Brian’s Rule of Synchronization:

If you are writing a variable that might next be read by another

thread, or reading a variable that might have last been written by

another thread, you must use synchronization, and further, both the

reader and the writer must synchronize using the same monitor lock.

作用:

可见性

获取锁后,该线程本地存储失效,临界区(就是获得锁后释放锁之前 的代码区)从主存获取数据,并在释放锁后刷入主存。

有序性(互斥)

保证临界区代码线程间互斥。

synchronized实现同步的基础:

synchronized通过对象的对象头(markwork)来实现锁机制。

java中每个对象都可以作为锁(准确的说,每个对象都有的对象头,那么都为synchronized实现提供的基础,每个对象都是一把对象锁)

具体表现(代码实例):

public class myTest {
//静态synchronized修饰:锁myTest.class对象(当前类的class对象)
public static synchronized void () throws IOException {
......
}
//锁当前对象(就是this指向的对象)
public synchronized void inc2() throws IOException {
......

}
Object lock=new Object();
//显示的指定锁对象 lock
public void lockObject() throws IOException {
synchronized(lock){
......
}
}


synchronized锁原理 字节码层面

我们先来看一下synchronized方法的字节码:

public class Synchronized {
public static void main(String[] args) {
synchronized (Synchronized.class) {

}
m();
}

public static synchronized void m() {
}
}


javap -v Synchronized.class:

............
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc #1 // class com/four/Synchronized
2: dup
3: monitorenter
4: monitorexit
5: invokestatic #16 // Method m:()V
8: return
LineNumberTable:
line 5: 0
line 8: 5
line 9: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;

public static synchronized void m();
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
}
............


synchronized 块是通过插入monitorenter,monitorexit完成同步的

通过javap命令生成的字节码中包含 monitorenter 和 monitorexit 指令,

这两个指令依次在临界区(就是需要同步的代码块)前后。

持有Monitor对象,通过进入、退出这个Monitor对象来实现锁机制,使用 monitorenter指令 与 moniterexit指令

那么monitorenter,monitorexit又是什么呢?从对象头说起

Synchronized锁存储与对象头:

上文说过,synchronized通过对象的对象头(markwork)来实现锁机制,java中每个对象都可以作为锁(准确的说,每个对象都有的对象头,那么都为synchronized实现提供的基础,每个对象都是一把对象锁)

对象头

对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充




对象头

对象头包括两部分:Mark Word 和 类型指针。

synchronized源码实现就用了Mark Word来标识对象加锁状态.

Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、synchronized锁信息(锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳)等等,占用内存大小与虚拟机位长一致。

类型指针

类型指针指向对象的class元数据,虚拟机通过这个指针确定该对象是哪个类的实例。

其中对象头存储了synchronized锁实现的细节:



monitorenter\monitorexit的背后:synchronizd锁升级 C++代码实现

synchronized关键字基于上述两个指令实现了锁的获取和释放过程,解释器执行monitorenter时会进入到InterpreterRuntime.cpp的InterpreterRuntime::monitorenter函数,具体实现如下:



可重入

一个任务可以多次获得锁,比如在一个线程中调用一个对象的 synchronized标记的方法,在这个方法中调用第二个synchronized标记的方法,然后在第二个synchronized方法中调用第三个synchronized方法。一个线程每次进入一个synchronized方法中JVM都会跟踪加锁的次数,每次+1,当该这个方法执行完毕,JVM计数-1;当JVM计数为0时,锁完全被释放,其他线程可以访问该变量。

显示锁

注意:return 语句放在try中,以避免过早的释放锁;JDOC推荐lock.lock后跟try{}finally{}

参考上篇文中的例子,我们用显示锁实现互斥机制:

private Lock lock = new ReentrantLock();
public int next() {
lock.lock();
try {
++currentEvenValue; // Danger point here!
Thread.yield(); // 加快线程切换
++currentEvenValue;
return currentEvenValue;
} finally {
lock.unlock();
}
}


使用 Lock lock =new ReentrantLock()的问题是代码不够优雅,增加代码量;我们一般都是使用synchronized实现互斥机制。但是1.当代码中抛出异常时,显示锁的finally里可以进行资源清理工作。2.ReentrantLock还给我们更细粒度的控制力

public class AttemptLocking {
private Lock lock = new ReentrantLock();

private void untimed() {
// lock.lock();
boolean captured = lock.tryLock();
try {
System.out.println("tryLock() " + captured);
} finally {
if (captured) {
lock.unlock();
}
}

}

private void timed() {
boolean captured = false;
try {
captured = lock.tryLock(2, TimeUnit.SECONDS);
} catch (Exception e) {
throw new RuntimeException(e);
}
try {
System.out.println("lock.tryLock(2, TimeUnit.SECONDS) " + captured);
} finally {
if (captured) {
lock.unlock();
}
}
}

public static void main(String[] args) {
final AttemptLocking attemptLocking = new AttemptLocking();
attemptLocking.untimed();
attemptLocking.timed();

new Thread() {
{
setDaemon(true);
}

public void run() {
attemptLocking.lock.tryLock();
System.out.println("acquired");
}
}.start();
Thread.yield();
attemptLocking.untimed();
attemptLocking.timed();
}

}


lock 互斥执行 原理是通过AQS实现的同步器实现.具体请看AQS详解

lock可见性 也是AQS,具体是state变量的happen-before规则。 AQS(或JDK锁)如何保证可见性

更深入的理解请阅读:

java并发编程——九 AbstractQueuedSynchronizer

AQS详解

java并发编程——ReentrantLock源码(重入锁、公平锁、非公平锁)

java并发编程——读写锁ReentrantReadWriteLock

java并发编程——Condition(wait\signal\notify的等待-通知模式)

Atomic&Volatie

什么是原子性(Atomic):不会被线程调度机制中断的操作,一旦操作开始,就会在线程上下文切换之前完成操作.

原子性应用于除了long\double之外其他的基本数据类型,因为long\double 是64bit ,JVM对于64bit会当作两个32bit的操作来执行,那么在这两个执行直接可能会发生上下文切换。

当我们给 long\double 加上 volatile,可以保证原子性操作,仅限于读、写操作,比如long l=0;l++,++操作就是典型的非原子性操作,因为“++”操作其实是一个读操作与一个写操作的组合操作!

volatile

保证共享变量的“可见性”(一个线程修改这个共享变量时,另一个线程可以读取到修改的值),某些情况下使用恰当的话,比synchronized性能更好,因为它不会竞争锁,就不会引起上下文切换。

原理解析

用volatile修饰后的变量,转化为汇编语言后,会多出“lock add1…. ”的指令,该指令会引发两件事情:

1.将当前处理器缓存行的数据写入系统主存

2.写回主存操作使其他cpu中缓存了该内存的数据失效(详见下文,volatile内存语义)

为了提高处理速度,cpu先将系统内存的数据放到内部缓存(L1,L2,L3…)中,然后再进行操作,下次会存在缓存命中(cache hit).如果变量声明了volatile,写操作时,JVM会向cpu发送一条lock前缀命令,会将该数据直接写入内存中,并使其他处理器缓存该变量的内存地址失效,保证缓存的一致性。

多个线程去访问一个非volatile域,并且不用synchronized,其中一个任务修改了这个域,很可能这时只是把这个“修改”放入了处理器缓存中,而非主内存。其他线程,可能并不会读到这个域的修改值(读操作发生在主内存!)。所以可以使用volatile去保证每次修改,都会把最新的值刷入主内存(或者你也可以使用synchronized去给每个访问这个域的方法加锁,synchronized也可以保证修改刷到主内存)!

当一个volatile域依赖于它之前的值(如++i 这种递增),或者它依赖于其他变量,volatile就无法工作了。 建议使用 synchronized而非 volatile.请看下边的例子:

慎重依赖基本类型的“原子性”

class CircularSet {
private int[] array;
private int len;
private int index = 0;

public CircularSet(int size) {
array = new int[size];
len = size;
for (int i = 0; i < size; i++) {
array[i] = -1;
}
}
public synchronized void add(int i) {
array[index] = i;
index = ++index % len;
}

public synchronized boolean contains(int val) {
for (int i = 0; i < len; i++) {
if (array[i] == val) {
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
}

public class SerialNumberChecker {
private static final int SIZE = 10;
private static CircularSet serials = new CircularSet(1000);
private static ExecutorService exec = Executors.newCachedThreadPool();

static class SerialChecker implements Runnable {
@Override
public void run() {
while (Boolean.TRUE) {
int serial = SerialNumberGenerator.nextSerialNumber();

if (serials.contains(serial)) {
System.out.println("Duplicate:" + serial);
System.exit(0);
}
serials.add(serial);

}
}

}

public static void main(String[] args) {
// 是个线程同时对SerialNumberGenerator的域serialNumber进行读写操作;
// 如果serialNumber++是原子性的,程序不会中断
for (int i = 0; i < SIZE; i++) {
exec.execute(new SerialChecker());
}

}
}


public class SerialNumberGenerator {
// 使用volatile保证 serialNumber的可见性(值改变变后刷入主内存)
private static volatile int serialNumber = 0;

public static int nextSerialNumber() {
// serialNumber++你认为是原子性吗?
return serialNumber++;
}

}
//Outp:Duplicate:3954


以上例子证明了,volatile 基本变量 自增时,并无法保证原子性!所以,

1.一般情况下使用synchronized 而非 volatile. 2. ++操作是非原子性的,典型的读写组合操作

public class Atomicity {
int i;
void f1() { i++; }
void f2() { i += 3; }
} /* Output: (Sample)
...
//字节码:
void f1();
Code:
0: aload_0
1: dup
2: getfield #2; //Field i:I   首先,get
5: iconst_1
6: iadd
7: putfield #2; //Field i:I  经过几个步骤,最后put
10: return
void f2();
Code:
0: aload_0
1: dup
2: getfield #2; //Field i:I  首先,get
5: iconst_3
6: iadd
7: putfield #2; //Field i:I  经过几个步骤,最后put
10: return
*///:~


原子性

cpu角度的原子性实现:

总线锁定

当一个线程在cpu1中执行i++操作时,会锁定系统内存与各个cpu之间的通信——总线,保证cup1执行i++操作时其他任务不会改变主存中i的值。但是在这个锁定期间其他任何指令都不会执行.

缓存锁定

当一个线程在cpu1中执行i++操作时,不会锁定系统内存与各个cpu之间的通信,会锁定这个数据缓存数据的内存地址,只允许cpu1的i++操作写入主存,阻止其他任务(cpu2 :i++)改变这个数据(缓存一致性),同时使其他cpu缓存失效



java原子性实现

使用CAS循环

伪代码:

for (;;) {
currentValue = getValue();// 获取当前数据,位操作原子性
boolean success = compareAndSet(currentValue, currentValue++);// 如果currentValue未改变(currentValue==getValue()),那么用currentValue++(新值)替换currentValue

if (success) {
break;
}
}


Volatile内存语义

public class VolatileTest {
volatile long i = 0;

private long  get() {
return i;
}

private void set(long i) {
this.i = i;
}

private void addOne() {
i++;
}

}

等同于:

class VolatileTest2 {
long i = 0;

private synchronized long get() {//原子性
return i;
}

private synchronized void set(long i) {//原子性
this.i = i;
}

private void addOne() {// i++并没有加方法锁,该操作不是原子性的
int tempI = get();
tempI += 1L;
set(tempI);
}

}


总结:volatile

原子性:volatile变量,读写操作原子性,但对于++i这种复合操作并非原子性

可见性:volatile变量的读操作总是可以看到最后一次写操作的更新数据。

volatile写操作内存语义:把该线程的本地存储刷入主存(与释放锁的内存语义相同)

volatile读操作内存语义:把该线程对应的本地存储置为无效,从主存中读取。(与获取锁的内存语义相同)

synchornized 与 volatile 的比较

synchornized与volatile共同点:

保证数据的可见性(读取主存);

synchornized缺点:

1 synchornized 会引发锁竞争,导致上下文切换,影响性能,volatile不会.

2 synchronized 因为锁竞争,有引发死锁、饿死等多线程问题,volatile不会.

volatile缺点:

1 volatile保证可见性但不保证原子性(如i++),synchronized保证可见性同时保证原子性

2 仅限于在变量级别使用,而synchronized用法更广泛

http://www.javaperformancetuning.com/news/qotm051.shtml

Lock(显式锁)与synchronized(隐式锁) 的对比

( tryLock()) 非阻塞的获取锁,也可设置等待时间

synchronized悲观锁的并发策略,获得独占锁,所谓独占锁就是一个线程进入synchronized后获得锁,其他线程如果也想获得这个锁只有进入(wait())阻塞状态,阻塞状态会引发上下文切换,当较多线程竞争,会产生频繁上下文切换。

Lock实现乐观锁的策略,使用CAS算法, 不会产生线程的阻塞

(lock.lockInterruptibly())与synchronized不同,获取到锁的线程如果中断,捕获interrupted异常,同时释放锁

当代码中抛出异常时,显示锁的finally里可以进行资源清理工作。

Lock还给我们更细粒度的控制力

JDK原子类

原子操作:不可中断的一个或一组操作

Atomiclnteger, AtomicLong, AtomicReference

还是建议使用 synchronized 或Lock,上述原子类使用,详见JDK Document
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: