多线程在访问同一个对象中的实例变量时,容易造成最终结果与预期值不一致的问题。
需要注意的是方法内部的私有变量不存在非线程安全问题。
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 blocke.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: Iflags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILEpublic com.hand.todo.jobs.VolatileDemo();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 10: 0public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: iconst_21: putstatic #2 // Field count:I4: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;7: getstatic #2 // Field count:I10: invokevirtual #4 // Method java/io/PrintStream.println:(I)V13: returnLineNumberTable:line 14: 0line 15: 4line 16: 13static {};descriptor: ()Vflags: ACC_STATICCode:stack=1, locals=0, args_size=00: iconst_11: putstatic #2 // Field count:I4: returnLineNumberTable: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) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
从代码中可以引出两个重要的类AbstractOwnableSynchronizer和AbstractQueuedSynchronizer
AbstractOwnableSynchronizer:创建锁和同步的基础,用来维护独占模式同步的当前所有者(线程)
public abstract class AbstractOwnableSynchronizerimplements 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) // overflowthrow 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());elseacquire(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 failureNode 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 GCfailed = 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做同步操作。
