Java知识点总结(不定时更新)
1、基于分代的垃圾收集算法
设计思路:把对象按照寿命长短来分组,分为年轻代和年老代,新创建的对象被分在年轻代,如果对象经过几次回收后仍然存活,那么再把这个对象划分到年老代。年老代的收集频率不像年轻代那么频繁,这样就减少了每次垃圾回收时所要扫描的对象的数量,从而提高了垃圾回收效率。
把堆划分为若干个子堆,每个堆对应一个年龄代:
JVM将整个堆划分为Young区、Old区和Perm区,存放不同年龄的对象,这个三个区存放的对象有如下区别:
Young区:又分为Eden区和两个Survivor区,其中所有新创建的对象都在Eden区,当Eden区满后会触发minor GC将Eden区仍然存活的对象复制到其中一个Survivor区中,另外一个Survivor区中的存活对象也复制到这个Survivor中,以保证始终有一个Survivor区是空的。
Old区:存放的是Young区的Survivor满后触发的minor GC后仍然存活的对象,当Eden区满后会将对象存放到Survivor区中,如果Survivor区仍然存不下这些对象,GC收集器会将这些对象直接存到Old区,如果在Survivor区中的对象足够老,也直接存放到Old区,如果Old区也满了,将会触发Full GC,回收整个堆内存。
Perm区:存放的主要是类的Class对象,Perm区的垃圾回收也是由Full GC触发的。
2、JVM参数配置
参考:http://www.cnblogs.com/likehua/p/3369823.html,http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html
最小内存值就是-Xms的值,即10240m
-Xmn 5120m:表示年轻代大小,-XXSurvivorRatio=3,即Eden:FromSurvivor:ToSurvivor=3:1:1;所以Survivor一共是10240*(2/5)=2048m。
3、集合框架的线程安全性
4、Collection接口和Map接口
5、HashMap排序
public class SortMap { public static void main(String[] args) { Map<String, Integer> map = new HashMap<String, Integer>(); map.put("1d", 4); map.put("2b", 3); map.put("3a", 1); map.put("4c", 2); System.out.println("原始数据:"); // 排序前 for(String s:map.keySet()) { System.out.println(s+":"+map.get(s)); } List<Map.Entry<String, Integer>> list = new ArrayList<Map.Entry<String, Integer>>(map.entrySet()); // 根据key值排序 Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() { @Override public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) { return o1.getKey().toString().compareTo(o2.toString()); } }); System.out.println("根据key值排序:"); // 根据key值排序后 for (Entry<String, Integer> entry : list) { System.out.println(entry.getKey() + ":" + entry.getValue()); } // 根据value排序 Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() { @Override public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) { return o1.getValue()-o2.getValue(); } }); System.out.println("根据value值排序:"); // 根据value值排序后 for (Entry<String, Integer> entry : list) { System.out.println(entry.getKey() + ":" + entry.getValue()); } } }
6、select/poll模型
select和poll都是事件触发机制,当等待的事件发生就触发进行处理,多用于Linux实现的服务器对客户端的处理。
可以阻塞的同时探测一组支持非阻塞的IO设备,是否有事件发生(如可读、可写,有高优先级错误输出等),直至某一个设备触发了事件或者超过了指定的等待时间。也就是它们的职责不是做IO,而是帮助调用者寻找当前就绪的设备。
select/poll的缺点在于:
1.每次调用时要重复地从用户态读入参数。
2.每次调用时要重复地扫描文件描述符。
3.每次在调用开始时,要把当前进程放入各个文件描述符的等待队列。在调用结束后,又把进程从各个等待队列中删除。
在实际应用中,select/poll监视的文件描述符可能会非常多,如果每次只是返回一小部分,那么,这种情况下select/poll显得不够高效。 epoll的设计思路,是把select/poll单个的操作拆分为1个epoll_create+多个epoll_ctrl+一个wait。此外,内核针对epoll操作添加了一个文件系统”eventpollfs”,每一个或者多个要监视的文件描述符都有一个对应的eventpollfs文件系统的inode节点,主要信息保存在eventpoll结构体中。而被监视的文件的重要信息则保存在epitem结构体中。所以他们是一对多的关系。
由于在执行epoll_create和epoll_ctrl时,已经把用户态的信息保存到内核态了所以之后即使反复地调用epoll_wait,也不会重复地拷贝参数,扫描文件描述符,反复地把当前进程放入/放出等待队列。这样就避免了以上的三个缺点。
参考:http://blog.csdn.net/wangxiaoqin00007/article/details/14448185
http://blog.csdn.net/historyasamirror/article/details/5778378(讲的非常好)
7、多态的实现机制
多态表示当同一个操作作用在不同不同对象时,会有不同的语义,从而产生不同的结果。
在Java中,多态主要有以下两种表现形式:
- 方法重载(overload):
是指同一个类中有多个同名的方法,但是这些方法有些不同的参数,因此在编译时就可以确定确定到底调用哪个方法,它是一种编译时多态。
- 方法覆盖(override):
子类可以覆盖父类的方法,因此同样的方法在父类和子类中有着不同的表现形式。在Java中,基类的引用变量不仅可以指向基类的示例对象,也可以指向其子类的实例对象。同样,接口的引用变量也可以指向其实现类的实例对象。
public class Base { public Base() { System.out.println("基类构造函数"); } public void f() { System.out.println("Base f"); } public void g() { System.out.println("Base g"); } public static void main(String[] args) { Base b=new Derived(); b.f(); b.g(); } } class Derived extends Base { public Derived() { System.out.println("子类构造函数"); } public void f() { System.out.println("Derived f"); } public void g() { System.out.println("Derived g"); } }
8、抽象类和接口
相同点:
1)都不能被实例化
2)接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能被实例化
不同点:
1)接口只有定义,其方法不能在接口中实现,而抽象类可以有定义与实现
2)接口需要实现,抽象类只能被继承,一个类可以实现多个接口,但是一个类只能继承一个类
3)接口中定义的成员变量默认为public static final,只能够有静态的不能被修改的数据变量,并且必须为其赋初始值,其所有方法都是public abstract的,而且只能被这两个关键字修饰。抽象类的抽象方法不能用private、static、synchronized等访问修饰符修饰。
4)接口被运用于实现比较常用的功能,便于日后维护或者添加删除方法,而抽象类更倾向于充当公共类角色,不适用于日后重新对里面的代码进行修改。
9、==、equals和hashCode有什么区别
“==”运算符用来比较两个变量的值是否相等,即比较变量对应的内存中所存储的数值是否相同。
具体来说,如果两个变量是基本数据类型,可以(只能)直接使用==来比较其对应的值是否相等。
如果一个变量指向的数据是对象(引用类型),那么涉及到了两块内存,对象本身占用一块内存(堆内存0),变量也占用一块内存。这时候如果用==表示这两个对象是否指向同一个对象(即指向同一块存储空间)。但是如果要比较这两个对象的内存是否相等,==运算符就无法实现了。
在没有覆盖equals方法的情况下,equals方法与==运算符一样,比较的是引用。equals方法的特殊之处在于它可以被覆盖,通过覆盖的方法让它不是比较引用而是比较数据内容,例如String类的equals方法就是用于比较两个独立的对象的内容是否相同,即堆中的内容是否相同。
public static void main(String[] args) { String s1=new String("hello"); String s2=new String("hello"); String s3=s1; System.out.println(s1==s2); System.out.println(s1==s3); System.out.println(s1.equals(s3)); System.out.println(s1.equals(s2)); }
hashCode()方法也是从Object类中继承过来的,也用来鉴定两个对象是否相等。Object类的hashCode方法返回对象在内存中的地址转换成的一个int值,如果没有重写hashCode方法,任何对象的hashCode方法都是不相等的。
equals方法一般是给用户调用的,如果需要判断两个对象是否相等,可以重写equals()方法。对于hashCode方法,用户一般不会调用它,在HashMap中,由于key是不可以重复的,在判断key是否重复就使用了hashCode方法,并且也用到了equals方法,此处不可以重复指的是equals方法和hashCode方法只要有一个不等就可以了。
一般在覆盖equals方法的同时也要覆盖hashCode方法,否则就会违反Object hashCode的通用约定,从而导致该类无法无所有基于散列值的集合类(HashMap、HashSet和HashTable)结合在一起正常运行。
原因:如果只覆盖了equals方法而没有覆盖hashCode方法,则两个不同的实例 A 和 B,虽然equals结果相等,但是却会有不同的‘HashCode,这样HashMap中会同时存在A和B,而实际上我们需要HashMap里面只能保存其中一个。比如你只覆盖了equals方法而没有覆盖hashCode方法,那么HashMap在第一步寻找链表的时候会出错,有同样值的两个对象Key1和Key2并不会指向同一个链表或桶,因为你没有提供自己的hashCode方法,那么就会使用Object的hashCode方法,该方法是根据内存地址来比较两个对象是否一致,由于Key1和Key2有不桶的内存地址,所以会指向不同的链表,这样HashMap会认为key2不存在,虽然我们期望Key1和Key2是同一个对象;反之如果只覆盖了hashCode方法而没有覆盖equals方法,那么虽然第一步操作会使Key1和Key2找到同一个链表,但是由于equals没有覆盖,那么在遍历链表的元素时,key1.equals(key2)也会失败(事实上Object的equals方法也是比较内存地址),从而HashMap认为不存在Key2对象,这同样也是不正确的。
10、finally块中的代码什么被执行?
若try{}里面有一个return语句,finally块里的代码也是在return之前执行的。
如果try-finally或者catch-finally中都有return,那么finally块中的return语句会覆盖别的return语句,最终返回到调用者的是finally中的return的值。
11、运行时异常和普通异常区别
Java提供了两种错误的异常类,分别为Error和Exception,且他们有共同的父类Throwable。
Error表示程序在运行期间出现了非常严重的错误,并且错误是不可恢复的。例如OutMemoryError、ThreadDeath、方法调用栈溢出等都属于错误。当这些异常发生时,JVM一般会选择将线程终止。
Exception表示可恢复的异常,是编译器可以捕捉到的。它有两种类型:检查异常和运行时异常
- 检查异常:最常见的IO异常和SQ异常,这种异常都发生在编译阶段,编译器强制程序去捕获此类型的异常,果没有try……catch也没有throws抛出,编译是通不过的。
- 运行时异常:编译器没有强制对其进行捕获处理,如果不对这种异常进行处理,当出现这一种异常时,会有JVM处理,例如NullPointException空指针异常,ClassCastException类转换异常,ArrayIndexOutBoundsException数组越界异常,算术异常等。
12、什么是序列化?
Java提供了两种对象持久化的方法,分别为序列化和外部序列化。
(1) 序列化
在分布式环境下,当进行远程通信时,无论是何种类型的数据,都会以二进制序列的形式在网络上传送。
序列化是一种将对象以一连串的字节描述的过程,用于解决在对对象进行读写操作时所引发的问题。序列化可以将对象的状态写在流里进行网络传输,或者保存到文件、数据库等系统里,并在需要时把该流读取出来重新构造一个相同的对象。
注:如果一个类能被序列化,那么它的子类也能够被序列化。
由于static代表类的成员transient声明一个实例变量,当对象存储时,它的值不需要维持,所以这种类型的数据成员是不能够被序列化的。
(2)外部序列化
使用外部序列化时,Externalizable接口中的读写方法必须都由开发人员来实现。优点是在编程时有更多的灵活性,对需要持久化的那些属性可以进行控制。
注:怎样实现只序列化部分属性?
- 实现Externalizable接口,开发人员可以根据实际需求来实现readExternal与writeExternal方法来控制序列化与反序列化所使用的属性。
- 使用关键字transient来控制序列化的属性。把不需要被序列化的属性用transient来修饰。
13、字符串创建与存储的机制
对于字符串,其对象引用都是存储在栈中的,如果是编译期已经创建好(eg:String s="hello")的就存储在常量池中,如果是运行期(String s=new String("hello"))才能确定的就存储在堆中。对于equals相等的字符串,在常量池中只有一份,在堆中有多份。
当创建一个字符串常量时,例如String s="hello",会首先在字符串常量池中查找是否已经有相同的字符串被定义,其判断依据是String.equals(Obj)方法的返回值,若已经定义,则直接获取对其的引用,此时不再创建新的对象,如果没有定义,则首先会创建这个对象,然后把它加入到字符串池中,再将它的引用返回。
通过new产生一个字符串时,例如String s=new String("hello")时,相当于有两个过程,第一个过程是新建对象的过程,即new String("hello"),第二个过程是赋值的过程,即String s=,第二个过程只是定义了一个名为s的String类型的变量,将一个String类型对象的引用赋值给s。第一个过程会调用String类的构造函数,若在字符串池中不存在“hello”,则会创建一个字符串常量“hello”,并将其添加到字符串池中,若存在,则不创建,然后new String()会在堆中创建一个新的对象。
14、多线程同步的实现方法
1、synchronize关键字
在Java中,每个对象都与一个对象锁与之相关联,该锁表明对象在任何时候只允许被一个线程所拥有,当一个线程调用对象的一个synchronize代码时,需要先获取这个锁,然后去执行相应的代码,执行结束后吗,释放锁。
2、wait方法与notify方法
线程调用对象的wait方法,进入等待状态,释放对象锁,并且可以调用notify方法通知正在等待的其他的线程。
package com.xj.offer.test; public class WaitAndNotify { class TestWait extends Thread { public void run() { test("wait线程"); } } class TestNotify extends Thread { public void run() { test("notify线程"); } } public void exe() { TestWait tw = new TestWait(); TestNotify tn = new TestNotify(); tn.start(); tw.start(); } // 监视器 public synchronized void test(String str) { for (int i = 0; i < 10; i++) { if (i == 5) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { notify(); } System.out.println(str + "-------------" + i); } } public static void main(String[] args) { new WaitAndNotify().exe(); } }
3、Lock
Lock接口和它的一个实现类ReentrantLock(重入锁)。
lock():以阻塞的方式获取锁,即若获取到锁,立即返回,如果别的线程持有锁,当前线程等待,直到获取锁后返回。
tryLock():以非阻塞的方式获取锁,只是尝试性的去获取一下锁,如果获取到锁,返回true,否则返回false
15、sleep方法与wait方法的区别
sleep和wait均是使线程暂停执行的方法,区别在于:
1、原理不同。sleep方法是Thread类的静态方法,是线程用来控制自身流程的,它会使线程暂停执行一段时间,而把执行机会让给其他线程,等到计时时间一到,此线程就会自动“苏醒”。wait方法是Object类的方法,用于线程间的通信,这个方法会使得拥有该对象锁的进程等待,直到其他线程调用notify方法时才醒来。
2、对锁的处理机制不同。由于sleep方法的主要作用是让线程暂停执行一段时间,时间一到则自动恢复,不涉及线程间的通信,因此调用sleep方法不会释放锁。而调用wait方法后,线程会释放掉它所占用的锁,从而使线程所在对象中其他synchronize数据可被别的线程使用。
3、使用区域不同。wait方法必须放在同步控制方法或同步语句块中使用,而sleep()可以在任何地方使用。
16、synchronize与Lock区别
1、用法不同。synchronize既可以加在方法上,也可以加载特定代码中,括号表示需要锁的对象。Lock需要显示的指定起始位置和终止位置。synchronize是托管给JVM执行的,Lock的锁定是通过代码实现的。
2、锁机制不一样。synchronize获取锁和释放锁的方式都在在块结构中,当获取多个锁时,必须以相反的顺序释放,并且是自动释放的。Lock则需要开发人员手动去释放,并且必须在finally块中释放,否则会引起死锁问题的发生。
17、join()方法的作用
join方法的作用是让调用该方法的线程在执行完run()方法后,再执行join方法后面的代码。例如可以通过线程A的join方法来等待线程A的结束。
18、Http中get与post方法的区别
get方法的作用主要用来获取服务器端资源信息,如同数据库中查询操作一样,不会影响到资源本身的状态。而post方法提供了比get方法更加强大的功能,它除了能够从服务器端获取资源外,同时还可以像服务器上传数据。
get方法主要用来从服务器上获取数据,也可以像服务器上传数据,但是不建议,原因如下:
1、采用get方法向服务器上传数据时,一般将数据添加到URL后面,并且二者用“?”连接,各个变量之间用“&”连接。由于URL对长度的限制,此种方法能上传的数据量非常小。post方法传递数据通过http请求的附件进行的,传送的数据量更大。
2、get方法上传数据被彻底暴露在url中,本身存在安全隐患。
19、Servlet中forward和redirect有什么区别
1、forward是服务器内部的重定向,服务器直接访问目标地址的url,把那个url的响应读取过来,而客户端并不知道,因此在客户端浏览器的地址栏不会显示转向后的地址。由于在整个定向的过程中用的是同一个Request,因此forward会将Request的信息带到被定向的jsp或Servlet中使用。
2、redirect是客户端的重定向,是完全的跳转,即客户端浏览器会获取到跳转后的地址,然后重新发送请求,比forward多一次网络请求。
20、getparameter和getattribute的区别
request.getParameter()方法传递的数据,会从Web客户端传到Web服务器端,代表HTTP请求数据。request.getParameter()方法返回String类型的数据。
request.setAttribute()和getAttribute()方法传递的数据只会存在于Web容器内部,在具有转发关系的Web组件之间共享。这两个方法能够设置Object类型的共享数据。
request.getParameter()取得是通过容器的实现来取得通过类似post,get等方式传入的数据。
request.setAttribute()和getAttribute()只是在web容器内部流转,仅仅是请求处理阶段。
getAttribute是返回对象,getParameter返回字符串
总的来说:request.getAttribute()方法返回reques,sessiont范围内存在的对象,而request.getParameter()方法是获取http提交过来的数据。
21、工厂模式
工厂模式专门负责实例化有大量公共接口的类,工厂模式可以动态的决定将哪一个类实例化,而不必事先知道每次要实例化哪一个类。客户类和工厂类是分开的。
public class Factory{ public static ISample creator(int which){ if (which==1) return new SampleA(); else if (which==2) return new SampleB(); } }
使用工厂方法 要注意几个角色,首先你要定义产品接口,如上面的Sample类的接口,产品接口下有ISample接口的实现类,如SampleA,其次要有一个Factory类,用来生成产品ISample接口的具体实例。
22、内连接、左外连接、右外连接和完全连接
##内连接 select sa.*,sb.* from sa inner join sb on sa.sid=sb.sid; ##左外连接 select sa.*,sb.* from sa left join sb on sa.sid=sb.sid ; ##右外连接 select sa.*,sb.* from sa right join sb on sa.sid=sb.sid ; ## 完全连接 select sa.*,sb.* from sa full join sb on sa.sid=sb.sid ;
内连接保证两个表中的所有行都满足连接条件
外连接不仅包含符号连接条件的行,而且还包括左表(左外连接时)、右表(右外连接)或者两个边连接表(全外连接)中所有数据行,下图为左外连接。
23、求一个数组的最大连续子数组累加和
package com.xj.offer.test; /** * 求一个数组的最大连续子数组累加和 * @author Administrator * */ public class MaxsumsubArray { public static void main(String[] args) { int a[]=new int[]{1,2,-3,2}; System.out.println(getMaxsubsum(a)); } public static int getMaxsubsum(int[] a) { if(a==null||a.length<=0) return 0; int cur=0; int max=Integer.MIN_VALUE; for(int i=0;i<a.length;i++) { cur+=a[i]; max=Math.max(cur, max); if(cur<0) cur=0; } return max; } }
24、给定一个无序矩阵,其中有证,有负,有0,求子矩阵的最大和
/** * 给定一个无序矩阵,其中有证,有负,有0,求子矩阵的最大和 * @author Administrator * */ public class SubMatrixMaxSum { public static int getMaxSum(int[][] m) { if(m == null || m.length == 0 || m[0].length == 0) return 0; int max = Integer.MIN_VALUE; int cur; int[] s = null; for(int i = 0; i < m.length; i++)//控制行 { s = new int[m[0].length]; for(int j = i; j < m[0].length; j++)//控制累加的行 { cur = 0; for(int k = 0; k < s.length; k++) { s[k] += m[j][k]; cur += s[k]; max = Math.max(cur,max); cur = cur < 0 ? 0 : cur; } } } return max; } }
25、JDK1.8中的ConcurrentHashMap
如jdk1.7相比,jdk1.8中主要做了2方面的改进:
改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
改进二:将原来table数组+单链表的数据结构,改为table数组+单链表+红黑树结构。采用单链表方式,查询某个节点的时间复杂度为O(n),因此,对于个数超过8的列表,jdk1.8中采用了红黑树的结构,查询时间复杂度可以降低到O(logN)。
下面试jdk1.8中ConcurrentHashMap中的put操作源码:
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; // 如果table为空,初始化;否则,根据hash值计算得到数组索引i,如果tab[i]为空,直接新建节点Node即可。注:tab[i]实质为链表或者红黑树的首节点。 if (tab == null || (n = tab.length) == 0) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } // 如果tab[i]不为空并且hash值为MOVED,说明该链表正在进行transfer操作,返回扩容完成后的table。 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; // 针对首个节点进行加锁操作,而不是segment,进一步减少线程冲突 synchronized (f) { if (tabAt(tab, i) == f) { if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; // 如果在链表中找到值为key的节点e,直接设置e.val = value即可。 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } // 如果没有找到值为key的节点,直接新建Node并加入链表即可。 Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } // 如果首节点为TreeBin类型,说明为红黑树结构,执行putTreeVal操作。 else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { // 如果节点数>=8,那么转换链表结构为红黑树结构。 if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } // 计数增加1,有可能触发transfer操作(扩容)。 addCount(1L, binCount); return null; }
HashEntry 类的 value 域被声明为 Volatile 型,Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程“看”到。在 ConcurrentHashMap 中,不允许用 null作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象(对链表的结构性修改都可能会导致value为null),需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。
26、Java中的Copy-On-Write容器
Copy-On-Write基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后修改,这是一种延时懒惰加载。Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。
Copy-On-Write容器即写时复制的容器,当我们往一个容器中添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器,这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
CopyOnWriteArrayList的实现原理
在向ArrayList中添加元素,是需要加锁的,否则多线程写的时候回Copy出N个副本出来。
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList。
CopyOnWrite应用场景
用于读多写少的并发场景,比如白名单、黑名单,商品类目的访问和更新场景。