多线程在访问同一个对象中的实例变量时,容易造成最终结果与预期值不一致的问题。
需要注意的是方法内部的私有变量不存在非线程安全问题
。
synchronized
同步方法
顾名思义是在多线程共同操作的方法上加上synchronized
关键字,这是多线程访问就回去争夺对该方法的访问权,一旦获取访问权,其他线程只能等待,直到该线程完成操作。
public class HasSelfPrivateNum {
private int num = 0;
synchronized public void addI(String username) {
try {
if (username.equals("a")) {
num = 100;
System.out.println("a set over!");
Thread.sleep(2000);
} else {
num = 200;
System.out.println("b set over!");
}
System.out.println(username + " num=" + num);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
注意:如果创建多个线程,但是也同时创建了多个对象,JVM会创建多个锁,这时对上述方法访问不是同步的,如下:
public static void main(String[] args) {
HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
HasSelfPrivateNum numRef = new HasSelfPrivateNum();
ThreadA athread = new ThreadA(numRef1);
athread.start();
ThreadB bthread = new ThreadB(numRef);
bthread.start();
}
锁的是谁
锁的是对象
从上述情况可见,创建多个线程多个对象访问同一个加了synchronized
的方法时,并不是同步的,如果锁的是方法,那么不论创建了多少个对象,都会是排队进入。
加锁与不加锁访问
对同一个类下的两个方法进行访问,其中一个为加了synchronized
关键字的,一个不加,下面为示例代码:
public class MyObject {
synchronized public void methodA() {
try {
System.out.println("begin methodA threadName="
+ Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("end endTime=" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void methodB() {
try {
System.out.println("begin methodB threadName="
+ Thread.currentThread().getName() + " begin time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyObject object = new MyObject();
ThreadA a = new ThreadA(object);
a.setName("A");
ThreadB b = new ThreadB(object);
b.setName("B");
a.start();
b.start();
}
结果
可见对A、B方法的访问为异步。对methodB也加上synchronized
,结果如下:
可见两者同步执行
由此可得到以下结论:
- A线程先持有object对象的锁,B线程可以以异步的方式访问object对象中非
synchronized
类型的方法。 - A线程先持有object对象的锁,B线程访问object对象中
synchronized
类型的方法时需要等待。
锁重入
当一个线程获得一个对象锁后,如果再次请求此对象锁
时可以再次获得对象的锁。
public class Service {
synchronized public void service1() {
System.out.println("service1");
service2();
}
synchronized public void service2() {
System.out.println("service2");
service3();
}
synchronized public void service3() {
System.out.println("service3");
}
}
注意:
- 可重入锁也支持在父子继承环境
- 一个线程出现异常时,持有锁会自动释放
- 防止出现无限等待的问题。
死锁诊断:
- cmd进入jdk的bin目录运行
jps
-
同步代码块
多线程访问时,如果在方法上加锁,会产生等待时间长的弊端,这时可以通过对代码块进行加锁的方式提高访问效率,如果所有线程也都访问这块代码也会进行等待。
public class ObjectService {
public void serviceMethod() {
try {
synchronized (this) {
System.out.println("begin time=" + System.currentTimeMillis());
Thread.sleep(2000);
System.out.println("end end=" + System.currentTimeMillis());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
同在方法上加锁类似,当一个线程访问加锁的代码块时,另一个线程仍然可以访问未加锁的代码。
但是,当一个线程访问object对象中A方法的synchronized
代码块时,另一个线程访问object对象中B方法的synchronized
代码块也会被阻塞,说明synchronized
使用的对象监视器
是一个。如下:
public class ObjectService {
public void serviceMethodA() {
try {
synchronized (this) {
System.out.println("A begin time=" + System.currentTimeMillis());
Thread.sleep(2000);
System.out.println("A end end=" + System.currentTimeMillis());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void serviceMethodB() {
synchronized (this) {
System.out.println("B begin time=" + System.currentTimeMillis());
System.out.println("B end end=" + System.currentTimeMillis());
}
}
}
public static void main(String[] args) {
ObjectService service = new ObjectService();
ThreadA a = new ThreadA(service);
a.setName("a");
a.start();
ThreadB b = new ThreadB(service);
b.setName("b");
b.start();
}
结果:
可见:synchronized(this)锁住的是当前对象(同方法加锁一致)
静态同步与synchronized(class)代码块
关键字synchronized
放在static方法上时相当于对Class加锁,与放在普通方法上不同,放在普通方法上是给对象
加锁
public class Service {
synchronized public static void printA() {
try {
System.out.println("进入printA");
Thread.sleep(3000);
System.out.println("离开printA");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized public static void printB() {
System.out.println("进入printB");
System.out.println("离开printB");
}
synchronized public void printC() {
System.out.println("进入printC");
System.out.println("离开printC");
}
}
public static void main(String[] args) {
Service service = new Service();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
ThreadC c = new ThreadC(service);
c.setName("C");
c.start();
}
结果:
异步的原因是持有不同的锁,一个为对象锁,其他为Class锁,而Class锁可以对类的所有对象实例起作用。如果上述代码没有C方法,则A,B同步执行。
同步非this对象
同锁住代码块类似,但此时锁住的代码块与同步方法之间是异步,不与其他锁this同步方法争夺this锁,提升效率。
注意:当为synchronized(String)
时注意字符串常量池缓存,不能直接传入字符串,应该通过new Object()
的方式传入。
synchronized同步实现
参考:hhttps://www.jianshu.com/p/e62fa839aa41
都依赖JVM,对一段synchronized(this)
代码反编译得到
- monitorenter
每个对象都是一个监视器锁(monitor),当monitor被占用时就处于锁定状态,线程加锁的过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
- monitorexit
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取。
方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步
monitor数据结构
(位于HotSpot虚拟机源码ObjectMonitor.hpp文件)
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:
- 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
- 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
- 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。
volatile
作用:强制从公共堆栈中取得变量值,不是从线程私有数据栈中取值。
volatile的使用增加了实例变量在多个线程之间的可见性
,但其不支持原子性
关键字synchronized与volatile区别:
- volatile是线程同步的轻量级实现,所以其性能比synchronized好,但volatile只能修饰变量,synchronized可以修饰方法、代码块。
- 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
- volatile保证数据可见性,但不保证原子性;synchronized保证数据原子性,简介保证可见性,它会将私有内存和公共内存做同步。
使用volatile出现非线程安全原因:
- read和load阶段:从主内存复制变量到当前线程工作内存
- use和asign阶段:修改共享变量的值
- store和write阶段:工作内存数据刷新主内存对应变量的值。
多线程环境中,use和asign多次出现,但不是原子性。在read和load之后,如果主内存中的共享变量发生改变,线程中的值由于已经加载,不会产生变化,这就产生了私有内存和公共内存中的数据不同步。对于volatile修饰的变量,JVM只能保证其加载的数据是最新的。
禁止指令重排
volatile
是通过编译器在生成字节码时,在指令序列中添加“内存屏障”来禁止指令重排序的。
硬件层面的“内存屏障”:
- sfence:即写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见
- lfence:即读屏障(Load Barrier),在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据,以保证读取的是最新的数据。
- mfence:即全能屏障(modify/mix Barrier ),兼具sfence和lfence的功能
- lock 前缀:lock不是内存屏障,而是一种锁。执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。
JMM层面的“内存屏障”:
- LoadLoad屏障: 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障: 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
底层实现
如下:
public class VolatileDemo {
public static volatile int count = 1;
public static void main(String[] args) {
count = 2;
System.out.println(count);
}
}
字节码查看:
编译成class文件:javac VolatileDemo.java 反编译查看字节码:javap -v VolatileDemo.class
{
public static volatile int count;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE
public com.hand.todo.jobs.VolatileDemo();
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 10: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: iconst_2
1: putstatic #2 // Field count:I
4: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
7: getstatic #2 // Field count:I
10: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
13: return
LineNumberTable:
line 14: 0
line 15: 4
line 16: 13
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #2 // Field count:I
4: return
LineNumberTable:
line 11: 0
}
可见修饰count的三个关键字在字节码层面上的访问标识:ACC_PUBLIC
, ACC_STATIC
, ACC_VOLATILE
。其中,volatile的访问标识为ACC_VOLATILE
,后续操作变量时判断该标识并决定是否遵循对应语义处理。
Lock
该锁不是Java关键字,而是通过代码完成,其中最重要的是AQS和unsafe
以一个小李子进去
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
lock.tryLock();
lock.lock();
try {
System.out.println("aa");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
tryLock
进入源码
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
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;
}
从代码中可以引出两个重要的类AbstractOwnableSynchronizer
和AbstractQueuedSynchronizer
AbstractOwnableSynchronizer:创建锁和同步的基础,用来维护独占模式同步的当前所有者(线程)
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 3737899427754241961L;
protected AbstractOwnableSynchronizer() { }
/**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
AbstractQueuedSynchronizer:这个就比较牛逼了,是加锁实现类,并且配合Unsafe
类食用,先看一下定义的基础量
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** 线程已取消 */
static final int CANCELLED = 1;
/** 后继线程需要取消驻留 */
static final int SIGNAL = -1;
/** 线程正在等待条件 */
static final int CONDITION = -2;
/** 下一个 acquireShared 应该无条件传播 */
static final int PROPAGATE = -3;
/** 节点等待状态 */
volatile int waitStatus;
/** 链接到当前节点/线程用于检查 waitStatus 的前驱节点。
在入队期间分配,并且仅在出队时清空(为了 GC)。
此外,在取消前任时,我们会在找到未取消的前任时进行短路,这将始终存在,
因为头节点永远不会被取消:节点只有在成功获取后才成为头。被取消的线程永远不会成功获取,
线程只会取消自己,不会取消任何其他节点。
*/
volatile Node prev;
/** 后继节点 */
volatile Node next;
/**
* 使该节点入队的线程。在构造时初始化并在使用后归零
*/
volatile Thread thread;
Node nextWaiter;
}
private transient volatile Node head;
/**
* 等待队列的尾部,延迟初始化。仅通过方法 enq 修改以添加新的等待节点
*/
private transient volatile Node tail;
/*
为0表示没有被占用
*/
private volatile int state;
从以上构造可见,锁的存储结构是双向链表+状态标识
,而且变量都是被transient
和volatile
修饰。
再通过查看源码:该锁分为公平锁
和非公平锁
,默认为非公平锁。
再次回到tryLock源码,
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;
}
通过CAS
设置加锁状态值(state+1
),并将当前线程放进AOS
,如果已经加锁而且是当前线程,则再次修改状态值(锁重入);同理可知,释放锁时state-1
,并清除AOS
中的本线程,以便后续线程可得到锁。
lock.lock
public void lock() {
sync.lock();
}
// 进去非公平实现
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 进入acquire
// tryAcquire:会尝试再次通过CAS获取一次锁。
// addWaiter:将当前线程加入上面锁的双向链表(等待队列)中
// acquireQueued:通过自旋,判断当前队列节点是否可以获取锁
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
addWaiter
添加当前线程到等待链表中
将当前线程加入到等待链表的尾部
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
acquireQueued
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;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
可以看到,当当前线程到头部的时候,尝试CAS更新锁状态,如果更新成功表示该等待线程获取成功。从头部移除。
该流程如下:
unlock
点进源码
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
点进NonfairSync的tryRelease
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
基本可以确认,释放锁就是对AQS中的状态值State进行修改。同时更新下一个链表中的线程等待节点。
总结
- lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
- lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
- lock释放锁的过程:修改状态值,调整等待链表。
- 可以看到在整个实现过程中,lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。目前官方对synchronized做了大量的锁优化(偏向锁、自旋、轻量级锁)。因此在非必要的情况下,建议使用synchronized做同步操作。