20190318,第一个面试邀请,仔细看了招聘条件才发现不小心投了偏大数据的职位,先看看呗
CAP(Consistency一致性、Availability可用性、Partition-tolerance分区可容忍性)理论普遍被当作是大数据技术的理论基础。同时,根据该理论,业界有一种非常流行、非常“专业”的认识,那就是:关系型数据库设计选择了C(一致性)与A(可用性),NoSQL数据库设计则不同。其中,HBase选择了C(一致性)与P(分区可容忍性),Cassandra选择了A(可用性)与P(分区可容忍性)。
CAP原理指的是,在分布式系统中这三个要素最多只能同时实现两点,不可能三者兼顾。因此在进行分布式架构设计时,必须做出取舍。而对于分布式数据系统,分区容忍性是基本要求,否则就失去了价值。因此设计分布式数据系统,就是在一致性和可用性之间取一个平衡。对于大多数web应用,其实并不需要强一致性,因此牺牲一致性而换取高可用性,是目前多数分布式数据库产品的方向。
BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性(StrongConsistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(EventualConsitency)。
BASE是指基本可用(BasicallyAvailable)、软状态(SoftState)、最终一致性(EventualConsistency)。
基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。
微服务是一个软件架构,就是把一个大型的单个应用程序和服务拆分为数十个的支持微服务。
DDD(DomainDrivenDesign(领域驱动设计))
领域就是问题域,有边界,领域中有很多问题;
任何一个系统要解决的那个大问题都对应一个领域;
通过建立领域模型来解决领域中的核心问题,模型驱动的思想;
领域模型在设计时应考虑一定的抽象性、通用性,以及复用价值;
通过领域模型驱动代码的实现,确保代码让领域模型落地,代码最终能解决问题;
领域模型是系统的核心,是领域内的业务的直接沉淀,具有非常大的业务价值;
技术架构设计或数据存储等是在领域模型的外围,帮助领域模型进行落地;
12.闭锁CountDownLatch
java.util.concurrent.CountDownLatch是一个并发构造,它允许一个或多个线程等待一系列指定操作的完成。
CountDownLatch以一个给定的数量初始化。countDown()每被调用一次,这一数量就减一。通过调用await()方法之一,线程可以阻塞等待这一数量到达零。
20190319,嘛,自从改了简历以后,面试突然变得很多,第一家面试的是根网,一家做证券的公司,有技术笔试和数学笔试,技术面也过了,技术总监劝我慎重考虑要不要入行。还问了两个问题,没准备的问题。应对能力有待加强,主要还是概念有些模糊,这两个问题我其实都知道一点的,技术总监想要唬住我,诱导性的面试,并行和并发的时候还问过一个cpu如何实现并行,再问我我就回答一个cup无法实现并行。
方法区中类静态属性引用的对象作为GCRoots,而单例对象是静态属性引用的对象。而单例类被回收的条件之一是该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。所以单例类和单例对象都不会被回收。
并行和并发
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
上面两个问题是总监面的,技术一面过了,不过感觉还是很多东西答的不好,沟通起来有些困难,有些东西想不到好的描述语言,还需要背啊。
项目中遇到的问题:
分布式:一个业务分拆多个子业务,部署在不同的服务器上集群:同一个业务,部署在多个服务器上
分布式锁一般有三种实现方式:1.Mysql乐观锁;2.基于Redis的分布式锁;3.基于ZooKeeper的分布式锁。
Mysql乐观锁:根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。
Redis分布式锁:
基于SETNX、EXPIRE
使用SETNX(setifnotexist)命令插入一个键值对时,如果Key已经存在,那么会返回False,否则插入成功并返回True。因此客户端在尝试获得锁时,先使用SETNX向Redis中插入一个记录,如果返回True表示获得锁,返回False表示已经有客户端占用锁。
基于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如果队列已满,则返回falsepoll移除并返问队列头部的元素如果队列为空,则返回nullpeek返回队列头部的元素如果队列为空,则返回nullput添加一个元素如果队列满,则阻塞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方法
调用该方法后当前线程进入睡眠状态,直到以下事件发生。
(1)其他线程调用了该对象的notify方法。
(2)其他线程调用了该对象的notifyAll方法。
(3)其他线程调用了interrupt中断该线程。
此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
8.notify方法:该方法唤醒在该对象上等待的某个线程。
9.notifyAll方法:该方法唤醒在该对象上等待的所有线程。
事物管理
自定义注解怎么实现
注解本质是一个继承了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(sortedset--有序集合)和hash(哈希类型)
MySQL存储引擎
MySQL默认会提供多种存储引擎,可以用showengines查看,一般常用的有两种Innodb和MyISAM
Innodb引擎提供了对数据库ACID事务的支持,并且实现了SQL标准的四种隔离级别。该引擎还提供了行级锁和外键约束,它的设计目标是处理大容量数据库系统,它本身其实就是基于MySQL后台的完整数据库系统,MySQL运行时Innodb会在内存中建立缓冲池,用于缓冲数据和索引。但是该引擎不支持FULLTEXT类型的索引,而且它没有保存表的行数,当SELECTCOUNT(*)FROMTABLE时需要扫描全表。当需要使用数据库事务时,该引擎当然是首选。由于锁的粒度更小,写操作不会锁定全表,所以在并发较高时,使用Innodb引擎会提升效率。但是使用行级锁也不是绝对的,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表。
MyISAM是MySQL默认的引擎,但是它没有提供对数据库事务的支持,也不支持行级锁和外键,因此当INSERT(插入)或UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低一些。不过和Innodb不同,MyISAM中存储了表的行数,于是SELECTCOUNT(*)FROMTABLE时只需要直接读取已经保存好的值而不需要进行全表扫描。如果表的读操作远远多于写操作且不需要数据库事务的支持,那么MyISAM也是很好的选择。
简单介绍区别:
1、MyISAM是非事务安全的,而InnoDB是事务安全的
2、MyISAM锁的粒度是表级的,而InnoDB支持行级锁
3、MyISAM支持全文类型索引,而InnoDB不支持全文索引
4、MyISAM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyISAM
5、MyISAM表保存成文件形式,跨平台使用更加方便
应用场景:
1、MyISAM管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择MyISAM2、InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB
explain
explain用来分析sql查询语句的执行效率,desc命令同样的效果。
语法:explain查询语句
举个栗子:explainselect*fromnews;
输出:
+----+-------------+-------+-------+-------------------+---------+---------+-------+------|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语句
DEPENDENTUNION:UNION中的第二个或后面的SELECT语句,取决于外面的查询
UNIONRESULT:UNION的结果。
SUBQUERY:子查询中的第一个SELECT
DEPENDENTSUBQUERY:子查询中的第一个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找到了与行相联合匹配的行,就不再搜索了
NotexistsMYSQL优化了LEFTJOIN,一旦它找到了匹配LEFTJOIN标准的行,
就不再搜索了
Rangecheckedforeach
Record(indexmap:#)没有找到理想的索引,因此对于从前面表中来的每一个行组合,MYSQL检查使用哪个索引,并用它来从表中返回行。这是使用索引的最慢的连接之一
Usingfilesort看到这个的时候,查询就需要优化了。MYSQL需要进行额外的步骤来发现如何对返回的行排序。它根据连接类型以及存储排序键值和匹配条件的全部行的行指针来排序全部行
Usingindex列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的,这发生在对表的全部的请求列都是同一个索引的部分的时候
Usingtemporary看到这个的时候,查询需要优化了。这里,MYSQL需要创建一个临时表来存储结果,这通常发生在对不同的列集进行ORDERBY上,而不是GROUPBY上
Usingwhere使用了WHERE从句来限制哪些行将与下一张表匹配或者是返回给用户。如果不想返回表中的全部行,并且连接类型ALL或index,这就会发生,或者是查询有问题
其他一些Tip:当type显示为“index”时,并且Extra显示为“UsingIndex”,表明使用了覆盖索引。
20190324,这周周四下午第一个面试是必要商城,经历了三轮技术面试,然后没有消息了。第二个是琳云科技,这是只差一个程序员的公司,因为必要商城面试比较久,这一家迟到了,只差一个程序员的这一家也没消息。一个offer都拿不到,第一家的那个offer还不如以前公司高,感觉我的弱项在于项目,没有背景不好搞啊。
缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决办法:
1.布隆过滤:对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃。还有最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
2.缓存空对象.将null变成一个值.。
缓存雪崩
Integerv1=127;Integerv2=127;Integerv3=128;Integerv4=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和设置值里面取最大值作为最大范围。
JDK1.7中,HashMap是由数组+链表实现的,使用一个Entry数组来存储数据,用key的hashcode取模来决定key会被放到数组里的位置。如果hashcode相同,或者hashcode取模后的结果相同,那么这些key会被定位到Entry数组的同一个格子里,这些Entry对象以链表的形式存储。
HashMap为了存取高效,要尽量减少碰撞,就是要尽量把数据分配均匀,使每个链表长度大致相同,这种算法实际就是取模。计算机中直接求余的效率不如位运算,HashMap源码中做了优化,用hash&(length-1)来代替取模运算。只有当length-1每一位都是1的时候,才能保证hash的后几位都被取到而不浪费空间,所以HashMap的长度要是2的n次方。
为了减少哈希碰撞,HashMap容量为2的n次方个(HashMap的内部也做了这样的限制),所以最少容量为128,当HashMap中装载的元素数量超过容量乘以加载因子的时候,会进行rehash操作,这是一个代价很大的操作,所以加载因子要大于100/128=0.78125,可以设置加载因子为0.8,即newHashMap(128,0.8)。
若采用默认的加载因子0.75,容量至少为100/0.75=134,且保证容量为2的n次方个,所以最少的容量为256,即newHashMap(256)。
HashMap是线程不安全的,多线程环境下使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以并发情况下不能使用HashMap。
JDK1.7中,多个线程对HashMap进行put操作时,有一定概率在map扩容的时候产生环形链表,导致死循环。1.8中链表插入方式改成尾部插入法以后,不会有死循环的问题,不过还是会有数据丢失的问题。
JDK1.7中ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
1.某个对象的实例锁,作用于某个实例对象,并且这个对象中有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法。此外,不同对象实例的synchronized方法是不相干预的。也就是说,其它线程可以同时访问此类下的另一个对象实例中的synchronized方法;
2.某个类的锁,作用于此类的所有对象实例。
此外,synchronized是不能被继承的,也就是说基类的方法synchronizedfun(){}在继承类中并不自动是synchronizedfun(){},而是变成了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是一个同步工具,相当于操作系统中的互斥量(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集合中,等待下一次被唤醒。
1.使用synchronized,当多个线程尝试获取锁时,未获取到锁的线程会不断的尝试获取锁,而不会发生中断,这样会造成性能消耗。
2.如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
3.synchronized无法知道线程有没有成功获取到锁。
Lock是java1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制。本质上Lock仅仅是一个接口(位于源码包中的java.util.concurrent.locks中),它包含以下方法
使用方法:多线程下访问(互斥)共享资源时,访问前加锁,访问结束以后解锁,解锁的操作推荐放入finally块中。
注意:加锁位于对资源访问的try块的外部,特别是使用lockInterruptibly方法加锁时就必须要这样做,这为了防止线程在获取锁时被中断,这时就不必(也不能)释放锁。
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
如果锁具备可重入性,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
看下面这段代码就明白了:
classMyClass{publicsynchronizedvoidmethod1(){method2();}publicsynchronizedvoidmethod2(){}}上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。
而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。
可中断锁:顾名思义,就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。
在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。
我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:ReentrantLocklock=newReentrantLock(true);如果参数为true表示为公平锁,为fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
可以通过readLock()获取读锁,通过writeLock()获取写锁。
在HotSpot虚拟机中,对象在内存中存储布局分为3块区域:对象头(Header)、实例数据(InstanceData)、对齐填充(Padding)
HotSpot虚拟机的对象头包括两部分(非数组对象)信息,如下图所示:
这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit。
例如,在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,那么MarkWord的32bit空间中的25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0,如下表所示:
在32位系统下,存放Class指针的空间大小是4字节,MarkWord空间大小也是4字节,因此头部就是8字节,如果是数组就需要再加4字节表示数组的长度,如下表所示:
在64位系统及64位JVM下,开启指针压缩,那么头部存放Class指针的空间大小还是4字节,而MarkWord区域会变大,变成8字节,也就是头部最少为12字节,如下表所示:
压缩指针:开启指针压缩使用算法开销带来内存节约,Java对象都是以8字节对齐的,也就是以8字节为内存访问的基本单元,那么在地理处理上,就有3个位是空闲的,这3个位可以用来虚拟,利用32位的地址指针原本最多只能寻址4GB,但是加上3个位的8种内部运算,就可以变化出32GB的寻址。
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。
对齐填充不是必然存在的,没有特别的含义,它仅起到占位符的作用。
由于HotSpotVM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍。对象头部分是8字节的倍数,所以当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
volatile是一个类型修饰符,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
1)volatile修饰布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
2)单例模式中的doublecheck,防止获得残缺对象。
CAS是compareandswap的缩写,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方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
3.只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
JVM内存共分为程序计数器,虚拟机栈,本地方法栈,堆,方法区五个部分。
每条线程都有一个独立的程序计数器,这类内存也称为“线程私有”的内存,指向当前正在执行的字节码的行号。正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果是Natice方法,则为空。
每个方法在执行的时候也会创建一个栈帧,存储了局部变量,操作数,动态链接,方法返回地址。每个方法从调用到执行完毕,对应一个栈帧在虚拟机栈中的入栈和出栈。通常所说的栈,一般是指在虚拟机栈中的局部变量部分。局部变量所需内存在编译期间完成分配,如果线程请求的栈深度大于虚拟机所允许的深度,则StackOverflowError。如果虚拟机栈可以动态扩展,扩展到无法申请足够的内存,则OutOfMemoryError。
和虚拟机栈类似,主要为虚拟机使用到的Native方法服务。也会抛出StackOverflowError和OutOfMemoryError。
被所有线程共享的一块内存区域,在虚拟机启动的时候创建,用于存放对象实例。堆可以按照可扩展来实现(通过-Xmx和-Xms来控制)当堆中没有内存可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
被所有方法线程共享的一块内存区域。用于存储已经被虚拟机加载的类信息,常量,静态变量等。这个区域的内存回收目标主要针对常量池的回收和堆类型的卸载。
1.堆(Heap)
2.方法区(MethodArea)
方法区也称"永久代",它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB(64位JVM由于指针膨胀,默认是85M),可以通过-XX:PermSize和-XX:MaxPermSize参数限制方法区的大小。它是一片连续的堆空间,永久代的垃圾收集是和老年代(oldgeneration)捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。不过,一个明显的问题是,当JVM加载的类信息容量超过了参数-XX:MaxPermSize设定的值时,应用将会报OOM的错误。参数是通过-XX:PermSize和-XX:MaxPermSize来设定的
运行时常量池(RuntimeConstantPool):是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。
从JDK7开始移除永久代(但并没有移除,还是存在),贮存在永久代的一部分数据已经转移到了JavaHeap或者是NativeHeap:符号引用(Symbols)转移到了nativeheap;字面量(internedstrings)转移到了javaheap;类的静态变量(classstatics)转移到了javaheap。从JDK8开始使用元空间(Metaspace),元空间的大小受本地内存限制,新参数(MaxMetaspaceSize)用于限制本地内存分配给类元数据的大小。如果没有指定这个参数,元空间会在运行时根据需要动态调整。
3.虚拟机栈(JVMStack)
局部变量表:存放了编译器可知的各种基本数据类型、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。
4.本地方法栈(NativeStack)
与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。(栈的空间大小远远小于堆)
5.程序计数器(PCRegister)
是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。
6.直接内存
直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小.
Java堆(JavaHeap)是Java虚拟机所管理的内存中最大的一块。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这个区域也是Java垃圾收集器管理的主要区域,因此很多时候也被称为”GC堆”。
在Java中,堆被划分成两个不同的区域:新生代(Young)、老年代(Old)。新生代(Young)又被划分为三个区域:Eden、FromSurvivor、ToSurvivor。这样划分的目的是为了使JVM能够更好的管理堆内存中的对象,包括内存的分配以及回收。
堆的内存模型大致如下图所示:
从图中可以看出:堆大小=新生代+老年代。
默认情况下,
JVM每次只会使用Eden和其中的一块Survivor区域来为对象服务,所以无论什么时候,总是有一块Survivor区域是空闲着的。因此,新生代实际可用的内存空间为9/10(即90%)的新生代空间。
Java中的堆也是GC收集垃圾的主要区域。GC分为两种:MinorGC、FullGC(或称为MajorGC)。
MinorGC是发生在新生代中的垃圾收集动作,所采用的是复制算法。新生代几乎是所有Java对象出生的地方,即Java对象申请的内存以及存放都是在这个地方。Java中的大部分对象通常不需长久存活,具有朝生夕灭的性质。当一个对象被判定为“死亡”的时候,GC就有责任来回收掉这部分对象的内存空间。新生代是GC收集垃圾的频繁区域。
回收过程如下:
当对象在Eden(包括一个Survivor区域,这里假设是from区域)出生后,在经过一次MinorGC后,如果对象还存活,并且能够被另外一块Survivor区域所容纳(上面已经假设为from区域,这里应为to区域,即to区域有足够的内存空间来存储Eden和from区域中存活的对象),则使用复制算法将这些仍然还存活的对象复制到另外一块Survivor区域(即to区域)中,然后清理所使用过的Eden以及Survivor区域(即from区域),并且将这些对象的年龄设置为1,以后对象在Survivor区每熬过一次MinorGC,就将对象的年龄+1,当对象的年龄达到某个值时(默认是15岁,可以通过参数-XX:MaxTenuringThreshold来设定),这些对象就会成为老年代。但这也不是一定的,对于一些较大的对象(即需要分配一块较大的连续内存空间)则是直接进入到老年代。
在堆里存放着Java中几乎所有的对象实例,在进行垃圾回收之前,首先要确定哪些对象应该被回收。
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减一;任何时候计数器为0的对象就是不可能再被使用的。
这种方法特点就是,实现简单,判定效率也很高,大部分情况下都是一个不错的选择。但是很多主流的Java虚拟机没有选择使用引用计数法类管理内存,主要原因它很难解决对象之间相互循环引用的问题。
publicclassMyObject{publicObjectref=null;publicstaticvoidmain(String[]args){MyObjectmyObject1=newMyObject();MyObjectmyObject2=newMyObject();myObject1.ref=myObject2;myObject2.ref=myObject1;myObject1=null;myObject2=null;}}
循环引用导致myObject1和myObject2两个被引用次数均为2,即使当两个对象都被置为null后,引用次数仍然为1,导致两个对象无法被回收。
在主流的商用程序语言(例如Java,C#)的主流实现中,都是通过可达性分析来判定对象是否存活的。
这个算法的基本思路就是通过一系列的称为“GCRoots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链。当一个对象到GCroots没有任何引用链相连时,则证明此对象是不可用的。如下图所示,对象object5,object6,object7虽然互相有关联,但是它们到GCRoots是不可达的,所以它们将会被判定为是可回收的对象。
Java中,可作为GCRoots的对象包括下面几种:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象。2.方法区中类静态属性引用的对象。3.方法区中常量引用的对象。4.本地方法栈中JNI(即一般说的Native方法)引用的对象。即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GCRoots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为”没有必要执行“。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个虚拟机自动建立的,低优先级的Finalizer线程中去执行它。这里所谓的”执行”是指虚拟机会触发这个方法,但并不会保证会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(极端情况),将很可能导致F-Queue队列中其他对象处于永久等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出”即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
常见的垃圾收集算法主要涉及到这几种,标记——清除算法,复制算法,标记整理算法,分代收集算法等。
这是一种最基础的收集算法(Mark-Sweep)。分为标记-清除两个阶段。
标记阶段:先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程其实就是上述的可达性分析算法中的标记过程。它是最基础的算法,后续的收集算法都是基于这种思路并对其不足进行改进而得到的。
不足之处主要有两个:1.效率问题,标记和清除两个过程效率都不高。2.空间问题,标记清除之后会产生大量不连续的碎片,可能会导致后续程序需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
在上述标记——清除算法的基础上,为了解决效率问题,复制算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动对顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点就是,将内存缩小为原来的一半,代价较高;当对象存活率较高时就要进行较多的复制操作,效率将会变低。
应用场景:应用于当今主流商业虚拟机新生代收集算法,所不同的是,实际的新生代收集算法,并不是将内存简单划分为等分的两半,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次只使用Eden和其中的一块Survivor。
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以老年代一般不能直接选用这种算法。标记-整理算法(Mark-Compact)的标记过程与”标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
分代收集算法是当前商业虚拟机的垃圾收集机制都采用的算法。分代收集算法并不是一种新的算法模型,它只是一种根据对象存活周期的不同特点而对不同收集算法的综合运用。具体来说就是对堆区所采用的垃圾收集方案。堆区分为新生代和老年代,新生代的特点就是,每次垃圾收集都会发现大批对象死去,只有少量存活,那么采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集,这就是所谓的MinorGC。老年代的特点就是对象存活率高,没有额外空间的对它进行分配担保,那么就必须使用“标记-清除”或者”标记-整理”算法。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
publicstaticExecutorServicenewSingleThreadExecutor(){returnnewFinalizableDelegatedExecutorService(newThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,newLinkedBlockingQueue
2、newFixedThreadPool,创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
publicstaticExecutorServicenewFixedThreadPool(intnThreads){returnnewThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,newLinkedBlockingQueue
3、newCachedThreadPool,创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
publicstaticExecutorServicenewCachedThreadPool(){returnnewThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,newSynchronousQueue
4、newScheduledThreadPool,创建一个定长线程池,支持定时及周期性任务执行。
publicstaticScheduledExecutorServicenewScheduledThreadPool(intcorePoolSize){returnnewScheduledThreadPoolExecutor(corePoolSize);}publicScheduledThreadPoolExecutor(intcorePoolSize){super(corePoolSize,Integer.MAX_VALUE,0,TimeUnit.NANOSECONDS,newDelayedWorkQueue());}
(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需要trycatch,否则程序会直接退出。
DiscardPolicy:直接抛弃,任务不执行,空方法
DiscardOldestPolicy:从队列里面抛弃head的一个任务,并再次execute此task。
CallerRunsPolicy:在调用execute的线程里面执行此command,会阻塞入口
用户自定义拒绝策略(最常用):实现RejectedExecutionHandler,并自己定义策略模式
dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。它最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦合)。
调用关系说明:
1.服务容器负责启动,加载,运行服务提供者。
2.服务提供者在启动时,向注册中心注册自己提供的服务。
3.服务消费者在启动时,向注册中心订阅自己所需的服务。
4.注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
5.服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
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协议,其主要的特点是跨语言、跨平台的。项目中不推荐使用,可用于不同公司间接口的调用。
3、使用dubbo。使用rpc协议进行远程调用,直接使用socket通信。传输效率高,并且可以统计出系统之间的调用关系、调用次数。使用Java语言开发,只能用于Java语言开发的项目间的通信,不具备跨语言,跨平台的特点!
一句话总结:一个类在Java虚拟机中只有一个对象,并提供一个全局访问点。
生活中例子:太阳、月亮、国家主席等。
解决什么问题:对象的唯一性,性能浪费太多。
项目里面怎么用:数据库连接对象,属性配置文件的读取对象。
模式结构:分为饿汉式和懒汉式(如果考虑性能问题的话,就使用懒汉式,因为懒汉式是在方法里面进行初始化的),构造器私有化,对外提供方法加同步关键字。
框架里面使用:Struts1的Action。
JDK里面使用:java.lang.Runtime#getRuntimejava.awt.Desktop#getDesktop。
常见的写法:饿汉式、懒汉式、静态内部类、枚举
对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。Android中的各种Listener就使用到了这一设计模式,只要用户对手机进行操作,对应的listener就会被通知,并作出响应的处理。
对已有的业务逻辑进一步的封装,使其增加额外的功能,如java中的IO流就使用了装饰者模式,用户在使用的时候,可以任意组装,达到自己想要的效果。
一句话总结:用一个方法来代替new关键字
生活中的例子:制衣厂、面包厂等生产厂。
解决什么问题:对象产生过多,或者经常有子类替换生成。
项目里面怎么用:对于经常生成的对象,或者父子类替换的对象。
框架里面使用:spring的核心就是工厂模式。
JDK里面使用:newInstance。
写法:简单工厂、工厂方法、抽象工厂
工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个。
工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。
一句话总结:为其他对象提供一个代理,以控制对当前对象的访问。
生活中的例子:房屋中介、婚姻介绍所。
解决什么问题:不能直接访问该对象,或者太大的资源耗费多。
项目里面怎么用:权限,或者大对象的访问权限。
模式结构:代理类和被代理类实现同一个接口,用户访问的时候先访问代理对象,然后让代理对象去访问被代理对象。
框架里面使用:Spring里面的AOP实现。
JDK里面使用:java.lang.reflect.Proxy。
一句话总结:将两个原来不兼容的类兼容起来一起工作。
生活中的例子:变压器、充电器
解决什么问题:已经存在的相同功能的代码,但是接口不兼容,不能直接调用。
项目里面怎么用:在使用旧的API的时候,没有源码,和新的不能兼容。
模式结构:分为类适配器和对象适配,一般常用的就是对象适配器,因为组合由于继承。
框架里面使用:单元测试里面的asserEquels。
JDK里面使用:java.util.Arrays#asListjava.io.InputStreamReader(InputStream)java.io.outputStreamWriter(OutputStream)。
一句话总结:定义一系列算法并可以互相替换。
生活中的例子:图片的格式,压缩文件的格式。
解决什么问题:做一件事情有很多种方法。
项目里面怎么用:购物车里面的付款方式。
框架里面使用:hibernate的主键生成策略。
JDK里面使用:java.util.Comparator#compare。
主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。超键:在关系中能唯一标识元组的属性集称为关系模式的超键。一个属性可以为作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。候选键:是最小超键,即没有冗余元素的超键。外键:在一个表中存在的另一个表的主键称此表的外键。
视图是虚拟的表,与包含数据的表不一样,视图只包含使用时动态检索数据的查询;不包含任何列或数据。使用视图可以简化复杂的sql操作,隐藏具体的细节,保护数据;视图创建后,可以使用与表相同的方式利用它们。视图不能被索引,也不能有关联的触发器或默认值,如果视图本身内有orderby则对视图再次orderby将被覆盖。创建视图:createviewXXXasXXXXXXXXXXXXXX;对于某些视图比如未使用联结子查询分组聚集函数DistinctUnion等,是可以对其更新的,对视图的更新将对基表进行更新;但是视图主要用于简化检索,保护数据,并不用于更新,而且大部分视图都不可以更新。
数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。
1.对于那些在查询中很少使用或者参考的列不应该创建索引。这是因为,既然这些列很少使用到,因此有索引或者无索引,并不能提高查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。2.对于那些只有很少数据值的列也不应该增加索引。这是因为,由于这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在表中搜索的数据行的比例很大。增加索引,并不能明显加快检索速度。3.对于那些定义为text,image和bit数据类型的列不应该增加索引。这是因为,这些列的数据量要么相当大,要么取值很少。4.当修改性能远远大于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。因此,当修改性能远远大于检索性能时,不应该创建索引。
唯一索引:唯一索引是不允许其中任何两行具有相同索引值的索引。
主键索引:数据库表经常有一列或列组合,其值唯一标识表中的每一行。该列称为表的主键。
聚集索引:数据行的物理顺序与列值(一般是主键的那一列)的逻辑顺序相同,一个表中只能拥有一个聚集索引。
1.主键是一种约束,唯一索引是一种索引,两者在本质上是不同的。2.主键创建后一定包含一个唯一性索引,唯一性索引并不一定就是主键。3.唯一性索引列允许空值,而主键列不允许为空值。4.主键列在创建时,已经默认为空值+唯一索引了。5.主键可以被其他表引用为外键,而唯一索引不能。6.一个表最多只能创建一个主键,但可以创建多个唯一索引。7.主键更适合那些不容易更改的唯一标识,如自动递增列、身份证号等。8.在RBO模式下,主键的执行计划优先级要高于唯一索引。两者可以提高查询的速度。
由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分之一,因此为了提高效率,要尽量减少磁盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。程序运行期间所需要的数据通常比较集中。
where子句的作用是在对查询结果进行分组前,将不符合where条件的行去掉,即在分组之前过滤数据,条件中不能包含聚集函数,使用where条件显示特定的行。having子句的作用是筛选满足条件的组,即在分组之后过滤数据,条件中经常包含聚集函数,使用having条件显示特定的组,也可以使用多个分组标准进行分组。
1.应尽量避免在where子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。2.应尽量避免在where子句中对字段进行null值判断,否则将导致引擎放弃使用索引而进行全表扫描。可以给该字段设置一个默认值3.很多时候用exists代替in是一个好的选择4.用Where子句替换HAVING子句因为HAVING只会在检索出所有记录之后才对结果集进行过滤5.避免在where子句中对字段进行函数操作6.使用连接代替子查询7.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销
存储过程和触发器都是SQL语句集;触发器不可用CALL调用,而是在用户执行某些语句后自动调用;
B+是一种平衡的多叉查找树,在B树的基础上满足
为什么红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用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差很多。
final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。
当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。
使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。
final修饰的方法表示此方法已经是“最后的、最终的”含义,亦即此方法不能被重写(可以重载多个final修饰的方法)。此处需要注意的一点是:因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。)
final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。
所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。
防止SQL注入,我们需要注意以下几个要点:
1.永远不要信任用户的输入。对用户的输入进行校验,可以通过正则表达式,或限制长度;对单引号和双"-"进行转换等。
2.永远不要使用动态拼装sql,可以使用参数化的sql或者直接使用存储过程进行数据查询存取。
3.永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接。
4.不要把机密信息直接存放,加密或者hash掉密码和敏感的信息。
5.应用的异常信息应该给出尽可能少的提示,最好使用自定义的错误信息对原始错误信息进行包装
6.sql注入的检测方法一般采取辅助软件或网站平台来检测,软件一般采用sql注入检测工具jsky,网站平台就有亿思网站安全平台检测工具。MDCSOFTSCAN等。采用MDCSOFT-IPS可以有效的防御SQL注入,XSS攻击等。
wait只能在同步控制方法或者同步控制块里面使用,sleep可以在任何地方使用。
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常,wait和sleep都响应中断
yield()方法和sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即yield()方法只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行,另外yield()方法只能使同优先级或者高优先级的线程得到执行机会,这也和sleep()方法不同。
join()方法会使当前线程等待调用join()方法的线程结束后才能继续执行
管道,信号,socket,共享内存,消息队列
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
进程是指在系统中正在运行的一个应用程序;程序一旦运行就是进程,或者更专业化来说:进程是指程序执行时的一个实例。线程是进程的一个实体。进程——资源分配的最小单位,线程——程序执行的最小单位。
1.因为进程拥有独立的堆栈空间和数据段,所以每当启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这对于多进程来说十分“奢侈”,系统开销比较大,而线程不一样,线程拥有独立的堆栈空间,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据,比进程更节俭,开销比较小,切换速度也比进程快,效率高,但是正由于进程之间独立的特点,使得进程安全性比较高,也因为进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。一个线程死掉就等于整个进程死掉。
2.体现在通信机制上面,正因为进程之间互不干扰,相互独立,进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制,而线程由于共享数据段所以通信机制很方便。。
3.属于同一个进程的所有线程共享该进程的所有资源,包括文件描述符。而不同过的进程相互独立。
4.线程又称为轻量级进程,进程有进程控制块,线程有线程控制块;
5.线程必定也只能属于一个进程,而进程可以拥有多个线程而且至少拥有一个线程;
1、需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的。
2、线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应
3、因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程;
4、并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求;
5、需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。
1.HashMap没有考虑同步,是线程不安全的;Hashtable使用了synchronized关键字,是线程安全的;
2.HashMap允许存空值空键,Hashtable不允许。
1、Vector的方法都是同步的(Synchronized),是线程安全的(thread-safe),而ArrayList的方法不是,由于线程的同步必然要影响性能,因此,ArrayList的性能比Vector好。
2、当Vector或ArrayList中的元素超过它的初始大小时,Vector会将它的容量翻倍,而ArrayList只增加50%的大小,这样,ArrayList就有利于节约内存空间。
ArrayList&LinkedList
ArrayList的内部实现是基于内部数组Object[],所以从概念上讲,它更象数组,但LinkedList的内部实现是基于一组连接的记录,所以,它更象一个链表结构,所以,它们在性能上有很大的差别:
NIO和IO的区别
NIO即NewIO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在JavaAPI中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
1、面向流与面向缓冲
JavaIO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。JavaIO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。JavaNIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
2、阻塞与非阻塞IO
3、选择器(Selectors)
JavaNIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。