1、String能被继承吗?为什么?
不可以,因为String类有final修饰符,而final修饰的类是不能被继承的,实现细节不允许改变。
2、String str=“abc”和String str=new String(“abc”); 产生几个对象?
前者1或0,后者2或1,先看字符串常量池,如果字符串常量池中没有,都在常量池中创建一个,如果有,前者直接引用,后者在堆内存中还需创建一个“abc”实例对象。
对于基础类型的变量和常量:变量和引用存储在栈中,常量存储在常量池中。
3、String, Stringbuffer, StringBuilder 的区别。
- String 字符串常量(final修饰,不可被继承),String是常量,当创建之后即不能更改。
- StringBuffer 字符串变量(线程安全),其也是final类别的,不允许被继承,其中的绝大多数方法都进行了同步处理,包括常用的Append方法也做了同步处理(synchronized修饰)。
- StringBuilder 字符串变量(非线程安全)自jdk1.5起开始出现。与StringBuffer一样都继承和实现了同样的接口和类,方法除了没使用synch修饰以外基本一致,不同之处在于最后toString的时候,会直接返回一个新对象。
4、ArrayList、LinkedList、Vector的区别
- Arraylist和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数据以便增加插入元素,都允许直接序号索引元素,但是插入数据要涉及到数组元素移动等内存操作,所以插入数据慢,查找有下标,所以查询数据快。
- Vector由于使用了synchronized方法-线程安全,所以性能上比ArrayList要差。
- LinkedList使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项前后项即可,插入数据较快。
5、HashMap、Hashtable、ConcurrentHashMap的区别
-
HashTable
- 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化。
- 初始size为11
-
HashMap
- 底层数组+链表实现,可以存储null键和null值,线程不安全
- 初始size为16,扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入。
-
ConcurrentHashMap
在 JDK 1.7 中采用 分段锁的方式;JDK 1.8 中直接采用了CAS(无锁算法)+ synchronized。- 底层采用分段的数组+链表实现,线程安全。
- 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
- Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
6、抽象类和接口的区别,
- 抽象类和接口都不能直接实例化,如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
- 抽象类要被子类继承,接口要被类实现。
- 抽象类里可以没有抽象方法,但一个类里有抽象方法,那么这个类只能是抽象类。
- 抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
7、Hash Map
- HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
- Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
- LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
- TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
8、Runnable接口和Callable接口的区别
-
Runnable需要实现run()方法,实现Runnable接口的任务线程不能返回执行结果,Runnable接口实现类中run()方法的异常必须在内部处理掉,不能向上抛出。
-
Callable需要实现call()方法,实现Callable接口的任务线程能返回执行结果,Callable接口实现类中run()方法允许将异常向上抛出,也可以直接在内部处理(try…catch);
9、currenthashmap
ConcurrentHash不允许null作为key/value
-
1.7 是数组+链表,头插法,由Segment和HashEntry组成的,其中Segmen是一种可重入的锁,继承于ReetrantLock。一个CurrentHashMap包括一个Segment数组,一个Segment元素包括一个HashEntry数组,HashEntry是一种链表型的结构,一个Segment对应数组的一段,当要对HashEntry中的数据进行修改的时候,我们必须先要获得与它对应的Segment。
-
1.8 是数组+链表+红黑树,尾插法,摒弃Segment的概念,直接用Node数组+链表+红黑树的数据结构来实现,Node的value和next都是由volatile关键字进行修饰,可以保证可见性。线程安全实现上,采用CAS+Synchronized替代Segment分段锁。synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发。
所以链表长度大于8且数组长度大于等于64时才会将链表转换为红黑树。
大于8的时候红黑树的整体插入查询效率大于链表。而6是链表大于红黑树。7是为防止频繁在红黑树和链表间切换浪费性能。
synchronized : 同步锁
当一个线程A 访问代码块时,A线程就会持续持有当前锁的状态,如果其他线程B-E 也要访问时,此时代码同步块将会阻塞,因此B-E线程需要排队等待A线程释放锁的状态后才可以持有【资源】。
synchronized 是一个非公平锁,通俗来讲暴力等待。
ReentrantLock :可重入锁
它表示该锁能支持一个线程对资源的重复加锁。同时还支持获取锁的公平和非公平性选择。
公平锁:根据线程请求锁的顺序依次获取锁,当一个线程A 访问的期间获取锁资源,内部存在的一个计数器num+1,其他线程访问时发现 A 线程持有当前资源,因此在后面的节点排队进入待唤醒状态,当线程 A 再次请求资源时,不需再次排队可直接获取资源(计数器+1,num=2)。当线程 A 释放锁时,唤醒 B 进行获取锁的操作,其他后续线程同理。
非公平锁:当线程 A 释放锁资源后,唤醒线程 B 获取资源时,这时线程 K 获取请求,此时进入竞争状态,当线程 B 没有竞争过线程 K 时,线程 K 获取请求,优先获取资源,线程 B 进入待唤醒状态。
10、分布式环境下,怎么保证线程安全
-
避免并发
在分布式环境中,如果存在并发问题,所以我们首先要想想是不是可以通过某些策略和业务设计来避免并发。比如通过合理的时间调度,避开共享资源的存取冲突。另外,可以在并行任务设计上可以通过适当的策略,保证任务与任务之间不存在共享资源。 -
时间戳
分布式环境中并发是没法保证时序的,无论是通过远程接口的同步调用或异步消息,因此很容易造成某些对时序性有要求的业务在高并发时产生错误。每日操作数据时带上一个能标识时序的时间戳,只有当通知的时间戳大于存在的时间戳,才做更新。但关键在于调用方一般要保证时间戳的时序有效性。 -
串行化
-
数据库
分布式环境中的共享资源不能通过Java里同步方法或加锁来保证线程安全,但数据库是分布式各服务器的共享点,可以通过数据库的高可靠一致性机制来满足需求。可以使用唯一性索引来解决并发过程中重复数据的生产或重复任务的执行;使用sql来完成计算和更新就可以通过数据库的锁机制来保证update操作的一致性。 -
行锁
有的事务比较复杂,无法通过一条sql解决问题,并且有存在并发问题,这时就需要通过行锁来解决。
在表里添加一个标示锁的字段,每次操作前,先通过update这个锁字段来完成类似竞争锁的操作,操作完成后在update锁字段复位,标示已归还锁。这种方式比较安全,不好的地方在于这些update锁字段的操作就是额外的性能消耗。 -
统一触发途径
当一个数据可能会被多个触发点或多个业务涉及到,就有并发问题产生的隐患,因此可以通过前期架构和业务设计,尽量统一触发途径。
静态变量,实例变量,局部变量线程安全吗,为什么?
-
静态变量:线程非安全。
静态变量即类变du量,位于方法区,zhi为所有对象共享,共享一份内存,一旦dao静态变量被修改,其他对象均对修改可见,故线程非安全。 -
实例变量:单例模式(只有一个对象实例存在)线程非安全,非单例线程安全。
实例变量为对象实例私有,在虚拟机的堆中分配,若在系统中只存在一个此对象的实例,在多线程环境下,“犹如”静态变量那样,被某个线程修改后,其他线程对修改均可见,故线程非安全;如果每个线程执行都是在不同的对象中,那对象与对象之间的实例变量的修改将互不影响,故线程安全。 -
局部变量:线程安全。
每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。
try. catch, finally都有return语句时执行哪个。
1、不管有没有出现异常,finally块中代码都会执行;
2、当try和catch中有return时,finally仍然会执行;
3、finally是在return后面的表达式运算之后执行的;
情况一:如果finally中有return语句,则会将try中的return语句“覆盖”掉,直接执行finally中的return语句,得到返回值,这样便无法得到try之前保留好的返回值。
情况二:如果finally中没有return语句,也没有改变要返回值,则执行完finally中的语句后,会接着执行try中的return语句,返回之前保留的值。
情况三:如果finally中没有return语句,但是改变了要返回的值,这里有点类似与引用传递和值传递的区别,分以下两种情况:
1)如果return的数据是基本数据类型或文本字符串,则在finally中对该基本数据的改变不起作用,try中的return语句依然会返回进入finally块之前保留的值。
2)如果return的数据是引用数据类型,而在finally中对该引用数据类型的属性值的改变起作用,try中的return语句返回的就是在finally中改变后的该属性的值。
start 和 run 的区别
start
- 通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run方法运行结束,此线程随即终止。
run
- run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码。
Theread类的方法
-
Thread.ssleep :sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复当一个线程处于睡眠阻塞时,若被其他线程调用.interrupt方法中断,则sleep方法会抛出InterruptedException异常。
-
后台线程:在运行程序时,操作系统会启动一个进程来运行jvm,jvm运行后会创建第一个前台线程来运行我们程序的main方法。同时也会创建一个后台线程运行GC。
-
void interrupt() 中断线程。
-
static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
-
void join() 等待该线程终止。
-
wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lockpool),如果线程重新获得对象的锁就可以进入就绪状态。
-
notfiy();
多线程并发编程要保证线程安全
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在调用代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
多线程并发编程三要素
-
原子性(Synchronized, Lock)即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。在Java中,基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
-
可见性(Volatile,Synchronized,Lock) 指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
更新主存的步骤:
当前线程将其他线程的工作内存中的缓存变量的缓存行设置为无效,然后当前线程将变量的值更新到主存,更新成功后将其他线程的缓存行更新为新的主存地址,其他线程读取变量时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。 -
有序性(Volatile,Synchronized, Lock) 即程序执行的顺序按照代码的先后顺序执行。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
可以通过volatile关键字来保证一定的“有序性”。
概念
按照 “线程安全” 的安全程度由强到弱排序,Java语言中各种操作共享的数据可分为五类:
-
不可变
在java语言中,不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。如final关键字修饰的数据不可修改,可靠性最高。 -
绝对线程安全
绝对的线程安全完全满足Brian GoetZ给出的线程安全的定义,这个定义其实是很严格的,一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”通常需要付出很大的代价。 -
相对线程安全
这就是我们通常意义上的线程安全,需要保证对象单独的操作时线程安全的。我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。 -
线程兼容
线程兼容就是我们通常意义上所讲的一个类不是线程安全的。
对象本身不是线程安全的,但可以通过同步手段实现。一般我们说的不是线程安全的,绝大多数是指这个。 -
线程对立
不管调用端是否采用了同步的措施,都无法在并发中使用的代码。
线程对立是指无论调用端是否采取了同步错误,都无法在多线程环境中并发使用的代码。由于java语言天生就具有多线程特性,线程对立这种排斥多线程的代码是很少出现的。
线程的安全实现
-
互斥同步:
互斥同步是最常见的一种并发正确性保障手段
在多线程访问的时候,保证同一时间只有一条线程使用。
而互斥是实现同步的一种手段,互斥是方法,同步是目的。
最基本的互斥同步手段就是synchronized关键字,synchronized关键字编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。
ReentrantLock也是通过互斥来实现同步。在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性。
互斥和同步最主要的问题就是阻塞和唤醒所带来的性能问题,所以这通常叫阻塞同步,(悲观锁策略)互斥同步属于一种悲观的并发策略,总是认为只要不去做正确地同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁。 -
非阻塞同步:
基于冲突检测的乐观并发策略:如果没有其他线程争用共享的数据,操作就成功,如果有,则进行其他的补偿(最常见就是不断的重试),这种乐观的并发策略许多实现都不需要把线程挂起。非阻塞的实现CAS(compareandswap):CAS指令需要有3个操作数,分别是内存地址(在java中理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,当且仅当V处的值符合旧预期值A时,处理器用B更新V处的值,否则它就不执行更新,但是无论是否更新了V处的值,都会返回V的旧值,上述的处理过程是一个原子操作。
CAS缺点:
ABA问题:
因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
解决思路:
使用版本号,可以通过AtomicStampedReference来解决 ,这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
无同步方案
要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性,因此会有一些代码天生就是线程安全的。
-
可重入代码
可重入代码(ReentrantCode)也称为纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。 -
线程本地储存
如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。这样无需同步也能保证线程之间不出现数据的争用问题。
11、 Synchronized
JDK1.5之前是一个重量级锁,1.6之后进行了各种优化,使得让它不那么重。
synchronized 规定了同一个时刻只允许一条线程可以进入临界区(互斥性),同时还保证了共享变量的内存可见性。此规则决定了持有同一个对象锁的多个同步块只能串行执行。
Java中的每个对象都有一个监视器,来监测并发代码的重入。在ynchronized 范围内,线程进入同步块,监视器发挥作用,线程获得内置锁。内置锁是一个互斥锁,最多只有一个线程能够获取该锁。这个锁由JVM自动获取和释放,线程进入synchronized方法时获取该对象的锁,synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。
12、List、Set、Map 之间的区别
13、LinkedHashMap
LinkedHashMap是HashMap的一个子类,它继承与HashMap、底层使用哈希表与双向链表来保存所有元素。其基本操作与父类HashMap相似,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。通过重写父类相关的方法,来实现自己的链接列表特性。保留插入的顺序,如果需要输出的顺序和输入时的相同,就可以使用LinkedHashMap 。
14、反射
Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称 为 java 语言的反射机制。
三种方式
第一种,使用 Class.forName 静态方法。
前提:已明确类的全路径名。
第二种,使用 .class 方法。
说明:仅适合在编译前就已经明确要操作的 Class
第三种,使用类对象的 getClass() 方法。
适合有对象示例的情况下
15、单例模式
== 单例模式的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点!==
- 懒汉,线程不安全
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
效率很低,99%情况下不需要同步。
- 饿汉
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
- 静态内部类
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
- 枚举
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
- 双重校验锁(jdk1.5)
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
16、深拷贝和浅拷贝区别。
深复制和浅复制最根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用。
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。
深复制 —-在计算机中开辟了一块新的内存地址用于存放复制的对象。
通俗一点理解就是浅拷贝出来的数据并不独立,如果被复制的对象改变了,那么浅拷贝的对象也会改变,深拷贝之后就会完全独立,与浅拷贝断绝关系。
17、== 和 equals 的区别是什么?
"=="是判断两个变量或实例是不是指向同一个内存空间。
"equals"是判断两个变量或实例所指向的内存空间的值是不是相同。
18、如何实现数组和 List 之间的转换?
List转换成为数组:调用ArrayList的toArray方法。
数组转换成为List:调用Arrays的asList方法。
19、Array 和 ArrayList 有何区别?
Array可以容纳基本类型和对象,而ArrayList只能容纳对象。
Array是指定大小的,而ArrayList初始大小是固定的。
Array没有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。
20、什么情况下会发生栈内存溢出。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
21、JVM 内存模型
- 1.7
-
程序计数器
每条线程都有一个独立的程序计数器,这类内存也称为“线程私有”的内存。 -
Java 虚拟机栈
线程私有。
每个方法在执行的时候也会创建一个栈帧,存储了局部变量,操作数,动态链接,方法返回地址。
如果线程请求的栈深度大于虚拟机所允许的深度,则StackOverflowError。
如果虚拟机栈可以动态扩展,扩展到无法申请足够的内存,则OutOfMemoryError。 -
本地方法栈
线程私有。
和虚拟机栈类似,主要为虚拟机使用到的Native方法服务。也会抛出StackOverflowError 和OutOfMemoryError。 -
Java 堆
线程共享。
被所有线程共享的一块内存区域,在虚拟机启动的时候创建,用于存放对象实例。
对可以按照可扩展来实现(通过-Xmx 和-Xms 来控制)
当队中没有内存可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。 -
方法区
线程共享。
被所有方法线程共享的一块内存区域。
用于存储已经被虚拟机加载的类信息,常量,静态变量等。
这个区域的内存回收目标主要针对常量池的回收和堆类型的卸载。
- 1.8
-
程序计数器
处理器在一个确定是时刻只会执行一个线程中的指令,线程切换后,是通过计数器来记录执行痕迹的,因而可以看出,程序计数器是每个线程私有的。 -
Java 虚拟机栈
名词:
局部变量表、异常、操作数栈、动态连接、方法返回地址 -
堆
堆时JVM内存占用最大,管理最复杂的一个区域。唯一的途径就是存放对象实例:所有的对象实例以及数组都在堆上进行分配。jdk1.7以后,字符串常量从永久代中剥离出来,存放在堆中。堆具有进一步的内存划分。按照GC分代手机角度划分。
老年代:2/3的堆空间
年轻代:1/3的堆空间
eden区:8/10 的年轻代
survivor0: 1/10 的年轻代
survivor1:1/10的年轻代
4.元数据区域
元数据区域取代了1.7版本及以前的永久代。元数据和永久代本质上都时方法区的实现。
- 直接内存
1、直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
2、直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显
22、JVM垃圾回收机制
- 引入计数算法
堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1(a = b, b被引用,则b引用的对象计数+1)。当引用失效时(一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时),计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1
但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因是引用计数法无法解决对象的循环引用问题。
- 可达性分析算法
可达性算法:将所有的引用关系看作一张图,从一个节点GC Root开始,寻找对应的引用节点,找到后继续寻找这个节点的引用节点,当所有引用节点寻找完毕后,剩余的节点就被认为是没有被引用的节点,即无用节点,无用节点被判定为可回收对象。
Java中可以作为GC Root的包括下面几种:
虚拟机栈中的引用对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象
23、垃圾回收算法
1、 标记-清除算法
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行垃圾回收
这种算法实现起来比较容易,但是会造成内存碎片
2、 标记-复制算法
复制算法是为了解决标记-清除算法的缺陷而提出的。
它将内存划分为大小相等的两块,每次只使用其中的一块。当这A快内存用完了,就将还存活的对象复制到B块上面,然后把A块的内存空间一次性清理掉.
这种算法虽然实现简单,运行高效且不易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能使用的空间缩减为原来的一半。很显然,复制算法的效率跟存活对象的数量有很大关联,若存活对象很多,那么效率将大大降低
3、标记-整理算法
该算法是为了解决复制算法的缺陷,充分利用内存空间而提出的。
该算法与标记-清除算法一样,但是在完成标记后,不直接清理可回收对象,而是将存活对象全部向一端移动,接着清理掉边界以外的内存。
4、 分代收集算法
年轻代:新创建的对象都存放在这里。因为年轻代会频繁的进行GC清理,JVM在年轻代采用的是标记-复制算法,先标记出存活的实例,然后清除掉无用实例,将存活的实例根据年龄(每个实例被经历一次GC后年龄会加1)拷贝到不同的年龄代。
老年代:老年代中是经历了N此垃圾祸首后仍然存活的对象,其中的N由JVM的参数决定。这块内存区域一般大于年轻代。GC发生的次数也比年轻代要少。
永久代:用于存放静态文件,如Java类、方法等。为方法区。
5、Scavenge GC
一般情况下,当新对象生成,并且在年轻代申请空间失败时,会触发Scavenge GC, 对年轻代进行垃圾回收。这种方式的GC不会影响到老年代。因为大部分对象都是年轻代开始的,同时年轻代内存不会分配的很大,所有年轻代的GC会频繁的进行。所以在这里要使用速度快、效率高的算法,使其空间尽快空出来。
若GC一次后仍不能满足内存分配,JVM会进行二次GC,若仍无法满足,则报“out of memory"的错误,Java应用将停止
6、Full GC
对整个内存进行整理,包括年轻代、老年代和永久代,所以Full GC比Scavenge GC要慢, 因此应该尽量减少Full GC的次数。以下可能引发Full GC的原因:
- 老年代被写满
- 永久代被写满
- System.gc()被显示调用
- 上一次GC后堆的各域分配策略动态变化。
24、分布式事务
1、事务的ACID原则
- 原子性:操作这些指令时,要么全部执行成功,要么全部不执行。只要其中一个指令执行失败,所有的指令都执行失败,数据进行回滚,回到执行指令前的数据状态。
- 一致性:事务的执行使数据从一个状态转换为另一个状态,但是对于整个数据的完整性保持稳定。
- 隔离性:在该事务执行的过程中,无论发生的任何数据的改变都应该只存在于该事务之中,对外界不存在任何影响。只有在事务确定正确提交之后,才会显示该事务对数据的改变。其他事务才能获取到这些改变后的数据。
- 持久性:当事务正确完成后,它对于数据的改变是永久性的。
2、分布式事务的四种解决方案:
- 两阶段提交(2PC)
准备阶段:
协调者询问参与者事务是否执行成功,参与者发回事务执行结果。
提交阶段:
如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
问题:
同步阻塞,没有完善的容错机制
- 补偿事务(TCC)
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。
相对 2PC 比较简单,
TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
- 本地消息表(异步确保)
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
- MQ 事务消息
有一些第三方的MQ是支持事务消息的,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
其思路大致为:
第一阶段Prepared消息,会拿到消息的地址。 第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
优点: 实现了最终一致性,不需要依赖本地数据库事务。
缺点: 实现难度大,主流MQ不支持,RocketMQ事务消息部分代码也未开源。
25、分布式锁
什么是分布式锁?
- 常规的我们多线程访问同一代码块的时候,为了保证同一时间只能 由一个线程访问,保证数据安全一致性,通常我们使用synchronized关键字来对方法加锁,以达到保证数据安全性。
- 现在越来越多的项目,为了追求性能与高并发,采用了soa架构,微服务架构,于是就会出现多个模块单独的服务。这个时候呢就会有一个问题,如何保证多个节点的现场同步执行呢? 这种情况呢,就会用到了分布式锁。
分布式锁的解决方案与实现
- 数据库解决方案思路:
- 数据库建一张表,字段方法名并且作为唯一性,当一个方法执行时插入,则相当于获得锁,其他线程将无法访问,方法执行完则释放锁。
- 使用select * from user u where username = ‘’ for update 来对记录加上排他锁。操作完成后使用commit命令释放锁
-
基于缓存实现
主要用到的redis函数是setnx(),这个应该是实现分布式锁最主要的函数。首先是将某一任务标识名(这里用Lock:order作为标识名的例子)作为键存到redis里,并为其设个过期时间,如果是还有Lock:order请求过来,先是通过setnx()看看是否能将Lock:order插入到redis里,可以的话就返回true,不可以就返回false -
Zookeeper分布式锁
每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上,所以性能上不如基于缓存实现。
26、Spring Cloud
1、Eureka
Eureka Client:负责将这个服务的信息注册到Eureka Server中
Eureka Server:注册中心,里面有一个注册表,保存了各个服务所在的机器和端口号
2、Fiegn
- 对接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理
- 调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心
- Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址
- 最后针对这个地址,发起请求、解析响应
3、Ribbon
- 首先Ribbon会从 Eureka Client里获取到对应的服务注册表,也就知道了所有的服务都部署在了哪些机器上,在监听哪些端口号。
- 然后Ribbon就可以使用默认的Round Robin算法,从中选择一台机器
- Feign就会针对这台机器,构造并发起请求。
4、Hystrix
- Hystrix具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能
5、Zuul
- 网络路由
- 做统一的降级、限流、认证授权、安全,等等
27、Spring 和 Spring Boot 的区别?
以下是Spring Boot中的一些特点:
1:创建独立的spring应用。
2:嵌入Tomcat, Jetty Undertow 而且不需要部署他们。
3:提供的“starters” poms来简化Maven配置
4:尽可能自动配置spring应用。
5:提供生产指标,健壮检查和外部化配置
6:绝对没有代码生成和XML配置要求
Spring Boot基本上是Spring框架的扩展,它消除了设置Spring应用程序所需的复杂例行配置。
它的目标和Spring的目标是一致的,为更快,更高效的开发生态系统铺平了道路。
28、Spring
Ioc — 控制反转。
他不是一种技术,只是一种思想,在Java开发中,这是spring的核心,贯穿始终。所谓IoC,对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系。
所有的类都会在spring容器中登记,告诉spring你是个什么东西,你需要什么东西,然后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。
IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。
Java 1.3之后一个重要特征是反射(reflection),它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。
Aop — 面向切面编程。
往往被定义为促使软件系统实现关注点的分离的技术。系统是由许多不同的组件所组成的,每一个组件各负责一块特定功能。除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。
AOP就是把与核心业务逻辑无关的代码全部抽取出来,放置到某个地方集中管理,然后在具体运行时,再由容器动态织入这些共有代码。
AOP用到了两种动态代理来实现拦截切入功能:jdk动态代理和cglib动态代理。两种方法同时存在,各有优劣。
jdk动态代理是由java内部的反射机制来实现的,cglib动态代理底层则是借助asm来实现的。
反射机制在生成类的过程中比较高效,而asm在生成类之后的相关执行过程中比较高效(可以通过将asm生成的类进行缓存,这样解决asm生成类过程低效问题)。还有一点必须注意:jdk动态代理的应用前提,必须是目标类基于统一的接口。如果没有上述前提,jdk动态代理不能应用。
28、Spring Boot
Spring Boot特点
- 搭建项目快,几秒钟就可以搭建完成;
- 让测试变的简单,内置了JUnit、Spring Boot Test等多种测试框架,方便测试;
- Spring Boot让配置变的简单,Spring Boot的核心理念:约定大约配置,约定了某种命名规范,可以不用配置,就可以完成功能开发,比如模型和表名一致就可以不用配置,直接进行CRUD(增删改查)的操作,只有表名和模型不一致的时候,配置名称即可;
- 内嵌容器,省去了配置Tomcat的繁琐;
- 方便监控,使用Spring Boot Actuator组件提供了应用的系统监控,可以查看应用配置的详细信息;
Spring Boot核心功能
- 独立运行的spring项目:Spring Boot可以以jar包形式直接运行,如java-jar xxxjar优点是:节省服务器资源
- 内嵌servlet 容器:Spring Boot 可以选择内嵌Tomcat,Jetty,这样我们无须以war包形式部署项目。
- 提供starter 简化Maven 配置:在Spring Boot 项目中为我们提供了很多的spring-boot-starter-xxx的项目(我们把这个依赖可以称之为起步依赖),我们导入指定的这些项目的坐标,就会自动导入和该模块相关的依赖包。
- 自动配置 spring:Spring Boot 会根据在类路径中的jar包,类,为jar包里的类自动配置Bean,这样会极大减少我们要使用的配置。
- 准生产的应用监控:Spring Boot 提供基于http,sh,telnet对运行时的项目进行监控
- 无代码生成和xml配置:Spring Boot大量使用spring4.x提供的注解新特性来实现无代码生成和xml 配置。spring4.x提倡使用Java配置和注解配置组合,而Spring Boot不需要 任何xml配置即可实现spring的所有配置。
29、Redis
特点
- Redis 采用单线程模式处理请求。这样做的原因有 2 个:一个是因为采用了非阻塞的异步事件处理机制;另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。
- Redis 支持持久化,所以 Redis 不仅仅可以用作缓存,也可以用作 NoSQL 数据库。
- Redis 支持多种数据格式,例如 list、set、sorted set、hash 等。
- Redis 提供主从同步机制,以及 Cluster 集群部署能力,能够提供高可用服务。
数据类型
-
String:这是最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。String 类型是 Redis 中最常使用的类型,内部的实现是通过 SDS(Simple Dynamic String )来存储的。SDS 类似于 Java 中的 ArrayList,可以通过预分配冗余空间的方式来减少内存的频繁分配。
-
Hash:这个是类似 Map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 Hash 里的某个字段。
-
List:List 是有序列表,Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。
-
Set:Set 是无序集合,会自动去重的那种。直接基于 Set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重。
-
Sorted Set:Sorted set 是排序的 Set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。有序集合的使用场景与集合类似,但是set集合不是自动有序的,而Sorted set可以利用分数进行成员间的排序,而且是插入时就排序好。所以当你需要一个有序且不重复的集合列表时,就可以选择Sorted set数据结构作为选择方案。
-
Bitmap :位图是支持按 bit 位来存储信息,可以用来实现 布隆过滤器(BloomFilter);
-
HyperLogLog:供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV;
-
Geospatial:以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。
-
pub/sub:功能是订阅发布功能,可以用作简单的消息队列。
事务
Redis 提供了 RDB 和 AOF 两种持久化方式,RDB 是把内存中的数据集以快照形式写入磁盘,实际操作是通过 fork 子进程执行,采用二进制压缩存储;AOF 是以文本日志的形式记录 Redis 处理的每一个写入或删除操作
-
RDB:RDB 把整个 Redis 的数据保存在单一文件中,比较适合用来做灾备,但缺点是快照保存完成之前如果宕机,这段时间的数据将会丢失,另外保存快照时可能导致服务短时间不可用。
-
AOF:AOF 对日志文件的写入操作使用的追加模式,有灵活的同步策略,支持每秒同步、每次修改同步和不同步,缺点就是相同规模的数据集,AOF 要大于 RDB,AOF 在运行效率上往往会慢于 RDB。
哨兵模式(高可用)
Redis 支持主从同步,提供 Cluster 集群部署模式,通过 Sentine l哨兵来监控 Redis 主服务器的状态。当主挂掉时,在从节点中根据一定策略选出新主,并调整其他从 slaveof 到新主。
哨兵必须用三个或以上的基数实例去保证自己的健壮性的,哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用。
功能点:
-
集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
-
消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
-
故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
-
配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
主从复制
主从同步的时候,新的slaver进来的时候用RDB,新的数据进入master使用AOF同步到slaver。
缓存穿透
产生这个问题的原因可能是外部的恶意攻击,例如,对用户信息进行了缓存,但恶意攻击者使用不存在的用户id频繁请求接口,导致查询缓存不命中,然后穿透 DB 查询依然不命中。这时会有大量请求穿透缓存访问到 DB。
解决:
-
对不存在的用户,在缓存中保存一个空对象进行标记,防止相同 ID 再次访问 DB。不过有时这个方法并不能很好解决问题,可能导致缓存中存储大量无用数据。
-
使用 BloomFilter 过滤器,BloomFilter 的特点是存在性检测,如果 BloomFilter 中不存在,那么数据一定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存在。非常适合解决这类的问题。
缓存击穿
某个热点数据失效时,大量针对这个数据的请求会穿透到数据源。
解决:
-
可以使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到 DB,减小 DB 压力。
-
使用随机退避方式,失效时随机 sleep 一个很短的时间,再次查询,如果失败再执行更新。
-
针对多个热点 key 同时失效的问题,可以在缓存时使用固定时间加上一个小的随机数,避免大量热点 key 同一时刻失效。
缓存雪崩
产生的原因是缓存挂掉,这时所有的请求都会穿透到 DB。
解决:
-
使用快速失败的熔断策略,减少 DB 瞬间压力;
-
使用主从模式和集群模式来尽量保证缓存服务的高可用。
实际场景中,这两种方法会结合使用。
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern
-
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
-
更新的时候,先更新数据库,然后再删除缓存。
Linux日志命令
tail:
-n 是显示行号;相当于nl命令;例子如下:
tail -100f test.log 实时监控100行日志
tail -n 10 test.log 查询日志尾部最后10行的日志;
tail -n +10 test.log 查询10行之后的所有日志;
head:
跟tail是相反的,tail是看后多少行日志;例子如下:
head -n 10 test.log 查询日志文件中的头10行日志;
head -n -10 test.log 查询日志文件除了最后10行的其他所有日志;
cat:
tac是倒序查看,是cat单词反写;例子如下:
cat -n test.log |grep "debug" 查询关键字的日志