【JUC系列-05】通过源码分析AQS和ReentrantLock的底层原理

JUC系列整体栏目


内容链接地址
【一】深入理解JMM内存模型的底层实现原理https://zhenghuisheng.blog.csdn.net/article/details/132400429
【二】深入理解CAS底层原理和基本使用https://blog.csdn.net/zhenghuishengq/article/details/132478786
【三】熟练掌握Atomic原子系列基本使用https://blog.csdn.net/zhenghuishengq/article/details/132543379
【四】精通Synchronized底层的实现原理https://blog.csdn.net/zhenghuishengq/article/details/132740980
【五】通过源码分析AQS和ReentrantLock的底层原理https://blog.csdn.net/zhenghuishengq/article/details/132857564

通过源码分析AQS和ReentrantLock的底层原理

  • 一,深入理解AQS和ReentrantLock的底层原理
    • 1,初识AQS
    • 2,AQS抽象类的源码分析
      • 2.1,同步等待队列
      • 2.2,条件等待队列
    • 3,Node结点分析
    • 4,ReentrantLock底层源码分析
      • 4.1,NonfairSync
        • 4.1.1,线程尝试获取锁
        • 4.1.2,双向链表的创建
        • 4.1.3,Node结点入队
        • 4.1.4,Node结点的阻塞
        • 4.1.5,同步监视器修改状态,释放资源和释放锁
        • 4.1.6,唤醒链表中的下一个结点
      • 4.2,FairSync
      • 4.3,ReentrantLock底层实现总结
    • 5,ReentrantLock的特性
    • 6,ReentrantLock和Synchronized联系

一,深入理解AQS和ReentrantLock的底层原理

在学习这个AQS之前,需要先了解synchronized设计思想和底层实现,因为AQS的底层是参考于synchronized,可以看本人的上一篇文章

1,初识AQS

在上一篇中了解了synchronized的底层原理,synchronized是jvm层面的一把隐式锁,在java层面很难的去控制这把锁,如某些场景需要手动加解锁等,并且在jdk6之前,synchronized还是一把重锁,并没有后来的锁升级过程的一大堆优化,其性能也比较慢,需要来回的从用户态到内核态之间切换,名副其实的一把重量级锁

为了实现一把java层面的锁,并且提高原来的性能的情况下,在编程大师 Doug Lea 的努力下,通过java代码实现锁的AQS终于问世。AQS主要是参考了synchronized的底层实现,如AQS内部也使用了同步等待队列和条件等待队列等,也是通过cas的方式抢锁,但是在性能上,是超过这个synchronized的,并且支持手动的加锁和解锁,对java开发者相对而言更加友好。即使后面synchronized经过了一系列锁的升级和优化,其性能也不如AQS

AQS:AbstractQueuedSynchronizer ,顾名思义,可以叫做抽象队列同步器,是一个java语言层面的一个抽象类,java层面主要是通过这个AQS的这种方式实现管程的,主要是支持jdk5及以上版本

2,AQS抽象类的源码分析

由于这个AQS是通过java语言实现的,所以直接来分析他的底层源码即可

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizerimplements java.io.Serializable {...}

AQS是参考Synchronized的底层来实现的,因此可以直接根据Synchronized的底层思想来看这个AQS。首先Synchronized有两个重要的队列组成,一个是同步等待队列,一个是条件等待队列,那根据这两个队列,看看AQS底层是如何实现的

2.1,同步等待队列

同步等待队列在synchronized是通过这个cxq队列实现的,其内部采用的是栈结构,先进后出,所以并不能保证锁的公平性。当线程一进来抢锁时,抢到锁的线程继续往下执行,没抢到锁的线程进入这个同步等待队列

在这个AbstractQueuedSynchronizer 抽象类中,该类继承了一个父类AbstractOwnableSynchronizer ,在这个类中,有一个重要的属性exclusiveOwnerThread ,表示当前的独占锁的线程,因为这个队列是一个同步的队列,因此需要通过这个参数来记录是哪个线程抢到了锁。在这里插入图片描述

如在ReentrantLock中,会进行一个尝试获取锁的状态,就会判断当前线程是否拿到了这把锁

if (Thread.currentThread() != getExclusiveOwnerThread())

在这个AbstractQueuedSynchronizer 抽象类中,里面有一个静态内部类Node,里面有如下参数

static final class Node {...
}

根据注释,很明显可以看出,AQS的同步等待队列的名称叫做CLH同步等待队列,其底层是通过一个双向链表的方式形成的,链表是通过各个Node结点组成,每个结点都有自己的生命周期,前驱指针和后继指针,以及一个结点所处的状态,如能否被唤醒状态等。和synchronized中的cxq队列不同的是,CLH采用的是先进先出的队列结构,cxq采用的是先进后出的栈结构

