零碎
项目介绍
UDESK
20190318,第一个面试邀请,仔细看了招聘条件才发现不小心投了偏大数据的职位,先看看呗
CAP(https://www.jdon.com/bigdata/how-to-understand-cap.html)
CAP(Consistency一致性、Availability可用性、Partition-tolerance分区可容忍性)理论普遍被当作是大数据技术的理论基础。同时,根据该理论,业界有一种非常流行、非常“专业”的认识,那就是:关系型数据库设计选择了C(一致性)与A(可用性),NoSQL数据库设计则不同。其中,HBase选择了C(一致性)与P(分区可容忍性),Cassandra选择了A(可用性)与P(分区可容忍性)。
CAP原理指的是,在分布式系统中这三个要素最多只能同时实现两点,不可能三者兼顾。因此在进行分布式架构设计时,必须做出取舍。而对于分布式数据系统,分区容忍性是基本要求,否则就失去了价值。因此设计分布式数据系统,就是在一致性和可用性之间取一个平衡。对于大多数web应用,其实并不需要强一致性,因此牺牲一致性而换取高可用性,是目前多数分布式数据库产品的方向。
BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。
BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。
基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。
最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。
微服务是一个软件架构,就是把一个大型的单个应用程序和服务拆分为数十个的支持微服务。
SOLID(https://www.cnblogs.com/huangenai/p/6219475.html)
DDD(Domain Driven Design(领域驱动设计))
https://blog.csdn.net/qq_769932247/article/details/87920116
领域就是问题域,有边界,领域中有很多问题;
任何一个系统要解决的那个大问题都对应一个领域;
通过建立领域模型来解决领域中的核心问题,模型驱动的思想;
领域建模的目标针对我们在领域中所关心的问题,即只针对核心关注点,而不是整个领域中的所有问题;
领域模型在设计时应考虑一定的抽象性、通用性,以及复用价值;
通过领域模型驱动代码的实现,确保代码让领域模型落地,代码最终能解决问题;
领域模型是系统的核心,是领域内的业务的直接沉淀,具有非常大的业务价值;
技术架构设计或数据存储等是在领域模型的外围,帮助领域模型进行落地;
12. 闭锁 CountDownLatch
java.util.concurrent.CountDownLatch 是一个并发构造,它允许一个或多个线程等待一系列指定操作的完成。
CountDownLatch 以一个给定的数量初始化。countDown() 每被调用一次,这一数量就减一。通过调用 await() 方法之一,线程可以阻塞等待这一数量到达零。
20190319,嘛,自从改了简历以后,面试突然变得很多,第一家面试的是根网,一家做证券的公司,有技术笔试和数学笔试,技术面也过了,技术总监劝我慎重考虑要不要入行。还问了两个问题,没准备的问题。应对能力有待加强,主要还是概念有些模糊,这两个问题我其实都知道一点的,技术总监想要唬住我,诱导性的面试,并行和并发的时候还问过一个cpu如何实现并行,再问我我就回答一个cup无法实现并行。
单例对象为什么不会被垃圾回收掉
http://www.cnblogs.com/ljy-cloudy/p/7485078.html
方法区中类静态属性引用的对象作为GC Roots,而单例对象是静态属性引用的对象。而单例类被回收的条件之一是该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。所以单例类和单例对象都不会被回收。
并行和并发
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
并发是在同一个cpu上同时(不是真正的同时,而是看来是同时,因为cpu要在多个程序间切换)运行多个程序。并行是每个cpu运行一个程序。简而言之就是并发是多个事件在同一时间段执行,而并行是多个事件在同一时间点执行。
上面两个问题是总监面的,技术一面过了,不过感觉还是很多东西答的不好,沟通起来有些困难,有些东西想不到好的描述语言,还需要背啊。
项目中遇到的问题:
- python版本兼容问题。一段python脚本单独运行没问题,放到crontab定时任务里面运行运行不起来,crontab里面用的是python2.7的版本,服务器上装的事3.6的版本。
- 文件流无法重复读取的问题,使用可重复读取的流ByteArrayInputStream,ByteArrayOutputStream,ByteArrayOutputStream将流数据缓存到内存中,达到多次读取的目的。但是要注意每次读前,调用reset()将游标重置到流的头部。
- 有些数据量大的接口响应速度很慢,解决方式用gzip进行压缩。
- 把项目部署在多个服务器上的时候,项目里面的定时任务不好处理。最后用的redis的incr方法,让取到1的项目做定时任务,取值不为1的项目跳过定时任务。
- Linux上面执行频率高的定时任务,连续的任务之间会有冲突。使用文件锁来解决,* * * * * flock -xn /tmp/mytest.lock -c 'php /home/test.php >>/home/test.log'。-x, --exclusive: 获得一个独占锁,-n, --nonblock: 如果没有立即获得锁,直接失败而不是等待。
分布式:一个业务分拆多个子业务,部署在不同的服务器上
集群:同一个业务,部署在多个服务器上
分布式锁一般有三种实现方式:1. Mysql乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。
Mysql乐观锁:根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。
Redis分布式锁:
基于 SETNX、EXPIRE
使用 SETNX(set if not exist)命令插入一个键值对时,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。因此客户端在尝试获得锁时,先使用 SETNX 向 Redis 中插入一个记录,如果返回 True 表示获得锁,返回 False 表示已经有客户端占用锁。
EXPIRE 可以为一个键值对设置一个过期时间,从而避免了死锁的发生。
基于zk的分布式锁
ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
如果一个已经获得锁的会话超时了,因为创建的是临时节点,因此该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库分布式锁的死锁问题。
优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。
缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。
20190320,今天不太顺利,上午是雪球,下午是udesk,下午那家是第一个给我面试机会的公司,本来还很期待的,特意准备了一些东西,没想到希望越大失望越大,这公司很让人失望。今天的两家公司连水都没给一口喝,继续面试呗。
总之基础的东西还得多记,冷静一点,不要被一些奇怪不专业的问法影响。
LinkedList是线程安全的吗
不是,可以把通过Collections.synchronizedList转成一个安全的List,或者使用ConcurrentLinkedQueue。
栈和队列的常用方法
队列:LinkedBlockingQueue :一个由链接节点支持的可选有界队列。ConcurrentLinkedQueue。 add 增加一个元索 如果队列已满,则抛出一个IIIegaISlabEepeplian异常
remove 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
element 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
offer 添加一个元素并返回true 如果队列已满,则返回false
poll 移除并返问队列头部的元素 如果队列为空,则返回null
peek 返回队列头部的元素 如果队列为空,则返回null
put 添加一个元素 如果队列满,则阻塞
take 移除并返回队列头部的元素 如果队列为空,则阻塞
栈:Stack
empty() 判断栈是否为空,为空返回true,否则返回false
peek() 取出栈顶元素,但是不从栈中移除元素
pop() 取出栈顶元素,并且将其从栈中移除
push(Eitem) 元素入栈
search(Objecto) 在栈中查找元素位置,位置从栈顶开始往下算,栈顶为1,
Object里面有哪些方法
总共有十三个方法,以下11个(wait方法有三个)+构造方法+registerNatives(注册本地函数)
1.clone方法
保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
主要是JAVA里除了8种基本类型传参数是值传递,其他的类对象传参数都是引用传递,我们有时候不希望在方法里讲参数改变,这是就需要在类中复写clone方法。
2.getClass方法:final方法,获得运行时类型。
3.toString方法:该方法用得比较多,一般子类都有覆盖。
4.finalize方法:该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。
5.equals方法:该方法是非常重要的一个方法。一般equals和==是不一样的,但是在Object中两者是一样的。子类一般都要重写这个方法。
6.hashCode方法
该方法用于哈希查找,可以减少在查找中使用equals的次数,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。
一般必须满足obj1.equals(obj2)==true。可以推出obj1.hash- Code()==obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。
如果不重写hashcode(),在HashSet中添加两个equals的对象,会将两个对象都加入进去。
7.wait方法
wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生。
(1)其他线程调用了该对象的notify方法。
(2)其他线程调用了该对象的notifyAll方法。
(3)其他线程调用了interrupt中断该线程。
(4)时间间隔到了。
此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
8.notify方法:该方法唤醒在该对象上等待的某个线程。
9.notifyAll方法:该方法唤醒在该对象上等待的所有线程。
事物管理
spring的事务声明有两种方式,编程式和声明式。spring主要是通过“声明式事务”的方式对事务进行管理,即在配置文件中进行声明,通过AOP将事务切面切入程序,最大的好处是大大减少了代码量。
自定义注解怎么实现
注解本质是一个继承了Annotation 的特殊接口,注解类型@interface,自定义注解需要使用到元注解
@Documented – 注解是否将包含在JavaDoc中
@Retention – 什么时候使用该注解
@Target – 注解用于什么地方
@Inherited – 是否允许子类继承该注解
ArrayList和HashMap查找哪个快
1、ArrayList 有序集合 底层为数组 按下标查找快 增删慢 按元素查找、增删都慢
2、LinkedList 有序集合 底层为链表 按下标查找慢 增删快 按元素查找慢 增删比arrayList快
3、HashMap 无序哈希表 底层哈希表 按下标查找一般比LinkedList快 增删快跟主体大小有关,按元素查找快,增删快跟主体大小有关,越大越慢。
以上是网上的说法,而且HashMap换成HashSet比较合适,Map和List是不同的数据接口。网上的说法有问题,HashSet是无序的,哪来的按下标查找,所以这个问题实际上应该是按元素查找,HashSet肯定快于ArrayList。
redis有哪几种数据结构
string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)
MySQL存储引擎
MySQL默认会提供多种存储引擎,可以用show engines查看,一般常用的有两种Innodb和MyISAM
Innodb引擎提供了对数据库ACID事务的支持,并且实现了SQL标准的四种隔离级别。该引擎还提供了行级锁和外键约束,它的设计目标是处理大容量数据库系统,它本身其实就是基于MySQL后台的完整数据库系统,MySQL运行时Innodb会在内存中建立缓冲池,用于缓冲数据和索引。但是该引擎不支持FULLTEXT类型的索引,而且它没有保存表的行数,当SELECT COUNT(*) FROM TABLE时需要扫描全表。当需要使用数据库事务时,该引擎当然是首选。由于锁的粒度更小,写操作不会锁定全表,所以在并发较高时,使用Innodb引擎会提升效率。但是使用行级锁也不是绝对的,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表。
MyISAM是MySQL默认的引擎,但是它没有提供对数据库事务的支持,也不支持行级锁和外键,因此当INSERT(插入)或UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低一些。不过和Innodb不同,MyISAM中存储了表的行数,于是SELECT COUNT(*) FROM TABLE时只需要直接读取已经保存好的值而不需要进行全表扫描。如果表的读操作远远多于写操作且不需要数据库事务的支持,那么MyISAM也是很好的选择。
简单介绍区别:
1、MyISAM是非事务安全的,而InnoDB是事务安全的
2、MyISAM锁的粒度是表级的,而InnoDB支持行级锁
3、MyISAM支持全文类型索引,而InnoDB不支持全文索引
4、MyISAM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyISAM
5、MyISAM表保存成文件形式,跨平台使用更加方便
应用场景:
1、MyISAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择MyISAM
2、InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB
explain
explain用来分析sql查询语句的执行效率,desc命令同样的效果。
语法:explain 查询语句
举个栗子:explain select * from news;
输出:
+----+-------------+-------+-------+-------------------+---------+---------+-------+------
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+-------------------+---------+---------+-------+-----
下面对各个属性进行了解:
1、id:这是SELECT的查询序列号
2、select_type:select_type就是select的类型,可以有以下几种:
SIMPLE:简单SELECT(不使用UNION或子查询等)
PRIMARY:最外面的SELECT
UNION:UNION中的第二个或后面的SELECT语句
DEPENDENT UNION:UNION中的第二个或后面的SELECT语句,取决于外面的查询
UNION RESULT:UNION的结果。
SUBQUERY:子查询中的第一个SELECT
DEPENDENT SUBQUERY:子查询中的第一个SELECT,取决于外面的查询
DERIVED:导出表的SELECT(FROM子句的子查询)
3、table:显示这一行的数据是关于哪张表的
4、type:这列最重要,显示了连接使用了哪种类别,有无使用索引,是使用Explain命令分析性能瓶颈的关键项之一。
结果值从好到坏依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
一般来说,得保证查询至少达到range级别,最好能达到ref,否则就可能会出现性能问题。
5、possible_keys:列指出MySQL能使用哪个索引在该表中找到行
6、key:显示MySQL实际决定使用的键(索引)。如果没有选择索引,键是NULL
7、key_len:显示MySQL决定使用的键长度。如果键是NULL,则长度为NULL。使用的索引的长度。在不损失精确性的情况下,长度越短越好
8、ref:显示使用哪个列或常数与key一起从表中选择行。
9、rows:显示MySQL认为它执行查询时必须检查的行数。
10、Extra:包含MySQL解决查询的详细信息,也是关键参考项之一。
Distinct
一旦MYSQL找到了与行相联合匹配的行,就不再搜索了Not exists
MYSQL 优化了LEFT JOIN,一旦它找到了匹配LEFT JOIN标准的行,就不再搜索了
Range checked for each
Record(index map:#)
没有找到理想的索引,因此对于从前面表中来的每一 个行组合,MYSQL检查使用哪个索引,并用它来从表中返回行。这是使用索引的最慢的连接之一Using filesort
看 到这个的时候,查询就需要优化了。MYSQL需要进行额外的步骤来发现如何对返回的行排序。它根据连接类型以及存储排序键值和匹配条件的全部行的行指针来 排序全部行Using index
列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的,这发生在对表 的全部的请求列都是同一个索引的部分的时候Using temporary
看到这个的时候,查询需要优化了。这 里,MYSQL需要创建一个临时表来存储结果,这通常发生在对不同的列集进行ORDER BY上,而不是GROUP BY上Using where
使用了WHERE从句来限制哪些行将与下一张表匹配或者是返回给用户。如果不想返回表中的全部行,并且连接类型ALL或index, 这就会发生,或者是查询有问题
其他一些Tip:当type 显示为 “index” 时,并且Extra显示为“Using Index”, 表明使用了覆盖索引。
20190324,这周周四下午第一个面试是必要商城,经历了三轮技术面试,然后没有消息了。第二个是琳云科技,这是只差一个程序员的公司,因为必要商城面试比较久,这一家迟到了,只差一个程序员的这一家也没消息。一个offer都拿不到,第一家的那个offer还不如以前公司高,感觉我的弱项在于项目,没有背景不好搞啊。
缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决办法:
1.布隆过滤: 对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃。还有最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
2. 缓存空对象. 将 null 变成一个值.。
缓存雪崩
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。
1.自动装箱
Integer v1 = 127; Integer v2 = 127; Integer v3 = 128; Integer v4 = 128; System.out.println(v1 == v2); System.out.println(v3 == v4);
以上代码一般情况会输出true、false,自动装箱实际上是调用Integer.valueOf()来完成的,当装箱的值在-128-127范围内,会从缓存数组里面取值,超出这个范围会创建新的对象,所以输出true,false;
缓存数组的范围下限-128固定,上限127可以通过虚拟机设置XX:AutoBoxCacheMax=size来设置,设置以后,缓存数组初始化的时候会从127和设置值里面取最大值作为最大范围。
2.HashMap在1.7和1.8中的区别
JDK1.7中,HashMap是由数组+链表实现的,使用一个Entry数组来存储数据,用key的hashcode取模来决定key会被放到数组里的位置。如果hashcode相同,或者hashcode取模后的结果相同,那么这些key会被定位到Entry数组的同一个格子里,这些Entry对象以链表的形式存储。
JDK1.8中,HashMap是由数组+链表+红黑树实现的,存储容器从Entry换成了Node,存储在一个格子里的Node数量小于8个的时候以链表存储,当Node数量达到8个以红黑树存储。这样的好处是当存储一批hashcode相同或者hashcode取模之后的结果一样的情况下,HashMap的读取操作时间复杂度从O(n)降到了O(logn)。
一个格子里面数据插入的方式在1.7中用头插法,1.8中用尾插法,大概是因为尾插法能保证数据的有序吧。1.7中不用尾插法是因为存储方式是单纯的链表,每次尾插都要遍历整个链表,而1.8中当格子中数据多的时候结构从链表转为红黑树,时间复杂度降低了,所以改成了尾插法(猜的)。
HashMap的长度为什么要是2的n次方
HashMap为了存取高效,要尽量减少碰撞,就是要尽量把数据分配均匀,使每个链表长度大致相同,这种算法实际就是取模。计算机中直接求余的效率不如位运算,HashMap源码中做了优化,用hash&(length-1)来代替取模运算。只有当 length-1每一位都是1的时候,才能保证hash的后几位都被取到而不浪费空间,所以HashMap的长度要是2的n次方 。
如果确定只装载100个元素,new HashMap(?)多少是最佳的,why?
为了减少哈希碰撞,HashMap容量为2的n次方个(HashMap的内部也做了这样的限制),所以最少容量为128,当HashMap中装载的元素数量超过容量乘以加载因子的时候,会进行rehash操作,这是一个代价很大的操作,所以加载因子要大于100/128=0.78125,可以设置加载因子为0.8,即new HashMap(128,0.8)。
若采用默认的加载因子0.75,容量至少为100/0.75=134,且保证容量为2的n次方个,所以最少的容量为256,即new HashMap(256)。
3.ConcurrentHashmap
线程不安全的HashMap
HashMap是线程不安全的,多线程环境下使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以并发情况下不能使用HashMap。
JDK1.7中,多个线程对 HashMap进行put操作时,有一定概率在map扩容的时候产生环形链表,导致死循环。1.8中链表插入方式改成尾部插入法以后,不会有死循环的问题,不过还是会有数据丢失的问题。
效率低下的HashTable容器
ConcurrentHashMap的实现
JDK1.7中ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。(有时间再看吧)
4.synchronized
加锁方式
1.某个对象的实例锁,作用于某个实例对象,并且这个对象中有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法。此外,不同对象实例的synchronized方法是不相干预的。也就是说,其它线程可以同时访问此类下的另一个对象实例中的synchronized方法;
2.某个类的锁,作用于此类的所有对象实例。
此外,synchronized是不能被继承的,也就是说基类的方法synchronized fun(){}在继承类中并不自动是synchronized fun(){},而是变成了fun(){}。
原理
使用synchronized加锁的同步代码块中在字节码引擎中执行时,其实是通过锁对象monitor的取用和释放来实现的。同步代码块在字节码文件中被编译为
monitorenter;//获取monitor许可证,进入同步块;
同步代码...;
monitorexit;//离开同步块后,释放monitor许可证
根据虚拟机规范要求,在执行monitorenter指令时,首先要尝试获取对象锁(monitor对象)。如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器(_count)加1。当然与之对应执行monitorexit指令时,锁的计数器(_count)也会减1。如果当前线程获取锁失败,那么就会被阻塞住,进入_WaitSet 中,等待锁被释放为止。而且一个monitorenter指令会对应两个monitorexit指令,因为编译器要确保程序中调用过的每条monitorenter指令都要执行对应的monitorexit指令,当程序异常时monitorenter和monitorexit指令也能正常配对执行,编译器会自动产生一个异常处理器,它的目的就是用来执行异常的monitorexit指令。而字节码中多出的monitorexit指令,就是异常结束时,被执行用来释放monitor的。
同步方法在被编译成字节码文件中会加上一个ACC_SYNCHRONIZED标识,JVM通过ACC_SYNCHRONIZED标识,就可以知道这是一个需要同步的方法,进而执行和同步代码块一样的操作。
JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。(了解)
Monitor
Monitor是一个同步工具,相当于操作系统中的互斥量(mutex),即值为1的信号量。 它内置于每一个Object对象中,相当于一个许可证。拿到许可证即可以进行操作,没有拿到则需要阻塞等待。
monitor是由ObjectMonitor实现(ObjectMonitor.hpp文件,C++实现的),里面定义了一些成员变量,_count,_owner ,_WaitSet,_WaitSetLock,_EntryList。当多个线程同时访问一段同步代码时,会先存放到 _EntryList 集合中,接下来当线程获取到对象的monitor时,就会把_owner变量设置为当前线程。同时count变量+1。如果线程调用wait() 方法,就会释放当前持有的monitor,那么_owner变量就会被置为null,同时_count减1,并且该线程进入 WaitSet集合中,等待下一次被唤醒。若当前线程顺利执行完方法,也会释放monitor,_owner置为null,_count减1,并且该线程进入WaitSet集合中,等待下一次被唤醒。
5.Lock
既然可以通过synchronized来实现同步访问,为什么还需要提供Lock(synchronized的缺陷)
1.使用synchronized,当多个线程尝试获取锁时,未获取到锁的线程会不断的尝试获取锁,而不会发生中断,这样会造成性能消耗。
2.如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
3.synchronized无法知道线程有没有成功获取到锁。
Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制。本质上Lock仅仅是一个接口(位于源码包中的java.util.concurrent.locks中),它包含以下方法
//尝试获取锁,获取成功则返回,否则阻塞当前线程 void lock(); //尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常 void lockInterruptibly() throws InterruptedException; //尝试获取锁,获取锁成功则返回true,否则返回false boolean tryLock(); //尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //释放锁 void unlock(); //返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量 Condition newCondition();
Lock有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。
使用方法:多线程下访问(互斥)共享资源时, 访问前加锁,访问结束以后解锁,解锁的操作推荐放入finally块中。
注意:加锁位于对资源访问的try块的外部,特别是使用lockInterruptibly方法加锁时就必须要这样做,这为了防止线程在获取锁时被中断,这时就不必(也不能)释放锁。
Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
锁的相关概念
1.可重入锁
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
看下面这段代码就明白了:
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。
而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。
2.可中断锁
可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。
3.公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。
我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:ReentrantLock lock =
new
ReentrantLock(
true
);
如果参数为true表示为公平锁,为fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。
4.读写锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
可以通过readLock()获取读锁,通过writeLock()获取写锁。
6.对象的内存布局(了解)
在 HotSpot 虚拟机中,对象在内存中存储布局分为 3 块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头(Header)
HotSpot 虚拟机的对象头包括两部分(非数组对象)信息,如下图所示:
- 第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳、对象分代年龄,这部分信息称为“Mark Word”;Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据自己的状态复用自己的存储空间。
- 第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
- 如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是从数组的元数据中无法确定数组的大小。
这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32bit 和 64bit。
例如,在 32 位的 HotSpot 虚拟机中,如果对象处于未被锁定的状态下,那么 Mark Word 的 32bit 空间中的 25bit 用于存储对象哈希码,4bit 用于存储对象分代年龄,2bit 用于存储锁标志位,1bit 固定为 0,如下表所示:
在 32 位系统下,存放 Class 指针的空间大小是 4 字节,Mark Word 空间大小也是4字节,因此头部就是 8 字节,如果是数组就需要再加 4 字节表示数组的长度,如下表所示:
在 64 位系统及 64 位 JVM 下,开启指针压缩,那么头部存放 Class 指针的空间大小还是4字节,而 Mark Word 区域会变大,变成 8 字节,也就是头部最少为 12 字节,如下表所示:
压缩指针:开启指针压缩使用算法开销带来内存节约,Java 对象都是以 8 字节对齐的,也就是以 8 字节为内存访问的基本单元,那么在地理处理上,就有 3 个位是空闲的,这 3 个位可以用来虚拟,利用 32 位的地址指针原本最多只能寻址 4GB,但是加上 3 个位的 8 种内部运算,就可以变化出 32GB 的寻址。
实例数据(Instance Data)
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在 Java 源码中定义顺序的影响。
对齐填充(Padding)
对齐填充不是必然存在的,没有特别的含义,它仅起到占位符的作用。
由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,也就是说对象的大小必须是 8 字节的整数倍。对象头部分是 8 字节的倍数,所以当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
7.volatile
volatile是一个类型修饰符,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
使用条件
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
适用场景
1)volatile修饰布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
2)单例模式中的double check,防止获得残缺对象。
8.CAS
CAS是compare and swap的缩写,CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。CAS通过比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。
CAS存在的问题
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。
1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
关于ABA问题参考文档: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html
2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
9.JVM内存划分
JVM 内存共分为程序计数器,虚拟机栈,本地方法栈,堆,方法区五个部分。
程序计数器(线程私有)
每条线程都有一个独立的程序计数器,这类内存也称为“线程私有”的内存,指向当前正在执行的字节码的行号。正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果是Natice方法,则为空。
java虚拟机栈(线程私有)
每个方法在执行的时候也会创建一个栈帧,存储了局部变量,操作数,动态链接,方法返回地址。
每个方法从调用到执行完毕,对应一个栈帧在虚拟机栈中的入栈和出栈。
通常所说的栈,一般是指在虚拟机栈中的局部变量部分。
局部变量所需内存在编译期间完成分配,
如果线程请求的栈深度大于虚拟机所允许的深度,则StackOverflowError。
如果虚拟机栈可以动态扩展,扩展到无法申请足够的内存,则OutOfMemoryError。
本地方法栈(线程私有)
和虚拟机栈类似,主要为虚拟机使用到的Native方法服务。也会抛出StackOverflowError 和OutOfMemoryError。
Java堆(线程共享)
被所有线程共享的一块内存区域,在虚拟机启动的时候创建,用于存放对象实例。
堆可以按照可扩展来实现(通过-Xmx 和-Xms 来控制)
当堆中没有内存可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
方法区(线程共享)
被所有方法线程共享的一块内存区域。
用于存储已经被虚拟机加载的类信息,常量,静态变量等。
这个区域的内存回收目标主要针对常量池的回收和堆类型的卸载。
详细版
1. 堆(Heap)
是java虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,该内存区域存放了对象实例及数组(但不是所有的对象实例都在堆中)。其大小通过-Xms(最小值)和-Xmx(最大值)参数设置(最大最小值都要小于1G),前者为启动时申请的最小内存,默认为操作系统物理内存的1/64,后者为JVM可申请的最大内存,默认为物理内存的1/4,默认当空余堆内存小于40%时,JVM会增大堆内存到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小堆内存的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,当然为了避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。堆内存 = 新生代+老年代+持久代。在我们垃圾回收的时候,我们往往将堆内存分成新生代和老年代(大小比例1:2),新生代由Eden和Survivor0,Survivor1组成,三者的比例是8:1:1,新生代的回收机制采用复制算法,在Minor GC的时候,我们都留一个存活区用来存放存活的对象,真正进行的区域是Eden+其中一个存活区。当我们的对象时长超过一定年龄时(默认15,可以通过参数设置),将会把对象放入老年代,当然大的对象会直接进入老年代。老年代采用的回收算法是标记整理算法。(更详细的内容将后续文章详细介绍GC回收)
2. 方法区(Method Area)
方法区也称"永久代",它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB(64位JVM由于指针膨胀,默认是85M),可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。它是一片连续的堆空间,永久代的垃圾收集是和老年代(old generation)捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。不过,一个明显的问题是,当JVM加载的类信息容量超过了参数-XX:MaxPermSize设定的值时,应用将会报OOM的错误。参数是通过-XX:PermSize和-XX:MaxPermSize来设定的
运行时常量池(Runtime Constant Pool):是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。
从JDK7开始移除永久代(但并没有移除,还是存在),贮存在永久代的一部分数据已经转移到了Java Heap或者是Native Heap:符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。从JDK8开始使用元空间(Metaspace),元空间的大小受本地内存限制,新参数(MaxMetaspaceSize)用于限制本地内存分配给类元数据的大小。如果没有指定这个参数,元空间会在运行时根据需要动态调整。
3.虚拟机栈(JVM Stack)
描述的是java方法执行的内存模型:每个方法被执行的时候都会创建一个"栈帧",用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。声明周期与线程相同,是线程私有的。栈帧由三部分组成:局部变量区、操作数栈、帧数据区。局部变量区被组织为以一个字长为单位、从0开始计数的数组,和局部变量区一样,操作数栈也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的,可以看作为临时数据的存储区域。除了局部变量区和操作数栈外,java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些数据都保存在java栈帧的帧数据区中。
局部变量表: 存放了编译器可知的各种基本数据类型、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。
4.本地方法栈(Native Stack)
与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。(栈的空间大小远远小于堆)
5.程序计数器(PC Register)
是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。
6.直接内存
直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小.
10.java堆内存划分
Java堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这个区域也是Java 垃圾收集器管理的主要区域,因此很多时候也被称为”GC堆”。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
堆的内存模型大致如下图所示:
从图中可以看出: 堆大小 = 新生代 + 老年代。
默认情况下,
-
新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2
-
Eden : from : to = 8 : 1 : 1
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
堆的垃圾回收方式
Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。
Minor GC
Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。
新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。
当一个对象被判定为 “死亡” 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。
回收过程如下:
当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳(上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。
但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。
Full GC
Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。
现实的生活中,老年代的人通常会比新生代的人 “早死”。堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长,一般是Minor GC的 10倍以上。
另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。
垃圾回收介绍
Java虚拟机内存划分讲到了Java 内存运行时区域的各个部分,其中程序计数器,虚拟机栈,本地方法栈三个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来是就已知了。因此这几个区域的内存分配和回收都具有确定性,在这几个区域不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的。而垃圾收集器所关注的是这部分内存。
垃圾对象的判定方法
在堆里存放着Java中几乎所有的对象实例,在进行垃圾回收之前,首先要确定哪些对象应该被回收。
1.引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减一;任何时候计数器为0的对象就是不可能再被使用的。
这种方法特点就是,实现简单,判定效率也很高,大部分情况下都是一个不错的选择。但是很多主流的Java虚拟机没有选择使用引用计数法类管理内存,主要原因它很难解决对象之间相互循环引用的问题。
public class MyObject { public Object ref = null; public static void main(String[] args) { MyObject myObject1 = new MyObject(); MyObject myObject2 = new MyObject(); myObject1.ref = myObject2; myObject2.ref = myObject1; myObject1 = null; myObject2 = null; } }
循环引用导致myObject1和myObject2两个被引用次数均为2, 即使当两个对象都被置为null后,引用次数仍然为1,导致两个对象无法被回收。
2.可达性分析算法
在主流的商用程序语言(例如Java,C#)的主流实现中,都是通过可达性分析来判定对象是否存活的。
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链。当一个对象到GC roots没有任何引用链相连时,则证明此对象是不可用的。如下图所示,对象object5,object6,object7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
Java 中,可作为GC Roots的对象包括下面几种:
1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
2. 方法区中类静态属性引用的对象。
3. 方法区中常量引用的对象。
4. 本地方法栈中JNI(即一般说的Native方法)引用的对象。
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为”没有必要执行“。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的,低优先级的Finalizer线程中去执行它。这里所谓的”执行”是指虚拟机会触发这个方法,但并不会保证会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(极端情况),将很可能导致F-Queue队列中其他对象处于永久等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出”即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
垃圾收集算法
常见的垃圾收集算法主要涉及到这几种,标记——清除算法,复制算法,标记整理算法,分代收集算法等。
1.标记清除算法
这是一种最基础的收集算法(Mark-Sweep)。分为标记-清除两个阶段。
标记阶段:先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程其实就是上述的可达性分析算法中的标记过程。它是最基础的算法,后续的收集算法都是基于这种思路并对其不足进行改进而得到的。
不足之处主要有两个:
1.效率问题,标记和清除两个过程效率都不高。
2.空间问题,标记清除之后会产生大量不连续的碎片,可能会导致后续程序需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2.复制算法
在上述标记——清除算法的基础上,为了解决效率问题,复制算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动对顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点就是,将内存缩小为原来的一半,代价较高;当对象存活率较高时就要进行较多的复制操作,效率将会变低。
应用场景:应用于当今主流商业虚拟机新生代收集算法,所不同的是,实际的新生代收集算法,并不是将内存简单划分为等分的两半,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次只使用Eden和其中的一块Survivor。
3.标记整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以老年代一般不能直接选用这种算法。标记-整理算法(Mark-Compact)的标记过程与”标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
4.分代收集算法
分代收集算法是当前商业虚拟机的垃圾收集机制都采用的算法。 分代收集算法并不是一种新的算法模型,它只是一种根据对象存活周期的不同特点而对不同收集算法的综合运用。具体来说就是对堆区所采用的垃圾收集方案。堆区分为新生代和老年代,新生代的特点就是,每次垃圾收集都会发现大批对象死去,只有少量存活,那么采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集,这就是所谓的Minor GC 。老年代的特点就是对象存活率高,没有额外空间的对它进行分配担保,那么就必须使用 “标记-清除”或者”标记-整理”算法。
11.线程池
线程池的作用
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的构成
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
常见的线程池
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
2、newFixedThreadPool,创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
3、newCachedThreadPool,创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
4、newScheduledThreadPool,创建一个定长线程池,支持定时及周期性任务执行。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); }
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS, new DelayedWorkQueue()); }
ThreadPoolExecutor的构造参数
- corePoolSize线程池中的核心线程数
- maximumPoolSize线程池中的最大线程数
- keepAliveTime线程池中的线程存活时间(准确来说应该是没有任务执行时的回收时间,后面会分析)
- unit时间单位
- workQueue来不及执行的任务存放的阻塞队列
- threadFactory新建woker线程(注意不是我们提交的任务)是进行一些属性设置,比如线程名,优先级等等,有默认实现。
- handler 任务拒绝策略,当运行线程数已达到maximumPoolSize,队列也已经装满时会调用该参数拒绝任务,有默认实现。
corePoolSize、 maximumPoolSize、workQueue之间的关系
(1)当前运行的线程数少于corePoolSize,则添加新的线程执行该任务。
(2)当前的线程数等于corePoolSize同时阻塞队列未满,则将任务入队列,而不添加新的线程。
(3)阻塞队列已满同时池中的线程数小于maximumPoolSize,则创建新的线程执行任务。
(4)阻塞队列已满同时池中的线程数等于maximumPoolSize,则根据构造函数中的handler指定的策略来拒绝新的任务。
线程池都有哪几种工作队列
1、ArrayBlockingQueue:一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue(默认):一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
3、SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
4、PriorityBlockingQueue一个具有优先级的无限阻塞队列。
线程池饱和策略
这里提到了线程池的饱和策略,那我们就简单介绍下有哪些饱和策略:
AbortPolicy为Java线程池默认的阻塞策略,不执行此任务,而且直接抛出一个运行时异常,切记ThreadPoolExecutor.execute需要try catch,否则程序会直接退出。
DiscardPolicy:直接抛弃,任务不执行,空方法
DiscardOldestPolicy:从队列里面抛弃head的一个任务,并再次execute 此task。
CallerRunsPolicy:在调用execute的线程里面执行此command,会阻塞入口
用户自定义拒绝策略(最常用):实现RejectedExecutionHandler,并自己定义策略模式
12.dubbo
dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。它最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合)。
调用关系说明:
1. 服务容器负责启动,加载,运行服务提供者。
2. 服务提供者在启动时,向注册中心注册自己提供的服务。
3. 服务消费者在启动时,向注册中心订阅自己所需的服务。
4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
dubbo的优势
1.透明化的远程方法调用,就像调用本地方法一样调用远程方法,配置简单,无API侵入
2.软负载均衡以及容错机制,可在内网替代F5等硬件负载均衡器,降低成本
3.服务的自动注册与发现,不需要写死服务提供方的地址,注册中心基于接口名查询服务提供着的IP地址,并且可以平滑的添加或者删除服务提供者。
4.Dubbo采用Spring配置的方式,透明化接入应用,只需要Spring加载Dubbo的配置即可,Dubbo基于Spring的Schema扩展进行加载。
Dubbo启动后服务的管理:Dubbo进行服务管理控制时使用dubbo-admin(dubbo管控台)对服务进行控制,设置负载均衡策略。当provider禁用服务提供时,会抛出illegelStateExeception
远程通信:Webservice、restful、dubbo
1、Webservice:效率不高基于soap协议,其主要的特点是跨语言、跨平台的。项目中不推荐使用,可用于不同公司间接口的调用。
2、使用restful形式的服务:http+json。很多项目中应用。如果服务太多,服务之间调用关系混乱,需要治理服务。
3、使用dubbo。使用rpc协议进行远程调用,直接使用socket通信。传输效率高,并且可以统计出系统之间的调用关系、调用次数。使用Java语言开发,只能用于Java语言开发的项目间的通信,不具备跨语言,跨平台的特点!
13. SSM
1.https://blog.csdn.net/weixin_39801925/article/details/80585758
2.https://blog.csdn.net/qq_38262968/article/details/79474455
3.https://blog.csdn.net/a745233700/article/details/80977133
14.设计模式
Singleton(单例模式)
一句话总结:一个类在Java虚拟机中只有一个对象,并提供一个全局访问点。
生活中例子:太阳、月亮、国家主席等。
解决什么问题:对象的唯一性,性能浪费太多。
项目里面怎么用:数据库连接对象,属性配置文件的读取对象。
模式结构:分为饿汉式和懒汉式(如果考虑性能问题的话,就使用懒汉式,因为懒汉式是在方法里面进行初始化的),构造器私 有化,对外提供方法加同步关键字。
框架里面使用:Struts1的Action。
JDK里面使用:java.lang.Runtime#getRuntimejava.awt.Desktop#getDesktop。
常见的写法:饿汉式、懒汉式、静态内部类、枚举
observer(观察者模式)
对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。Android中的各种Listener就使用到了这一设计模式,只要用户对手机进行操作,对应的listener就会被通知,并作出响应的处理。
Decorator Pattern(装饰着模式)
对已有的业务逻辑进一步的封装,使其增加额外的功能,如java中的IO流就使用了装饰者模式,用户在使用的时候,可以任意组装,达到自己想要的效果。
Factory(工厂模式)
一句话总结:用一个方法来代替new关键字
生活中的例子:制衣厂、面包厂等生产厂。
解决什么问题:对象产生过多,或者经常有子类替换生成。
项目里面怎么用:对于经常生成的对象,或者父子类替换的对象。
模式结构:写一个对外声明的方法,方法里面使用new关键字代替。
框架里面使用:spring的核心就是工厂模式。
JDK里面使用:newInstance。
写法:简单工厂、工厂方法、抽象工厂
工厂方法和 抽象工厂的区别:
工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个。
工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。
Proxy(代理模式)
一句话总结:为其他对象提供一个代理,以控制对当前对象的访问。
生活中的例子:房屋中介、婚姻介绍所。
解决什么问题:不能直接访问该对象,或者太大的资源耗费多。
项目里面怎么用:权限,或者大对象的访问权限。
模式结构:代理类和被代理类实现同一个接口,用户访问的时候先访问代理对象,然后让代理对象去访问被代理对象。
框架里面使用:Spring里面的AOP实现。
JDK里面使用:java.lang.reflect.Proxy。
Adapter(适配器模式)
一句话总结:将两个原来不兼容的类兼容起来一起工作。
生活中的例子:变压器、充电器
解决什么问题:已经存在的相同功能的代码,但是接口不兼容,不能直接调用。
项目里面怎么用:在使用旧的API的时候,没有源码,和新的不能兼容。
模式结构:分为类适配器和对象适配,一般常用的就是对象适配器,因为组合由于继承。
框架里面使用:单元测试里面的asserEquels。
JDK里面使用:java.util.Arrays#asListjava.io.InputStreamReader(InputStream) java.io.outputStreamWriter(OutputStream)。
Strategy(策略模式)
一句话总结:定义一系列算法并可以互相替换。
生活中的例子:图片的格式,压缩文件的格式。
解决什么问题:做一件事情有很多种方法。
项目里面怎么用:购物车里面的付款方式。
模式结构:声明一个顶级接口,定义一个策略方法,具体的实例都要实现这个接口。
框架里面使用:hibernate的主键生成策略。
JDK里面使用:java.util.Comparator#compare。
15.Kafka
kafka是一个发布订阅消息系统,由topic区分消息种类,每个topic中可以有多个partition,每个kafka集群有一个多个broker服务器组成,producer可以发布消息到kafka中,consumer可以消费kafka中的数据。kafka就是生产者和消费者中间的一个暂存区,可以保存一段时间的数据保证使用。
Kafka的优势
- 可靠性:Kafka是一个具有分区机制、副本机制和容错机制的分布式消息系统
- 可扩展性:Kafka消息系统支持集群规模的热扩展
- 高性能:Kafka在数据发布和订阅过程中都能保证数据的高吞吐量。即便在TB级数据存储的情况下,仍然能保证稳定的性能。
名词解释
- Topic:在Kafka中,使用一个类别属性来划分数据的所属类,划分数据的这个类称为topic。如果把Kafka看做为一个数据库,topic可以理解为数据库中的一张表,topic的名字即为表名。
- Partition:topic中的数据分割为一个或多个partition。每个topic至少有一个partition。每个partition中的数据使用多个segment文件存储。partition中的数据是有序的,partition间的数据丢失了数据的顺序。如果topic有多个partition,消费数据时就不能保证数据的顺序。在需要严格保证消息的消费顺序的场景下,需要将partition数目设为1。
- Partition offset:每条消息都有一个当前Partition下唯一的64字节的offset,它指明了这条消息的起始位置。
- Replicas of partition:副本是一个分区的备份。副本不会被消费者消费,副本只用于防止数据丢失,即消费者不从为follower的partition中消费数据,而是从为leader的partition中读取数据。
- Broker:
- Kafka 集群包含一个或多个服务器,服务器节点称为broker。
- broker存储topic的数据。如果某topic有N个partition,集群有N个broker,那么每个broker存储该topic的一个partition。
- 如果某topic有N个partition,集群有(N+M)个broker,那么其中有N个broker存储该topic的一个partition,剩下的M个broker不存储该topic的partition数据。
- 如果某topic有N个partition,集群中broker数目少于N个,那么一个broker存储该topic的一个或多个partition。在实际生产环境中,尽量避免这种情况的发生,这种情况容易导致Kafka集群数据不均衡。
- Producer:生产者即数据的发布者,该角色将消息发布到Kafka的topic中。broker接收到生产者发送的消息后,broker将该消息追加到当前用于追加数据的segment文件中。生产者发送的消息,存储到一个partition中,生产者也可以指定数据存储的partition。
- Consumer:消费者可以从broker中读取数据。消费者可以消费多个topic中的数据。
- Leader:每个partition有多个副本,其中有且仅有一个作为Leader,Leader是当前负责数据的读写的partition。
- Follower:Follower跟随Leader,所有写请求都通过Leader路由,数据变更会广播给所有Follower,Follower与Leader保持数据同步。如果Leader失效,则从Follower中选举出一个新的Leader。当Follower与Leader挂掉、卡住或者同步太慢,leader会把这个follower从“in sync replicas”(ISR)列表中删除,重新创建一个Follower。
- ISR (In-Sync Replicas),这个是指副本同步队列。副本数对Kafka的吞吐率是有一定的影响,但极大的增强了可用性。默认情况下Kafka的replica数量为1,即每个partition都有一个唯一的leader,为了确保消息的可靠性,通常应用中将其值(由broker的参数offsets.topic.replication.factor指定)大小设置为大于1,比如3。 所有的副本(replicas)统称为Assigned Replicas,即AR。ISR是AR中的一个子集,由leader维护ISR列表,follower从leader同步数据有一些延迟(包括延迟时间replica.lag.time.max.ms和延迟条数replica.lag.max.messages两个维度, 当前最新的版本0.10.x中只支持replica.lag.time.max.ms这个维度),任意一个超过阈值都会把follower剔除出ISR, 存入OSR(Outof-Sync Replicas)列表,新加入的follower也会先存放在OSR中。AR=ISR+OSR。
16. MySQL
主键、超键、候选键、外键
主 键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。
超 键:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。
候选键:是最小超键,即没有冗余元素的超键。
外 键:在一个表中存在的另一个表的主键称此表的外键。
事务
原子性:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性:在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
隔离性:隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请 求,使得在同一时间仅有一个请求用于同一数据。
持久性:在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
视图
视图是虚拟的表,与包含数据的表不一样,视图只包含使用时动态检索数据的查询;不包含任何列或数据。使用视图可以简化复杂的sql操作,隐藏具体的细节,保护数据;视图创建后,可以使用与表相同的方式利用它们。
视图不能被索引,也不能有关联的触发器或默认值,如果视图本身内有order by 则对视图再次order by将被覆盖。
创建视图:create view XXX as XXXXXXXXXXXXXX;
对于某些视图比如未使用联结子查询分组聚集函数Distinct Union等,是可以对其更新的,对视图的更新将对基表进行更新;但是视图主要用于简化检索,保护数据,并不用于更新,而且大部分视图都不可以更新。
drop,delete与truncate的区别
drop直接删掉表 truncate删除表中数据,再插入时自增长id又从1开始 delete删除表中数据,可以加where字句。
1.DELETE语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作。TRUNCATE TABLE 则一次性地从表中删除所有的数据并不把单独的删除操作记录记入日志保存,删除行是不能恢复的。并且在删除的过程中不会激活与表有关的删除触发器。执行速度快。
2.表和索引所占空间。当表被TRUNCATE 后,这个表和索引所占用的空间会恢复到初始大小,而DELETE操作不会减少表或索引所占用的空间。drop语句将表所占用的空间全释放掉。
3.一般而言,drop > truncate > delete
4.应用范围。TRUNCATE 只能对TABLE;DELETE可以是table和view
5.TRUNCATE 和DELETE只删除数据,而DROP则删除整个表(结构和数据)。
6.truncate与不带where的delete :只删除数据,而不删除表的结构(定义)drop语句将删除表的结构被依赖的约束(constrain),触发器(trigger)索引(index);依赖于该表的存储过程/函数将被保留,但其状态会变为:invalid。
7.delete语句为DML(data maintain Language),这个操作会被放到 rollback segment中,事务提交后才生效。如果有相应的 tigger,执行的时候将被触发。
8.truncate、drop是DLL(data define language),操作立即生效,原数据不放到 rollback segment中,不能回滚
9.在没有备份情况下,谨慎使用 drop 与 truncate。要删除部分数据行采用delete且注意结合where来约束影响范围。回滚段要足够大。要删除表用drop;若想保留表而将表中数据删除,如果与事务无关,用truncate即可实现。如果和事务有关,或想触发trigger,还是用delete。
10.Truncate table 表名 速度快,而且效率高,因为:truncate table 在功能上与不带 WHERE 子句的 DELETE 语句相同:二者均删除表中的全部行。但 TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少。DELETE 语句每次删除一行,并在事务日志中为所删除的每行记录一项。TRUNCATE TABLE 通过释放存储表数据所用的数据页来删除数据,并且只在事务日志中记录页的释放。
11.TRUNCATE TABLE 删除表中的所有行,但表结构及其列、约束、索引等保持不变。新行标识所用的计数值重置为该列的种子。如果想保留标识计数值,请改用 DELETE。如果要删除表定义及其数据,请使用 DROP TABLE 语句。
12.对于由 FOREIGN KEY 约束引用的表,不能使用 TRUNCATE TABLE,而应使用不带 WHERE 子句的 DELETE 语句。由于 TRUNCATE TABLE 不记录在日志中,所以它不能激活触发器。
索引
数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。
索引的优点
1.通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
2.可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
3.可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
4.在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
5.通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
索引的缺点
1.创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
2.索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
3.当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
应该创建索引的列
1.在经常需要搜索的列上,可以加快搜索的速度;
2.在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构;
3.在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度;
4.在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;
5.在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;
6.在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。
不该创建索引的列
1.对于那些在查询中很少使用或者参考的列不应该创建索引。这是因为,既然这些列很少使用到,因此有索引或者无索引,并不能提高查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。
2.对于那些只有很少数据值的列也不应该增加索引。这是因为,由于这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在表中搜索的数据行的比例很大。增加索引,并不能明显加快检索速度。
3.对于那些定义为text, image和bit数据类型的列不应该增加索引。这是因为,这些列的数据量要么相当大,要么取值很少。
4.当修改性能远远大于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。因此,当修改性能远远大于检索性能时,不应该创建索引。
唯一索引:唯一索引是不允许其中任何两行具有相同索引值的索引。
主键索引:数据库表经常有一列或列组合,其值唯一标识表中的每一行。该列称为表的主键。
聚集索引:数据行的物理顺序与列值(一般是主键的那一列)的逻辑顺序相同,一个表中只能拥有一个聚集索引。
主键索引和唯一索引的区别
1.主键是一种约束,唯一索引是一种索引,两者在本质上是不同的。
2.主键创建后一定包含一个唯一性索引,唯一性索引并不一定就是主键。
3.唯一性索引列允许空值,而主键列不允许为空值。
4.主键列在创建时,已经默认为空值 + 唯一索引了。
5.主键可以被其他表引用为外键,而唯一索引不能。
6.一个表最多只能创建一个主键,但可以创建多个唯一索引。
7.主键更适合那些不容易更改的唯一标识,如自动递增列、身份证号等。
8.在 RBO 模式下,主键的执行计划优先级要高于唯一索引。 两者可以提高查询的速度。
局部性原理和磁盘预读
由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分之一,因此为了提高效率,要尽量减少磁盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。程序运行期间所需要的数据通常比较集中。
having和where的区别
where 子句的作用是在对查询结果进行分组前,将不符合where条件的行去掉,即在分组之前过滤数据,条件中不能包含聚集函数,使用where条件显示特定的行。
having 子句的作用是筛选满足条件的组,即在分组之后过滤数据,条件中经常包含聚集函数,使用having 条件显示特定的组,也可以使用多个分组标准进行分组。
char和varchar的区别
1.char的长度是不可变的,而varchar的长度是可变的。定义一个char[10]和varchar[10],如果存进去的是'abcd',那么char所占的长度依然为10,除了字符'abcd'外,后面跟六个空格,而varchar就立马把长度变为4了,取数据的时候,char类型的要用trim()去掉多余的空格,而varchar是不需要的,
2.char的存取数度还是要比varchar要快得多,因为其长度固定,方便程序的存储与查找;但是char也为此付出的是空间的代价,因为其长度固定,所以难免会有多余的空格占位符占据空间,可谓是以空间换取时间效率,而varchar是以空间效率为首位的。
3.char的存储方式是,对英文字符(ASCII)占用1个字节,对一个汉字占用两个字节;而varchar的存储方式是,对每个英文字符占用2个字节,汉字也占用2个字节,两者的存储数据都非unicode的字符数据。
数据库优化
1.应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描。可以给该字段设置一个默认值
3.很多时候用 exists 代替 in 是一个好的选择
4.用Where子句替换HAVING 子句 因为HAVING只会在检索出所有记录之后才对结果集进行过滤
5.避免在where子句中对字段进行函数操作
6.使用连接代替子查询
7.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销
存储过程与触发器的区别
存储过程和触发器都是SQL语句集;触发器不可用CALL调用,而是在用户执行某些语句后自动调用;
索引原理B+树
B+是一种平衡的多叉查找树,在B树的基础上满足
- 有n棵子树的节点含有n个关键字(也有认为是n-1个关键字)。
- 所有的关键字全部存储在叶子节点上,且叶子节点本身根据关键字自小而大顺序连接。
- 非叶子节点可以看成索引部分,节点中仅含有其子树(根节点)中的最大(或最小)关键字。
为什么红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用B-/+Tree作为索引结构
一般使用磁盘I/O次数评价索引结构的优劣。先从B-Tree分析,根据B-Tree的定义,可知检索一次最多需要访问h个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:
每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。
B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存),渐进复杂度为O(h)=O(logdN)。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)。
综上所述,用B-Tree作为索引结构效率是非常高的。
而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的I/O渐进复杂度也为O(h),效率明显比B-Tree差很多。
2. https://www.cnblogs.com/coderxuyang/p/3718856.html
https://blog.csdn.net/sidihuo/article/details/78489820
3. https://www.cnblogs.com/study-everyday/p/6430462.html
https://blog.csdn.net/wo2niliye/article/details/69396812
https://blog.csdn.net/madongyu1259892936/article/details/79281612
https://www.cnblogs.com/heyonggang/p/9112731.html
http://www.importnew.com/28263.html
https://www.cnblogs.com/ITtangtang/p/3948786.html
4. https://baijiahao.baidu.com/s?id=1612142459503895416&wfr=spider&for=pc
https://www.cnblogs.com/ygj0930/p/6561667.html
http://www.importnew.com/23511.html(锁优化)
5. https://www.cnblogs.com/baizhanshi/p/6419268.html
https://www.cnblogs.com/duanxz/p/3559510.html
6. https://www.cnblogs.com/zhengbin/p/6490953.html
7. https://www.cnblogs.com/dolphin0520/p/3920373.html
https://www.cnblogs.com/zhengbin/p/5654805.html#autoid-0-1-0
https://blog.csdn.net/vking_wang/article/details/9982709
8. https://www.cnblogs.com/barrywxx/p/8487444.html
https://blog.csdn.net/v123411739/article/details/79561458
https://www.jianshu.com/p/84c75074fa03
9. https://blog.csdn.net/u011972171/article/details/80398771
https://www.cnblogs.com/itar/p/7424311.html
10. https://blog.csdn.net/ll305vc/article/details/79650467
https://blog.csdn.net/ylyg050518/article/details/52245922
11. https://www.imooc.com/article/51147
https://blog.csdn.net/xiamiflying/article/details/80776416
https://www.jianshu.com/p/6c6f396fc88e
https://www.cnblogs.com/dolphin0520/p/3932921.html
https://blog.csdn.net/jubaoquan/article/details/79198780
https://www.cnblogs.com/diaozhaojian/p/6391992.html
12. https://blog.csdn.net/pengwenrui/article/details/80724371
https://www.cnblogs.com/shan1393/p/9338530.html
http://www.cnblogs.com/yang-lq/p/9168216.html
14. https://blog.csdn.net/qq_33326449/article/details/78946364
https://blog.csdn.net/qq_24309787/article/details/82257772
15. https://blog.csdn.net/wypersist/article/details/80102803
https://blog.csdn.net/lingbo229/article/details/80761778
https://www.cnblogs.com/BYRans/p/6054930.html
16. https://blog.csdn.net/u011010851/article/details/80074550
https://www.cnblogs.com/frankielf0921/p/5930743.html
概念问题
final
final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。
当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。
使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。
final修饰的方法表示此方法已经是“最后的、最终的”含义,亦即此方法不能被重写(可以重载多个final修饰的方法)。此处需要注意的一点是:因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。)
final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。
final修饰一个成员变量(属性),必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值。
当函数的参数类型声明为final时,说明该参数是只读型的。即你可以读取使用该参数,但是无法改变该参数的值。
mysql的sql注入
所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。
实例:SQL注入攻击实例,比如在一个登录界面,要求输入用户名和密码:可以这样输入实现免帐号登录:用户名: ‘or 1 = 1 –密 码:点登陆,如若没有做特殊处理,那么这个非法用户就很得意的登陆进去了.
防止SQL注入,我们需要注意以下几个要点:
1.永远不要信任用户的输入。对用户的输入进行校验,可以通过正则表达式,或限制长度;对单引号和 双"-"进行转换等。
2.永远不要使用动态拼装sql,可以使用参数化的sql或者直接使用存储过程进行数据查询存取。
3.永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接。
4.不要把机密信息直接存放,加密或者hash掉密码和敏感的信息。
5.应用的异常信息应该给出尽可能少的提示,最好使用自定义的错误信息对原始错误信息进行包装
6.sql注入的检测方法一般采取辅助软件或网站平台来检测,软件一般采用sql注入检测工具jsky,网站平台就有亿思网站安全平台检测工具。MDCSOFT SCAN等。采用MDCSOFT-IPS可以有效的防御SQL注入,XSS攻击等。
wait和sleep 的区别
wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程(唤醒后的线程是阻塞状态)。sleep() 方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。
wait只能在同步控制方法或者同步控制块里面使用,sleep可以在任何地方使用。
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常,wait和sleep都响应中断
sleep()
sleep()方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。
wait()
wait()方法需要和notify()及notifyAll()两个方法一起介绍,这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized语句块内使用,也就是说,调用wait(),notify()和notifyAll()的任务在调用这些方法前必须拥有对象的锁。注意,它们都是Object类的方法,而不是Thread类的方法。
wait()方法与sleep()方法的不同之处在于,wait()方法会释放对象的“锁标志”。当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的notifyAll()方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。
除了使用notify()和notifyAll()方法,还可以使用带毫秒参数的wait(long timeout)方法,效果是在延迟timeout毫秒后,被暂停的线程将被恢复到锁标志等待池。
此外,wait(),notify()及notifyAll()只能在synchronized语句中使用,但是如果使用的是ReenTrantLock实现同步,该如何达到这三个方法的效果呢?解决方法是使用ReenTrantLock.newCondition()获取一个Condition类对象,然后Condition的await(),signal()以及signalAll()分别对应上面的三个方法。
yield()
yield()方法和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同。
join()
join()方法会使当前线程等待调用join()方法的线程结束后才能继续执行
进程间的通信方式有哪几种:
管道,信号,socket,共享内存,消息队列
死锁(https://www.cnblogs.com/xiaoxi/p/8311034.html)
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
产生的必要条件
- 互斥条件:一个资源每次只能被一个线程使用;
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放;
- 不可剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺;
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
避免死锁
- 加锁顺序(线程按照一定的顺序加锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
- 死锁检测
线程和进程的区别
进程是指在系统中正在运行的一个应用程序;程序一旦运行就是进程,或者更专业化来说:进程是指程序执行时的一个实例。线程是进程的一个实体。进程——资源分配的最小单位,线程——程序执行的最小单位。
区别
1.因为进程拥有独立的堆栈空间和数据段,所以每当启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这对于多进程来说十分“奢侈”,系统开销比较大,而线程不一样,线程拥有独立的堆栈空间,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据,比进程更节俭,开销比较小,切换速度也比进程快,效率高,但是正由于进程之间独立的特点,使得进程安全性比较高,也因为进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。一个线程死掉就等于整个进程死掉。
2.体现在通信机制上面,正因为进程之间互不干扰,相互独立,进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制,而线程由于共享数据段所以通信机制很方便。。
3.属于同一个进程的所有线程共享该进程的所有资源,包括文件描述符。而不同过的进程相互独立。
4.线程又称为轻量级进程,进程有进程控制块,线程有线程控制块;
5.线程必定也只能属于一个进程,而进程可以拥有多个线程而且至少拥有一个线程;
进程与线程的选择取决以下几点:
1、需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的。
2、线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应
3、因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程;
4、并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求;
5、需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。
HashMap和Hashtable的区别
1.HashMap 没有考虑同步,是线程不安全的;Hashtable 使用了 synchronized 关键字,是线程安全的;
2.HashMap允许存空值空键,Hashtable不允许。
Vector & ArrayList
1、Vector的方法都是同步的(Synchronized),是线程安全的(thread-safe),而ArrayList的方法不是,由于线程的同步必然要影响性能,因此,ArrayList的性能比Vector好。
2、当Vector或ArrayList中的元素超过它的初始大小时,Vector会将它的容量翻倍,而ArrayList只增加50%的大小,这样,ArrayList就有利于节约内存空间。
ArrayList & LinkedList
ArrayList的内部实现是基于内部数组Object[],所以从概念上讲,它更象数组,但LinkedList的内部实现是基于一组连接的记录,所以,它更象一个链表结构,所以,它们在性能上有很大的差别:
从上面的分析可知,在ArrayList的前面或中间插入数据时,你必须将其后的所有数据相应的后移,这样必然要花费较多时间,所以,当你的操作是在一列 数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能; 而访问链表中的某个元素时,就必须从链表的一端开始沿着连接方向一个一个元素地去查找,直到找到所需的元素为止,所以,当你的操作是在一列数据的前面或中 间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了。
NIO和IO的区别
NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
1、面向流与面向缓冲
Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
2、阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
3、选择器(Selectors)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。