【设计模式】单例模式的前世今生
文章目录
- 引言
- 简介
- 起航!向“确保某个类在系统中只有一个实例”进发 ⛵️
- Lazy Singleton
- Double-checked locking(DCL) Singleton
- Volatile Singleton
- Atomic Singleton
- Meyers Singleton
- 附:C++静态对象的初始化
引言
说起单例模式,我想,即便屏幕前的你此前没有系统学习过设计模式,也应该听说过它的大名。
但是,这篇文章的重点不是去聊这个模式在实际生产过程中怎么用,而是想聊一下这个模式发展的历史。如果你的目的是想了解其具体用法,你可以在检索一下其他人写的总结,再往下看的话,可能不会有你想要的答案。
简介
在软件系统中,经常有一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性以及良好的效率。
单例模式是一种设计模式,其核心目的是确保某个类在系统中只有一个实例,并提供一个全局访问点来访问这个实例。
“确保某个类在系统中只有一个实例”——这个目的听起来似乎很简单,不要觉得荒谬,某些特定的情况下,我们的系统中确实只需要某个类的一个实例就可以了,这样既能满足实际使用场景,又能减少内存开销,避免资源的多重占用,提升性能。
倘若我们从这个目的出发——“确保某个类在系统中只有一个实例”,现在的任务就是:设计某种手段以达到我们的目的。
起航!向“确保某个类在系统中只有一个实例”进发 ⛵️
也许,刚看到这个目标的时候你会有点疑惑:这不是很简单吗?既然你想要确保系统中只有一个某个类的对象,那我就只创建一个对象不就好了吗?
听起来好像没错,但是“确保某个类在系统中只有一个实例”,这应该是类设计者的责任,而不是使用者的责任。
现在,让我们从类设计者的角度重新审视这个问题。
我们知道,创建类的实例——这个动作是借由类的构造函数完成的,换句话说,我们可以确定问题的突破点是在构造函数身上。那么,如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例呢?
首先,我们先解决构造函数的权限问题。C++中的权限说起来一共有三种:public,protect,private。而无论对于用户还是派生类来讲,真正的权限事实上只有两种:
- 对于用户而言,public权限是可访问的,private权限和protect权限是不可访问的;
- 对于派生类而言,private是不可访问的,protect与public是可访问的;
而如果将这个类的构造函数用public去修饰,意味着用户可以随意创建对象,“创建对象”这个动作无法受到我们的管控,因此,如果想要限制用户“不那么自由”的创建实例,我们应当将构造函数声明为private:
class Singleton{private:Singleton();//私有构造函数static Singleton* m_instance;public:static Singleton* getInstance();//全局访问点
}
Singleton* Singleton::m_instance = NULL;
Lazy Singleton
那么如何“确保某个类在系统中只有一个实例”?很容易想到:
1 Singleton* Singleton::getInstance(){
2 if(m_instance == nullptr){
3 m_instance = new Singleton();
4 }
5 return m_instance;
6 }
懒汉版(Lazy Singleton):单例实例在第一次被使用时才进行初始化,这叫做延迟初始化,也叫做懒加载。
Lazy Singleton存在内存泄露的问题,这里有两种解决方法:
- 使用智能指针
- 使用静态的嵌套类对象
对于第二种解决方法,代码如下:
// version 1.1
class Singleton
{
private:static Singleton* instance;
private:Singleton() { };~Singleton() { };Singleton(const Singleton&);Singleton& operator=(const Singleton&);
private:class Deletor {public:~Deletor() {if(Singleton::instance != NULL)delete Singleton::instance;}};static Deletor deletor;
public:static Singleton* getInstance() {if(instance == NULL) {instance = new Singleton();}return instance;}
};// init static member
Singleton* Singleton::instance = NULL;
在程序运行结束时,系统会调用静态成员deletor
的析构函数,该析构函数会删除单例的唯一实例。使用这种方法释放单例对象有以下特征:
- 在单例类内部定义专有的嵌套类。
- 在单例类内定义私有的专门用于释放的静态成员。
- 利用程序在结束时析构全局变量的特性,选择最终的释放时机。
这是一个简单的实现版本,”有条件“ 的完成了我们的目标,因为这个版本只能针对于单线程下的程序,是个“线程非安全”版本,一旦线程数大于1,这个版本将不再起作用。
假设现在有两个线程:thread A与thread B。
thread A 执行完第2行,还没来得及执行第3行时,thread B 抢到了时间片,由于此时的m_instance仍为空,因此thread也能进入if分支,然后m_instance就被创建了两次。
有没有什么办法能够快速修复这个“bug“呢?
Double-checked locking(DCL) Singleton
很自然的,你会想到加锁:
1 Singleton* Singleton::getInstance(){
2 Lock lock;
3 if(m_instance == nullptr){
4 m_instance = new Singleton();
5 }
6 return m_instance;
7 }
如你所愿,我们在这个版本里加了一个锁,再遇到上述场景时,由于thread A抢到了锁并且还没释放,因此,thread A能正常创建实例,并且当thread A出了函数体释放了锁之后,thread B 进入函数体,由于此时m_instance已经被创建,因此并不会被创建两次。
问题解决了吗?
按照上面的分析,好像是的。但是,你有没有注意到当实例已经被创建后的场景?
假设实例m_instance已经被创建,在之后的场景中,程序再次进入该函数时,都会先创建锁,然后判断m_instance是否为空,然后返回。每次进入函数体都会创建锁,但是这个锁只有第一次才有真正的作用,之后都是在浪费资源。
这个版本能够保证线程安全,但是锁的代价过高。
还有没有改进版本呢?
于是,双检查锁版本诞生了:
1 Singleton* Singleton::getInstance(){
2 if(m_instance == nullptr){
3 Lock lock; 基于作用域的加锁,超出作用域,自动调用析构函数解锁
4 if(m_instance == nullptr){
5 m_instance = new Singleton();
6 }
7 }
8 return m_instance;
9 }
之前的版本是不管三七二十一,都加锁,现在的版本是进入函数体之后,先问一次m_instance是不是空,根据结果去决定是否加锁。规避了上一个版本锁的代价过高的问题。
有的小伙伴可能会在这里犯迷糊:认为第二个if分支没有必要,即可以删去第4行。
事实上,如果删去了第4行,那么情况就会变得跟第一个版本一模一样,只要线程能同时通过第2行的检查,那么这个实例就有被创建多次的可能。就算此时加了这个锁,无非也就是多等一会儿,没有其他作用。
这个版本看起来很完美,问题似乎已经被我们解决了!
但是我要告诉你,这个版本在很长一段时间内迷惑了很多人,包括一些专家都认为这个版本已经达到目标了。直到2000年左右,Java领域的某些研究者才发现有问题,而且很快在几乎所有的语言领域都发现这种实现有漏洞。由于内存读写reorder不安全,会导致双检查锁失效。
怎么样的一个失效问题呢?
让我们将目光聚焦到这行代码上:
m_instance = new Singleton();
这行代码最终会被编译器编译成一段指令序列,线程是在指令层次抢时间片的。但是这个指令有时候跟我们的假设不一样。
比如上面那行代码通常情况下到了指令层次之后,可以划分为三个动作:
- 分配一片内存;
- 在这片内存上执行初始化操作;
- 将得到的内存地址赋值给m_instance;
是这三个动作没错,但是到了指令层面之后,它们的顺序却可能由于编译器优化而被打乱成下面这样:
- 分配一片内存;
- 将最后得到的内存地址赋值给m_instance;
- 在这片内存上执行初始化操作;
看到了吗?第二步和第三步的顺序可能会被颠倒!
1 Singleton* Singleton::getInstance(){
2 if(m_instance == nullptr){
3 Lock lock;
4 if(m_instance == nullptr){
5 m_instance = new Singleton();
6 }
7 }
8 return m_instance;
9 }
现在再次回到之前的场景,假设有两个thread,thread A执行第5步之后,由于编译器优化而执行了:
- 分配一片内存;
- 将最后得到的内存地址赋值给m_instance;
第三步还没来得及执行,时间片就被thread B抢走了,由于此时m_instance已经被赋予了地址,因此m_instance不再为空!当thread B再次进入函数体之后,由于第2步判断m_instance是否为空的结果为false,导致被直接返回。而事实上m_instance并没有完成初始化操作,此时还不能使用。
当这个问题被发现后,由于是编译器优化导致了此类问题的出现,于是人们敦促编译器厂商给出问题解决方案。
Volatile Singleton
反过来想想,编译器优化的目的是提升程序性能,只是不巧导致了这个问题的出现,如果为了一个单例模式的实现直接禁止这种优化,属实有点说不过去。这个时候java和C#就很聪明,在各自的语言中加了一个关键字:Volatile,其作用也很直截了当:禁止指令重排。
C++呢?Visual C++嫌标准委员会动作太慢,2005年左右,在自家编译器里也加入了volatile关键字,但是由于是个人行为,很显然不能跨平台。之后C++11正式将volatile作为关键字纳入标准:
class Singleton {
public: static Singleton* instance() { if (pInstance == 0) { Lock lock; if (pInstance == 0) { pInstance = new Singleton; } } return pInstance; }
private: static Singleton * volatile pInstance; Singleton(){ }
};
volatile这个关键字有两层语义:
第一层语义是可见性。可见性指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中,即看到的都是最新的结果。
第二层语义是禁止指令重排序优化。我们写的代码(尤其是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。
Atomic Singleton
另外在C++11 将原子操作纳入了标准,我们可以通过标准提供的原子操作来处理该问题。
通过给原子变量设置 std::std::memory_order_xxx
来防止 CPU 的指令重排操作。
//C++11版本之后的跨平台实现(volatile)std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;Singleton* Singleton::getInstance(){Singleton* tmp = m_instance.load(std::memory_order_relaxed);std::atomic_thread_fence(std::memory_order_acquire);//获取内存fenceif(tmp == nullptr){std::lock_guard<std::mutex> lock(m_mutex);tmp = m_instance.load(std::memory_order_relaxed);if(tmp == nullptr){tmp = new Singleton;std::atomic_thread_fence(std::memory_order_relaced);//释放内存fencem_instance.store(tmp,std::memory_order_relaxed);}}return tmp;
}
Meyers Singleton
《Effective C++》的作者Meyer,在<<Effective C++>>3rd Item4中,提出了一种到目前为止最简洁高效的解决方案:
template<typename T>
class Singleton
{
public:static T& getInstance(){static T value;return value;}private:Singleton();~Singleton();
};
非常优雅的一种实现。
先说结论:
- 单线程下,正确。
- C++11及以后的版本(如C++14)的多线程下,正确。
- C++11之前的多线程下,不一定正确。
原因在于在C++11之前的标准中并没有规定local static变量的内存模型,所以很多编译器在实现local static变量的时候仅仅是进行了一次check(参考《深入探索C++对象模型》),于是getInstance函数被编译器改写成这样了:
bool initialized = false;
char value[sizeof(T)];T& getInstance()
{if (!initialized){initialized = true;new (value) T();}return *(reinterpret_cast<T*>(value));
}
于是乎它就是不是线程安全的了。
但是在C++11却是线程安全的,这是因为新的C++标准规定了当一个线程正在初始化一个变量的时候,其他线程必须得等到该初始化完成以后才能访问它。
附:C++静态对象的初始化
non-local static对象(函数外)
C++规定,non-local static 对象的初始化发生在main函数执行之前,也即main函数之前的单线程启动阶段,所以不存在线程安全问题。但C++没有规定多个non-local static 对象的初始化顺序,尤其是来自多个编译单元的non-local static对象,他们的初始化顺序是随机的。
local static 对象(函数内)
对于local static 对象,其初始化发生在控制流第一次执行到该对象的初始化语句时。多个线程的控制流可能同时到达其初始化语句。
在C++11之前,在多线程环境下local static对象的初始化并不是线程安全的。具体表现就是:如果一个线程正在执行local static对象的初始化语句但还没有完成初始化,此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该local static对象的构造函数中。这会造成这个local static对象的重复构造,进而产生内存泄露问题。所以,local static对象在多线程环境下的重复构造问题是需要解决的。
而C++11则在语言规范中解决了这个问题。C++11规定,在一个线程开始local static 对象的初始化后到完成初始化前,其他线程执行到这个local static对象的初始化语句就会等待,直到该local static 对象初始化完成。
相关文章:
【设计模式】单例模式的前世今生
文章目录 引言简介起航!向“确保某个类在系统中只有一个实例”进发 ⛵️Lazy SingletonDouble-checked locking(DCL) SingletonVolatile SingletonAtomic SingletonMeyers Singleton 附:C静态对象的初始化 引言 说起单例模式&…...
厦门网上在线教育系统,线下老师怎么转型到线上网上授课?
现在很多 线下老师都想转到线上做网课,但是在转线上过程中会出现很多问题,很多人都不知道怎么开始,今天小编和大家说一下要注意的。 一、你要有一套适合线上的教学视频 首先你要准备做的课程内容是什么,怎么讲,讲什么&…...
Spring底层入门(九)
boot的执行流程分为构造SpringApplication对象、调用run方法两部分 1、Spring Boot 执行流程-构造 通常我们会在SpringBoot的主启动类中写以下的代码: 参数一是当前类的字节码,参数二是main的args参数。 public class StartApplication {public static…...
掌握Android Fragment开发之魂:Fragment的深度解析(下)
在上一篇文章中,我们深入探讨了Fragment 通信,包含Fragment 向 Activity 传递数据、Activity 向 Fragment 传递数据、Fragment 之间的通信方式。感兴趣的朋友,请前往查阅: 掌握Android Fragment开发之魂:Fragment的深度…...
小巧简单实用的Linux端口转发工具Rinetd
Linux下实现端口转发有很多种方法,尤其是在可以联网的情况下,更是容易。最近在资源受限的定制系统中,找到一个方便离线安装和使用的端口转发工具Rinetd,安装包仅几十K,而且有很多版本的Linux发行系统的支持。 1、安装…...
HackBar 新手使用教程(入门)
啥是Hackbar? Hackbar是一个Firefox 的插件,它的功能类似于地址栏,但是它里面的数据不受服务器的相应触发的重定向等其它变化的影响。 有网址的载入于访问,联合查询,各种编码,数据加密功能。 这个Hackbar可以帮助你在测试SQL注入,XSS漏洞和网站的安全性,主要是帮助…...
<Linux> 权限
目录 权限人员相对于文件来说的分类更改权限文件的拥有者与所属组umask粘滞位 权限 权限是操作系统用来限制对资源访问的机制,权限一般分为读、写、执行。系统中的每个文件都拥有特定的权限、所属用户及所属组,通过这样的机制来限制哪些用户、哪些组可以…...
Nacos Docker 快速部署----解决nacos鉴权漏洞问题
Nacos Docker 快速部署 1. 说明 1.1 官方文档 官方地址 https://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html docker启动文件的gitlhub地址 https://github.com/nacos-group/nacos-docker.git 问题: 缺少部分必要配置与说明 1.2 部署最新版本Nacos&…...
存储或读取时转换JSON数据
一、 数据库类型 二、使用Hutool工具 存储时将数据转换为JSON数据 获取时将JSON数据转换为对象 发现问题: 原本数据对象是Address 和 Firend但是转换完成后数据变成了JSONArray和JSONObject 三、自定义TypeHandler继承Mybatis的BaseTypeHandler处理器 package …...
Spring Data JPA的作用和用法
Spring Data JPA 是 Spring 框架的一个模块,它提供了一种数据访问抽象,允许以一种声明式和简洁的方式来处理数据库操作。它基于 Java Persistence API (JPA),是一个行业标准的 ORM(对象关系映射)规范,用于将…...
【go项目01_学习记录08】
学习记录 1 模板文件1.1 articlesStoreHandler() 使用模板文件1.2 统一模板 1 模板文件 重构 articlesCreateHandler() 和 articlesStoreHandler() 函数,将 HTML 抽离并放置于独立的模板文件中。 1.1 articlesStoreHandler() 使用模板文件 . . . func articlesSt…...
Java中的线程
一、创建线程的几种方式? ① 通过继承Thread类并重写run方法 ,实现简单但不可以继承其他类 Thread底层也是实现了Runnable接口,重写的是run而不是start方法 ②实现Runnable接口并重写run方法, 避免了单继承的局限性ÿ…...
顺序表的实现(迈入数据结构的大门)(完整代码)
seqlist.h #pragma once typedef int SLDataType;#include<stdio.h> #include<stdlib.h> #include<assert.h>typedef struct SeqList {SLDataType* a;int size; // 有效数据个数int capacity; // 空间容量 }SL;//初始化和销毁 void SLInit(SL* ps); void SL…...
neo4j-5.11.0安装APOC插件or配置允许使用过程的权限
在已经安装好neo4j和jdk的情况下安装apoc组件,之前使用neo4j-community-4.4.30,可以找到配置apoc-4.4.0.22-all.jar,但是高版本neo4j对应没有apoc-X.X.X-all.jar。解决如下所示: 1.安装好JDK与neo4j 已经安装对应版本的JDK 17.0…...
mybatis 中 #{}和 ${}的区别是什么?
在 MyBatis 中,#{} 和 ${} 是两种用于参数替换的语法,但它们之间存在一些重要的区别,主要体现在安全性、预编译和动态 SQL 上。 安全性: #{}:这是预编译处理,MyBatis 会为传入的参数生成 PreparedStatement…...
深入解析C#中的接口设计原则
深入解析C#中的接口设计原则 目录 深入解析C#中的接口设计原则 一、接口设计的SOLID原则 二、接口设计的最佳实践 三、接口设计的高级技术 四、结论 接口在面向对象编程中扮演着至关重要的角色。它们是定义行为契约的一种方式,允许实现者提供这些行为的具体实现…...
106短信群发平台在金融和法务行业的应用分析
一、金融行业应用 1.客户通知与提醒:银行、证券、保险等金融机构经常需要向客户发送各类通知和提醒,如账户余额变动、交易确认、扣费通知、理财产品到期提醒等。106短信群发平台可以快速、准确地将这些信息发送到客户的手机上,确保客户及时获…...
Spring AOP(2)
目录 Spring AOP详解 PointCut 切面优先级Order 切点表达式 execution表达式 切点表达式示例 annotation 自定义注解MyAspect 切面类 添加自定义注解 Spring AOP详解 PointCut 上面代码存在一个问题, 就是对于excution(* com.example.demo.controller.*.*(..))的大量重…...
Spring-依赖注入的处理过程
前置知识 1 入口 DefaultListableBeanFactory#resolveDependency 2 每个依赖都有对应的DependencyDescriptor 3 自定绑定候选对象处理器AutowireCapableBeanFactory 注入处理 我们可以看到接口AutowireCapableBeanFactory中有两个方法。 第一个是单个注入: Null…...
2.用python爬取的保存在text文件中的格式为MP4的视频url
文章目录 一、url的保存格式二、MP4视频获取 一、url的保存格式 爬取的视频名字和url保存在text文件中,每一个视频都是一个单独的text,其中text的文件名就是视频的名字,text内容是视频的下载url,并且所有的text都保存在同一个文件…...
Java基于B/S医院绩效考核管理平台系统源码java+springboot+MySQL医院智慧绩效管理系统源码
Java基于B/S医院绩效考核管理平台系统源码javaspringbootMySQL医院智慧绩效管理系统源码 医院绩效考核系统是一个关键的管理工具,旨在评估和优化医院内部各部门、科室和员工的绩效。一个有效的绩效考核系统不仅能帮助医院实现其战略目标,还能提升医疗服…...
UE 蓝图堆栈调试
蓝图打断点后如果想查看断点前的执行逻辑,Tools→Debug→BlueprintDebugger 然后打断点运行,执行顺序是从下往上...
UE4_摄像机_使用摄像机的技巧
学习笔记,不喜勿喷!祝愿生活越来越好! 知识点: a.相机跟随。 b.相机抖动。 c.摄像机移动 d.四元数插值(保证正确旋转方向)。 e.相机注视跟踪。 1、新建关卡序列,并给小车添加动画。 2、创…...
ssm115乐购游戏商城系统+vue
毕业生学历证明系统 设计与实现 内容摘要 如今社会上各行各业,都喜欢用自己行业的专属软件工作,互联网发展到这个时候,人们已经发现离不开了互联网。新技术的产生,往往能解决一些老技术的弊端问题。因为传统毕业生学历信息管理难…...
【可实战】被测需求理解(需求文档是啥样的、从哪些角度进行需求评审、需求分析需要分析出哪些内容、如何提高需求分析能力)
产品人员会产出一个需求文档,然后组织一个需求的宣讲。测试人员的任务就是在需求宣讲当中,分析需求有没有存在一些问题,然后在需求宣讲结束之后通过分析需求文档,分析里面的测试点并预估一个排期。 一、需求文档是什么样的&#x…...
伪类和伪元素的区别是什么?
一、两者的定义 1.伪类(pseudo-class)是一个以冒号作为前缀,被添加到一个选择器末尾的关键字,当你希望样式在特定状态才被呈现到指定的元素时,你可以往元素的选择器后面加上对应的伪类。 2.伪元素用于创建一些不在文档…...
gorm-sharding分表插件升级版
代码地址: GitHub - 137/gorm-sharding: Sharding 是一个高性能的 Gorm 分表中间件。它基于 Conn 层做 SQL 拦截、AST 解析、分表路由、自增主键填充,带来的额外开销极小。对开发者友好、透明,使用上与普通 SQL、Gorm 查询无差别.解决了原生s…...
MoviePy(Python音视频开发)
音视频基础帧率、码率、分辨率视频格式H.264和H.265视频压缩算法 Moviepy常见剪辑类VideoFlieClipImageFlieClipColorClipTextClipCompositeVideoClipAudioFlieClipCompositeAudioClip 常见操作音视频的读入与导出截取音视频 音视频基础 帧率、码率、分辨率 体积(V…...
Spring中的FileCopyUtils:文件复制的利器与详解
1. 概述 在Spring框架中,FileCopyUtils是一个用于文件复制操作的实用工具类。它提供了一系列静态方法,简化了文件从输入流到输出流、从文件到文件等的复制过程。这些方法都基于NIO(New I/O)技术,提供了高效的文件复制…...
【操作系统】读者—写者问题python解析
一个数据问价或记录可以被多个进程共享,我们把只读该文件的进程称为“读者进程”,其他进程为“写者进程”。允许多个进程同时读一个共享对象,但不允许一个写者进程和其他写者进程或读者进程同时访问共享对象。即:保证一个写者进程…...
网站后台功能需求/目前在哪个平台做推广好
科学研究的目的是通过观察、实验和思考来更好地理解我们周围的世界,从而改进我们的生活和社会。科学研究可以帮助我们解决许多关键问题,比如改善医疗和公共卫生、保护环境、提高农业生产率、发明新技术和产品等。 基础研究是科学研究的一种,它…...
青岛网站设计价格/公司个人怎么做网络推广
我在reactjs中使用formik,有一个onsubmit方法,它有一个setsubmitting参数。我想把它传给我的“然后”方法,但不知道怎么做。onSubmit{(values, { setSubmitting, setErrors }) > {setSubmitting(true);// how can I get away from doing this.const submitting setSubmitti…...
量子秘密网站怎么做/网站分析培训班
(注意!非广告洗脑文,请注意甄别。) 执行力不够怎么办? 今天想学习3个小时内容,可惜只坚持了30分钟。 今天想把某功能用例写完,可惜只写了一半。 今天想开发个自动化脚本,可惜开个…...
济南网站建设公司有哪些/百度seo快速见效方法
OpenVAS漏洞扫描基础教程之创建用户 OpenVAS管理服务 默认情况下,OpenVAS服务仅创建了一个名为admin的用户,而且是管理员用户(拥有最高的权限)。如果想要其它客户端登陆的话,不可能都以管理员身份访问,否则…...
网站开发怎么谈/seo超级外链工具免费
爬取网站时经常会遇到需要登录的问题,这是就需要用到模拟登录的相关方法。python提供了强大的url库,想做到这个并不难。这里以登录学校教务系统为例,做一个简单的例子。 首先得明白cookie的作用,cookie是某些网站为了辨别用户身份…...
公众号添加wordpress/给公司做网站要多少钱
在事业单位考试中,直言命题的上下反对关系是一个高频考点。为此,这里给大家提供了上下反对巧解模型。一、上反对关系-----“所有是”与“所有非”在直言命题中上反对关系常见呈现形式是:“所有是”与“所有非。1、特性:上反对关系…...