|
5 | 5 |
|
6 | 6 |
|
7 | 7 | #### [1.进程与线程的区别是什么?](#进程与线程的区别是什么?) |
8 | | - |
9 | | -#### [2.Java中单例有哪些写法?](#Java中单例有哪些写法?) |
10 | | - |
11 | | -#### [3.Java中创建线程有哪些方式?](#Java中创建线程有哪些方式?) |
12 | | - |
13 | | -#### [4.如何解决序列化时可以创建出单例对象的问题?](#如何解决序列化时可以创建出单例对象的问题?) |
14 | | -#### [5.悲观锁和乐观锁是什么?](#悲观锁和乐观锁是什么?) |
15 | | - |
16 | | -#### [6.volatile 关键字有什么用?怎么理解可见性,一般什么场景去用可见性?](#volatile 关键字有什么用) |
| 8 | +#### [2.进程间如何通信?](#进程间如何通信?) |
| 9 | +#### [3.Java中单例有哪些写法?](#Java中单例有哪些写法?) |
| 10 | +#### [4.Java中创建线程有哪些方式?](#Java中创建线程有哪些方式?) |
| 11 | +#### [5.如何解决序列化时可以创建出单例对象的问题?](#如何解决序列化时可以创建出单例对象的问题?) |
| 12 | +#### [6.悲观锁和乐观锁是什么?](#悲观锁和乐观锁是什么?) |
| 13 | +#### [7.volatile 关键字有什么用?怎么理解可见性,一般什么场景去用可见性?](#volatile 关键字有什么用?怎么理解可见性,一般什么场景去用可见性?) |
| 14 | +#### [8.sychronize的实现原理是怎么样的?](#sychronize的实现原理是怎么样的?) |
17 | 15 |
|
18 | 16 |
|
19 | 17 |
|
@@ -564,4 +562,131 @@ instance = new Singleton(); // instance是volatile变量 |
564 | 562 |
|
565 | 563 | 用于做状态位标志,如果多个线程去需要根据一个状态位来执行一些操作,使用volatile修饰可以保证内存可见性。 |
566 | 564 |
|
567 | | -用于单例模式用于保证内存可见性,以及防止指令重排序。 |
| 565 | +用于单例模式用于保证内存可见性,以及防止指令重排序。 |
| 566 | + |
| 567 | +### sychronize的实现原理是怎么样的? |
| 568 | + |
| 569 | +```java |
| 570 | +public class SyncTest { |
| 571 | + public void syncBlock(){ |
| 572 | + synchronized (this){ |
| 573 | + System.out.println("hello block"); |
| 574 | + } |
| 575 | + } |
| 576 | + public synchronized void syncMethod(){ |
| 577 | + System.out.println("hello method"); |
| 578 | + } |
| 579 | +} |
| 580 | +``` |
| 581 | + |
| 582 | +当SyncTest.java被编译成class文件的时候,`synchronized`关键字和`synchronized`方法的字节码略有不同,我们可以用`javap -v` 命令查看class文件对应的JVM字节码信息,部分信息如下: |
| 583 | + |
| 584 | +```java |
| 585 | +{ |
| 586 | + public void syncBlock(); |
| 587 | + descriptor: ()V |
| 588 | + flags: ACC_PUBLIC |
| 589 | + Code: |
| 590 | + stack=2, locals=3, args_size=1 |
| 591 | + 0: aload_0 |
| 592 | + 1: dup |
| 593 | + 2: astore_1 |
| 594 | + 3: monitorenter // monitorenter指令进入同步块 |
| 595 | + 4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; |
| 596 | + 7: ldc #3 // String hello block |
| 597 | + 9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V |
| 598 | + 12: aload_1 |
| 599 | + 13: monitorexit // monitorexit指令退出同步块 |
| 600 | + 14: goto 22 |
| 601 | + 17: astore_2 |
| 602 | + 18: aload_1 |
| 603 | + 19: monitorexit // monitorexit指令退出同步块 |
| 604 | + 20: aload_2 |
| 605 | + 21: athrow |
| 606 | + 22: return |
| 607 | + Exception table: |
| 608 | + from to target type |
| 609 | + 4 14 17 any |
| 610 | + 17 20 17 any |
| 611 | + |
| 612 | + |
| 613 | + public synchronized void syncMethod(); |
| 614 | + descriptor: ()V |
| 615 | + flags: ACC_PUBLIC, ACC_SYNCHRONIZED //添加了ACC_SYNCHRONIZED标记 |
| 616 | + Code: |
| 617 | + stack=2, locals=1, args_size=1 |
| 618 | + 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; |
| 619 | + 3: ldc #5 // String hello method |
| 620 | + 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V |
| 621 | + 8: return |
| 622 | + |
| 623 | +} |
| 624 | +``` |
| 625 | + |
| 626 | +对于`synchronized`关键字而言,`javac`在编译时,会生成对应的`monitorenter`和`monitorexit`指令分别对应`synchronized`同步块的进入和退出,有两个`monitorexit`指令的原因是为了保证抛异常的情况下也能释放锁,所以`javac`为同步代码块添加了一个隐式的try-finally,在finally中会调用`monitorexit`命令释放锁。 |
| 627 | + |
| 628 | +而对于`synchronized`方法而言,`javac`为其生成了一个`ACC_SYNCHRONIZED`关键字,在JVM进行方法调用时,发现调用的方法被`ACC_SYNCHRONIZED`修饰,则会先尝试获得锁。 |
| 629 | + |
| 630 | +#### 锁 |
| 631 | + |
| 632 | +这是网上看到的一个流程图: |
| 633 | + |
| 634 | + |
| 635 | + |
| 636 | +就是Java对象的内存布局其实由对象头+实例数据+对齐填充三部分组成,而对象头主要包含Mark Word+指向对象所属的类的指针组成。Mark Word主要用于存储对象自身的运行时数据,哈希码,GC分代年龄,锁标志等。 |
| 637 | + |
| 638 | +下面就是Mark Word的数据映射表 |
| 639 | + |
| 640 | + |
| 641 | + |
| 642 | +##### 偏向锁 |
| 643 | + |
| 644 | +根据上面的表来看,Mark Word后三位为101时,加锁对象的状态为偏向锁,偏向锁的意义在于同一个线程访问sychronize代码块时不需要进行加锁,解锁操作,性能开销更低(HotSpot[1]的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。) |
| 645 | + |
| 646 | +因为正常情况下,当一个线程访问同步块并获取轻量级锁时,需要进行CAS操作将对象头的锁记录里指向当前线程的栈中的锁记录,执行完毕后需要释放轻量级锁。如果是同一个线程多次访问sychronize代码块,多次获取和释放轻量级,开销会偏大,所以会一开始判断对象是无锁状态,会将对象头设置为偏向锁,并且这个的线程ID到Mark Word,后续同一线程判断加锁标志是偏向锁,并且线程ID一致就可以直接执行。 |
| 647 | + |
| 648 | + |
| 649 | + |
| 650 | +#### 轻量级锁 |
| 651 | + |
| 652 | +JVM的开发者发现在很多情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。 |
| 653 | + |
| 654 | +线程在执行同步块之前,JVM会先在当前的线程的栈帧中创建一个`Lock Record`,其包括一个用于存储对象头中的 `mark word`(官方称之为`Displaced Mark Word`)以及一个指向对象的指针。下图右边的部分就是一个`Lock Record`。 |
| 655 | + |
| 656 | + |
| 657 | + |
| 658 | +(1)轻量级锁加锁 |
| 659 | + |
| 660 | +线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并 将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁,自旋获取锁失败的次数达到一定次数后就会进行锁升级,将锁升级为重量级锁,当前线程就会被阻塞,直到获得轻量级锁的线程执行完毕,释放锁,唤醒阻塞的线程。 |
| 661 | + |
| 662 | +(2)轻量级锁解锁 |
| 663 | + |
| 664 | +轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成 功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。图2-2是 两个线程同时争夺锁,导致锁膨胀的流程图。 |
| 665 | + |
| 666 | + |
| 667 | + |
| 668 | +### 重量级锁 |
| 669 | + |
| 670 | +重量级锁是我们常说的传统意义上的锁,其利用操作系统底层的同步机制去实现Java中的线程同步。 |
| 671 | + |
| 672 | +重量级锁的状态下,对象的`mark word`为指向一个堆中monitor对象的指针。 |
| 673 | + |
| 674 | +一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。 |
| 675 | + |
| 676 | +其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。 |
| 677 | + |
| 678 | + |
| 679 | + |
| 680 | +当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到cxq的队列尾部,然后暂停当前线程。当持有锁的线程释放锁前,会将cxq中的所有元素移动到EntryList中去,并唤醒EntryList的队首线程。 |
| 681 | + |
| 682 | +如果一个线程在同步块中调用了`Object#wait`方法,会将该线程对应的ObjectWaiter从EntryList移除并加入到WaitSet中,然后释放锁。当wait的线程被notify之后,会将对应的ObjectWaiter从WaitSet移动到EntryList中。 |
| 683 | + |
| 684 | +#### 三种锁的优缺点对比 |
| 685 | + |
| 686 | + |
| 687 | + |
| 688 | +参考文章: |
| 689 | + |
| 690 | +[死磕Synchronized底层实现--概论](https://github.com/farmerjohngit/myblog/issues/12) |
| 691 | + |
| 692 | +[浅谈偏向锁、轻量级锁、重量级锁](https://www.jianshu.com/p/36eedeb3f912) |
0 commit comments