在这里插入图片描述

在该类中,也能看到内部的抢锁逻辑,是通过这个cas比较与交换来实现的,主要是判断当前结点的线程是否为同步监视器中 exclusiveOwnerThread 线程

在这里插入图片描述

CLH同步等待队列的大概图像如下,当然底层有着更加复杂的逻辑,下文会继续讲解。大概原理就是会有一个同步监视块(蓝色部分),里面会记录当前获取到锁的线程exclusive和一个state状态,state为0表示队列中的线程可以抢锁,队列是由各个node结点组成的双向链表,每个结点里面会有一个前驱指针和后继指针,以及一个waitStatus的状态,为-1时表示可唤醒状态。

在这里插入图片描述

2.2,条件等待队列

在synchronized中,当拿到锁的线程在调用wait方法之后,就会进入这个waitSet的条件等待队列中,在这个AQS中,也实现了这个条件等待队列的功能。在这个AbstractQueuedSynchronizer类中,有一个内部类ConditionObject类,如下,很明显这个条件等待队列是一个单向链表组成,并且里面也是由各个node结点组成,此时结点的waitState的值为-2.

public class ConditionObject implements Condition, java.io.Serializable {/** First node of condition queue. */private transient Node firstWaiter;/** Last node of condition queue. */private transient Node lastWaiter;...
}

synchronized主要是通过这个wait,notify和notyfyAll的方式来实现阻塞和唤醒功能,而在这个AQS中,是通过这个await,signal,signalAll 这三个方法实现

public final void await() throws InterruptedException {}
public final void signal() {}
public final void signalAll() {}

await方法的实现主要如下,内部会调用这个LockSupport.park进行阻塞,并且通过调用这个 addConditionWaiter 方法获取结点,可以发现这个结点时存放在链表的尾部的。在调用这个await方法之后,线程会释放资源,线程被sifnalAll唤醒的时候,会调用LockSupport.unpark的方式唤醒

public final void await() throws InterruptedException {...Node node = addConditionWaiter();while (!isOnSyncQueue(node)) {LockSupport.park(this);  //阻塞if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}
}

CLH同步等待队列和条件等待队列的关系如下,当拿到锁的线程调用await方法之后,此时结点会加入到条件等待队列的队尾里面,当某个结点被唤醒或者被全部唤醒的时候,会将条件等待队列的结点按顺序加入到同步等待队列的队尾,这样就实现了同步等待队列和条件等待队列之间的转换。(橙色部分表示CLH同步等待队列,绿色部分表示条件等待队列)

在这里插入图片描述

3,Node结点分析

上面了解了不管是条件等待队列还是同步等待队列,内部都是由node结点组成,接下来重点聊一下这个Node结点

public final class Node{static final Node SHARED = new Node();	//共享结点static final Node EXCLUSIVE = null;    	//独占结点static final int CANCELLED =  1;		//异常状态static final int SIGNAL    = -1;		//可以被唤醒的状态static final int CONDITION = -2;		//条件等待状态static final int PROPAGATE = -3;		//传播volatile int waitStatus;				//当前结点的生命状态,对应上面的1,-1,-2,-3volatile Node prev;						//前驱指针volatile Node next;						//后继指针volatile Thread thread;					//当前线程Node nextWaiter;						//下一个waiter
}

根据上面Node中的各个变量可知,Node可以分为共享结点和独占结点,每个结点有一个waitStatus的状态表示,主要有异常状态、可被唤醒状态、条件等待状态和可传播状态,每个结点可以有当前结点的生命状态,同时有一个前驱指针和后继指针,并有一个记录线程id的指针。

在阻塞队列中Node结点的waitStatus的值为-2,在信号量中Node结点的waitStatus值为-3

4,ReentrantLock底层源码分析

上面介绍了AQS的底层,接下来就可以分析一下这个耳熟能详的ReentrantLock了,依旧先分析一下他的底层源。这个类实现了Lock接口,在该接口中规范了如何加锁,解锁,尝试获取锁等

public class ReentrantLock implements Lock, java.io.Serializable {}

img

在这个ReentrantLock内部类中,有一个 Sync 的抽象的静态内部类,并且继承了这个AbstractQueuedSynchronizer 抽象队列同步器类,也就是说Sync已经具有该类的所有特性了。

abstract static class Sync extends AbstractQueuedSynchronizer{}

在这个Sync中类中,有两个静态的子类,分别是FairSync和NonfairSync类,分别代表着公平锁和非公平锁

static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}

