`
寻梦者
  • 浏览: 639343 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

双重检查锁定及单例模式【转】

阅读更多

   单例创建模式是一个通用的编程习语。和多线程一起使用时,必需使用某种类型的同步。在努力创建更有效的代码时,Java 程序员们创建了双重检查锁定习语,将其和单例创建模式一起使用,从而限制同步代码量。然而,由于一些不太常见的 Java 内存模型细节的原因,并不能保证这个双重检查锁定习语有效。它偶尔会失败,而不是总失败。此外,它失败的原因并不明显,还包含 Java 内存模型的一些隐秘细节。这些事实将导致代码失败,原因是双重检查锁定难于跟踪。在本文余下的部分里,我们将详细介绍双重检查锁定习语,从而理解它在何处失效。

 

单例创建习语

    要理解双重检查锁定习语是从哪里起源的,就必须理解通用单例创建习语,如清单 1 中的阐释:

 

import java.util.*;

class Singleton {
	private static Singleton instance;
	private Vector v;
	private boolean inUse;

	private Singleton() {
		v = new Vector();
		v.addElement(new Object());
		inUse = true;
	}

	public static Singleton getInstance() {
		if (instance == null) // 1
			instance = new Singleton(); // 2
		return instance; // 3
	}
}

 

      此类的设计确保只创建一个 Singleton 对象。构造函数被声明为 privategetInstance() 方法只创建一个对象。这个实现适合于单线程程序。然而,当引入多线程时,就必须通过同步来保护 getInstance() 方法。如果不保护 getInstance() 方法,则可能返回 Singleton 对象的两个不同的实例。假设两个线程并发调用 getInstance() 方法并且按以下顺序执行调用:

  1. 线程 1 调用 getInstance() 方法并决定 instance 在 //1 处为 null

  2. 线程 1 进入 if 代码块,但在执行 //2 处的代码行时被线程 2 预占。

  3. 线程 2 调用 getInstance() 方法并在 //1 处决定 instancenull

  4. 线程 2 进入 if 代码块并创建一个新的 Singleton 对象并在 //2 处将变量 instance 分配给这个新对象。

  5. 线程 2 在 //3 处返回 Singleton 对象引用。

  6. 线程 2 被线程 1 预占。

  7. 线程 1 在它停止的地方启动,并执行 //2 代码行,这导致创建另一个 Singleton 对象。

  8. 线程 1 在 //3 处返回这个对象。

      结果是 getInstance() 方法创建了两个 Singleton 对象,而它本该只创建一个对象。通过同步 getInstance() 方法从而在同一时间只允许一个线程执行代码,这个问题得以改正,如清单 2 所示:


清单 2. 线程安全的 getInstance() 方法

public static synchronized Singleton getInstance()
{
  if (instance == null)          //1
    instance = new Singleton();  //2
  return instance;               //3
}

 

     清单 2 中的代码针对多线程访问 getInstance() 方法运行得很好。然而,当分析这段代码时,您会意识到只有在第一次调用方法时才需要同步。由于只有第一次调用执行了 //2 处的代码,而只有此行代码需要同步,因此就无需对后续调用使用同步。所有其他调用用于决定 instance 是非 null 的,并将其返回。多线程能够安全并发地执行除第一次调用外的所有调用。尽管如此,由于该方法是 synchronized 的,需要为该方法的每一次调用付出同步的代价,即使只有第一次调用需要同步。 为使此方法更为有效,一个被称为双重检查锁定的习语就应运而生了。这个想法是为了避免对除第一次调用外的所有调用都实行同步的昂贵代价。同步的代价在不同的 JVM 间是不同的。在早期,代价相当高。随着更高级的 JVM 的出现,同步的代价降低了,但出入 synchronized 方法或块仍然有性能损失。不考虑 JVM 技术的进步,程序员们绝不想不必要地浪费处理时间。

因为只有清单 2 中的 //2 行需要同步,我们可以只将其包装到一个同步块中,如清单 3 所示:


 清单 3. getInstance() 方法

public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {
      instance = new Singleton();
    }
  }
  return instance;
}
 

    清单 3 中的代码展示了用多线程加以说明的和清单 1 相同的问题。当 instancenull 时,两个线程可以并发地进入 if 语句内部。然后,一个线程进入 synchronized 块来初始化 instance,而另一个线程则被阻断。当第一个线程退出 synchronized 块时,等待着的线程进入并创建另一个 Singleton 对象。注意:当第二个线程进入 synchronized 块时,它并没有检查 instance 是否非 null

 

   为处理清单 3 中的问题,我们需要对 instance 进行第二次检查。这就是“双重检查锁定”名称的由来。将双重查锁定习语应用到清单 3 的结果就是清单 4 。



清单 4. 双重检查锁定示例

public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {  //1
      if (instance == null)          //2
        instance = new Singleton();  //3
    }
  }
  return instance;
}

 

     双重检查锁定背后的理论是:在 //2 处的第二次检查使(如清单 3 中那样)创建两个不同的 Singleton 对象成为不可能。假设有下列事件序列:

  1. 线程 1 进入 getInstance() 方法。

  2. 由于 instancenull,线程 1 在 //1 处进入 synchronized 块。

  3. 线程 1 被线程 2 预占。

  4. 线程 2 进入 getInstance() 方法。

  5. 由于 instance 仍旧为 null,线程 2 试图获取 //1 处的锁。然而,由于线程 1 持有该锁,线程 2 在 //1 处阻塞。

  6. 线程 2 被线程 1 预占。

  7. 线程 1 执行,由于在 //2 处实例仍旧为 null,线程 1 还创建一个 Singleton 对象并将其引用赋值给 instance

  8. 线程 1 退出 synchronized 块并从 getInstance() 方法返回实例。

  9. 线程 1 被线程 2 预占。

  10. 线程 2 获取 //1 处的锁并检查 instance 是否为 null

  11. 由于 instance 是非 null 的,并没有创建第二个 Singleton 对象,由线程 1 创建的对象被返回。

    双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。

    双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓“无序写入”,这也是这些习语失败的一个主要原因。

 

 

无序写入

 

     为解释该问题,需要重新考察上述清单 4 中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量instance 来引用此对象。这行代码的问题是:在 Singleton 构造函数体执行之前,变量 instance 可能成为非 null 的。

      什么?这一说法可能让您始料未及,但事实确实如此。在解释这个现象如何发生前,请先暂时接受这一事实,我们来考察一下双重检查锁定是如何被破坏的。假设清单 4 中代码执行以下事件序列:

  1. 线程 1 进入 getInstance() 方法。

  2. 由于 instancenull,线程 1 在 //1 处进入 synchronized 块。

  3. 线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非 null

  4. 线程 1 被线程 2 预占。

  5. 线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将 instance 引用返回给一个构造完整但部分初始化了的 Singleton 对象。

  6. 线程 2 被线程 1 预占。

  7. 线程 1 通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。

     此事件序列发生在线程 2 返回一个尚未执行构造函数的对象的时候。

为展示此事件的发生情况,假设为代码行 instance =new Singleton(); 执行了下列伪代码: instance =newSingleton();

 

mem = allocate();             //Allocate memory for Singleton object.
instance = mem;               //Note that instance is now non-null, but
                              //has not been initialized.
ctorSingleton(instance);      //Invoke constructor for Singleton passing
                              //instance.

 

这段伪代码不仅是可能的,而且是一些 JIT 编译器上真实发生的。执行的顺序是颠倒的,但鉴于当前的内存模型,这也是允许发生的。JIT 编译器的这一行为使双重检查锁定的问题只不过是一次学术实践而已。

为说明这一情况,假设有清单 5 中的代码。它包含一个剥离版的 getInstance() 方法。我已经删除了“双重检查性”以简化我们对生成的汇编代码(清单 6)的回顾。我们只关心 JIT 编译器如何编译 instance=new Singleton(); 代码。此外,我提供了一个简单的构造函数来明确说明汇编代码中该构造函数的运行情况。


清单 5. 用于演示无序写入的单例类

class Singleton
{
  private static Singleton instance;
  private boolean inUse;
  private int val;  

  private Singleton()
  {
    inUse = true;
    val = 5;
  }
  public static Singleton getInstance()
  {
    if (instance == null)
      instance = new Singleton();
    return instance;
  }
}

 

清单 6 包含由 Sun JDK 1.2.1 JIT 编译器为清单 5 中的 getInstance() 方法体生成的汇编代码。


清单 6. 由清单 5 中的代码生成的汇编代码

;asm code generated for getInstance
054D20B0   mov         eax,[049388C8]      ;load instance ref
054D20B5   test        eax,eax             ;test for null
054D20B7   jne         054D20D7
054D20B9   mov         eax,14C0988h
054D20BE   call        503EF8F0            ;allocate memory
054D20C3   mov         [049388C8],eax      ;store pointer in 
                                           ;instance ref. instance  
                                           ;non-null and ctor
                                           ;has not run
054D20C8   mov         ecx,dword ptr [eax] 
054D20CA   mov         dword ptr [ecx],1   ;inline ctor - inUse=true;
054D20D0   mov         dword ptr [ecx+4],5 ;inline ctor - val=5;
054D20D7   mov         ebx,dword ptr ds:[49388C8h]
054D20DD   jmp         054D20B0

 

注: 为引用下列说明中的汇编代码行,我将引用指令地址的最后两个值,因为它们都以 054D20 开头。例如,B5 代表 test eax,eax

     汇编代码是通过运行一个在无限循环中调用 getInstance() 方法的测试程序来生成的。程序运行时,请运行 Microsoft Visual C++ 调试器并将其附到表示测试程序的 Java 进程中。然后,中断执行并找到表示该无限循环的汇编代码。

 

   B0B5 处的前两行汇编代码将 instance 引用从内存位置 049388C8 加载至 eax 中,并进行 null 检查。这跟清单 5 中的 getInstance() 方法的第一行代码相对应。第一次调用此方法时,instancenull,代码执行到 B9BE 处的代码为 Singleton 对象从堆中分配内存,并将一个指向该块内存的指针存储到 eax 中。下一行代码,C3,获取 eax 中的指针并将其存储回内存位置为 049388C8 的实例引用。结果是,instance 现在为非 null 并引用一个有效的 Singleton 对象。然而,此对象的构造函数尚未运行,这恰是破坏双重检查锁定的情况。然后,在 C8 行处,instance 指针被解除引用并存储到 ecxCAD0 行表示内联的构造函数,该构造函数将值 true5 存储到 Singleton 对象。如果此代码在执行 C3 行后且在完成该构造函数前被另一个线程中断,则双重检查锁定就会失败。

 

      不是所有的 JIT 编译器都生成如上代码。一些生成了代码,从而只在构造函数执行后使 instance 成为非 null。针对 Java 技术的 IBM SDK 1.3 版和 Sun JDK 1.3 都生成这样的代码。然而,这并不意味着应该在这些实例中使用双重检查锁定。该习语失败还有一些其他原因。此外,您并不总能知道代码会在哪些 JVM 上运行,而 JIT 编译器总是会发生变化,从而生成破坏此习语的代码。
  

 

解决方案

      底线就是:无论以何种形式,都不应使用双重检查锁定,因为您不能保证它在任何 JVM 实现上都能顺利行。JSR-133 是有关内存模型寻址问题的,尽管如此,新的内存模型也不会支持双重检查锁定。因此,您有两种选择:

  • 接受如清单 2 中所示的 getInstance() 方法的同步。

  • 放弃同步,而使用一个 static 字段。

     选择项 2 如清单 10 中所示


清单 10. 使用 static 字段的单例实现

class Singleton
{
  private Vector v;
  private boolean inUse;
  private static Singleton instance = new Singleton();

  private Singleton()
  {
    v = new Vector();
    inUse = true;
    //...
  }

  public static Singleton getInstance()
  {
    return instance;
  }
}

 

      清单 10 的代码没有使用同步,并且确保调用 static getInstance() 方法时才创建 Singleton。如果您的目标是消除同步,则这将是一个很好的选择。

 

 

 

String 不是不变的

    鉴于无序写入和引用在构造函数执行前变成非 null 的问题,您可能会考虑 String 类。假设有下列代码:

private String str;
//...
str = new String("hello");

 

    String 类应该是不变的。尽管如此,鉴于我们之前讨论的无序写入问题,那会在这里导致问题吗?答案是肯定的。考虑两个线程访问 String str。一个线程能看见 str 引用一个 String 对象,在该对象中构造函数尚未运行。事实上,清单 11 包含展示这种情况发生的代码。注意,这个代码仅在我测试用的旧版 JVM 上会失败。IBM 1.3 和 Sun 1.3 JVM 都会如期生成不变的 String


清单 11. 可变 String 的例子

class StringCreator extends Thread
{
  MutableString ms;
  public StringCreator(MutableString muts)
  {
    ms = muts;
  }
  public void run()
  {
    while(true)
      ms.str = new String("hello");          //1
  }
}
class StringReader extends Thread
{
  MutableString ms;
  public StringReader(MutableString muts)
  {
    ms = muts;
  }
  public void run()
  {
    while(true)
    {
      if (!(ms.str.equals("hello")))         //2
      {
        System.out.println("String is not immutable!");
        break;
      }
    }
  }
}
class MutableString
{
  public String str;                         //3
  public static void main(String args[])
  {
    MutableString ms = new MutableString();  //4
    new StringCreator(ms).start();           //5
    new StringReader(ms).start();            //6
  }
}

 

      此代码在 //4 处创建一个 MutableString 类,它包含了一个 String 引用,此引用由 //3 处的两个线程共享。在行 //5 和 //6 处,在两个分开的线程上创建了两个对象 StringCreatorStringReader。传入一个 MutableString 对象的引用。StringCreator 类进入到一个无限循环中并且使用值“hello”在 //1 处创建 String 对象。StringReader 也进入到一个无限循环中,并且在 //2 处检查当前的 String 对象的值是不是 “hello”。如果不行,StringReader 线程打印出一条消息并停止。如果 String 类是不变的,则从此程序应当看不到任何输出。如果发生了无序写入问题,则使 StringReader 看到 str 引用的惟一方法绝不是值为“hello”的 String 对象。

在旧版的 JVM 如 Sun JDK 1.2.1 上运行此代码会导致无序写入问题。并因此导致一个非不变的 String

 

 

结束语

      为避免单例中代价高昂的同步,程序员非常聪明地发明了双重检查锁定习语。不幸的是,鉴于当前的内存模型的原因,该习语尚未得到广泛使用,就明显成为了一种不安全的编程结构。重定义脆弱的内存模型这一领域的工作正在进行中。尽管如此,即使是在新提议的内存模型中,双重检查锁定也是无效的。对此问题最佳的解决方案是接受同步或者使用一个 static field

分享到:
评论

相关推荐

    受激拉曼散射计量【Stimulated-Raman-Scattering Metrology】 附Matlab代码.rar

    1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。

    MMC整流器技术解析:基于Matlab的双闭环控制策略与环流抑制性能研究,Matlab下的MMC整流器技术文档:18个子模块,双闭环控制稳定直流电压,环流抑制与最近电平逼近调制,优化桥臂电流波形,高效

    MMC整流器技术解析:基于Matlab的双闭环控制策略与环流抑制性能研究,Matlab下的MMC整流器技术文档:18个子模块,双闭环控制稳定直流电压,环流抑制与最近电平逼近调制,优化桥臂电流波形,高效并网运行。,MMC整流器(Matlab),技术文档 1.MMC工作在整流侧,子模块个数N=18,直流侧电压Udc=25.2kV,交流侧电压6.6kV 2.控制器采用双闭环控制,外环控制直流电压,采用PI调节器,电流内环采用PI+前馈解耦; 3.环流抑制采用PI控制,能够抑制环流二倍频分量; 4.采用最近电平逼近调制(NLM), 5.均压排序:电容电压排序采用冒泡排序,判断桥臂电流方向确定投入切除; 结果: 1.输出的直流电压能够稳定在25.2kV; 2.有功功率,无功功率稳态时波形稳定,有功功率为3.2MW,无功稳定在0Var; 3.网侧电压电流波形均为对称的三相电压和三相电流波形,网侧电流THD=1.47%<2%,符合并网要求; 4.环流抑制后桥臂电流的波形得到改善,桥臂电流THD由9.57%降至1.93%,环流波形也可以看到得到抑制; 5.电容电压能够稳定变化 ,工作点关键词:MMC

    Boost二级升压光伏并网结构的Simulink建模与MPPT最大功率点追踪:基于功率反馈的扰动观察法调整电压方向研究,Boost二级升压光伏并网结构的Simulink建模与MPPT最大功率点追踪:基

    Boost二级升压光伏并网结构的Simulink建模与MPPT最大功率点追踪:基于功率反馈的扰动观察法调整电压方向研究,Boost二级升压光伏并网结构的Simulink建模与MPPT最大功率点追踪:基于功率反馈的扰动观察法调整电压方向研究,Boost二级升压光伏并网结构,Simulink建模,MPPT最大功率点追踪,扰动观察法采用功率反馈方式,若ΔP>0,说明电压调整的方向正确,可以继续按原方向进行“干扰”;若ΔP<0,说明电压调整的方向错误,需要对“干扰”的方向进行改变。 ,Boost升压;光伏并网结构;Simulink建模;MPPT最大功率点追踪;扰动观察法;功率反馈;电压调整方向。,光伏并网结构中Boost升压MPPT控制策略的Simulink建模与功率反馈扰动观察法

    STM32F103C8T6 USB寄存器开发详解(12)-键盘设备

    STM32F103C8T6 USB寄存器开发详解(12)-键盘设备

    2011-2020广东21市科技活动人员数

    科技活动人员数专指直接从事科技活动以及专门从事科技活动管理和为科技活动提供直接服务的人员数量

    Matlab Simulink仿真探究Flyback反激式开关电源性能表现与优化策略,Matlab Simulink仿真探究Flyback反激式开关电源的工作机制,Matlab Simulimk仿真

    Matlab Simulink仿真探究Flyback反激式开关电源性能表现与优化策略,Matlab Simulink仿真探究Flyback反激式开关电源的工作机制,Matlab Simulimk仿真,Flyback反激式开关电源仿真 ,Matlab; Simulink仿真; Flyback反激式; 开关电源仿真,Matlab Simulink在Flyback反激式开关电源仿真中的应用

    基于Comsol的埋地电缆电磁加热计算模型:深度解析温度场与电磁场分布学习资料与服务,COMSOL埋地电缆电磁加热计算模型:温度场与电磁场分布的解析与学习资源,comsol 埋地电缆电磁加热计算模型

    基于Comsol的埋地电缆电磁加热计算模型:深度解析温度场与电磁场分布学习资料与服务,COMSOL埋地电缆电磁加热计算模型:温度场与电磁场分布的解析与学习资源,comsol 埋地电缆电磁加热计算模型,可以得到埋地电缆温度场及电磁场分布,提供学习资料和服务, ,comsol;埋地电缆电磁加热计算模型;温度场分布;电磁场分布;学习资料;服务,Comsol埋地电缆电磁加热模型:温度场与电磁场分布学习资料及服务

    ibus-table-chinese-yong-1.4.6-3.el7.x64-86.rpm.tar.gz

    1、文件内容:ibus-table-chinese-yong-1.4.6-3.el7.rpm以及相关依赖 2、文件形式:tar.gz压缩包 3、安装指令: #Step1、解压 tar -zxvf /mnt/data/output/ibus-table-chinese-yong-1.4.6-3.el7.tar.gz #Step2、进入解压后的目录,执行安装 sudo rpm -ivh *.rpm 4、更多资源/技术支持:公众号禅静编程坊

    基于51单片机protues仿真的汽车智能灯光控制系统设计(仿真图、源代码)

    基于51单片机protues仿真的汽车智能灯光控制系统设计(仿真图、源代码) 一、设计项目 根据本次设计的要求,设计出一款基于51单片机的自动切换远近光灯的设计。 技术条件与说明: 1. 设计硬件部分,中央处理器采用了STC89C51RC单片机; 2. 使用两个灯珠代表远近光灯,感光部分采用了光敏电阻,因为光敏电阻输出的是电压模拟信号,单片机不能直接处理模拟信号,所以经过ADC0832进行转化成数字信号; 3. 显示部分采用了LCD1602液晶,还增加按键部分电路,可以选择手自动切换远近光灯; 4. 用超声模块进行检测距离;

    altermanager的企业微信告警服务

    altermanager的企业微信告警服务

    MyAgent测试版本在线下载

    MyAgent测试版本在线下载

    Comsol技术:可调BIC应用的二氧化钒VO2材料探索,Comsol模拟二氧化钒VO2的可调BIC特性研究,Comsol二氧化钒VO2可调BIC ,Comsol; 二氧化钒VO2; 可调BIC

    Comsol技术:可调BIC应用的二氧化钒VO2材料探索,Comsol模拟二氧化钒VO2的可调BIC特性研究,Comsol二氧化钒VO2可调BIC。 ,Comsol; 二氧化钒VO2; 可调BIC,Comsol二氧化钒VO2材料:可调BIC技术的关键应用

    C++学生成绩管理系统源码.zip

    C++学生成绩管理系统源码

    基于Matlab与Cplex的激励型需求响应模式:负荷转移与电价响应的差异化目标函数解析,基于Matlab与CPLEX的激励型需求响应负荷转移策略探索,激励型需求响应 matlab +cplex 激励

    基于Matlab与Cplex的激励型需求响应模式:负荷转移与电价响应的差异化目标函数解析,基于Matlab与CPLEX的激励型需求响应负荷转移策略探索,激励型需求响应 matlab +cplex 激励型需求响应采用激励型需求响应方式对负荷进行转移,和电价响应模式不同,具体的目标函数如下 ,激励型需求响应; matlab + cplex; 负荷转移; 目标函数。,Matlab与Cplex结合的激励型需求响应模型及其负荷转移策略

    scratch介绍(scratch说明).zip

    scratch介绍(scratch说明).zip

    深度学习模型的发展历程及其关键技术在人工智能领域的应用

    内容概要:本文全面介绍了深度学习模型的概念、工作机制和发展历程,详细探讨了神经网络的构建和训练过程,包括反向传播算法和梯度下降方法。文中还列举了深度学习在图像识别、自然语言处理、医疗和金融等多个领域的应用实例,并讨论了当前面临的挑战,如数据依赖、计算资源需求、可解释性和对抗攻击等问题。最后,文章展望了未来的发展趋势,如与量子计算和区块链的融合,以及在更多领域的应用前景。 适合人群:对该领域有兴趣的技术人员、研究人员和学者,尤其适合那些希望深入了解深度学习原理和技术细节的读者。 使用场景及目标:①理解深度学习模型的基本原理和结构;②了解深度学习模型的具体应用案例;③掌握应对当前技术挑战的方向。 阅读建议:文章内容详尽丰富,读者应在阅读过程中注意理解各个关键技术的概念和原理,尤其是神经网络的构成及训练过程。同时也建议对比不同模型的特点及其在具体应用中的表现。

    day02供应链管理系统-补充.zip

    该文档提供了一个关于供应链管理系统开发的详细指南,重点介绍了项目安排、技术实现和框架搭建的相关内容。 文档分为以下几个关键部分: 项目安排:主要步骤包括搭建框架(1天),基础数据模块和权限管理(4天),以及应收应付和销售管理(5天)。 供应链概念:供应链系统的核心流程是通过采购商品放入仓库,并在销售时从仓库提取商品,涉及三个主要订单:采购订单、销售订单和调拨订单。 大数据的应用:介绍了数据挖掘、ETL(数据抽取)和BI(商业智能)在供应链管理中的应用。 技术实现:讲述了DAO(数据访问对象)的重用、服务层的重用、以及前端JS的继承机制、jQuery插件开发等技术细节。 系统框架搭建:包括Maven环境的配置、Web工程的创建、持久化类和映射文件的编写,以及Spring配置文件的实现。 DAO的需求和功能:供应链管理系统的各个模块都涉及分页查询、条件查询、删除、增加、修改操作等需求。 泛型的应用:通过示例说明了在Java语言中如何使用泛型来实现模块化和可扩展性。 文档非常技术导向,适合开发人员参考,用于构建供应链管理系统的架构和功能模块。

    清华大学104页《Deepseek:从入门到精通》

    这份长达104页的手册由清华大学新闻与传播学院新媒体研究中心元宇宙文化实验室的余梦珑博士后及其团队精心编撰,内容详尽,覆盖了从基础概念、技术原理到实战案例的全方位指导。它不仅适合初学者快速了解DeepSeek的基本操作,也为有经验的用户提供了高级技巧和优化策略。

    MXTU MAX仿毒舌自适应主题源码 苹果CMSv10模板.zip

    主题说明: 1、将mxtheme目录放置根目录 | 将mxpro目录放置template文件夹中 2、苹果cms后台-系统-网站参数配置-网站模板-选择mxpro 模板目录填写html 3、网站模板选择好之后一定要先访问前台,然后再进入后台设置 4、主题后台地址: MXTU MAX图图主题,/admin.php/admin/mxpro/mxproset admin.php改成你登录后台的xxx.php 5、首页幻灯片设置视频推荐9,自行后台设置 6、追剧周表在视频数据中,节目周期添加周一至周日自行添加,格式:一,二,三,四,五,六,日

    基于matlab平台的数字信号处理GUI设计.zip

    运行GUI版本,可二开

Global site tag (gtag.js) - Google Analytics