2. 抽象类中的成员变量可以是各种类型的(可以有非抽象的类型); 而接口中的成员变量 只能是 public static final 类型(常量类型)
从设计目的上面回答:
抽象类的设计目的,是代码复用;
当不同的类具有某些共同的行为的时候,可以把这些行为抽取出来,派生成一个抽象类;
抽象类是对类本质的抽象; 抽象类包含并实现子类的通用特性,将子类中存在差异的特性进行抽象,交由子类实现
接口的设计目的,是对类的行为进行约束,要求不同的类具有相同的行为,只是约束行为的有无,并不关心具体实现的细节;
从使用场景上面来回答
抽象类的功能要远超过接口,但是定义抽象类的代价高,因为 抽象类只能是单继承的; 而接口 是多实现的
List: 有序的,并且允许为空,允许重复,可以使用iterator取出所有的值,并逐一遍历,并可以根据下标获取对应的值;
Set: 无序的,不允许为空,不允许重复, 取元素的时候只能使用iterator 来获取所有的值并遍历;
HashCode()的作用是获取哈希码,也称为散列值;它实际上是返回一个int整数. 这个哈希码的作用是确定该对象在哈希表中的索引位置.hashCode() 定义在 JDK的object.Java中,Java中的任何类都包含了hashCode()函数. 散列表存储的是键值队(key-value) 他的特点是: 可以根据“键”快速的检索出对应的“值” .这其中就利用到了散列码! 可以快速找到所需要的对象
为什么要有hashCode?
以“HashSet如何检查重复”来说明为什么要有hashCode:
对象加入HashSet时,HashSet会先计算对象的hashCode值来判断对象加入的位置,看该位置是否有值,如果没有,hashSet会假设对象没有重复出现;但是如果发现有值,这时回调用equal() 方法来检查两个对象是否真的相同,如果两个对象真的相同,hashSet 就不会让其加入操作成功. 如果不一样的话,就会重新散列到其他位置. 这样就大大减少了equal的次数,相应的就提高了执行速度.
ArrayList: 基于动态数组,连续内存存储,适合下标访问(随机访问); 扩容机制: 因为数组长度固定,超出长度存数据的时候,需要新建数组,然后将旧数组中的数据拷贝到新的数组中. 如果不是 尾部插入数据(例如在中间插入数据)还会涉及到元素的移动(往后复制一份,插入新元素); 使用 尾插法 并指定初始容量可以极大的提升性能,甚至超过LinkedList(因为linkedList需要维护大量的Node节点对象)
LinkedList: 基于链表,可以存储在分散的内存中,适合做数据的插入和删除操作,不适合查询; 需要逐一遍历,遍历LinkedList 必须使用iterator,不能使用for循环,因为每次for循环,循环体内通过get(i)获取某一元素的时候都需要对list值重新遍历,性能消耗极大; 另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexOf 对list进行遍历,当结果为空时会遍历整个列表
区别:
底层实现:
数组+链表(双向)实现
jdk8开始,链表的高度达到8,长度达到64, 链表就会转换为红黑树(平衡二叉树), 元素内部类Node节点存在;
jdk1.7:
数据结构: ReentrantLock(可重入锁)+Segment(分段锁)+HashEntry . 一个Segment包含一个HashEntry数组,每个HashEntry又是一个链表结构;
元素查询: 二次hash, 第一次hash定位到 Segment,第二次Hash定位到元素所在的链表的头部
锁: Segmment分段锁 Segment继承了ReentrantLock(可重入锁),锁定操作的Segment(分段锁),其他的Segment不受影响,并发度为Segment个数,可以通过构造函数指定,数组扩容不会影响其他的Segment(分段锁)
get方法无需加锁, 使用volatile 可以让多线程相互可见
JDK1.8:
数据结构: synchronized+CAS+Node+红黑树 , Node的 Val和next 都是使用volatile修饰,保证可见性
查找,替换,赋值操作都是用CAS
锁: 锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作,并发扩容
读操作无锁: Node的val和 next是用volatile修饰,读写线程对该变量互相可见
Java中的编译器和解释器
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的主机.这台虚拟的机器在任何平台上都提供给编译程序的一个共同的接口.
编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行.在Java中,这种供虚拟机理解的代码叫做字节码(也就是以.class结尾的文件),它不面向任何特定的处理器,只面向虚拟机.
每一种平台的解释器是不同的,但是实现的虚拟机是相同的. Java源程序经过编译器编译后变为字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码发送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这也就解释了Java的编写与解释并存的特点.
执行步骤:
Java源代码--->编译器---->jvm可执行的java字节码---->jvm----->jvm中解释器---->机器可以执行的二进制机器码---->程序运行
采用字节码的好处
多平台,编写一次,到处运行
jdk自带的有3个类加载器, 1⃣️BootstrapClassLoader 2⃣️ExtClassLoader 3⃣️AppClassLoader
BootstrapClassLoader: 是 ExtClassLoader的父类, 负责加载 %JAVA_HOME%/lib下的 jar包和 class文件
ExtClassLoader 是 AppClassLoader的父类, 负责加载 %JAVA_HOME%/lib/ext 文件夹下的jar包和class类
AppClassLoader是自定义类加载器的父类, 负责加载 classpath下的类文件, 系统类加载器,线程上下文加载器,继承ClassLoader实现自定义类加载器
Java中所有的异常都来自顶级父类Throwable.
Throwable下面有2个子类 Exception和Error
Error是程序无法处理的错误,一旦出现了这个错误,表示程序无法处理,会被迫停止运行,例如: (OOM 内存溢出异常);
Exception: 不会导致程序停止,又分为2个部分: RunTimeException(运行时异常) 和 CheckedException(检查异常,编译器无法通过)
RunTimeException发生在程序运行的过程中,会导致当前线程执行失败;
CheckedException 发生在编译过程中,回导致线程编译不通过
轻量级的开源的J2EE框架. 是一个容器框架; 用来装Java Bean(Java对象) 的,管理Java bean; 中间层框架(万能胶) 可以起到一个连接的作用; 比如说可以和 Struts和 Hibernate连接在一起使用,可以让企业开发效率更快,更简洁
Spring是一个轻量级的控制反转(IOC) 和 面向切面(AOP)的容器框架
高内聚低耦合
)
Spring的配置文件是一个xml的配置文件,这个文件包含类的信息,描述如何配置他们,以及类之间的相互调用关系
IOC 控制反转,SpringIOC 来创建对象,管理对象; 通过依赖注入DI 来 装配对象,配置对象,并且管理这些对象的生命周期
控制反转: 就是之前创建对象是通过 new一个对象或者newInstance 来获取一个对象,现在把创建对象的权利交给Spring 来进行管理,只需要描述需要创建的对象即可.
容器概念、控制反转、依赖注入
IOC容器:
实际上就是一个map(key,value),里面存放的是各种对象(在XML里面撇胡子的bean节点,@repository @service 、@controller、@component) 在项目启动的时候会读取配置文件中的bean节点,会根据全限定类名使用反射创建对象放在map中,扫描到打上注解的类, 然后通过反射创建对象,放到map里
这个时候,map里面已经有很多的对象了,接下来我们在代码里需要用到里面的对象时,再通过DI
注入(autowired、resource等注解,XML里bean节点的ref属性,项目启动的时候会读取XML节点ref属性,根据ID注入,也会扫描注解,根据类型或者ID注入,ID就是对象名
控制反转:
没有引入IOC容器之前,对象A依赖于对象B,那么,对象A在初始化的时候或者运行到某一点的时候,自己需要主动创建对象B或者使用已经创建的对象B,控制权都在自己手上.
引入IOC容器之后,对象A与对象B之间就失去了直接联系,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方.
通过前后的对比,可以看出来,对象A获得了 依赖对象B的过程,由 主动行为变成了被动行为
控制权颠倒过来了,这也就是控制反转的意思
全部对象的控制权全部交给第三方“IOC容器” ,所以这个IOC容器是整个系统的关键核心,起到了一个类似粘合剂
的作用,把系统中的所有对象黏在了一起发挥作用,如果没有这种“粘合剂”, 对象和对象之间就会彼此失去联系,这也有人把IOC容器比喻为“粘合剂”的由来.
依赖注入:
获得依赖对象的过程被反转了
控制被反转之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入.
依赖注入是实现IOC的方法,就是由IOC容器在运行期间,动态的将某种依赖关系注入到对象之中.
IOC(或DI) 依赖注入把代码量降低,实现了松耦合
详细点回答如下:
AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。
OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。
不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此。
这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP技术恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为“Aspect”,即切面。
“切面”: 简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
使用“横切”技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。
业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。AOP核心就是切面,它将多个类的通用行为封装成可重用的模块,该模块含有一组API提供横切功能。比如,一个日志模块可以被称作日志的AOP切面。根据需求的不同,一个应用程序可以有若干切面。在Spring AOP中,切面通过带有@Aspect注解的类实现。
● BeanFactory
基础类型的IOC容器,提供完成的IOC服务支持。如果没有特殊指定,默认采用延迟初始化策略。相对来说,容器启动初期速度较快,所需资源有限。
● ApplicationContext
ApplicationContext是在BeanFactory的基础上构建,是相对比较高级的容器实现,除了BeanFactory的所有支持外,ApplicationContext还提供了事件发布、国际化支持等功能。ApplicationContext管理的对象,在容器启动后默认全部初始化并且绑定完成。
第二种说法:
ApplicationContext 是BeanFactory的子接口;
ApplicationContext 提供了更加完整的功能
详细解析:
如果Bean的某个属性没有注入,BeanFactory加载后,直到第一次使用getBean()方法的时候才抛出异常
在容器启动的时候就会一次性加载所有的Bean
.这样,在容器启动的时候,我们就可以发现Spring中的配置错误,有利于检查依赖的属性是否注入. ApplicationContext启动后预载入所有的单实例Bean, 通过预载入单例bean, 确保第一次使用的时候,直接可以使用,不需要等待,因为已经创建好了BeanFactory需要手动注册,而ApplicationContext 是自动注册
平常的java开发中,程序员在某个类中需要依赖其它类的方法,则通常是new一个依赖类再调用类实例的方法,这种开发存在的问题是new的类实例不好统一管理,spring提出了依赖注入的思想,即依赖类不由程序员实例化,而是通过spring容器帮我们new指定实例并且将实例注入到需要该对象的类中。依赖注入的另一种说法是“控制反转”,通俗的理解是:平常我们new一个实例,这个实例的控制权是我们程序员,而控制反转是指new实例工作不由我们程序员来做而是交给spring容器来做。
常用的是构造器注入和setter方式注入
5种
Spring中的Bean默认是单例模式的,框架并没有对bean进行多线程的封装处理.
如果bean是有状态的,那就需要开发人员自己来进行线程安全的保证,最简单的方法就是改变bean的作用域,把singleton(单例模式) 改为 prototype(原型模式),这样每次请求bean就相当于new Bean() 这样就可以保证线程的安全了
Dao会操作数据库Collection连接,数据库连接是有状态的,比如说数据库事务,Spring的事务管理器使用ThreadLocal 为不同的线程维护了一套独立的C onnection副本,保证线程之间不会相互影响(Spring是如何保证事务获取同一个Connection的, 使用一个关键字叫做: volatile 关键字 ) ThreadLocal
不要在bean中声明任何有状态的实例变量或者类变量,如果必须如此,不那么久使用ThreadLocal 把变量编程线程私有的,如果bean的实例变量或者类变量需要在多个线程之间共享,那么久只能使用synchronized, lock, CAS 这些实现同步的方法了.
Spring中的BeanFactory就是一个简单工厂模式的体现,根据器传入一个唯一的标识来获得Bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来决定
实现了FactoryBean接口的bean的类叫做 factory的bean 特点是: Spring会在使用getBean() 调用获得该bean的时候,会自动调用该bean的getObject() 方法,所以返回的不是factory这个bean, 而是这个bean.getObject() 方法的返回值
保证一个类仅有一个实例,并提供一个访问这个类的全局访问点
Spring 对单例的实现: Spring中的单例模式实现了后半句话, 就是提供了全局的访问点: BeanFactory
但是没有从构造器级别去控制单例,这是因为Spring管理的是任意的Java对象
spring定义了一个 适配接口
,使得每一种Controller有一种对应的适配器实现类, 让适配器代替controller执行相应的方法. 这样在扩展controller的时候,只需要增加一个适配器类久完成了 Spring MVC 的扩展了
动态的给一个对象添加一些额外的职责
就增加功能来说, Decorator 装饰器
模式比生成子类更加灵活Spring中用到的包装饰器模式在类名上有2种表现: 一种是类名中含有Wrapper, 另一种是类名中含有Decorator
Spring 框架中的资源访问Resource接口
该接口提供了更强的资源访问能力, Spring框架本身大量使用了Resource 接口来访问底层资源
切面在应用运行的时候被织入. 一般情况下,在织入切面时,AOP容器会为目标对象创建动态的创建一个代理对象,SpringAOP 就是以这种方式织入切面的.
织入: 把切面应用到目标对象,并创建新的代理对象的过程
Spring 的事件驱动模型使用的就是 观察者模式
Spring中的Observer(观察者模式) 模式常用的地方是 listener的实现
就是不需要在Spring的配置文件中描述bean之间的关系,IOC容器会自动建立bean之间的关系
自动装配的方式
开启自动装配,只需要在xml配置文件<bean> 中定义“autowire"属性
<bean id="customer" class="com.wlc.Customer" autowire=""/>
autowire属性有5种装配方式:
手动装配: 以value 或者ref的方式明确指定属性值都是手动装配, 需要通过‘ref’属性来指定连接的bean
Customer的属性名称是person, Spring会将 bean ID 为 person的bean 通过setter 方式进行自动装配 <bean id= "customer" class = "com.wlc.Customer" autowire="byName"/> <bean id="person" class = "come.wlc.Person"/>
Customer 的属性person的类型是Person, Spring会将Person类型通过setter方式进行自动装配 <bean id="customer" class="com.wlc.Customer" autowire="byType"/> <bean id ="person" class="com.wlc.Person"/>
Customer 构造函数的参数是person 的类型是Person, Spring会将Person类型通过构造方法进行自动装配 <bean id="customer" class="com.wlc.Customer" autowire="construtor"/> <bean id="person" class="com.wlc.Person" />
使用注解@Autowired
自动装配bean ,可以在 字段、setter方法 、构造函数上面使用
在Java文件中使用注解来实现在xml中的相同的功能,例如 使用@Bean注解,可以 返回一个Bean对象
相对于xml的配置,是在配置文件中使用 <> 标签进行声明;
注解的话是在类或者方法或者属性上面使用注解的方式直接在组建类中进行配置
关注点就是Java中我们要实现的功能,这些功能的方法;
横切关注点:而这些方法有些功能在其他的方法中也会用到, 所以把这些方法抽取出来,也就是横切关注点
连接点: 就是被拦截到的方法;因为在Spring中,只支持方法类型的连接点
切入点: 就是告诉具体是在什么位置执行;通过表达式或者匹配的方式指明具体位置
通知是在方法之前或者之后要做的内容,也就是方法执行之后触发的代码片段;
通知有:
spring事务的原理是AOP, 进行了切面增强, 那么失效的根本原因是这个AOP不起作用了,常见的情况有如下几种:
@Transactional 只能用于public
修饰的方法上面, 否则事务会失效; 如果非要用在非public 方法上面,可以开启 AspectJ 代理模式
例如mysql 的 MyISAM
模式
多个事务方法之间进行相互调用,事务是如何在这些方法之间传播的
https://blog.csdn.net/likun557/article/details/118248305
(如果serviceA.method 开了事务,那么serviceB就加入到serviceA中来运行,如果serviceA.method 没有开启事务,那么serviceB自己也不开启事务)
(serviceB.method强制性自己开启一个新的事务,然后serviceA.method 的事务会被卡住,等serviceB事务完成了自己再继续. 这就是影响会滚了,如果serviceA 报错了,serviceB是不会受到影响的; serviceB报错了,serviceA 也可以选择性的会滚或者提交)
(serviceB.method不支持事务,servcieA的事务执行到serviceB的那里,就挂起来了,serviceB用非事务的方式运行结束,serviceA事务再继续执行; 这个好处就是 servcieB的代码报错不会让serviceA会滚)
(不能被一个已经开启了事务的方法来调用,serviceA.method开启了事务,那么调用serviceB.method 就会报错)
(开启嵌套事务,serviceB 开启了一个子事务,如果会滚的话,那么serviceB就会滚到开启了子事务的这个save point)
在使用Spring框架的时候,可以有2种实用事务的方式. 1⃣️编程式事务 2⃣️声明式事务 @Transaction 注解就是声明式的
首先,事务这个概念是数据库层面的,Spring只是基于数据库中的事务做了扩展,以及提供了一些能够让程序员更加方便的操作事务的方式.
比如我们可以通过在某个方法上面增加@Transactional注解,就可与你开启事务, 这个方法中所有的sql都会在一个事务中执行,要么都成功,要么都失败!
在一个方法上加了@Transaction注解之后,Spring会基于这个类生成一个代理对象,会将这个代理对象作为bean,当在使用这个代理对象的方法时,如果这个方法上有@Transactional注解
,那么代理逻辑会先把事务的自动提交设置为false(手动提交), 然后再去执行原来的业务逻辑方法,如果执行业务逻辑方法没有出现异常,那么代理逻辑中就会将事务进行提交, 如果执行业务逻辑方法中出现了异常,那么则会将事务进行会滚.
当然针对哪些异常进行事务回滚是可以进行配置的,可以利用@Transactional注解的 rollbackFor属性设置,默认情况下是会对RuntimeException
和 Error
进行会滚
Spring中的事务隔离级别就是数据库中的事务隔离级别, 外加了一个默认隔离级别
数据库配置的隔离级别是 Read committed , 而 Spring配置的隔离级别死Repeatable Read ,那么这个时候的隔离级别是按照哪个为准?
以Spring配置的为准,如果Spring设置的隔离级别数据库不支持,效果取决于数据库
参考
https://blog.csdn.net/keenstyle/article/details/104820909
Spring
spring是一个IOC容器,用来管理Bean,使用依赖注入DI的方式实现控制反转,可以很方便的整合各种框架,提供AOP机制,弥补OOP重复代码的问题, 可以更加方便的对不同的类不同方法中的共同处理抽取出来形成切面,然后自动注入给方法执行, 比如: 日志,异常,性能等等
SpringMVC
springMVC 是Spring对web框架的一个解决方案,提供了一个总的前端控制器DispatcherServlet ,用来接受轻轻,然后定义了一套路由策略,根据url 映射到具体的handler ,以及适配handler, 将handler的结果使用视图解析技术生成视图展现给前端
SpringBoot
SpringBoot是Spring提供的 一个快速开发工具包,让程序更加方便的,快速的开发,相当于 : Spring+SpringMVC 的结合体, 核心是: 约定大于配置, spring boot整合了一些列的starter, 只需要引入starter 就可以开箱即用; 例如: redis,rabbitmq,es , mybatis 等等
Handler: 也就是处理器. 它直接对应着MVC中的C 也就是Controller 层, 它的具体表现形式有很多,可以是类,可以是方法. 在controller层中@RequestMapping标注的所有方法都可以堪称是一个Handler ,只要可以试剂处理请求的就是Handler
主要的组件有:
initHandlerMappings(context), 处理器映射器,根据用户请求的资源URL来查询Handler的. 再SpringMVC中会有很多请求,每个请求都需要一个Handler处理, 具体接收到一个请求之后使用哪个Handler进行,这就是HandlerMapping需要做的事情
initHandlerAdapters(context) 适配器, 因为SpringMVC 中的Handler可以是任意的形式,只要能处理请求的就OK,但是Servlet需要的处理方法的结构是固定,都是以Request 和Response为参数的方法. 如何让固定的Servlet处理方法调用灵活的Handler来进行处理? 这就是
HandlerAdapter
要做的事情.
Handler
是用来干活的工具;
HandlerMapping
是用来根据需要干的活着到对应的工具;
HandlerAdapter
是使用工具的人
initHandlerExceptionResolvers(context) ,其他组件都是用来干活的. 在干活的时候难免会出现问题,出问题后怎么办呢? 这就是需要有一个专门的角色对异常情况进行处理,在Spring MVC中就是 HandlerExceptionResolver
具体来说,这个组件的作用是根据异常设置ModelAndView, 然后再交给render方法来进行渲染
initVIewResolvers(context), viewResolver 是用来将String类型的视图名和Locale解析为view 类型的视图. view是用来渲染页面的,也就是将程序返回的参数填入到模板里,生成html文件,也可能是其他类型的文件.
这里关键点: 使用哪个模板? 用什么技术填入参数? 这个也就是ViewResolver 主要要做的工作, ViewResolver需要找到渲染所用的模板和所用的技术(也就是视图的类型) 进行渲染,具体渲染的过程则交给不同的视图自己完成
initRequestToViweNameTranslator(context) ,ViewResolver 是根据view Name查找view, 但是有的Handler 处理完成之后并没有设置view也没有设置ViewName, 这时就需要从request获取ViewName了, 如何从request中获取ViewName 就是RequestToViewNameTranslator要做的事情了, RequestToViewNameTranslator在SpringMVC 容器里面只可以配置一个,所以,所有的request到ViewName 的转换规则都要在一个Translator里面全部实现
initLocaleResolver(context) 解析试图需要2个参数: 1⃣️视图 2⃣️Locale , 视图名是处理器返回的,Locale是从哪里来的? 这就是LocalResolver要做的事情. LocaleResolver 用于从request解析出Locale, Locale就是 zh-cn 之类, 表示一个区域,有了这个就可以对不同区域的用户显示不同的结果,
SpringMVC 主要有2个地方用到了Locale: 一个是ViewResolver 视图解析的时候; 另一个是国际化资源或者主题的时候
initThemeResolver(context) 用于解析主题, SpringMVC 中有一个主题对应一个properties文件,里面存放着跟当前主题相关的所有资源,如图片,Css 样式等, SpringMVC 的主题也支持国际化,同一个主题不同区域也可以显示不同的风格, Spring MVC中跟主题相关的类有
ThemeResolver, ThemeSource 和Theme 主题是通过一些列资源来具体体现的, 要得到一个主题的资源,首先要得到资源的名称,这时ThemeResolver的工作,然后通过主题名称找到对应的主题(可以理解为一个配置) 文件, 这是ThemeSource 的工作,最后从主题获取资源就可以了
initMultipartResolver(context) 用于处理上传文件的请求,处理方法是将普通的request包装成MultipartHttpServletRequest , 这个可以直接调用getFile 方法获取File ,如果上传多个文件,还可以调用getFileMap得到FileName -> 的file结构的map.
这个组件中有3个方法:
initFlashMapManager(conntext) 用来管理FlashMap 的, FlashMap 主要用在redirect中传递参数
@Import + @Configuration + Spring spi 机制
自动配置类由各个starter提供,使用@Configuration+ @Bean 定义配置类, 放到 META-INF/spring.factories下
使用Spring SPI 扫描META-INF/spirng.factories下的配置类
使用@import 导入自动配置类
使用Spring+SpringMVC 时候,如果需要引入mybatis等框架,需要到xml 中定义mybatis需要的bean
starter就是定义一个starter的jar包,写一个@configuration配置类, 将这些bean定义在里面,然后在starter包的META-INF/spring.factories中写入该配置类,Spring boot会按照约定来配置该配置类
开发人员只需要将响应的starter包依赖进应用,进行响应的属性配置(使用默认配置时,不需要配置) 就可以直接进行开发了, 使用对应的功能了. 例如: mybatis-spring-boot-starter
, spring-boot-starter-redis
节省了下载安装tomcat ,应用不需要再打成war包, 然后放到webapp 目录下面再运行
只需要安装jvm虚拟机, 就可以直接在上面部署运行程序了
因为spring boot已经内嵌了tomcat.jar 运行main方法的时候就会启动tomcat, 并利用tomcat的spi机制加载Spring MVC
Mybatis 是半自动的ORM框架; Hibernate是全自动的ORM框架;
Mybatis 适用于有复杂业务的sql 的场景中; Hibernate适用于简单的sql场景中;
Mybatis 可以让程序员控制sql语句; hibernate则完全由框架本身管理
#{} 是预编译处理,是占位符; ${} 是字符串替换,是拼接符
Mybatis 在处理#{} 时候,会将sql中的#{} 替换为? 号, 调用PreparedStatement 来赋值;
Mybatis 在处理${} 时, 就是将${} 替换成变量的值, 调用Statement来赋值;
#{} 变量替换是在DBMS中, 变量替换之后,#{} 对应的变量自动加上单引号;
${}的变量替换是在DBMS外,变量替换之后, ${} 对应的变量不会加上单引号;
使用#{} 可以有效的方式SQL注入,提高系统安全性
Mybatis 只支持针对 ParameterHandler
、ResultSetHandler
、StatementHandler
、Executor
这4种接口的插件, Mybatis使用
JDK的动态代理, 为需要拦截的借口生成代理对象,以实现借口方法拦截功能,每当执行这4种接口方法的对象时,就会进入拦截方法,具体就是InvocationHandler
的invoke()方法, 拦截哪些你需要拦截的方法
编写插件
实现Mybatis 的Interceptor接口并复写intercept()方法, 然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可, 在配置文件中配置编写的插件
# 使用注解 @Intercepts({@Signature(type=StatementHandler.class,method="query",args = {Statement.class,ResultHandler.class}), @Signature(type=StatementHandler.class,method="update",args = {Statement.class,ResultHandler.class}), @Signature(type=StatementHandler.class,method="batch",args = {Statement.class,ResultHandler.class})}) 再使用@Componment 交给Spring容器管理 使用 invocation.proceed() 执行具体的业务逻辑,可以在这个方法上下执行要拦截的功能
索引用来快速的查找哪些具有特定值的记录. 如果没有索引,一般来说需要查询遍历整张表,效率非常低;
索引的原理: 就是把无序的数据变成有序的查询
相同点: 都是B+树的数据结构
聚族索引
将数据和索引放在一起, 按照一定的顺序排序组织的结构, 找到了索引,也就找到了 数据, 数据的物理顺序和索引的顺序是一致的. 只要索引是相邻的,那么对应的数据一定也是相邻的存放在磁盘上面的
非聚族索引
叶子节点不存储数据,存储的是数据行的地址,也就是说根据索引查找数据行的位置,再去磁盘查找数据,相当于二次查找, 这个就有点查找一本书的目录,比如要查找第三章第一节,可以在这个目录里面查找,找到对应的页码之后再去对应的页码查找文章
聚族索引的优势:
聚族索引的劣势:
Optimize Table
优化表,因为必须被移动的行数据可能造成碎片,使用独享空间可以弱化碎片;
InnoDB中一定有主键,主键一定是聚族索引,不手动设置,会使用unique索引(唯一索引) ,没有unique索引(唯一索引) ,则会使用数据库内部的一个行的隐藏ID作为主键来作为主键索引 在聚族索引之上创建的索引称为辅助索引,辅助索引访问数据总是要进行二次查找, 非聚族索引都是辅助索引,像 复合索引,前缀索引,唯一索引, 辅助索引叶子节点存储的不再是行的物理位置,而是主键值;
MyISAM 使用的是非聚族索引, 没有聚族索引, 非聚族索引的两颗B+树 看上去没有什么不同,节点的结构完全一致,只是存储的内容不同而已
主键索引B+树 的节点存储了主键, 辅助索引B+树 存储了辅助键. 表数据存储在独立的地方,这2颗B+树的叶子节点都是用一个地址只想真正的表数据,对于表数据来说,这2个键没有任何差别,由于索引树是独立的,通过辅助键检索无需访问主键的索引树
如果需要大数据量的排序,全表扫描,count之类的操作的话,使用MyISAM ;因为索引所占的空间小,并且这些操作是在内存中完成的.
索引的数据结构和具体的存储引擎有关,在mysql中使用较多的索引有: Hash索引, B+树索引等等,
InnoDB 存储索引的默认实现为: B+树 索引.
对于哈希索引来说,底层的数据结构就是哈希表,因为在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询效率更快;
其他场景选择B+tree索引
B+树:
B+树是一个平衡二叉树,从根节点到每个叶子节点的高度差值不会超过1,而且同层级的节点间有指针相互链接.
在B+树上的常规检索,从根节点到叶子节点的搜索效率基本相当, 不会出现大幅波动,而且基于索引的顺序扫描时,也可以利用双向指针快速左右移动,效率非常高.
因此,B+树索引常用来应用于数据库,文件系统等场景
哈希索引
哈希索引就是采用一定的哈希 算法,把键值换算成新的哈希值,检索时候不需要类似B+树那样从根节点到叶子节点逐级查找,只需要一次哈希算法就可以立即定位到响应的位置,速度非常快
索引设计的原则: 为了查询更快,占用空间更小
基于锁的属性分类: 共享锁,排他锁
根据锁的粒度分类: 行级锁(InnoDb), 表级锁(InnoDB,MyISAM), 页级锁(BDB引擎) ,记录锁,间隙锁,临键锁
根据锁的状态分类: 意向共享锁, 意向排他锁
共享锁又称为读锁,简称S锁: 当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其数据 持有写锁.
共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题
排他锁又称为写锁,简称X锁: 当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,知道该锁释放之后,其他事务才能对数据进行加锁.
排他锁的目的是在数据修改的时候,不允许其他人同时修改,也不允许其他人读取, 避免了出现脏数据和脏读的问题
表锁是指: 上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等钱一个事务释放了锁才能对表进行访问
表锁的特点: 粒度大,加锁简单,容易冲突
行锁
行锁是指: 上锁的时候锁住的是表的某一行或者多行记录,其他事务访问同一张表的时候,只有被锁住的记录无法访问,其他记录可以正常访问;
行锁的特点: 粒度小,加锁比表锁麻烦, 不容易冲突,相比表锁支持的并发度更高
记录锁也属于行锁的一种,只不过记录锁的范围值时表中的某一条记录,记录锁是说事务在加锁之后锁住的只是表的某一条记录.
精准条件名中,并且命中的条件字段是唯一索引;
加了记录锁之后数据可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交之前被其他事务读的脏读问题
页级锁 是mysql中锁定粒度 介于 行级锁和表级锁之间的一种锁.
表级锁速度快,但是冲突多;
行级锁速度慢,但是冲突锁;
所以取了折衷的页级锁, 一次锁定相邻的一组记录.
特点: 开销和加锁时间介于表锁和行锁之间; 会出现死锁; 锁定粒度介于 表锁和行级锁之间,并发度一般
属于行锁的一种,间歇锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成一个区间,遵循左开右闭原则;
范围查询并且查询未命中记录,查询条件必须命中索引,间隙锁只会出现在 REPEATABLE_READ(重复度)的事务级别中;
触发条件: 防止幻读问题,事务并发的时候,如果没有间隙锁,就会出现如下的问题:
在一个事务里,A事务的2次查询出的结果会不一样
临建锁也属于行锁的一种,并且是INNODB的行锁默认算法
总结来说就是他是记录 锁和锁间隙的组合 临键锁会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,也会把相邻的下一个区间也会锁住
触发条件: 范围查询并命中,查询命中了索引
结合记录锁和间隙锁的特性, 邻键锁避免了在范围查询时出现脏读,重复读,幻读问题 加了临键锁之后,在范围区间内数据不允许被修改和插入
如果当事务A加锁之后就设置一个状态告诉后面的人,已经有人对表里的行加了一个排他锁了,你们不能对整个表加共享锁或排他锁了,那么后面需要对整个表加锁的人只需要获取这个状态就知道自己是不是可以对表加锁, 避免了对整个索引树的每个节点扫描是否加锁,而这个状态就是意向锁
当一个事务试图对整个表进行加共享锁之前,首先需要获得这个表的意向共享锁
当一个事务试图对整个表进行加排他锁之前,首先需要获得这个表的意向排他锁
执行计划: sql的执行查询的顺序,以及如何使用索引查询,返回的结果集的行数
命令:
`explain select * from A where X=? and Y=?
id |
select_type |
table |
partitions |
type |
possible_keys |
key |
key_len |
ref |
rows |
filtered |
extra |
id 是一个有顺序的编号,时查询的顺序号,有几个select就显示几行.
ID的顺序是按照select出现的顺序增长的.
ID列的值越大执行的优先级越高越先执行,ID列的值相同则从上往下执行
ID列的值为null则最后执行
表示该语句查询的表
type是优化sql语句的重要字段,也是我们判断sql性能和优化程度重要指标. 取值范围是:
执行效率:
all< index< range< ref< eq_ref< const< system 最好时避免all 和index
表示mysql在执行该sql语句的时候,可以用到的索引信息, 仅仅是可能, 实际不一定会用到
此字段是mysql在当前查询时所真正使用到的索引. 是 possible_keys 的子集
表示查询优化器使用了索引的字节数,这个字段可以评估组合索引是否完全被使用,这也是我们优化sql时,评估索引的重要指标
mysql查询优化器根据统计信息,估算该sql返回结果集需要扫描读取的行数,
rows: 这个值相当重要,索引优化之后,扫描读取的行越多,说明索引设置不对,或者字段传入的类型之类的问题,说明要优化空间越大
返回结果的行站需要读取到的行(row列的值) 的百分比,
注意: 百分比越高,说明需要查询到的数据越准确,百分比越小,说明查询到的数据量大,而结果集很少,准确度低
事务的基本特性:ACID分别是:
原子性
指在一个事务中的操作要么全部成功,要么全部失败
一致性:
一致性指的是: 数据库总是从一个一致性的状态转换到另外一个一致性的状态.
比如: A转账给B 100元, 假设A只有90快,支付之前我们数据库里的数据都是符合约束的,但是如果事务执行成功了,我们的数据库数据就破坏约束了,因此,这个事务不能成功,这里我们说事务提供了一致性的保证
隔离型:
指的是一个事务的修改在嘴中提交前,对其他事务是不可见的.
持久性
指的是一旦事务提交,所做的修改就会永久保存到数据库中
隔离性有4个隔离级别
1.read uncommit
读未提交, 可能会读到其他事务未提交的数据,也叫做脏读
用户本来应该读取到ID=1的用户age应该是10, 但是读取到了其他事务还没有提交的事务,结果读取结果age = 20 这个就是脏读
read commit
读已提交, 两次读取结果不一致,叫做不可重复读不可重复读解决了脏读的问题,它只会读取到已经提交的事务
例如: 用户开启了事务,读取ID=1 的用户,查询到age=10, 再次读取到发现结果=20, 在同一个事务里面同一个查询读取到不同的结果就是不可重复读
repeatable read
可重复读, 这是mysql的默认级别, 就是每次读取结果都一样,但是有可能产生幻读
serializable
串行化, 一般是不会使用的,会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题
脏读
某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个rollback之后,另一个事物读取的数据就会是不正确的
不可重复读
在一个事务的两次查询中数据不一致,这可能是两次查询过程中, 插入了一个事务更新的原有的数据
幻读
在一个事务的两次查询中数据笔数不一致
例如: 有一个事务,查询了几列数据, 而另外一个事务却在此刻插入了新的几列数据, 之前的事务在接下来的查询中,就会发现有几列数据是之前锁没有的
在业务系统中,除了使用主键进行的查询,其他的都会在测试库上面进行测试耗时,慢查询的统计主要由运维在做,会定期的将业务中的慢查询反馈给我们
优化方向:
MyISAM:
不支持事务,但是每次查询都是原子的;
支持表级锁,即每次操作的是对整个表加锁;
存储表的总行数;
一个MyISAM表有3个文件: 索引文件, 表结构文件, 数据文件
采用非聚集索引, 索引文件的数据域存储只想数据文件的指针; 辅索引和主索引基本一致,但是辅索引不用保证唯一性
InnoDB:
支持ACID事务,支持事务的4中隔离级别;
支持行级锁以及外键约束;因此可以支持并发;
不存储表的总行数,需要每次计算;
一个InnoDB引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里)
也可能为多个(设置为独立空间,表大小受操作系统文件大小限制,一般为2G) 受操作系统文件大小的限制;
主键索引采用: 聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值; 因此呢从辅索引查找数据,需要先通过辅索引查找到主键值,再访问辅索引; 最好使用自增主键,防止插入数据时,为维护B+树结构,文件的大调整.
倒排索引
, 可以极大的提升键锁效率,解决判断字符是否包含的问题,是目前搜索引擎使用的一种关键技术,可以通过: alter table table_name add fulltext(column);
创建全文索引
索引优点
可以极大的提高数据的查询效率
使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能
索引缺点
会降低插入,删除,更新表的速度,因为在执行这些写操作的时候,还是要操作索引文件;
索引要占用物理空间,除了数据表展数据空间以外,每一个索引还要占用一定的物理空间,如果要建立聚族索引,那么需要的空间就会更大,如果非聚族索引很多,一旦聚族索引改变,那么所有的非聚族索引就会跟着改变
一些命令:
1、主从
2、哨兵模式
3、
服务端分片
回答:
我们new 一个Thread 的时候,线程进入创建状态, 当调用start方法让线程进入就绪状态(可用状态),当分配到时间片 之后就可开始运行了; 而 直接调用run() 方法,会把run()方法当作main方法线程的一个普通方法,并不是在某个线程中执行他,所以这个并不是多线程工作
总结: 调用start方法 让线程进入就绪状态; 而run方法知识Thread 类的一个普通方法,还是在主线程里执行
1、线程通常有5种状态: 创建, 就绪, 运行, 阻塞, 死亡状态
2、阻塞的情况分为以下三种:
线程的5种状态分别代表的含义:
1.锁池
所有需要竞争同步锁的线程都会被放到锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池中进行等待,当前面的线程释放同步锁之后,锁池中的线程去竞争同步锁,当某个线程得到后会进行入就绪队列中进行等待CPU资源的分配;
2.等待池
当我们调用wait()方法之后,线程会放到等待池当中,等待池中的线程是不会去竞争同步锁的.只有调用了 notify()或者notifyAll()后等待池中的线程才会开始去竞争锁,notify()是随机从等待池中选出一个线程放到锁池,而notifyAll()是将等待池中的所有线程放到锁池当中,去竞争获取CPU执行资源;
sleep就是把 CPU的执行资格和执行权释放出去,不再运行此线程,当定时时间结束之后再取回CPU资源,参与CPU的调度,获取CPU资源后就可以继续运行了.而如果sleep时,该线程有锁,那么sleep是不会释放这个锁,而是把锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本就无法获取到这个锁,也就是说无法执行程序. 如果再睡眠期间其他线程调用了这个线程的interrupt方法,那么这个线程也会抛出interruptexception异常返回,这点和wait是一样的.
yield() (礼让)方法执行后,线程直接进入就绪状态,马上释放CPU的执行权,但是依然保留了CPU的执行资格,所以有可能CPU下次进入线程调度还会让这个线程获取到执行权继续执行;
join()执行后线程进入阻塞状态, 例如在线程B中 调用线程A的join()方法,那么线程B会进入阻塞队列,直到线程A结束或者中断才会继续执行线程B剩余的代码;
package com.wlc.thread; /** * @author 王立朝 * @date 2022/4/14 * @description: join() 中途加入一个线程,使得被加入的线程阻塞,直到加入的线程执行完,原来的线程才可以继续执行 */ public class ThreadJoinDemo { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("2222222222"); }); t1.start(); t1.join(); System.out.println("11111"); } }
结果:
2222222222 11111 // 由于使用t1.join(); 会先执行 线程t1的结果,然后再执行main方法中的值
Thread和Runnable的实质是继承关系,没有可比性. 无论使用Runnable还是Thread, 都会new Thread, 然后执行run方法.
用法上:
如果有复杂的线程操作要求,使用继承Thread类;
如果知识简单的执行一个任务,就是实现Runnable接口
package com.wlc.thread; /** * @author 王立朝 * @date 2022/4/15 * @description: */ public class ThreadAndRunnableDemo { public static void main(String[] args) { new MyThread().start();// ticket 张数为5 new MyThread().start();// ticket 张数为5 System.out.println("------------"); MyThread2 t2 = new MyThread2(); new Thread(t2).start(); new Thread(t2).start(); } static class MyThread extends Thread { private int ticket = 5; @Override public void run() { while (true) { System.out.println("ticket剩余张数" + ticket--); if (ticket < 0) { break; } } } } static class MyThread2 implements Runnable{ private int ticket = 5; @Override public void run() { while (true) { System.out.println("ticket2剩余张数" + ticket--); if (ticket == 0) { break; } } } } }
结果为:
ticket剩余张数5 ticket剩余张数5 ticket剩余张数4 ------------ ticket剩余张数3 ticket剩余张数4 ticket剩余张数3 ticket剩余张数2 ticket剩余张数2 ticket剩余张数1 ticket剩余张数0 ticket剩余张数1 ticket剩余张数0 ticket2剩余张数5 ticket2剩余张数3 ticket2剩余张数2 ticket2剩余张数4 ticket2剩余张数1
守护线程: 为所有的非守护线程提供服务的线程; 任何一个守护线程都是整个JVM 中的所有的非守护线程的保姆; 是一个后台线程
ThreadLocal的使用场景?
Spring框架在事务开始时候会给当前线程绑定一个JDBC Connection 在整个事务过程中都是使用该线程绑定的connection来执行数据库草走,实现了事务的隔离型. Spring框架里面就是使用Thread Local来实现的事物隔离的
内存泄漏的概念:
不再被使用的对象或者变量还持续占有的内存,不能被回收,就是内存泄漏; 内存泄漏会导致内存溢出 OOM(OutOfMemoryError)错误
强引用的概念:
使用最普遍的引用,new 一个对象具有强引用,不会被垃圾回收器回收. 当内存空间不足的时候,Java虚拟机宁愿抛出 OutOfMemoryError错误,使程序异常终止,也不回收这种对象
如果想要取消强引用和某个对西那个之间的关联,可以显示的将引用赋值为null, 这样就可以在JVM在合适的时间就会回收该对象
弱引用的概念:
JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象.在Java中,用java.lang.ref.WeakReference类来表示.可以在缓存中使用弱引用
ThreadLocal实现原理,每一个Thread维护一个ThreadLocalMap , key 为使用弱引用 的ThreadLocal实例,value为线程变量的副本
ThreadLocalMap 使用ThreadLocal 的弱引用作为key, 如果一个ThreadLocal不存在外部强引用的时候, key(ThreadLocal)就会被GC垃圾回收,这样就会导致ThreadLocalMap中key为null, 而value还存在强引用,只有thread线程退出以后,value的强引用链条才会断掉,但是如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链(红色链条)
key使用强引用
当ThreadLocalMap的key为强引用回收Thread Local时,因为ThreadLocalMap还持有Thread Local的强引用,如果没有手动删除,Thread Local不会被回收,导致Entry内存泄漏.
key使用弱引用
当ThreadLocalMap的key作为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收. 当key为null,在下次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值
因此,ThreadLocal内存泄漏的根本原因是: 由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用
ThreadLocal的正确使用方法:
串行: 在时间上不能重叠,一个时间点只能执行一个任务, 前一个任务没有执行完毕,下一个任务就只能等待;
并行: 在时间上是可以重叠的,一个时间点可以执行多个任务,同时执行,互不影响
并发: 允许2个任务彼此干扰,同一个时间点,只有一个任务在运行, 然后交替执行.
原子性是指在一个操作中CPU不可以在中途暂停后再调度其他操作, 要么全部执行完成,要么都不执行;
例如:转账操作,从A账户转出1000元,那么必然包含2个操作:
1.从账户A中减去1000元;
2. 往账户B中加上1000元, 2个操作必须全部完成
private long count = 0; public void calc(){ count++; }
calc 这个方法执行的流程, count++ 这个不是线程安全的,包含以下4个步骤:
详细解析:
程序中原子性指的是最小的操作单元,比如自增操作,它其实不是原子性操作,分了3步的,包括
1⃣️读取变量的原始值;
2⃣️进行加一操作
3⃣️写入工作内存.
所以多线程中,有可能一个线程还没有自增玩呢,可能才执行到第二步,另一个线程就已经读取了值,导致结果错误. 那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据.
可以使用 关键字: synchronized
当多个线程访问同一个变量的时候,一个线程修改了这个变量的值,那么其他线程能够立即看到修改的值;
若2个线程在不同的CPU,那么线程1改变了i的值还没有刷新到主内存,线程2又使用了i,那么这个值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题.
例如
// 线程1 boolean stop = false; while(!stop){ doSomething(); } // 线程2 stop=true;
如果线程2先执行,改变了stop的值,线程1一定会停下来吗? 不一定,当线程2 改变了stop的值以后,但是还没有来得及写入主内存中,线程2转去做其他事情了,那么线程1由于不知道线程2修改了stop的值,就会陷入死循环
解决可见性的方法:
使用关键字: volatile , synchronized ,final
虚拟机在进行代码编译的时候,对于那些改变顺序之后不会对最终结果有影响的代码,虚拟机不一定会按照我们写的代码的顺序执行,有可能会进行重排序(也就是指令重排) . 实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但是 又可能会出现线程安全问题
如下代码所示:
int a = 0; boolean flag = false; public void write(){ a = 2; //执行顺序 1 flag = true; //执行顺序 2 } public void multiply(){ if(flag){ //执行顺序 3 int result = a * a; //执行顺序 4 } }
write方法里面的 指令1 和指令2 进行了指令重排,线程1先对flag进行赋值为true, 随后执行到线程2, result直接计算结果,再到线程1,这个时候a才赋值为2,很明显迟了一步
解决方法: 使用关键字: volatile synchronized
volatile本身就禁止指令重排序的语义, synchronized关键字是 由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则明确的
线程池的参数:
corePoolSize
代表核心线程数, 也就是正常情况下创建工作的线程数,这些线程创建之后并不会被消除,而是一种常驻线程;maxinumPoolSize
最大线程数,它与核心线程数相对应,表示允许被创建的最大的线程数,比如当前任务比较多,将核心线程数都用完了,还无法满足需求,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数keepAliveTime
、unit
表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会消除,我们可以通过 setKeepAliveTime
来设置空闲时间workQueue
用来存放待执行的任务,假设我们现在核心线程都已被停用,还有任务进来则全部放入队列,直到整个队列被放慢但任务还再持续进入则会开始创建新的线程Handler
任务拒绝策略,有2种情况shutdown
等方法关闭线程池之后,这时候即使线程池内部还有没执行完的任务正在执行,但由于线程池已经被关闭,我们再继续想线程池提交任务就会遭到拒绝;
阻塞队列
一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞
可以保留住当前想要继续入队的任务.
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,让线程进入wait状态,释放CPU资源.
阻塞队列自带 阻塞和唤醒
功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而保持核心线程的存活,不至于一直占用CPU资源.
先添加队列后创建最大线程?
在创建新线程的时候,是要获取全局锁的,这个时候就会阻塞其他的线程,影响了整体效率.
就好比一个企业里面有10个正式工的名额,最多招10个正式工,要是任务超过正式工人数( 任务数 task > core(核心任务数))的情况下,工厂领导(线程池)不是首先扩招工人,还是这10个人,但是任务可以稍微积压一下,即 先放到队列中去(代价低). 10个正式工慢慢干,迟早会干完的,要是任务还再继续增加,超过了正式工的加班忍耐极限了(队列满了),就得招外包帮忙了(注意是临时工)要是正式工+ 外包工还是无法完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)
线程池将线程和任务进行解藕,线程是线程, 任务是任务, 摆脱了之前通过Thread创建线程时的一个线程必须对应一个任务的限制
在线程池中,同一个线程可以从阻塞队列中不断的获取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start()
来创建新线程,而是让每个线程去执行一个循环任务
,在这个循环任务
中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的run()
方法,将run方法当成普通的方法执行, 通过这种方式只使用固定的线程就将所有的任务run方法串联起来