目录
一、java并发
解析:
- A、CopyOnWriteArrayList适用于写少读多的并发场景
- 读操作不加锁,读写互斥,写写互斥;适用于读多写少的情景
- 参考网址:《CopyOnWriteArrayList和ReentrantReadWriteLock》https://blog.csdn.net/lovesman/article/details/107190307
- B、ReadWriteLock即为读写锁,他要求写与写之间互斥,读与写之间互斥,读与读之间可以并发执行。在读多写少的情况下可以提高效率
- 参考网址:《【Java并发】ReadWriteLock读写锁的使用》https://www.jianshu.com/p/9cd5212c8841
- C、ConcurrentHashMap是同步的HashMap,读操作不加锁,写操作加锁
- 参考网址:《经典面试题:为什么 ConcurrentHashMap 的读操作不需要加锁?》https://www.cnblogs.com/tiancai/p/13297793.html
- D、volatile只保证多线程操作的可见性,不保证原子性。自增操作不是原子性操作(包括读取变量、变量+1、写入内存三步),在执行时可能会有安全问题。
- 概念
- volatile 是一个类型修饰符。volatile 的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略
- 特性
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
- 禁止进行指令重排序。(实现有序性)
- volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。关于volatile 原子性可以理解为把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。
- 参考网址:《volatile 关键字,你真的理解吗?》https://zhuanlan.zhihu.com/p/138819184、《浅谈volatile在i++情况下失效》https://blog.csdn.net/liuzhixiong_521/article/details/85246543
- 概念
二、字符串与数组的长度
解析:
- String获取长度用的是length()方法,而数组类型我们直接用属性length获取长度,所以String[]数组类型我们应该用length获取长度;
- 总结来说,因为原字符串不包含分隔符,所以直接返回原字符串,分割出来只有一个空的字符串数组,所以结果是1。(注意,虽然原字符串为空,存到字符串数组为空,但是这个空字符串也会算一个元素)
三、Java的变量命名
解析:
Java标识符由数字,字母和下划线(_),美元符号($)或人民币符号(¥)组成。
在Java中是区分大小写的,而且还要求首位不能是数字,可以是下划线。最重要的是,Java关键字不能当作Java标识符。
四、CMS垃圾回收
解析:
CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器,对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。
CMS的基础算法是:标记—清除。它的过程可以分为以下6个步骤:
- 初始标记(STW initial mark)
- 在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的"根对象"开始,只扫描到能够和"根对象"直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。
- 并发标记(Concurrent marking)
- 这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。
- 并发预清理(Concurrent precleaning)
- 并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段"重新标记"的工作,因为下一个阶段会Stop The World。
- 重新标记(STW remark)
- 这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"根对象"开始向下追溯,并处理对象关联。
- 并发清理(Concurrent sweeping)
- 清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。
- 并发重置(Concurrent reset)
- 这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收。
五、单例设计模式
解析:
- 单例设计模式定义
- 确保一个类只有一个实例,并提供该实例的全局访问点。
- 这样做的好处是:有些实例,全局只需要一个就够了,使用单例模式就可以避免一个全局使用的类,频繁的创建与销毁,耗费系统资源。
- 单例模式的设计要素
- 创建一个私有构造函数 (确保只能单例类自己创建实例)
- 创建一个私有静态变量 (确保只有一个实例)
- 创建一个公有静态函数 (给使用者提供调用方法)
单例设计模式的实现与各自优缺点:
1、懒汉式(线程不安全)
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
} return uniqueInstance;
}
}
- 说明:先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。
- 优点:延迟了实例化,如果不需要使用该类,就不会被实例化,节约了系统资源。
- 缺点:线程不安全,多线程环境下,如果多个线程同时进入了 if (uniqueInstance == null) ,若此时还未实例化,也就是uniqueInstance == null,那么就会有多个线程执行 uniqueInstance = new Singleton(); ,就会实例化多个实例;
- 解决方案:在getUniqueInstance()方法上添加锁:private static synchronized Singleton getUinqueInstance(),避免多个线程同时获取实例
2、饿汉式(线程安全)
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getUniqueInstance() {
return uniqueInstance;
}
}
- 说明:先不管需不需要使用这个实例,直接先实例化好实例 (饿死鬼一样),然后当需要使用的时候,直接调方法就可以使用了。
- 优点:提前实例化好了一个实例,避免了线程不安全问题的出现。
- 缺点:直接实例化好了实例,不再延迟实例化;若系统没有使用这个实例,或者系统运行很久之后才需要使用这个实例,都会操作系统的资源浪费
3、双重检查锁实现(线程安全)
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
- 说明:双重检查锁相当于是改进了线程安全的懒汉式。
- 线程安全的懒汉式的缺点是性能降低了,造成的原因是因为即使实例已经实例化,依然每次都会有锁。而现在,我们将锁的位置变了,并且多加了一个检查。 也就是,先判断实例是否已经存在,若已经存在了,则不会执行判断方法内的有锁方法了。 而如果,还没有实例化的时候,多个线程进去了,也没有事,因为里面的方法有锁,只会让一个线程进入最内层方法并实例化实例。如此一来,最多最多,也就是第一次实例化的时候,会有线程阻塞的情况,后续便不会再有线程阻塞的问题。
- 为什么使用 volatile 关键字修饰了 uniqueInstance 实例变量 ?
- uniqueInstance = new Singleton(); 这段代码执行时分为三步:
- 1.为 uniqueInstance 分配内存空间
- 2.初始化 uniqueInstance
- 3.将 uniqueInstance 指向分配的内存地址
- 正常的执行顺序当然是 1>2>3 ,但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。
- 单线程环境时,指令重排并没有什么问题;多线程环境时,会导致有些线程可能会获取到还没初始化的实例。
- 例如:线程A 只执行了 1 和 3 ,此时线程B来调用 getUniqueInstance(),发现 uniqueInstance 不为空,便获取 uniqueInstance 实例,但是其实此时的 uniqueInstance 还没有初始化。
- 解决办法就是加一个 volatile 关键字修饰 uniqueInstance ,volatile 会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。
- 优点:延迟实例化,节约了资源;线程安全;并且相对于 线程安全的懒汉式,性能提高了。
- 缺点:volatile 关键字,对性能也有一些影响。
4、静态内部类实现(线程安全)
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getUniqueInstance() {
return SingletonHolder.INSTANCE;
}
}
- 说明:
- 首先,当外部类 Singleton 被加载时,静态内部类 SingletonHolder 并没有被加载进内存。
- 当调用 getUniqueInstance() 方法时,会运行 return SingletonHolder.INSTANCE;
- 触发了 SingletonHolder.INSTANCE ,此时静态内部类 SingletonHolder 才会被加载进内存,并且初始化 INSTANCE 实例,而且 JVM 会确保 INSTANCE 只被实例化一次。
- 优点:延迟实例化,节约了资源;且线程安全;性能也提高了。
5、枚举类实现(线程安全)
public enum Singleton {
INSTANCE; //添加自己需要的操作 public void doSomeThing() {}
}
- 说明:默认枚举实例的创建就是线程安全的,且在任何情况下都是单例。
- 优点:写法简单,线程安全,天然防止反射和反序列化调用。
防止反序列化
序列化:把java对象转换为字节序列的过程;
反序列化:通过这些字节序列在内存中新建java对象的过程;
也就是说,反序列化 将一个单例实例对象写到磁盘再读回来,从而获得了一个新的实例。
我们要防止反序列化,避免得到多个实例。
其中枚举类天然防止反序列化。
其他单例模式可以通过重写 readResolve() 方法,从而防止反序列化,使实例唯一,重写 readResolve() :
private Object readResolve() throws ObjectStreamException{ return singleton; }
六、Hashtable 和 HashMap 的区别
解析:
- A、×,Hashtable 是一个哈希表,该类继承自Dictionary类,实现了 Map 接口
- B、√,HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。该类继承AbstractMap,实现Map接口
- C、√,Hashtable 线程安全的,而 HashMap 是线程不安全的。Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。
- D、√,讲的没错,但是不符合题意
- E、√,语句挑不出毛病。
Hashtable 和 HashMap 的区别:
- 1、继承的父类不同
- Hashtable继承自Dictionary类
- HashMap继承自AbstractMap类
- 但二者都实现了Map接口
- 2、线程的安全性
- Hashtable是同步(方法中使用了Synchronize)的,是线程安全的
- HashMap是未同步(方法中缺省Synchronize)的,是线程不安全的
- 3、是否有contains方法
- Hashtable有一个contains(Object value)方法,功能和containsValue(Object value)功能一样
- HashMap把Hashtable的contains方法去掉了,改成containsValue(Object value)和containsKey(Object key)
- 4、是否允许null值
- Hashtable中,key和value都不允许出现null值
- HashMap允许null值(key和value都可以),因为在HashMap中null可以作为健,而它对应的值可以有多个null
- 5、遍历方式
- HashTable使用Enumeration
- HashMap使用Iterator
- 6、Hash值
- Hashtable直接使用对象的hashCode
//hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length; - HashMap要重新计算key值和hash值
int hash = hash(k);
int i = indexFor(hash, table.length);
static int hash(Object x) {
int h = x.hashCode();
h += ~(h << 9);
h ^= (h >>> 14);
h += (h << 4);
h ^= (h >>> 10);
return h;
}
static int indexFor(int h, int length) {
return h & (length-1);
}
- Hashtable直接使用对象的hashCode
- 7、数组初始化和扩容
- Hashtable中hash数组默认大小是11,增加的方式是2*n+1(n为当前数组的大小)
- HashMap中hash数组的默认大小是16,增加的方式是2*n(n为当前数组的大小)