java多线程基础(volatile、synchronized、Lock)
java多线程安全性问题简单分析
并发编程模型的两个关键问题:
1.线程间的通信: java线程间通信是通过共享变量来解决的,所以我们主要解决的是java内存可见性问题
2.同步:同步是指程序中用于控制不同线程间操作发生相对顺序的机制。
下面分析一下问题原因和在java中出现这两个问题的解决方案。
java解决多线程内存可见性问题
- 问题原因分析: 在cpu工作的中,每个线程都会一个独立的缓存(就CPU的一级缓存二级缓存),而cpu为了提高工作效率,在运算中只操作缓存中的数据,之后在异步的刷到主内存中(jvm)。如下图,线程A修改了某个全局变量,它只能保证线程A缓存中的变量值是正确的,线程A缓存的变量值什么时候刷新到主内存中这个是不确定的,如果线程A修改了变量值没有及时刷新道主内存中,线程B正好用到线程A修改的变量,这个时候线程B缓存的变量值就是错误的。
如下代码:
public class Test {
private volatile static int i = 0;
public static void main(String[] args) throws InterruptedException {
new Thread() {
@Override
public void run() {
while (i == 0){
}
}
}.start();
Thread.sleep(1000);
i = 1;
}
}
上面代码:我们把main线程当做图中A线程,new Thread().start(); 当做图中的B线程,i是全局变量。运行结果B线程会线图死循环中,因为A线程修改了i,没有刷新到主内存中所以B线程看到的i一直是0。
- 内存可见性问题解决方案:
1.变量增加volatile
2.CAS操作变量,本质也是用到的volatile(原子类:AtomicInteger、AtomicLong、Atomic*...)
3.synchronized,到这我们都会有个疑问synchronized是jvm级别的一个锁,为什么锁会解决内存可见性问题,等我们分析JMM的时候就清楚了。
java解决多线程同步问题
- 问题现状: 在系统并行执行时经常会遇见某一个操作或者一组操作是线程之间互斥的,例如:队列的入列和出列、栈的压栈 出栈等。这时就需要用到java的锁来实现同步功能。
- 同步问题解决方案:~~~~
1.synchronized 锁实现同步 例子:Hashtable、Vector、StringBuffer
2.Lock 锁实现同步 例子: jdk1.7 ConcurrentHashMap
volatile
volatile简介
volatile主要作用是解决全局变量多线程之间可见性问题,在多线程中一个变全局量被volatile修饰了,一个线程修改了该变量,其它线程会立刻感知到。
volatile实现原理
java代码:
instance = new Singleton(); // instance 是 volatile变量
转变成汇编代码:
0x01a3de1d: movb $0×0,0×1104800(%esi);
0x01a3de24: lock addl $0×0,(%esp);
我们可以看到有volatile修饰的变量进行写的时候会多出第二行汇编代码。我们说一下这个第二行代码lock都干了什么事:
1.Lock前缀指令会引起处理器缓存会写到内存
2.一个处理器的缓存会写到内存会导致其他处理的缓存无效,CPU使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。
synchronized
synchronized简介
synchronized,jvm实现的同步锁,当一个线程执行到synchronized的时,需要先获取到锁,退出或异常时会释放锁。
synchronized三种形态
对于普通同步方法,锁的是当前实例类对象
对于静态同步方法,锁的是当前类的Class对象
对于同步方法快,所得是synchronized括号里面配置的对象。
synchronized实现原理
JVM基于进入和退出monitor对象来实现方法同步和代码块同步,但是两者实现的细节不一线,代码块同步是使用monitorrenters和
monitorexit指令实现的,方法同步是ACC_SYNCHRONIZED标示实现的。下面我们看一下synchronized实现的同步的代码。
java代码:
public class Test {
public synchronized void fun1(){
int i = 1;
}
public void fun2(){
synchronized (this){
try{
int i = 1;
}catch (Exception e){
}
}
}
}
上述java代码 fun1方法是synchronized方法同步的例子,fun2是synchronized代码块同步的例子。我们把这个类变异成class,看一看这个类的字节码信息。
public io.netty.example.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lio/netty/example/Test;
public synchronized void fun1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=1, locals=2, args_size=1
0: iconst_1
1: istore_1
2: return
LineNumberTable:
line 6: 0
line 7: 2
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this Lio/netty/example/Test;
2 1 1 i I
public void fun2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: iconst_1
5: istore_2
6: goto 10
9: astore_2
10: aload_1
11: monitorexit
12: goto 20
15: astore_3
16: aload_1
17: monitorexit
18: aload_3
19: athrow
20: return
Exception table:
from to target type
4 6 9 Class java/lang/Exception
4 12 15 any
15 18 15 any
LineNumberTable:
line 11: 0
line 13: 4
line 16: 6
line 14: 9
line 17: 10
line 19: 20
LocalVariableTable:
Start Length Slot Name Signature
0 21 0 this Lio/netty/example/Test;
StackMapTable: number_of_entries = 4
frame_type = 255 /* full_frame */
offset_delta = 9
locals = [ class io/netty/example/Test, class java/lang/Object ]
stack = [ class java/lang/Exception ]
frame_type = 0 /* same */
frame_type = 68 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
在上面class文件中我们可以看出来
fun1方法同步在flags后面有个标志ACC_SYNCHRONIZED来描述此方法是同步方法需要现获取锁。
fun2方法中我们我可看到在CODE 3 , 11 , 17 处,有个monitorenter,monitorexit ,分别对应 monitorenter 获取锁,11 monitorexit正常退出释放锁,17 monitorexit 异常退出释放锁。
通过上面我们知道了synchronized 方法同步是通过ACC_SYNCHRONIZED 实现获取锁和释放锁,代码块同步是通过 monitorenter,monitorexit两个指令实现获取锁和释放锁。
synchronized的锁是存在java对象头里的。我们来看一下java对象头mark word的结构。
32位虚拟机:
64位虚拟机:
锁标志位:
01:偏向锁或者无锁
00:轻量级锁
10:重量级锁
11:GC标记
是否是偏向锁
0:否
1:是
java对象头mark word的状态变化:
无锁状态:
偏向锁:
轻量锁:
重量锁:
被GC标记过的mark word:
到这我们就应该明白了,synchronized是怎么控制锁的,所有机关都是对象mark word中。判断对象是否有锁先查看mark word里面的信息。 获取锁和释放锁都会修改mark word 信息。
synchronized锁升级
-
偏向锁
简单的讲就是在对象头上记录threadId, 第一次获取锁如果将NULL替换成当前线程的threadId,下次在获取锁的时候发现threadid是一样的就可以直接认为当前线程获取到了锁,忽略了轻量锁和重量锁提高了加锁效率。偏向锁使用了一种等到竞争出现才释放锁的机制,所以其他线程尝试竞争偏向锁是,持有偏向锁的线程才会释放。偏向锁的撤销,需要等待全局安全点(线程暂停,在这个时间点上没有正在执行的字节码)。
偏向锁获取和撤销流程
-
轻量锁、重量级锁
轻量锁:轻量锁是通过CAS把Mark Word更新成指向栈中所记录指针(有心的可以看一下C++的代码 , 获取锁的时候会创建一个BasicLock的对象,Mark Word 记录的就是这个对象的指针),所标记更新成00,更新失败会有一个自旋的过程,通过-XX:PreBlockSpin=10来设置自旋次数,默认是10次。
重量锁:重量级锁是把Mark Word更新成monitor对象指针。通过monitor对象来维护锁的等待和获取。monitor对象关键属性:
_recursions: 重入锁的次数
_owner: 当前持有所得线程
_WaitSet:等待线程组成的双向循环链表,_WaitSet是第一个节点
_cxq: 多线程竞争锁进入时的单向链表
_EntryList: _owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
java底层维护了一个两个队列一个是待获取锁队列一个是等待队列,_owner属性表示当前持有锁的线程。谁先占有_owner属性谁就获取到线程锁,队列响应就会移出去一个节点。锁膨胀流程图:
-
偏向锁、轻量锁、重量锁升级过程
来自网上的一张图讲述了偏向锁到轻量锁再到重量锁全部过程, 很NB分享一下。
Lock
Lock简介
synchronized是JVM实现的锁,Lock是JDK实现的锁,提供了雨synchronized关键字类似的功能,只是需要显示的获取或和释放锁。虽然Lock锁少了隐式获取锁释放锁的便捷性,但是却拥有获取锁与释放锁的操作性、可中断性已经超时获取锁等多种。Lock支持ReentranLock可重入锁,ReentrantReadWriteLock可重入读写锁,以上两个锁都支持公平非公平两种模式。
Lock接口Api
Lock实现原理
AQS 简介
Lock锁实现主要依赖的是AbstarctQueuedSynchronized(简称AQS)同步器是实现的锁的一系列功能,我们来分析一下AQS的主要代码。
AQS->方法概述
可重写方法:
模板方法:
AQS->关键属性
/* 等待队列链表头节点 AQS用的双向链表做的FIFO队列 /
private transient volatile Node head;
/ 等待队列链表尾节点 */
private transient volatile Node tail;
/* 同步状态: 锁没有被线程获取时是0 */
private volatile int state;
AQS->Node属性介绍
final class Node {
/** 标记共享节点 */
static final Node SHARED = new Node();
/** 标记独占节点 */
static final Node EXCLUSIVE = null;
/**
* 节点等待状态
*
* CANCELLED = 1;
* 由于队列中的等待的线程等待超时或者中断,需要从同步队列中取消等待,节点进入该状态不会变化
*
* SIGNAL = -1;
* 后续几点处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后续节点,是后续节点的线程得以运行
*
* CONDITION = -2;
* 节点在等待队列中,节点线程在Condition上,当其他线程对Condition调用了signal方法后,该节点将会从等待队列中转义到同步队列中,加入到对同步状态的获取中
*
* PROPAGATE = -3;
* 表示下一次共享式同步状态获取将会无条件地传播下去
*
* 0 初始状态
*/
volatile int waitStatus;
/**
* 队列上一个节点
*/
volatile Node prev;
/**
* 队列下一个节点
*/
volatile Node next;
/**
* 当前获取节点线程
*/
volatile Thread thread;
/**
* 下一个等待节点
*/
Node nextWaiter;
}
独占锁
- 独占锁状态获取
public final void acquire(int arg) {
// tryAcquire 尝试获取锁如果成功直接退出
// addWaiter 创建节点并添加到队列尾部(EXCLUSIVE 表示独占节点)
// acquireQueued 采用死循环方式获取同步状态,为获取到活执行park线程阻塞
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
addWaiter创建等待节点,添加至等待队列尾部:
/**
* 节点添加至队列尾部 mode: SHARED,EXCLUSIVE 共享独占两种方式
*/
private Node addWaiter(Node mode) {
// 创建节点对象
Node node = new Node(Thread.currentThread(), mode);
// tail 队列尾节点
Node pred = tail;
if (pred != null) {
// 新节点前序节点指向 tail节点
node.prev = pred;
// 通过CAS 方式替换尾部节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// tail为空 或者 CAS 失败循环添加节点到队列尾部
enq(node);
return node;
}
/**
* 循环通过CAS方式添加节点至队列尾部,
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // tail 是空初始队列
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// 通过CAS 方式替换尾部节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued 获取同步状态:
/**
* FIFO队列获取同步状态
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 当前节点的前序节点
final Node p = node.predecessor();
// 如果前序节点是头节点 尝试获取同步状态
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// shouldParkAfterFailedAcquire 通过waitStatus判断线程是否需要阻塞
// parkAndCheckInterrupt 阻塞线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 执行了interrupt 终止线程,撤销获取同步状态
if (failed)
cancelAcquire(node);
}
}
/**
* 通过waitStatus判断线程是否需要阻塞
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前序节点是等待状态 当前节点返回阻塞
if (ws == Node.SIGNAL)
return true;
// 前序节点放弃了获取同步状态继续往前找节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// CAS 替换节点为 等待状态, 如果成功阻塞线程
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
- 独占锁释放
/**
* 释放锁
*/
public final boolean release(int arg) {
// 同步状态 归0
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 初始等待状态 获取下一个节点 并唤醒线程
unparkSuccessor(h);
return true;
}
return false;
}
/**
* 初始等待状态、唤后序节点醒线程
*/
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 等待状态初始为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取下一个节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒阻塞线程
LockSupport.unpark(s.thread);
}
获取共享锁
- 共享锁状态获取
/**
* 创建共享模式等待节点 ,死循环获取同步状态,获取失败阻塞线程
*/
private void doAcquireShared(int arg) {
// 创建等待节点 独占模式是一个方法
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 当前节点前序 节点
final Node p = node.predecessor();
if (p == head) {
// 尝试获取共享锁 大于0 获取成功
int r = tryAcquireShared(arg);
if (r >= 0) {
// 向后传播共享模式同步状态
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// shouldParkAfterFailedAcquire 通过waitStatus判断线程是否需要阻塞
// parkAndCheckInterrupt 阻塞线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- 共享模式同步状态释放
public final boolean releaseShared(int arg) {
// 释放同步状态
if (tryReleaseShared(arg)) {
// 唤醒等待线程
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 等待线程唤醒
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
AQS->公平锁、非公平锁
ReentrantLock来说公平锁和非公平锁。
公平锁:公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
优点:保证时间顺序获取锁,锁获取相对公平,不会有线程饥饿问题 缺点:效率低会频繁的CPU上下文切换
非公平锁: 只要CAS成功就获取到锁,不按照时间顺序获取说
有点:效率高 缺点:会产生线程饥饿
公平锁获取源码分析:
/**
* 公平锁
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
// 直接执行AQS获取同步状态方法、严格按照FIFO顺序获取锁
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 可重入锁 如果当前线程和以获取锁的线程是一个 可以再次获取锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
非公平锁源码分析:
/**
* 非公平锁
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
// 先更新一个同步锁的状态,成功就获取到锁,不成功在行AQS获取锁的方法
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
重入锁
ReentrantLock 可重入锁,用的AQS独占锁。
读写锁
ReentrantReadWriteLock 可重入读写锁。读写锁和可重入锁不同的是:
1.写锁用的是AQS的独占锁,读锁用AQS的共享锁;
2.锁状态区分读写锁:高16位表示读锁, 低16位表示写锁
源码分析一下读锁的实现,写锁是低16位标示的实现和上面的可重入锁基本一致。我们只分析锁的获取即可:
/**
* 尝试获取读锁
*/
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
/**
*exclusiveCount (c & ((1 << SHARED_SHIFT) - 1)) 返回非0标示已经有写锁占用
*getExclusiveOwnerThread() != current 判断已获取读锁的线程和当前线程不是一个线程
*返回-1 获取锁失败
*/
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 锁共享次数
int r = sharedCount(c);
// readerShouldBlock
// 公平锁下:判断当前线程是否需要被阻塞 ,如果队列中有等待返回true
// 非公平锁下: 判断如果head 下一个节点是写锁, 先让写锁执行 避免写锁饥饿
if (!readerShouldBlock() &&
// 必须要小于最大获取锁次数
r < MAX_COUNT &&
// CAS 占用锁状态
compareAndSetState(c, c + SHARED_UNIT)) {
// 到这了就标示已经获取到锁了
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
// 循环获取锁
return fullTryAcquireShared(current);
}
Condition源码分析
Api
Condition 提供了类似Object的wait/notify的方法和Lock配合使用
await线程等待源码分析
ConditionObject 类属性分析:
// 第一个等待节点
private transient Node firstWaiter;
/** 最末尾等待节点 */
private transient Node lastWaiter;
/** 可以看出来Condition,用链表做了一个等待对了。 */
await方法源码
/**
* 持有LOCK线程进入阻塞状态 并释放锁
*/
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 创建等待节点
Node node = addConditionWaiter();
// 释放同步状态 并返回原同步状态值
int savedState = fullyRelease(node);
int interruptMode = 0;
// isOnSyncQueue 节点是否在同步队列中 在同步队列中的不能 wait
while (!isOnSyncQueue(node)) {
// 线程阻塞
LockSupport.park(this);
// 线程恢复 进入同步队列的带获取同步状态
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// acquireQueued 获取同步状态
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) //
// 清除等待队列的垃圾节点
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
signal线程等待源码分析
signal 源码:
public final void signal() {
// 当前线程没有锁 则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
// 真正的唤醒线程
doSignal(first);
}
private void doSignal(Node first) {
do {
//清除第一个等待节点
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// transferForSignal 真正真正的唤醒线程
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
// 节点等待状态 恢复初始状态
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 等待节点添加到同步队列末尾,返回上一个等待节点
Node p = enq(node);
int ws = p.waitStatus;
//ws > 0,同步状态未占用, 可以立即恢复线程
//上一个节点 状态改成等待状态,标示当前节点就是head节点可以立即恢复线程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 恢复线程
LockSupport.unpark(node.thread);
return true;
}
signalall源码:
private void doSignalAll(Node first) {
// 等待队列收尾都值为空
lastWaiter = firstWaiter = null;
do {
// 循环每个等待节点, 请求transferForSignal添加到等待队列或者 直接恢复线程
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
想整理的基本就这么多,大部分是《java并发编程艺术》观后整理的一些东西, 一少部分是自己带着疑问去研究的。