在ReentrantLock类的构造方法中,默认的是一个非公平锁,减少阻塞,其效率相对较高

public ReentrantLock(boolean fair) sync = fair ? new FairSync() : new NonfairSync();
public ReentrantLock()  sync = new NonfairSync();

4.1,NonfairSync

因为ReentrantLock默认采用的是非公平锁的,因此先研究这个 NonfairSync ,如下

ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.unlock()

接下来研究这个lock方法,其内部实现如下,会先结果一个cas的比较与交换操作,随后将同步器中的Exclusive的值设置为当前线程的值。同步状态器为0时可以抢锁,为1时不能抢锁,在释放锁时又会设置成0。

 final void lock() {//cas操作,比较与交换if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread());else acquire(1);
}

上面方法中的state状态和exclusive独占线程对应的就是下图中蓝色部分的同步监视器。

在这里插入图片描述

如果cas抢锁失败,那么就会调用这个acquire()方法,并传入参数1

public final void acquire(int arg) {if (!tryAcquire(arg) &&  //尝试加锁acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //加入队列中selfInterrupt();  //阻塞
}

4.1.1,线程尝试获取锁

尝试获取锁的代码如下,会判断同步状态器的state值以及判断是否为重入锁

//判断是否获取锁
final boolean nonfairTryAcquire(int acquires) {//获取当前获取锁的线程final Thread current = Thread.currentThread();//获取当前同步器状态,被volatile修饰的整型值,默认为0int c = getState();//如果同步状态器的值为0,说明外面的线程可以进行加锁操作if (c == 0) {//compareAndSetState:原子操作,比较与交换,进行加锁的操作,将state变量将0变为1//setExclusiveOwnerThread:设置获取锁的的线程拥有者if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}//如果状态同步器不为0,可能由自己持有,也可能由别的线程持有锁//重复加锁,如定义一个全局锁,出现了这个可重入锁的问题else if (current == getExclusiveOwnerThread()) {//重入一次就+1int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}//别的线程获有锁,直接返回return false;
}

4.1.2,双向链表的创建

如果尝试加锁失败,则会进入下一步操作,就是将这个Node加入队列中,即进入这个addWaiter方法中,其底层实现主要如下,首先会判断当前的队列是否为空,如果不为空,则将当前结点的前驱指针指向队列中的尾结点,随后通过cas的方式将当前结点作为尾结点,反之出现并发问题。如果队列中尾结点不存在,那么需要一个初始化队列和一个入队的操作,调用的是enq方法

//线程入队操作
private Node addWaiter(Node mode) {//获取当前线程的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;
}

4.1.3,Node结点入队

enq方法的底层实现如下,如果尾结点为空,就会通过自旋+cas的方式进行一个队列初始化的工作,然后队列不为空,此时大量线程依旧在自旋,则执行一个结点插入到队列的尾结点的操作。通过这个for循环,可以保证当前线程是一定可以入队成功的

private Node enq(final Node node) {for (;;) {Node t = tail;//如果尾结点为空if (t == null) { // Must initialize//给头结点定义一个新的结点,自旋+cas实现,实现队列的初始化if (compareAndSetHead(new Node()))//此时头结点和尾结点是同一个结点tail = head;} else {//当前结点的前驱指针指向尾结点node.prev = t;//通过比较与交换if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}

4.1.4,Node结点的阻塞

在线程入队之后,就需要堆线程进行阻塞操作,主要是通过acquireQueued 方法实现,其实现的底层逻辑如下,其逻辑和入队的逻辑一样,也是用了for自旋的方式来保证该结点一定会拿到锁。在拿到锁之前,就需要在队列中阻塞以及将当前结点修改成可被唤醒的状态

final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {//中断机制boolean interrupted = false;for (;;) { //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) &&  //如果获取锁失败,修改Node标志parkAndCheckInterrupt())   //阻塞interrupted = true;}} finally {if (failed) cancelAcquire(node);}
}

可以再研究一下以下这段代码,在这个shouldParkAfterFailedAcquire 方法中,将这个Node结点的waitStatus的状态修改成-1,即可被唤醒的状态

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 compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  //设置成可被唤醒状态return false;
}

在修改完状态之后,又会调用这个 parkAndCheckInterrupt 方法,接下来查看这个方法可以得知,在该方法中会调用 LockSupport.park() 这个方法实现阻塞,并且调用这个interupted实现线程中断

private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();
}

4.1.5,同步监视器修改状态,释放资源和释放锁

在线程被阻塞之后,需要通过调用unlock方法对线程进行解锁操作,接下来看看底层是如何实现的。

public void unlock() {sync.release(1);
}

接下来继续往下看这个release 方法,首先会进行一个tryRelease尝试释放锁的方法

public final boolean release(int arg) {if (tryRelease(arg)) {  //尝试释放锁Node h = head;		if (h != null && h.waitStatus != 0) //判断当前链表的头结点是否存在unparkSuccessor(h);		 return true;}return false;
}

接下来查看这个tryRelease 的底层实现,如下由于此时已经有线程拿到了锁,因此此时同步监视器中的state状态为1(没拿到为0),因此这个c的值为0,此时会将将同步监视器中的Exclusive的线程设置为null,如下图看图理解好一点,这样的话就是一步释放锁的操作,此时state状态为0,其他线程也可以来抢锁

在这里插入图片描述

protected final boolean tryRelease(int releases) {int c = getState() - releases;   // 0 = 1-1if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true; setExclusiveOwnerThread(null); //将exclusive的的线程清空}setState(c);	//修改同步监视器状态,将状态0返回return free;
}

4.1.6,唤醒链表中的下一个结点

接下来进入release方法中的这个unparkSuccessor 方法中,查看其底层逻辑如下:首先会将当前结点的状态从-1可被唤醒状态改成0默认状态,由于队列中的线程都被阻塞,因此需要唤醒线程来抢锁,所以将当前结点的下一个结点给唤醒,通过调用:LockSupport.unpark 方法实现线程的唤醒

private void unparkSuccessor(Node node) {int ws = node.waitStatus;  //获取当前释放锁的node结点状态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);  //唤醒下一个结点
}

在3.1.4的acquireQueued方法中,有一个 setHead() 方法,就是会将当前当前结点作为头结点,当前结点的前驱指针指向null,这样就可以实现上一个结点出队的逻辑

private void setHead(Node node) {head = node;node.thread = null;node.prev = null;
}

4.2,FairSync

fairSync表示的是公平锁,其底层逻辑实现和非公平锁的步骤几乎一模一样,就是在入队之前,非公平锁可以直接进行一次cas抢锁的机会,但是公平锁需要判断队列中是否有数据,有就需要排队的操作。因此在这不做过多的解释,理解了非公平锁自然就能理解公平锁的底层实现了。

4.3,ReentrantLock底层实现总结

ReentrantLock是Lock的一个实现类,在该类内部定义了一个继承AQS的Sync内部类,该类有两个子类,分别是实现非公平锁的NonfairSync和实现公平锁的FairSync,默认使用的是非公平锁。以非公平锁为例,如下:

调用lock方法进行加锁: 首先会进行一个cas的抢锁逻辑,如果抢锁失败,则会加入到CLH同步等待队列,加入队列之前,会判断该队列是否已经创建,如果没有创建则创建,该队列是由Node结点组成的双向链表,如果队列已经创建,则将该线程当做一个Node结点对象加入到队列的尾部,此时通过调用LockSupport.park的方法将Node结点阻塞,并修改状态为-1,即可被唤醒状态。

调用unlock方法进行解锁: 内部首先会先释放同步监视器资源,并修改state的状态为0,这样可以保证锁没有被抢占,其次是将当前结点的下一个结点通过调用LockSupport.unpark的方式唤醒,这样就会有线程去获取锁,最后就是将被释放锁的结点移除,下一个获取锁的结点作为头结点。

5,ReentrantLock的特性

通过上面的源码可知,ReentrantLock具有一下特性

  • 阻塞等待队列:CLH同步等待队列,每个结点在队列中处于阻塞状态
  • 共享/独占:每个Node结点分为独占模式和共享模式
  • 公平/非公平:通过fair和unfair两种方式实现公平锁和非公平锁
  • 可重入:在尝试获取锁时会对锁是否重复加进行判断和累加
  • 允许中断 :在调用LockSupport.park方法之后,会对当前线程中断

6,ReentrantLock和Synchronized联系

  • ReentrantLock是通过java语言层次实现的锁;Synchronized是通过jvm层次实现的
  • ReentrantLock采用的是队列(FIFO)方式实现的同步队列,因此可以实现公平锁和非公平锁锁;Synchronized是通过栈(LIFO)的方式实现的同步队列,因此是一把非公平锁
  • ReentrantLock可以在代码中获取锁的状态,Synchronized不行
  • ReentrantLock可以在代码中实现中断,Synchronized不行
  • ReentrantLock需要手动的加锁解锁,Synchronized不需要
  • ReentrantLock可以保证先进来的线程先执行(队列),Synchronized可以保证后进来的线程先执行(栈)
  • ReentrantLock是通过CLH队列(队列FIFO)的方式实现的同步等待队列;Synchronized是通过CXQ(栈LIFO)的方式实现的同步等待队列
  • ReentrantLock和Synchronized都是支持可重入锁

如有转载,请标明出处:https://zhenghuisheng.blog.csdn.net/article/details/132857564

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/107804.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

android studio platform使用体验分享(as无法跳转c/c++等native源码的福音,强烈推荐)

hi&#xff0c;粉丝朋友们&#xff1a; 大家好&#xff01;这些天粉丝朋友们分享了一下Android Studio for Platform 这个最新的google开发的阅读aosp源码的工具&#xff0c;特别适合做原生系统开发。具体官方介绍如下地址&#xff1a; 参考链接&#xff1a;https://developer.…

Cookie和Session有什么区别和关系?

在技术面试中&#xff0c;经常被问到“Cookie和Session的区别”&#xff0c;大家都知道一些&#xff0c;Session比Cookie安全&#xff0c;Session是存储在服务器端的&#xff0c;Cookie是存储在客户端的&#xff0c;然而如果让你更详细地说明&#xff0c;恐怕就不怎么清楚了。 …

包装类、多线程的基本使用

包装类 1.基本数据类型对应的引用数据类型(包装类) 1.概述:所谓的包装类就是基本类型对应的类(引用类型),我们需要将基本类型转成包装类,从而让基本类型具有类的特性(说白了,就是将基本类型的数据转成包装类,就可以使用包装类中的方法来操作此数据)2.为啥要学包装类:a.将来有…

pyechart练习(一):画图小练习

1、使用Map制作全球人口分布图 import math import osimport matplotlib.pyplot as plt from pyecharts.charts import Map from pyecharts import options as opts# 只有部分国家的人口数据 POPULATION [["China", 1420062022], ["India", 1368737513],…

AI文本创作在百度App发文的实践

作者 | 内容生态端团队 导读 大语言模型&#xff08;LLM&#xff09;指包含数百亿&#xff08;或更多&#xff09;参数的语言模型&#xff0c;这些模型通常在大规模数据集上进行训练&#xff0c;以提高其性能和泛化能力。在内容创作工具接入文心一言AI能力后&#xff0c;可以为…

大数据课程L2——网站流量项目的算法分析数据处理

文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 了解网站流量项目的算法分析; ⚪ 了解网站流量项目的数据处理; 一、项目的算法分析 1. 概述 网站流量统计是改进网站服务的重要手段之一,通过获取用户在网站的行为,可以分析出哪些内…

学习Bootstrap 5的第八天

目录 加载器 彩色加载器 实例 闪烁加载器 实例 加载器大小 实例 加载器按钮 实例 分页 分页的基本结构 实例 活动状态 实例 禁用状态 实例 分页大小 实例 分页对齐 实例 面包屑&#xff08;Breadcrumbs&#xff09; 实例 加载器 彩色加载器 在 Bootstr…

【css | loading】好看的loading特效

示例&#xff1a; https://code.juejin.cn/pen/7277764394618978365 html <div class"pl"><div class"pl__dot"></div><div class"pl__dot"></div><div class"pl__dot"></div><div c…

科技云报道:青云科技为何成为IDC云转型的“神队友”?

科技云报道原创。 如今随着出海企业数量的不断增长&#xff0c;跨境业务也逐渐从蓝海变红海&#xff0c;从“价格战”到“智能战”。 一个明显的变化&#xff0c;来自企业对于出海效率的提升。《埃森哲2022中国企业国际化研究》指出&#xff0c;企业想要在出海浪潮中取胜&…

顺序表详解(接口详解)

顺序表&#xff08;接口详解&#xff09;&#x1f996; 1.线性表2.顺序表2.1 概念及结构 3.接口的实现3.1 定义SeqList3.2 初始化3.3 销毁3.4 打印3.5 扩容3.6 数据插入1.头插2.尾插3.下标插入 3.7 数据删除1.头删2.尾删3.下表删除 3.8 查询数据3.9 数据修改 4.顺序表存在的部分…

工厂设计模式

github&#xff1a;GitHub - QiuliangLee/pattern: 设计模式 概念 根据产品是具体产品还是具体工厂可分为简单工厂模式和工厂方法模式&#xff0c;根据工厂的抽象程度可分为工厂方法模式和抽象工厂模式。 简单工厂模式、工厂方法模式和抽象工厂模式有何区别&#xff1f; - 知…