一.单例模式
1.饿汉式单例(立即加载方式)
// 饿汉式单例
public class Singleton {
// 私有构造
private Singleton() {}
private static Singleton single = new Singleton();
// 静态工厂方法
public static Singleton getInstance() {
return single;
}
}
2.懒汉式单例(延迟加载方式)
// 懒汉式单例
public class Singleton {
// 私有构造
private Singleton() {}
private static Singleton single = null;
public static Singleton getInstance() {
if(single == null){
single = new Singleton();
}
return single;
}
}
3.双检锁/双重校验锁
public class Singleton {
//使用 Volatile 保证了指令重排序在这个对象创建的时候不可用
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
//这次判空是避免了,保证的多线程只有第一次调用getInstance 的时候才会加锁初始化
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
4.登记式/静态内部类
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
5.枚举
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
通过 Volatile 避免指令重排序
注:volatile 作用有以下两点:
- 可以保证多线程条件下,内存区域的可见性,即使用 volatile 声明的变量,将对在一个线程从内主内存(线程共享的内存区域)读取变量,并写入后,通知其他线程,改变量被我改变了,别的线程在使用的时候,将会重新从主内存中去读改变量的最新值。
- 可以保证再多线程的情况下,指令重排这个操作将会被禁止。
二.列出object常用的方法
- toString()
(1) Object默认方法 toString方法,toString() 输出一个对象的地址字符串(哈希code码)
(2)可以通过重写toString方法,获取对象的属性! 快捷键 alt+shift+s创建Override toString() - equals()
(1) Object类equals()比较的是对象的引用是否指向同一块内存地址
(2) 重写equals()方法比较俩对象的属性值是否相同
三.Map集合中哪些是有序的,无序的,线程安全的并且解释1~2种的实现原理
- HashMap和HashTable的实现原理和底层分析
HashMap:无序不安全
HashTable:无序安全效率低
主要看的是下面链接中的两者区别、如何扩容以及数据存储结构
https://www.cnblogs.com/java-jun-world2099/p/9258605.html - LinkHashMap分析
LinkHashMap有序(实现Map接口,继承HashMap,在HashMap的基础上自己维护了一个双链表,从而保证迭代的顺序)
LinkHaskMap包括访问排序和插入排序
(1)插入排序:就是插入(put)的时候的顺序是什么,取出来的时候就是什么样子
LinkedHashMap<String, String> map = new LinkedHashMap<>(10,0.75f,false);
for(int i=0; i<10; i++) {
map.put("key" + i, i+"");
}
String s = map.get("key2");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
结果如下所示(如果两个key一样的话,会覆盖点前面的值但是顺序不变)
Key = key0, Value = 0
Key = key1, Value = 1
Key = key2, Value = 2
Key = key3, Value = 3
Key = key4, Value = 4
Key = key5, Value = 5
Key = key6, Value = 6
Key = key7, Value = 7
Key = key8, Value = 8
Key = key9, Value = 9
(2)访问排序:就是你get的时候,会改变元素的顺序,会把该元素移到数据的末尾
LinkedHashMap<String, String> map = new LinkedHashMap<>(10,0.75f,true);
for(int i=0; i<10; i++) {
map.put("key" + i, i+"");
}
String s = map.get("key2");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
结果如下:(key2变成了数据的最后一个元素)
Key = key0, Value = 0
Key = key1, Value = 1
Key = key3, Value = 3
Key = key4, Value = 4
Key = key5, Value = 5
Key = key6, Value = 6
Key = key7, Value = 7
Key = key8, Value = 8
Key = key9, Value = 9
Key = key2, Value = 2
注:参考链接(https://www.cnblogs.com/xiaoxi/p/6170590.html?_t=1559876431)
四:解释ConcurrentHashMap是实现原理。jdk1.8后有何改变?
- 底层采用分段的数组+链表实现,线程安全。
- 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是volatile的,也能保证读取到最新的值。)
- Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。
- 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。
- 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容。
jdk 1.8改变
- 链表改成了红黑树,当链表中的结点达到一个阀值TREEIFY_THRESHOLD时,会将链表转换为红黑树,查询效率提从原来的O(n),提高为O(logn)
- 将每个segment的分段锁ReentrantLock改为CAS(比较交换【乐观锁】)+Synchronized
CAS解释:https://blog.csdn.net/u011381576/article/details/79922538
五:值传递和应用传递
- 值传递:
方法调用时,实际参数把它的值传递给对应的形式参数,函数接收的是原始值的一个copy,此时内存中存在两个相等的基本类型,即实际参数和形式参数,后面方法中的操作都是对形参这个值的修改,不影响实际参数的值。 - 引用传递:
也称为传地址。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,函数接收的是原始值的内存地址; 在方法执行中,形参和实参内容相同,指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象。
例:分别传参int和对象类型:
public class ReferencePkValue2 {
public static void main(String[] args) {
ReferencePkValue2 t = new ReferencePkValue2();
int a=99;
t.test1(a);//这里传递的参数a就是按值传递
System.out.println(a);
MyObj obj=new MyObj();
t.test2(obj);//这里传递的参数obj就是引用传递
System.out.println(obj.b);
}
public void test1(int a){
a=a++;
System.out.println(a);
}
public void test2(MyObj obj){
obj.b=100;
System.out.println(obj.b);
}
}
class MyObj{
public int b=99;
}
结果:
99
99
100
100
可以看到,int值没有发生变化,但是在test2方法中对obj类做的修改影响了obj这个对象。
这里要特殊考虑String,以及Integer、Double等几个基本类型包装类,它们都是immutable类型,因为没有提供自身修改的函数,每次操作都是新生成一个对象,所以要特殊对待,可以认为是和基本数据类型相似,传值操作。
public class ReferencePkValue1 {
public static void main(String[] args){
ReferencePkValue1 pk=new ReferencePkValue1();
//String类似基本类型,值传递,不会改变实际参数的值
String test1="Hello";
pk.change(test1);
System.out.println(test1);
//StringBuffer和StringBuilder等是引用传递
StringBuffer test2=new StringBuffer("Hello");
pk.change(test2);
System.out.println(test2.toString());
}
public void change(String str){
str=str+"world";
}
public void change(StringBuffer str){
str.append("world");
}
}
结果:
Hello
Helloworld
对String和StringBuffer的操作产生了不同的结果。
结论:
(1)基本数据类型传值,对形参的修改不会影响实参;
(2)引用类型传引用,形参和实参指向同一个内存地址(同一个对象),所以对参数的修改会影响到实际的对象;
(3)String, Integer, Double等immutable的类型特殊处理,可以理解为传值,最后的操作不会修改实参对象。
六.JVM中的堆(heap),栈(stack),方法区(method)
- 堆:存储对象(实例),被所有线程共享
- 栈:存储对象引用和基本数据类型,每个线程都有一个线程栈
- 方法区:也叫静态区,存储class信息和static变量,被所有线程共享
例:
public class Test {
public static void main(String[] args) {
public Test2 t2 = new Test2();
//JVM将Test2类信息加载到方法区,new Test2()实例保存在堆区,Test2引用保存在栈区
}
}
七.Cooike应用的例子
未登录:购物车里面商品的信息都存储在cookie中(最大4k)
登陆后:会把原先购物车(cookie)中的商品和现有数据库或缓存中的商品叠加起来放入数据库或缓存中