多线程在访问同一个对象中的实例变量时,容易造成最终结果与预期值不一致的问题。
需要注意的是方法内部的私有变量不存在非线程安全问题

synchronized

同步方法

顾名思义是在多线程共同操作的方法上加上synchronized关键字,这是多线程访问就回去争夺对该方法的访问权,一旦获取访问权,其他线程只能等待,直到该线程完成操作。

  1. public class HasSelfPrivateNum {
  2. private int num = 0;
  3. synchronized public void addI(String username) {
  4. try {
  5. if (username.equals("a")) {
  6. num = 100;
  7. System.out.println("a set over!");
  8. Thread.sleep(2000);
  9. } else {
  10. num = 200;
  11. System.out.println("b set over!");
  12. }
  13. System.out.println(username + " num=" + num);
  14. } catch (InterruptedException e) {
  15. // TODO Auto-generated catch block
  16. e.printStackTrace();
  17. }
  18. }
  19. }

注意:如果创建多个线程,但是也同时创建了多个对象,JVM会创建多个锁,这时对上述方法访问不是同步的,如下:

  1. public static void main(String[] args) {
  2. HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
  3. HasSelfPrivateNum numRef = new HasSelfPrivateNum();
  4. ThreadA athread = new ThreadA(numRef1);
  5. athread.start();
  6. ThreadB bthread = new ThreadB(numRef);
  7. bthread.start();
  8. }

结果
image.png

锁的是谁
锁的是对象

从上述情况可见,创建多个线程多个对象访问同一个加了synchronized的方法时,并不是同步的,如果锁的是方法,那么不论创建了多少个对象,都会是排队进入。

加锁与不加锁访问

