面试题
向ArrayList中插入一个元素的过程。
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
首先检查index是不是超限,然后确保容量,然后复制将元素后移,然后插入。
Error和Exception的区别
首先,他们都是继承自Throwable类。
Error是系统中的错误,程序员是不能改变和处理的,只能通过修改程序才能修正。一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。
Exception表示程序可以处理的异常,可以捕获且能恢复。遇到这类异常,应该尽可能处理,使程序恢复。
参见:https://www.cnblogs.com/lcl-dcr/p/7653274.html
说说Cookie和Session的区别?
1、Cookie和Session都是会话技术,Cookie是运行在客户端,Session是运行在服务器端。
2、Cookie有大小限制,以及浏览器存在cookie的个数也有限制,Session没有大小限制,和服务器的内存大小有关。
3、Cookie有安全隐患,通过拦截或本地文件找你的cookie后可以进行攻击。
4、Session保存在服务器端上,存在一段时间会消失,如果session过多会增加服务器的压力。
数据库连接池,为什么使用?
数据库的连接和关闭是及其耗费系统资源的,频繁地打开、关闭连接将会造成系统性能低下。数据库连接池的解决方案是:当应用程序启动时,系统主动建立足够的数据库连接,并将这些连接组成一个连接池,每次应用程序请求数据库连接时,无须重新打开连接,而是从数据库连接池中取出已有连接使用,使用完后不在关闭数据库连接,而是直接把连接归还给连接池。
JDBC的数据库连接池使用javax.sql.DataSource来表示,DataSource只是一个接口,该接口通常由商用服务器(WebLogic,WebSphere)等提供实现,也有一些开源的组织提供实现,如DBCP,C3P0。
下面是C3P0连接池的使用方式:
1、引入jar包
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.0.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
2、
public static void init() throws PropertyVetoException{
dataSource = new ComboPooledDataSource();
dataSource.setDriverClass("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/dbgirl");
dataSource.setUser("root");
dataSource.setPassword("123456");
dataSource.setMaxPoolSize(40);
dataSource.setMinPoolSize(2);
dataSource.setInitialPoolSize(10);
dataSource.setMaxStatements(180);
}
运行时异常有哪些?
IndexOutOfBoundsException;
ClassCastException;
NullPointerException;
ArithmeticException;
遍历集合时若删除元素,会抛出ConcurrentModificationException
运行时异常。
MySQL支持的数据库引擎?
常用的MyISAM、InnoDB。
一般来说,MyISAM适合:
1、做很多count的计算。
2、插入不频繁,查询非常频繁。
3、没有事务。
InnoDB适合:
1、可靠性要求比较高,或者要求事务。
2、表更新
- 事务:InnoDB支持事务,可以使用Commit和Rollback语句。
- 并发:MyISAM只支持表级锁,而InnoDB还支持行级锁。
- 外键:InnoDB支持外键。
- 备份:InnoDB支持在线热备份。
- 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB高很多,而且恢复的速度也更慢。
- 其它特性:MyISAM 支持压缩表和空间数据索引。
表的水平切分和垂直切分
水平切分:它是将同一个表中的记录拆分到多个结构相同的表中。
垂直切切分:是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以将经常被使用的列和不经常被使用的列切分到不同的表中。
与索引有关的优化
1、not in不能使用索引,
2、前导模糊查询不能使用索引
如:select name from user where name like '%zhangsan'
非前导则可以:select name from user where name like 'zhangsan%'
3、字段的默认值不要为 null
4、在字段上进行计算不能命中索引。select name from user where FROM_UNIXTIME(create_time) < CURDATE();
5、不要让数据库帮我们做强制类型转换。
单例模式
public class Singleton {
private static volatile Singleton instance=null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance=new Singleton();
}
}
}
return instance;
}
}
实例变量加volatile是为了保证有序性,因为JVM为了提高程序整体的效率会进行指令重排。instance=new Singleton();
这行代码可分为3步:
- 分配内存空间。(1)
- 初始化对象。(2)
- 将instance对象指向分配的内存地址。(3)
加上volatile是为了让以上的三个操作顺序执行,反之(3)可能在(2)之前被执行,就有可能导致某个线程拿到的对象还没有初始化,以至于使用报错。
类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
双亲委派模型
从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(bootstrap classloader),这个类加载器使用C++实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都是由Java实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.Classloader。
从Java开发人员的角度来看,类加载器具还可以划分得更细致一些,绝大部分Java程序都会使用到以下3中系统提供的类加载器。
bootstrap classloader->extension classloader->application classloader->自定义类加载器
双亲委派模型:当一个类收到了类加载请求时,自己不会首先加载,而是委派给父类加载器进行加载,只有当父类加载器不能完成这个加载请求时,才会让子类加载器加载,每个层次的类加载器都是这样。所以最终每个加载请求都会经过启动类加载器。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种加载器环境中都是同一个类。相反如果没有使用双亲委派模型,由各个类加载器自行加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基本的行为也就无法保证,应用程序也将会变得一片混乱。
双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,如下代码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
可以看到双亲委派模型的具体逻辑就是在这个方法之中实现的,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
JDK1.2之后以不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。
面试提问:什么是双亲委派模型?
从Java虚拟机的角度讲,只存在两种不同的类加载器:一种是启动类加载器,这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其它的类加载器,这些类加载器都是由Java实现,独立于虚拟机外部并且全都继承自java.lang.ClassLoader。
从Java开发人员的角度来看,类加载器还可以划分的更细致一些,绝大部分Java程序都会使用到以下3中系统提供的类加载器。
启动类加载器-》扩展类加载器-》应用类加载器。
如果有必要还可以加入自己定义的类加载器。
他们之间的层次关系,称为类加载器的双亲委派模型。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,只有当父类加载器不能完成这个加载请求时,子类加载器才会尝试自己去加载。
双亲委派模型对于保证Java程序的稳定性运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中:先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父类加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
自定义类加载器:继承ClassLoader抽象类,重写loadClass()或findClass()方法,JDK1.2之后已不提倡用户再去覆盖loadClass()方法,而应该把自己的类加载逻辑写到findClass()方法中。这样就可以保证新写出来的类加载器是符合双亲委派规则的。
抽象类和接口的区别
-
语法层面上的区别:
1、抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final的。
2、抽象类里的方法可以是public、protected、default,接口里的方法只能是public,不可以使用其它的修饰符。
3、一个类只能继承一个抽象类,却可以实现多个接口。
4、抽象类可以有构造器,接口不能有构造器。
5、抽象类中可以有静态代码块和静态方法,接口不能。
6、抽象类可以提供成员方法的实现细节,接口里只能存在抽象方法,Java8之后接口可以有default方法和静态方法,以减少抽象类和接口之间的差异,现在,我们可以为接口提供默认实现的方法并且不用强制子类来实现它。 -
设计层面上的区别:
1、抽象类是对事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整体进行抽象,包括属性、行为,但是接口却是对局部(行为)进行抽象。举个简单的例子,飞机和鸟是不同的事物,但是他们都有一个共性,就是会飞。那么在设计的时候,可以将飞机设计成为一个Airplane,将鸟设计为一个Bird,但是不能将飞行这个特性也设计为类,因为它只是一个行为特征,并不是对一类事物的抽象描述。此时可以将飞行设计为一个接口,包含fly(),然后Airplane和Bird分别根据自己的需要实现Fly这个接口。然后至于有不同种类的飞机,比如战斗机、民用机等直接继承Airplane即可,对于鸟也是类似的,不同种类的鸟直接继承Bird类即可。
从这里可以看出,继承是一个“是不是”的关系,而接口实现则是“有没有”的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具不具备的关系,比如鸟是否能飞,能飞行则可以实现这个接口,不能飞行就不实现这个接口。
Hashtable和HashMap区别?HashMap的工作原理?
HashMap可以放入null的键和值,Hashtable不能放入null的键和值,否则会抛出NullPointerException。
Hashtable是线程安全的,HashMap线程不安全。
HashMap底层是基于数组和链表实现的,其中有两个重要的参数:容量、负载因子。
容量的默认大小是16,负载因子是0.75,当HashMap的size>16*0.75时,就会发生扩容。
1、put()方法
首先会将传入的key做hash运算计算出hashcode,然后根据数组长度取模计算出在数组中的index下标。由于数组长度有限,所以难免会出现不同的key通过运算得到的index相同,这种情况可以利用链表来解决,HashMap会在table[index]处形成链表,采用头插法将数据插入到链表中。
2、get()方法
首先通过传入的key计算出index,如果该位置是一个链表就需要遍历整个链表,通过key的equals()方法来找到对应的元素。
3、如果HashMap的大小超过了负载因子定义的容量,怎么办?
默认负载因子是0.75,也就是说,当一个map填满75%的bucket时,就会创建bucket数组的大小为原来的两倍,并将原来的对象放入新的bucket数组中,这个过程叫做rehash,因为它调用hash方法找到新的bucket位置。
4、在并发环境下使用HashMap容易出现死循环
并发场景发生扩容,调用resize()方法里的rehash()时,容易出现环形链表。这样当获取一个不存在的key时,计算出的index正好是环形链表的下标的就会出现死循环。
5、在JDK1.8中对HashMap进行了优化:当hash碰撞之后写入链表的长度超过了闸值(默认为8),链表将会转变为红黑树。
假设hash冲突非常严重,一个数组后面接了很长的链表,此时检索的时间复杂度就是O(n),如果是红黑树,时间复杂度就是O(logn)。
5、讲讲ConcurrentHashMap?
java1.7的ConcurrentHashMap是由Segment数组、HashEntry组成,和HashMap一样,仍然是数组加链表。
其中Segment是ConcurrentHashMap的一个内部类,它里面有一个HashEntry数组。
原理上讲:ConcurrentHashMap采用了分段锁技术,其中Segment继承于ReentrantLock。不会像Hashtable那样不管是put还是get操作都需要做同步处理,理论上ConcurrentHashMap支持CurrencyLevel(Segment数组数量)个线程并发。每当一个线程占用锁访问一个Segment时,不会影响到其他的Segment。
1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7版本中的问题:那就是查询遍历链表效率太低。
Java1.8也对ConcurrentHashMap做了一些调整,其中抛弃了原有的Segment分段锁,而采用了 CAS + synchronized 来保证并发安全性。
详细讲解:https://crossoverjie.top/2018/07/23/java-senior/ConcurrentHashMap/
悲观锁和乐观锁
1、悲观锁:在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延迟,引起性能问题。一个线程持有锁会导致其它所有需要此锁的线程挂起。
2、乐观锁:相对于悲观锁,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
CAS就是乐观锁的一种实现,多个线程尝试更新同一个变量时,只有其中一个线程能更新变量的值,其他线程都失败,失败的线程不会挂起,而是被告知这次竞争失败,并可以再次尝试。
在JDK1.5 中新增java.util.concurrent(J.U.C)就是建立在CAS之上的。相对于对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。
我们以java.util.concurrent中的AtomicInteger为例,看一下在不使用锁的情况下是如何保证线程安全的。主要理解getAndIncrement方法,该方法的作用相当于 ++i 操作。
数据库事务
什么是事务?
原子性、一致性、隔离性、持久性
-
事务的隔离级别
1、读未提交
2、读提交
3、可重复读
4、序列化
Sql Server和oracle默认的隔离级别是读提交,MySQL默认的隔离级别是可重复读。 -
事务的传播行为:
常用的有:
1、REQUIRED:当方法调用时,如果不存在事务,那么就创建事务;如果之前的方法已经存在事务了,那么就沿用之前的事务。
2、REQUIRED_NEW:无论是否存在事务,方法都会在新的事务中运行。
3、NESTED:嵌套事务,也就是调用方法如果抛出异常只回滚自己内部执行的SQL,而不回滚主方法的SQL。
get和post区别
1、get是向服务器请求资源,post是向服务器发送资源。
2、get请求参数通过url传递,post放在request body中。
3、get请求参数直接暴露在url中,不安全。
4、get产生一个TCP数据包;post产生两个TCP数据包。对于get请求,浏览器会把http header和data一并发送出去,服务器响应200;对于post请求,浏览器先发送header,服务器响应100,表示接收的请求正在处理,浏览器再发送data,服务器响应200。
如何使用线程池,优点
在Java5之前,开发者必须手动实现自己的线程池;从Java5开始,Java内建支持线程池,新增一个Executors工厂类来产生线程池,该工厂类包含如下几个静态的工厂方法来创建线程池:
- public static ExecutorService newCachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。
- public static ExecutorService newFixedThreadPool(int nThreads):创建一个可重用的、具有固定线程数的线程池。
- public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize):创建具有指定线程数的线程池,它可以指定延迟后执行线程任务。
ExecutorService代表尽快执行线程的线程池,程序只要将一个Runnable对象或Callable对象提交给该线程池(submit方法),该线程池就会尽快执行该任务。
用完一个线程池后,应该调用该线程池的shutdown()方法,该方法将启动线程池的关闭序列,调用shutdown()方法后的线程池不再接收新任务。
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
ExecutorService executorService= Executors.newFixedThreadPool(3);
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("第一个线程池执行第一个线程任务");
}
});
executorService.shutdown();
ScheduledExecutorService scheduledExecutorService=Executors.newScheduledThreadPool(3);
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println("第二个线程池执行第一个线程任务");
}
},10, TimeUnit.SECONDS);
scheduledExecutorService.shutdown();
优点:
1、系统启动一个新线程的成本是比较高的,因为它涉及与操作系统交互,在这种情况下,使用线程池可以很好地提高性能,尤其是当程序中需要创建大量生存期很短的线程时,应该使用线程池。
2、使用线程池可以有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数参数可以控制系统中并发线程数量。
3、能够对线程进行简单的管理,并提供定时执行、间隔执行等功能。
垃圾回收算法
1、标记-清除算法:算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2、复制算法:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
3、标记-整理算法:复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况。
标记-整理算法的标记过程和标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
4、分代收集算法:根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来进行回收。
JVM内存划分
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。Java虚拟机所管理的内存将会包括以下几个运行时数据区域。
1、程序计数器(Program Counter Register):是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2、Java虚拟机栈(Java Virtual Machine Stacks):与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程中,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
3、本地方法栈(Native Method Stack):与虚拟机栈所发挥的作用非常相似,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。
4、Java堆(Java Heap):是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象,几乎所有的对象实例都在这里分配内存,这一点在Java虚拟机规范的描述是:所有的对象实例以及数组都要在堆上分配。
5、方法区(Method Area):与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
6、运行时常量池(Runime Constant Pool):运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存
放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
什么是符号引用?
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口,也就无法被虚拟机使用。当虚拟机运行时需要从常量池中获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
如何判断对象已死
在堆里存放着Java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”,哪些已经“死去”(即不能再被任何途径使用的对象)。
1、引用记数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
该算法实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。但在主流的Java虚拟机里面没有选用引用计数算法来管理内存,主要的原因是它很难解决对象之间相互循环引用的问题。
举个例子:对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是他们因为互相引用着对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知GC收集器回收他们。
public class ReferenceCountigGC {
public Object instance=null;
private static final int _1Mb=1024*1024;
public static void testGC() {
ReferenceCountigGC objA=new ReferenceCountigGC();
ReferenceCountigGC objB=new ReferenceCountigGC();
objA.instance=objB;
objB.instance=objA;
objA=null;
objB=null;
//假设在这里发生GC
System.gc();
}
}
2、可达性分析算法
在主流的商用程序语言的主流实现中,都是通过可达性分析(Reachability Analysis)来判断对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
3、生存还是死亡?
即使可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候他们暂时处与“缓刑”阶段,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此时对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalze()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后有一个有虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会出发这个方法,但并承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将很可能会导致F-Queue队列中其他的对象永久处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue()中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己-只要重新与引用链上的任何一个对象建立联系即可,譬如把自己(this)赋值给某个类变量或者对象的成员变量,那在第二次标记它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
Java内存模型
Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在此之前,主流程序语言(如C/C++等)直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台上内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另外一套平台上并发访问却经常出错。
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。(这里的变量与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题)。
Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量。线程变量值的传递需要通过主内存来完成,线程、主内存、工作内存三者的关系如下:
这里所讲的主内存、工作内存和上面讲的Java内存区域中的Java堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的,如果两者一定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。从更低层次上主内存就直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。
原子性、可见性、有序性
Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的。
1、原子性:由Java内存模型来直接保证的原子性的变量操作包括read、load、assign、use、store、write,我们大致可以认为基本数据类型的访问读写是具备原子性的。如果需要更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块-synchronized关键字,因此在synchronized块之间的操作也具有原子性。
2、可见性:是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。有三个关键字可保证变量在多线程操作时的可见性:volatile,synchronized、final。
3、有序性:Java内存模型的有序性在前面讲解volatile时也详细地讨论过了,Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。
先行发生规则
如果Java内存模型中所有的有序性都仅仅靠volatile和synchronized来完成,那么有一些操作将会变得很繁琐,但是我们在编写Java并发代码的时候并没有感觉到这一点,这是因为Java语言中有一个“先行发生”(happens-before)的原则。这个原则非常重要,它是判断数据是否存在竞争、线程是否安全的的主要依据,依靠这个原则,我们可以通过几条规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题。
先行发生是Java内存模型中定义的两项操作之间的偏序关系,下面是Java内存模型下一些“天然的”先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们没有顺序性保障,虚拟机可以对他们随意地进行重排序。
- 程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。
- 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock()操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
- volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
- 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.iAlive()的返回值等手段检测到线程已经终止执行。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
- 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。
- 传递性:如果A操作先行发生于B操作,B操作先行发生与C操作,那就可以得出操作A先行发生于操作C。
Callable接口实现线程
该接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大。
- call()方法可以有返回值。
- call()方法可以声明抛出异常。
Callable不是Runnable的子接口,不能作为Thread的target。Java5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该接口也实现了Runnable接口,它可以作为Thread类的target。
下面是使用Callable接口实现线程的方式。
List<Integer> list=new ArrayList<>(Arrays.asList(1,2,3,4,5));
FutureTask<Integer> futureTask=new FutureTask<>(()->{
int sum=0;
for (Integer i:list) {
sum+=i;
System.out.println(Thread.currentThread().getName()+"遍历的集合中的值:"+i);
}
return sum;
});
new Thread(futureTask).start();
try {
System.out.println("线程返回值sum="+futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
运行结果如下:
Thread-0遍历的集合中的值:1
Thread-0遍历的集合中的值:2
Thread-0遍历的集合中的值:3
Thread-0遍历的集合中的值:4
Thread-0遍历的集合中的值:5
线程返回值sum=15
实现Runnable、Callable接口创建线程的优缺点:
1、还可以继承其它类
2、多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况。
劣势是:
编程稍微复杂,要访问当前线程,必须使用Thread.currentThread()方法。
线程状态
Java语言定义了5种线程状态,在任何一个时间点,一个线程只能有且只有其中的一种状态。
- 新建:New
- 运行:start()
- 无限期等待:wait()-notity()/notityAll()
- 限期等待:sleep()
- 阻塞:synchronized
- 结束:run()方法结束
SQL连接查询
Spring MVC请求流程
1、用户发送请求至前端控制器DispatcherServlet。
2、DispatcherServlet收到请求调用HandlerMapping处理器映射器。
3、处理器映射器找到具体的处理器,生成处理器对象及处理器拦截器(如果有的话)一并返回给DispatcherServlet。
4、DispatcherServlet调用HanlerAdapter处理器适配器。
5、HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
6、Controller执行完成返回ModelAndView。
7、HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
8、DispatcherServlet将ModelAndView传给ViewResolver试图解析器。
9、ViewResolver解析后返回具体View。
10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
11、DispatcherServlet响应用户。
redis的六种数据结构?
string,list,set,hash,zset,hyperLog
Redis对事务的支持
在Redis中使用事务会经过如下3个过程:
-
开启事务。
-
命令进入队列。
-
执行事务。
主要有: -
multi:开启事务命令,之后的命令就会进入队列,而不会马上执行。
-
exec:执行事务命令。
-
discard:回滚事务命令
redis数据能不能持久化
可以。一种是快照(snapshotting),它是备份当前瞬间Redis在内存中的数据记录;另一种是只追加文件(Append-Only File ,AOF),其作用就是当Redis执行写命令后,在一定的条件下将执行过的写命令依次保存在Redis的文件中,将来就可以依次执行那些保存的命令恢复Redis的数据了。
悲观锁,乐观锁
悲观锁:是一种利用数据库内部机制提供的锁的方法,也就是对更新的数据加锁。在并发期间一旦有一个事务持有了数据库记录的锁,其他的线程将不能在对数据进行更新。
对于悲观锁来说,当一条线程抢占了资源后,其他的线程将得不到资源,那么这个时候,CPU就会将这些得不到资源的线程挂起,挂起的线程也会消耗CPU资源,这样在高并发场景下,使用悲观锁会造成大量线程频繁地被挂起和恢复,这将十分消耗资源。有时候悲观锁也会称为独占锁,毕竟只有一个线程可以独占这个资源,或者称为阻塞锁,因为他会造成其它线程的阻塞。
乐观锁:是一种不会阻塞其他线程并发的机制,它不会使用数据库的锁进行实现,它的设计里面由于不阻塞其他线程,所以不会引发线程频繁挂起和恢复。这样便能够提高并发能力,所以有人把他称为非阻塞锁。乐观锁使用的是CAS原理。
CAS原理
在CAS原理中,对于多个线程共同的资源,先保存一个旧值,经过一定的业务逻辑处理后,再比较数据库当前的值和旧值是否一致,如果一致则进行更新数据的操作。
CAS原理并不排斥并发,也不独占资源,只是在线程开始阶段就读入线程共享数据,保存为旧值。当处理完逻辑,需要更新数据的时候,会进行一次比较,即比较各个线程当前共享的数据是否和旧值保持一致,如果不一致,则认为该数据已经被其他线程修改过了,那么就不再更数据,可以考虑重试或者放弃。有时候可以重试,这样就是一个可重入锁,但是CAS原理会有一个问题,那就是ABA问题。
ABA问题
ABA问题就是当线程1记下了这个共享数据的旧值为A,另一个线程2修改了这个数据的值为B,进行业务处理后,又把这个数据改为旧值A,这是线程1就认为这个共享数据没有变过。人们形象地把这类问题称为ABA问题。
ABA问题的发生,是因为业务逻辑存在回退的可能性。如果加入一个非业务逻辑的属性,比如在一个数据中加入版本号(version),对于版本号以一个约定,只要修改X变量的数据,强制版本号(version)只能递增,不会回退,即使是其他业务数据回退,它也会递增,那么ABA问题就解决了。
乐观锁重入机制(以抢红包为例)
经过3万次的抢夺,还会存在大量的因为版本不一致的原因造成抢红包失败的请求,不过这个失败率太高了。为了克服这个问题,提高成功率,还会考虑使用重入机制。也就是一旦因为版本原因没有抢到红包,则重新尝试强红包,但是过多的重入会造成大量的SQl执行,所以目前流行的重入会加入两种限制,一种是按时间戳的重入,也就是在一定时间戳内(比如1000秒内),不成功的会循环到成功为止,直至超过时间戳,不成功才会退出,返回失败。一种是按次数重入,比如限定3次,程序尝试超过3次抢红包后,就判定请求失效,这样有助于提高用户抢红包的成功率。
因为乐观锁造成大量更新失败的问题,使用时间戳执行乐观锁重入,是一种提高成功率的方法,比如哦考虑在100毫秒内允许重入。
单点登陆
跨域请求
分布式系统
jsp和servlet
- 区别:JSP在静态html内容中嵌入Java代码,jsp经Servlet容器编译变成Servlet,Servlet在Java代码中通过HttpServletResponse对象动态输出html内容。
- 各自特点:1、Servlet能够很好地组织业务逻辑代码,但是在Java源文件中通过字符串拼接的方式生成动态HTML内容会导致代码维护困难、可读性差。
JSP虽然规避了Servlet在生成HTML内容方面的劣势,但是在HTML中混入大量、复杂的业务逻辑同样也是不可取的。
所以在MVC模式中,它们各自有分工,JSP用于表现层,Servlet作为控制器负责转发请求,对请求作处理。
设计模式
单例模式、简单工厂模式、工厂方法模式、抽象工厂模式、建造者模式、装饰模式、适配器模式、组合模式.......
SQL中的for update
SQL中加入for update语句,如果使用的是主键查询,就会对行加锁;如果使用的是非主键,就是对全表加锁,加锁后可能引发其他查询的阻塞,那就意味着在高并发的场景下,当一条事务持有了这个更新锁才能往下操作,其他的线程如果要更新这条记录,都需要等待。
Sping
bean的作用域
Spring提供了4中作用域。
1、单例(singleton):它是默认的选项,在整个应用中,Sping只为其生成一个bean实例。
2、原型(prototype):当每次注入,或则通过Sping IoC容器中获取bean时,Spring都会为它创建一个实例。
3、会话(session):在Web应用中使用,就是在会话过程中Spring只创建一个实例。
4、请求(request):在Web应用中使用的,就是在一次请求中Spring会创建一个实例。
Spring bean的生命周期
上述生命周期接口,大部分都是针对单个bean而言的;BeanPostProcessor接口则是针对所有bean而言的。当一个bean实现了上述的接口,我们只需要在Spring IoC容器中定义它就可以了,Spring IoC容器会自动识别。
https://www.cnblogs.com/kenshinobiy/p/4652008.html
Spring AOP
为什么会有面向切面编程(AOP)?我们知道Java是一个面向对象(OOP)的语言,但它有一些弊端,比如当我们需要为多个不具有继承关系的对象引入一个公共行为,例如日志、权限验证、事务等功能时,只能在每个对象里引用公共行为,这样做不便于维护,而且有大量重复代码。AOP的出现弥补了OOP的这点不足。
在电商网站购物需要经过交易系统、财务系统,对于交易系统存在一个交易记录的对象,而财务系统则存在账号的信息对象。我们还需要对交易记录和账户操作形成一个统一的事务管理,交易和账户的事务,要么全部成功,要么全部失败。这就不是面向对象可以解决的问题,而需要用到面向切面的编程,这里的切面环境就是数据库事务。
使用@Transactional注解,没有任何关于打开或者关闭数据库资源的代码,更没有任何提交或则回滚数据库事务的代码,主要的代码都集中在业务处理上,而不是数据库事务和资源管控上,这些都是Spring AOP框架完成的。