对同一个类下的两个方法进行访问,其中一个为加了synchronized关键字的,一个不加,下面为示例代码:

  1. public class MyObject {
  2. synchronized public void methodA() {
  3. try {
  4. System.out.println("begin methodA threadName="
  5. + Thread.currentThread().getName());
  6. Thread.sleep(5000);
  7. System.out.println("end endTime=" + System.currentTimeMillis());
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. public void methodB() {
  13. try {
  14. System.out.println("begin methodB threadName="
  15. + Thread.currentThread().getName() + " begin time="
  16. + System.currentTimeMillis());
  17. Thread.sleep(5000);
  18. System.out.println("end");
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. }
  24. public static void main(String[] args) {
  25. MyObject object = new MyObject();
  26. ThreadA a = new ThreadA(object);
  27. a.setName("A");
  28. ThreadB b = new ThreadB(object);
  29. b.setName("B");
  30. a.start();
  31. b.start();
  32. }

结果
image.png
可见对A、B方法的访问为异步。对methodB也加上synchronized,结果如下:
image.png
可见两者同步执行
由此可得到以下结论:

  1. A线程先持有object对象的锁,B线程可以以异步的方式访问object对象中非synchronized类型的方法。
  2. A线程先持有object对象的锁,B线程访问object对象中synchronized类型的方法时需要等待。

锁重入

当一个线程获得一个对象锁后,如果再次请求此对象锁时可以再次获得对象的锁。

  1. public class Service {
  2. synchronized public void service1() {
  3. System.out.println("service1");
  4. service2();
  5. }
  6. synchronized public void service2() {
  7. System.out.println("service2");
  8. service3();
  9. }
  10. synchronized public void service3() {
  11. System.out.println("service3");
  12. }
  13. }

注意:

  1. 可重入锁也支持在父子继承环境
  2. 一个线程出现异常时,持有锁会自动释放
  3. 防止出现无限等待的问题。

死锁诊断:

  1. cmd进入jdk的bin目录运行jps
  2. 执行jstack -l Run线程的id可见。

    同步代码块

    多线程访问时,如果在方法上加锁,会产生等待时间长的弊端,这时可以通过对代码块进行加锁的方式提高访问效率,如果所有线程也都访问这块代码也会进行等待。

    1. public class ObjectService {
    2. public void serviceMethod() {
    3. try {
    4. synchronized (this) {
    5. System.out.println("begin time=" + System.currentTimeMillis());
    6. Thread.sleep(2000);
    7. System.out.println("end end=" + System.currentTimeMillis());
    8. }
    9. } catch (InterruptedException e) {
    10. e.printStackTrace();
    11. }
    12. }
    13. }

    同在方法上加锁类似,当一个线程访问加锁的代码块时,另一个线程仍然可以访问未加锁的代码。

但是,当一个线程访问object对象中A方法的synchronized代码块时,另一个线程访问object对象中B方法的synchronized代码块也会被阻塞,说明synchronized使用的对象监视器是一个。如下:

  1. public class ObjectService {
  2. public void serviceMethodA() {
  3. try {
  4. synchronized (this) {
  5. System.out.println("A begin time=" + System.currentTimeMillis());
  6. Thread.sleep(2000);
  7. System.out.println("A end end=" + System.currentTimeMillis());
  8. }
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. }
  13. public void serviceMethodB() {
  14. synchronized (this) {
  15. System.out.println("B begin time=" + System.currentTimeMillis());
  16. System.out.println("B end end=" + System.currentTimeMillis());
  17. }
  18. }
  19. }
  20. public static void main(String[] args) {
  21. ObjectService service = new ObjectService();
  22. ThreadA a = new ThreadA(service);
  23. a.setName("a");
  24. a.start();
  25. ThreadB b = new ThreadB(service);
  26. b.setName("b");
  27. b.start();
  28. }

结果:
image.png
可见:synchronized(this)锁住的是当前对象(同方法加锁一致)

静态同步与synchronized(class)代码块

关键字synchronized放在static方法上时相当于对Class加锁,与放在普通方法上不同,放在普通方法上是给对象加锁

  1. public class Service {
  2. synchronized public static void printA() {
  3. try {
  4. System.out.println("进入printA");
  5. Thread.sleep(3000);
  6. System.out.println("离开printA");
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. }
  11. synchronized public static void printB() {
  12. System.out.println("进入printB");
  13. System.out.println("离开printB");
  14. }
  15. synchronized public void printC() {
  16. System.out.println("进入printC");
  17. System.out.println("离开printC");
  18. }
  19. }
  20. public static void main(String[] args) {
  21. Service service = new Service();
  22. ThreadA a = new ThreadA(service);
  23. a.setName("A");
  24. a.start();
  25. ThreadB b = new ThreadB(service);
  26. b.setName("B");
  27. b.start();
  28. ThreadC c = new ThreadC(service);
  29. c.setName("C");
  30. c.start();
  31. }

结果:
image.png
异步的原因是持有不同的锁,一个为对象锁,其他为Class锁,而Class锁可以对类的所有对象实例起作用。如果上述代码没有C方法,则A,B同步执行。

同步非this对象

同锁住代码块类似,但此时锁住的代码块与同步方法之间是异步,不与其他锁this同步方法争夺this锁,提升效率。
注意:当为synchronized(String)时注意字符串常量池缓存,不能直接传入字符串,应该通过new Object()的方式传入。

synchronized同步实现

参考:hhttps://www.jianshu.com/p/e62fa839aa41

都依赖JVM,对一段synchronized(this)代码反编译得到
image.png

  1. monitorenter

每个对象都是一个监视器锁(monitor),当monitor被占用时就处于锁定状态,线程加锁的过程如下:

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
  1. monitorexit

指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取。

方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步

monitor数据结构

位于HotSpot虚拟机源码ObjectMonitor.hpp文件

  1. ObjectMonitor() {
  2. _header = NULL;
  3. _count = 0; // 记录个数
  4. _waiters = 0,
  5. _recursions = 0;
  6. _object = NULL;
  7. _owner = NULL;
  8. _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
  9. _WaitSetLock = 0 ;
  10. _Responsible = NULL ;
  11. _succ = NULL ;
  12. _cxq = NULL ;
  13. FreeNext = NULL ;
  14. _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
  15. _SpinFreq = 0 ;
  16. _SpinClock = 0 ;
  17. OwnerIsThread = 0 ;
  18. }

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

  1. 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
  2. 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
  3. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);

同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。
image.png

volatile

作用:强制从公共堆栈中取得变量值,不是从线程私有数据栈中取值。
image.png
volatile的使用增加了实例变量在多个线程之间的可见性,但其不支持原子性
关键字synchronized与volatile区别:

  1. volatile是线程同步的轻量级实现,所以其性能比synchronized好,但volatile只能修饰变量,synchronized可以修饰方法、代码块。
  2. 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
  3. volatile保证数据可见性,但不保证原子性;synchronized保证数据原子性,简介保证可见性,它会将私有内存和公共内存做同步。

使用volatile出现非线程安全原因:
image.png

  1. read和load阶段:从主内存复制变量到当前线程工作内存
  2. use和asign阶段:修改共享变量的值
  3. 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的写入对所有处理器可见。

底层实现

如下:

  1. public class VolatileDemo {
  2. public static volatile int count = 1;
  3. public static void main(String[] args) {
  4. count = 2;
  5. System.out.println(count);
  6. }
  7. }

字节码查看:

编译成class文件:javac VolatileDemo.java 反编译查看字节码:javap -v VolatileDemo.class

  1. {
  2. public static volatile int count;
  3. descriptor: I
  4. flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE
  5. public com.hand.todo.jobs.VolatileDemo();
  6. descriptor: ()V
  7. flags: ACC_PUBLIC
  8. Code:
  9. stack=1, locals=1, args_size=1
  10. 0: aload_0
  11. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  12. 4: return
  13. LineNumberTable:
  14. line 10: 0
  15. public static void main(java.lang.String[]);
  16. descriptor: ([Ljava/lang/String;)V
  17. flags: ACC_PUBLIC, ACC_STATIC
  18. Code:
  19. stack=2, locals=1, args_size=1
  20. 0: iconst_2
  21. 1: putstatic #2 // Field count:I
  22. 4: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
  23. 7: getstatic #2 // Field count:I
  24. 10: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
  25. 13: return
  26. LineNumberTable:
  27. line 14: 0
  28. line 15: 4
  29. line 16: 13
  30. static {};
  31. descriptor: ()V
  32. flags: ACC_STATIC
  33. Code:
  34. stack=1, locals=0, args_size=0
  35. 0: iconst_1
  36. 1: putstatic #2 // Field count:I
  37. 4: return
  38. LineNumberTable:
  39. line 11: 0
  40. }

可见修饰count的三个关键字在字节码层面上的访问标识:ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE。其中,volatile的访问标识为ACC_VOLATILE,后续操作变量时判断该标识并决定是否遵循对应语义处理。

Lock

该锁不是Java关键字,而是通过代码完成,其中最重要的是AQS和unsafe
以一个小李子进去

  1. public static void main(String[] args) {
  2. ReentrantLock lock = new ReentrantLock();
  3. lock.tryLock();
  4. lock.lock();
  5. try {
  6. System.out.println("aa");
  7. } catch (Exception e) {
  8. e.printStackTrace();
  9. } finally {
  10. lock.unlock();
  11. }
  12. }

tryLock

进入源码

  1. public boolean tryLock() {
  2. return sync.nonfairTryAcquire(1);
  3. }
  4. final boolean nonfairTryAcquire(int acquires) {
  5. final Thread current = Thread.currentThread();
  6. int c = getState();
  7. if (c == 0) {
  8. if (compareAndSetState(0, acquires)) {
  9. setExclusiveOwnerThread(current);
  10. return true;
  11. }
  12. }
  13. else if (current == getExclusiveOwnerThread()) {
  14. int nextc = c + acquires;
  15. if (nextc < 0) // overflow
  16. throw new Error("Maximum lock count exceeded");
  17. setState(nextc);
  18. return true;
  19. }
  20. return false;
  21. }

从代码中可以引出两个重要的类AbstractOwnableSynchronizerAbstractQueuedSynchronizer
AbstractOwnableSynchronizer:创建锁和同步的基础,用来维护独占模式同步的当前所有者(线程)

  1. public abstract class AbstractOwnableSynchronizer
  2. implements java.io.Serializable {
  3. private static final long serialVersionUID = 3737899427754241961L;
  4. protected AbstractOwnableSynchronizer() { }
  5. /**
  6. * The current owner of exclusive mode synchronization.
  7. */
  8. private transient Thread exclusiveOwnerThread;
  9. protected final void setExclusiveOwnerThread(Thread thread) {
  10. exclusiveOwnerThread = thread;
  11. }
  12. protected final Thread getExclusiveOwnerThread() {
  13. return exclusiveOwnerThread;
  14. }
  15. }

AbstractQueuedSynchronizer:这个就比较牛逼了,是加锁实现类,并且配合Unsafe类食用,先看一下定义的基础量

  1. static final class Node {
  2. /** Marker to indicate a node is waiting in shared mode */
  3. static final Node SHARED = new Node();
  4. /** Marker to indicate a node is waiting in exclusive mode */
  5. static final Node EXCLUSIVE = null;
  6. /** 线程已取消 */
  7. static final int CANCELLED = 1;
  8. /** 后继线程需要取消驻留 */
  9. static final int SIGNAL = -1;
  10. /** 线程正在等待条件 */
  11. static final int CONDITION = -2;
  12. /** 下一个 acquireShared 应该无条件传播 */
  13. static final int PROPAGATE = -3;
  14. /** 节点等待状态 */
  15. volatile int waitStatus;
  16. /** 链接到当前节点/线程用于检查 waitStatus 的前驱节点。
  17. 在入队期间分配,并且仅在出队时清空(为了 GC)。
  18. 此外,在取消前任时,我们会在找到未取消的前任时进行短路,这将始终存在,
  19. 因为头节点永远不会被取消:节点只有在成功获取后才成为头。被取消的线程永远不会成功获取,
  20. 线程只会取消自己,不会取消任何其他节点。
  21. */
  22. volatile Node prev;
  23. /** 后继节点 */
  24. volatile Node next;
  25. /**
  26. * 使该节点入队的线程。在构造时初始化并在使用后归零
  27. */
  28. volatile Thread thread;
  29. Node nextWaiter;
  30. }
  31. private transient volatile Node head;
  32. /**
  33. * 等待队列的尾部,延迟初始化。仅通过方法 enq 修改以添加新的等待节点
  34. */
  35. private transient volatile Node tail;
  36. /*
  37. 为0表示没有被占用
  38. */
  39. private volatile int state;

从以上构造可见,锁的存储结构是双向链表+状态标识,而且变量都是被transientvolatile修饰。
再通过查看源码:该锁分为公平锁非公平锁,默认为非公平锁。
再次回到tryLock源码,

  1. final boolean nonfairTryAcquire(int acquires) {
  2. final Thread current = Thread.currentThread();
  3. int c = getState();
  4. if (c == 0) {
  5. if (compareAndSetState(0, acquires)) {
  6. setExclusiveOwnerThread(current);
  7. return true;
  8. }
  9. }
  10. else if (current == getExclusiveOwnerThread()) {
  11. int nextc = c + acquires;
  12. if (nextc < 0) // overflow
  13. throw new Error("Maximum lock count exceeded");
  14. setState(nextc);
  15. return true;
  16. }
  17. return false;
  18. }

通过CAS设置加锁状态值(state+1),并将当前线程放进AOS,如果已经加锁而且是当前线程,则再次修改状态值(锁重入);同理可知,释放锁时state-1,并清除AOS中的本线程,以便后续线程可得到锁。

lock.lock

  1. public void lock() {
  2. sync.lock();
  3. }
  4. // 进去非公平实现
  5. final void lock() {
  6. if (compareAndSetState(0, 1))
  7. setExclusiveOwnerThread(Thread.currentThread());
  8. else
  9. acquire(1);
  10. }
  11. // 进入acquire
  12. // tryAcquire:会尝试再次通过CAS获取一次锁。
  13. // addWaiter:将当前线程加入上面锁的双向链表(等待队列)中
  14. // acquireQueued:通过自旋,判断当前队列节点是否可以获取锁
  15. public final void acquire(int arg) {
  16. if (!tryAcquire(arg) &&
  17. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  18. selfInterrupt();
  19. }

addWaiter

添加当前线程到等待链表中
将当前线程加入到等待链表的尾部

  1. private Node addWaiter(Node mode) {
  2. Node node = new Node(Thread.currentThread(), mode);
  3. // Try the fast path of enq; backup to full enq on failure
  4. Node pred = tail;
  5. if (pred != null) {
  6. node.prev = pred;
  7. if (compareAndSetTail(pred, node)) {
  8. pred.next = node;
  9. return node;
  10. }
  11. }
  12. enq(node);
  13. return node;
  14. }

acquireQueued

  1. final boolean acquireQueued(final Node node, int arg) {
  2. boolean failed = true;
  3. try {
  4. boolean interrupted = false;
  5. for (;;) {
  6. final Node p = node.predecessor();
  7. if (p == head && tryAcquire(arg)) {
  8. setHead(node);
  9. p.next = null; // help GC
  10. failed = false;
  11. return interrupted;
  12. }
  13. if (shouldParkAfterFailedAcquire(p, node) &&
  14. parkAndCheckInterrupt())
  15. interrupted = true;
  16. }
  17. } finally {
  18. if (failed)
  19. cancelAcquire(node);
  20. }
  21. }

可以看到,当当前线程到头部的时候,尝试CAS更新锁状态,如果更新成功表示该等待线程获取成功。从头部移除。
该流程如下:
image.png

unlock

点进源码

  1. public final boolean release(int arg) {
  2. if (tryRelease(arg)) {
  3. Node h = head;
  4. if (h != null && h.waitStatus != 0)
  5. unparkSuccessor(h);
  6. return true;
  7. }
  8. return false;
  9. }

点进NonfairSync的tryRelease

  1. protected final boolean tryRelease(int releases) {
  2. int c = getState() - releases;
  3. if (Thread.currentThread() != getExclusiveOwnerThread())
  4. throw new IllegalMonitorStateException();
  5. boolean free = false;
  6. if (c == 0) {
  7. free = true;
  8. setExclusiveOwnerThread(null);
  9. }
  10. setState(c);
  11. return free;
  12. }

基本可以确认,释放锁就是对AQS中的状态值State进行修改。同时更新下一个链表中的线程等待节点。

总结

  • lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
  • lock获取锁的过程:本质上是通过CAS来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
  • lock释放锁的过程:修改状态值,调整等待链表。
  • 可以看到在整个实现过程中,lock大量使用CAS+自旋。因此根据CAS特性,lock建议使用在低锁冲突的情况下。目前官方对synchronized做了大量的锁优化(偏向锁、自旋、轻量级锁)。因此在非必要的情况下,建议使用synchronized做同步操作。