java摘要
1.ha shma p结构,ha shMa p的数组长度⼀定保持2的次幂,什么对象能作为ha shma p的
shm a
抽象类的使用k ey
HashMap的主⼲是⼀个Entry数组。Entry是HashMap的基本组成单元,每⼀个Entry包含⼀个key-value键值对。hashMap的数组长度⼀定保持2的次幂可以使新的数组索引和⽼数组索引⼀致,减少原来散列后数据的复制和移动,减少碰撞。不可变对象可以作为Key,可变对象可能造成数据丢失。
Bootstrap ClassLoader:启动类加载器,属于虚拟机的⼀部分,负责加载JAVA_HOME>\lib⽬录下并且被虚拟机识别的类库(细分的还Bootstrap ClassLoader:
Extension ClassLoader,加载lib\ext⽬录下的类库)
有Extension ClassLoader,
Application ClassLoader:
Application ClassLoader:系统类加载器,负责加载⽤户类路径(ClassPath)上指定的类库,开发者可以直接使⽤,如果没有⾃定义的类加载器,这个时程序中默认的类加载器。
类加载顺序:
1、先执⾏⽗类的静态代码块和静态变量初始化,并且静态代码块和静态变量的执⾏顺序只跟代码中出现的顺序有关。
2、执⾏⼦类的静态代码块和静态变量初始化。
3、执⾏⽗类的实例变量初始化
4、执⾏⽗类的构造函数
5、执⾏⼦类的实例变量初始化
6、执⾏⼦类的构造函数
7、静态⽅法与⾮静态⽅法只有被调⽤的时候才会被加载
2.ha shta ble,c o nc ur r entH a shMa p,ha shta ble⽐较
HashTable
安全,实现线程安全的⽅式是在修改数据时锁住整个HashTable,效率
不能为null,线程安全
底层数组+链表实现,⽆论key还是value都不能为null
低,ConcurrentHashMap做了相关优化
11,扩容:newsize = olesize*2+1
初始size为11
计算index的⽅法:index = (hash & 0x7FFFFFFF) % tab.length
HashMap
不安全
以存储null键和null值,线程不安全
底层数组+链表实现,可以存储null键和null值
16,扩容:newsize = oldsize*2,size⼀定为2的n次幂
初始size为16
扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插⼊
插⼊元素后才判断该不该扩容,有可能⽆效扩容(插⼊后如果扩容,如果没有再次插⼊,就会产⽣⽆效扩容)
当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
计算index⽅法:index = hash & (tab.length – 1)
ConcurrentHashMap
安全
底层采⽤分段的数组+链表实现,线程安全
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进⾏,其关键在于使⽤了锁分离技术
有些⽅法需要跨段,⽐如size()和containsValue(),它们可能需要锁定整个表⽽⽽不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,⼜按顺序释放所有段的锁
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进⾏扩容),插⼊前检测需不需要扩容,有效避免⽆效扩容
3.抽象类和接⼝区别
如果你拥有⼀些⽅法并且想让它们中的⼀些有默认实现,那么使⽤抽象类吧。
如果你想实现多重继承,那么你必须使⽤接⼝。由于Java不⽀持多继承,⼦类不能够继承多个类,但可以实现多个接⼝。因此你就可以使⽤接⼝来解决它。
如果基本功能在不断改变,那么就需要使⽤抽象类。如果不断改变基本功能并且使⽤接⼝,那么就需要改变所有实现了该接⼝的类。
抽象类可以有默认的⽅法实现,接⼝完全是抽象的。它根本不存在⽅法的实现。抽象⽅法可以继承⼀个类和实现多个接⼝ 接⼝只可以继承⼀个或多个其它接⼝。抽象类速度稍快。
4.String,Str ingBuilder,Str ingBuffer
4.Str ing,Str ingBuilder,StringBuffer
这三个类之间的区别主要是在两个⽅⾯,即运⾏速度和线程安全这两⽅⾯。⾸先说运⾏速度,或者说是执⾏速度,在这⽅⾯运⾏速度快慢为:StringBuilder > StringBuffer > String。String为字符串常量,⽽StringBuilder和StringBuffer均为字符串变量,即String对象⼀旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。在线程安全上,StringBuilder是线程不安全的,⽽StringBuffer是线程安全
的,String:适⽤于少量的字符串操作的情况,StringBuilder:适⽤于单线程下在字符缓冲区进⾏⼤量操作的情况,StringBuffer:适⽤多线程下在字符缓冲区进⾏⼤量操作的情况。
5.对象的深浅复制
在浅克隆中,如果原型对象的成员变量是值类型,将复制⼀份给克隆对象;如果原型对象的成员变量是引⽤类型,则将引⽤对象的地址复制⼀份给克隆对象,也就是说原型对象和克隆对象的成员变量指
向相同的内存地址。在深克隆中,⽆论原型对象的成员变量是值类型还是引⽤类型,都将复制⼀份给克隆对象,深克隆将原型对象的所有引⽤对象也复制⼀份给克隆对象。
6.w a it,sleep分别是谁的⽅法,区别
sleep()⽅法,属于Thread类中的。⽽wait()⽅法,则是属于Object类。虽然两者都是⽤来暂停当前运⾏的线程,但是 sleep() 实际上只是短暂停顿,因为它不会释放锁,⽽ wait() 意味着条件等待,这就是为什么该⽅法要释放锁,因为只有这样,其他等待的线程才能在满⾜条件时获取到该锁。
7.Co untDo w nLa tc h的a w a it⽅法是否安全,怎么改造
主线程必须在启动其他线程后⽴即调⽤CountDownLatch.await()⽅法。这样主线程的操作就会在这个⽅法上阻塞,直到其他线程完成各⾃的任务。其他N 个线程必须引⽤闭锁对象,因为他们需要通知CountDownLatch对象,他们已经完成了各⾃的任务。这种通知机制是通过untDown()⽅法来完成的;每调⽤⼀次这个⽅法,在构造函数中初始化的count值就减1。所以当N个线程都调 ⽤了这个⽅法,count的值等于0,然后主线程就能通过await()⽅法,恢复执⾏⾃⼰的任务。
主任务调⽤⼀次CountDownLatch实例的await()⽅法时,当前线程就会⼀直占⽤⼀个活动线程,如果多
次调⽤,那么就会⼀直占⽤多个活动线程,如果调⽤次数⼤于固定活动线程数,那么就可能造成阻塞队列中某些⼦任务⼀直不被执⾏。解决⽅法主任务不要和⼦任务放到同⼀线程池。
⼦任务有可以因为其它原因,不去执⾏CountDownLatch实例的countDown()⽅法,造成主任务所在线程⽆限等待。解决办法是最好不要⽤CountDownLatch实例的await(),归避长时间阻塞线程的风险,任何多线程应⽤程序都有死锁风险,改⽤CountDownLatch实例的await(long timeout, TimeUnit unit),设定超时时间,如果超时,将返回false,这样我们得知超时后,可以做异常处理。
9.a qs,c a s
AQS的全称是AbstractQueuedSynchronizer,即抽象队列同步器,其底层是volatile与CAS。 AQS最核⼼的数据结构是⼀个volatile int
state 和 ⼀个FIFO线程等待对列。state代表共享资源的数量,如果是互斥访问,⼀般设置为1,⽽如果是共享访问,可以设置为N(N为可共享线程的个数);⽽线程等待队列是⼀个双向链表,⽆法⽴即获得锁⽽进⼊阻塞状态的线程会加⼊队列的尾部。当然对state以及队列的操作都是采⽤了volatile + CAS + ⾃旋的操作⽅式,采⽤的是乐观锁的概念。
1、⾸先你要知道的是AQS的锁的类型是分为两种的:第⼀种是独占式锁的获取和释放(),第⼆中的是共享式的锁的释放和获取
2、第⼀步⾸先是⽤acquire(int arg)⽅法先拿到这个线程的共享资源的状态,这个状态变量是⽤volatile来修饰的,只有当获取到的state⼤于等于0时才表⽰获取锁成功(重⼊⼀次锁状态就加1,释放⼀次锁状态就减⼀),若果失败就把当前线程包装成⼀个node节点⼊队列(FIFO)
3、第⼆步就是第⼀步获取状态⼩于0的时候说明失败,然后调⽤的 addWaiter(Node mode)⽅法把该线程包装成⼀个节点,为了提搞性能,⾸先执⾏⼀次快速⼊队操作,即直接尝试将新节点加⼊队尾
4、然后再上⼀步的基础上,如果尝试把该节点加⼊队列尾部失败,这⾥⼜会去调⽤ enq(final Node node)⽅法,(1)先去判断这个队列是不是已经初始化了,如果初始化了就⽤CAS保证只有⼀个头结点可以初始化成功,如果没有初始化县初始化再CAS来保证只有⼀个线程节点创建成功,(2)最后在enq⽅法中进⾏⽆限次的⾃旋,直到,如果成功会直接使⽤CAS操作只能返回⼀个节点(compareAndSetTail(pred, node))
5、操作完了上⾯的操作之后,就相当于是线程节点已经成功的加⼊的等待队列中,然后进⾏的就是挂起当前线程,等待被唤醒。然后调⽤boolean acquireQueued(final Node node, int arg) ,先把锁的标记为默认为false,然后去判断该节点的前置结点是不是头结点,如果是把当前结点线程⽤setHead(node);设置为头结点(这⾥有个⼩坑,就是只有head头结点才是cpu正在执⾏的线程节点,后⾯的节点都是等待线程节点,⽽且在这个头结点执⾏的线程过程中头结点是可以中断的),如果设置头结点成功,就把锁标记为设置为true并返回,
6、然后再接着上⼀个步骤继续判断,如果没有获取锁成功,则进⼊挂起逻辑,也就是如果没有成功的话就进⼊下⼀个⽅法,node是当前线程的节点,pred是它的前置节点
boolean shouldParkAfterFailedAcquire(Node pred, Node node),在这个⽅法判断成功之后,会继续接着5的步骤把锁的标记设置为true,然后如果判断锁的标记是与否,否的话继续 cancelAcquire(node);
7、处理获取锁失败之后的挂起逻辑boolean shouldParkAfterFailedAcquire(Node pred, Node node)的⽅法,(1)前置节点的waitStatus 是Node.SIGNAL则返回true,然后会执⾏parkAndCheckInterrupt()⽅法进⾏挂起,(2)如果前置节点的waitStatus⼤于0的话,把当前结点赋给前置结点的下⼀个结点,如果不⼤于0 的话,使⽤CAS的compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 最后挂起的状态改为false,它是⽤来判断当前节点是否可以被挂起,也就是唤醒条件是否已经具备,即如果挂起了,那⼀定是可以由其他线程来唤醒的。该⽅法如果返回false,即挂起条件没有完备,那就会重新执⾏acquireQueued⽅法的循环体,进⾏重新判断,如果返回true,那就表⽰万事俱备
8、这⾥继续执⾏步骤5的 void cancelAcquire(Node node) ⽅法,拿到当前失败线程节点的等待状态是不是⼩于0,⼤于的话直接
Node.CANCELLED;赋值给正在遍历的线程节点的waitStatus 中,然后继续判断当前节点是不是尾节
点,是的话使⽤CAS操作compareAndSetNext(pred, predNext, null);把节点设置为空,若不是尾节点的话,当⼤于0的时候跳出循环,继续如果当前节点的后继节点没有被取消就把前置节点跟后置节点进⾏连接,相当于删除了当前节点compareAndSetNext(pred, predNext, next);
9、最后是释放锁,先去过去当前节点的waitStatus,然后如果waitStatus⼩于0尝试去释放锁使⽤compareAndSetWaitStatus(node, ws, 0)CAS操作,然后去判断如果当前线程节点的下⼀个节点,如果发现节点的waitStatus ⼩于0,就说明到了待唤醒的节点,然后不为空的时候,就去唤醒该节点。
CAS的全称是Compare and Swap,即⽐较并交换。⽐较的是当前内存中存储的值与预期原值,交换的是新值与内存中的值。这个操作是硬件层⾯的指令,因此能够保证原⼦性。CAS操作包含三个操作数——内存位置、预期原值和新值。在执⾏CAS操作时,先进⾏Compare操作,即⽐较内存位置的值与预期原值是否相等,若相等,则执⾏Swap操作将新值放⼊该内存位置。若不相等,则不进⾏Swap操作。
10.Thr ea
10.Threa dLo c a l原理,注意事项,参数传递
ThreadLocal的实例代表了⼀个线程局部的变量,每条线程都只能看到⾃⼰的值,并不会意识到其它的
线程中也存在该变量。它采⽤采⽤空间来换取时间的⽅式,解决多线程中相同变量的访问冲突问题。每个Thread的对象都有⼀个ThreadLocalMap,当创建⼀个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型。
ThreadLocal⽤于保存某个线程共享变量:对于同⼀个static ThreadLocal,不同线程只能从中get,set,remove⾃⼰的变量,⽽不会影响其他线程的变量。
1、: 获取ThreadLocal中当前线程共享变量的值。
2、ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。
3、ve: 移除ThreadLocal中当前线程共享变量的值。
4、ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调⽤remove⽅法后调⽤get⽅法,返回此⽅法值。
对于ThreadLocal使⽤前或者使⽤后⼀定要先remove,线程不销毁意味着上条线程set的ThreadLocal.ThreadLocalMap中的数据依然存在,那么在下⼀条线程重⽤这个Thread的时候,很可能get到的是上条线程set的数据⽽不是⾃⼰想要的内容。
11.Ja va的锁,内置锁,显⽰锁,各种容器及锁优化:锁消除,锁粗化,锁偏向,轻量级锁
公平锁/⾮公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁。⾮公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程⽐先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
可重⼊锁:可重⼊锁⼜名递归锁,是指在同⼀个线程在外层⽅法获取锁的时候,在进⼊内层⽅法会⾃动获取锁。
独享锁/共享锁:独享锁是指该锁⼀次只能被⼀个线程所持有。共享锁是指该锁可被多个线程所持有。
互斥锁/读写锁:上⾯讲的独享锁/共享锁就是⼀种⼴义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是
ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock。
乐观锁/悲观锁:乐观锁与悲观锁不是指具体的什么类型的锁,⽽是指看待并发同步的⾓度。悲观锁认为对于同⼀个数据的并发操作,⼀定是会发⽣修改的,哪怕没有修改,也会认为修改。因此对于同⼀个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作⼀定会出问题。乐观锁则认为对于同⼀个数据的并发操作,是不会发⽣修改的。在更新数据的时候,会采⽤尝试更新,不断
重新的⽅式更新数据。乐观的认为,不加锁的并发操作是没有事情的。悲观锁适合写操作⾮常多的场景,乐观锁适合读操作⾮常多的场景。
分段锁:分段锁其实是⼀种锁的设计,ConcurrentHashMap来说⼀下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有⼀个Entry数组,数组中的每个元素⼜是⼀个链表;同时⼜是⼀个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个hashmap进⾏加锁,⽽是先通过hashcode来知道他要放在那⼀个分段中,然后对这个分段进⾏加锁,所以当多线程put的时候,只要不是放在⼀个分段中,就实现了真正的并⾏的插⼊。
偏向锁/轻量级锁/重量级锁:偏向锁是指⼀段同步代码⼀直被⼀个线程所访问,那么该线程会⾃动获取锁。降低获取锁的代价。轻量级锁是指当锁是偏向锁的时候,被另⼀个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过⾃旋的形式尝试获取锁,不会阻塞,提⾼性能。重量级锁是指当锁为轻量级锁的时候,另⼀个线程虽然是⾃旋,但⾃旋不会⼀直持续下去,当⾃旋⼀定次数的时候,还没有获取到锁,就会进⼊阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进⼊阻塞,性能降低。
⾃旋锁:⾃旋锁是指尝试获取锁的线程不会⽴即阻塞,⽽是采⽤循环的⽅式去尝试获取锁,这样的好处是减少线程上下⽂切换的消耗,缺点是循环会消耗CPU。
锁粗化:为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是⼤某些情况下,⼀个程序对同⼀个锁不间断、⾼频地请求、
同步与释放,会消耗掉⼀定的系统资源,因为锁的讲求、同步与释放本⾝会带来性能损耗,这样⾼频的锁请求就反⽽不利于系统性能的优化了,虽
锁粗化就是告诉我们任何事情都有个度,有些情况下我们反⽽希望把很多次锁的请求合并成⼀个请求,然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反⽽希望把很多次锁的请求合并成⼀个请求,
以降低短时间内⼤量锁请求、同步、释放带来的性能损耗。
锁消除:锁消除是发⽣在编译器级别的⼀种锁优化⽅式。有时候我们写的代码完全不需要加锁,却执⾏了加锁操作。
锁消除:锁消除是发⽣在编译器级别的⼀种锁优化⽅式。
优化策略:1:减少锁持有时间 ;2减⼩锁粒度;3:锁分离 ,根据同步操作的性质,把锁划分为的读锁和写锁,读锁之间不互斥,提⾼了并发
性。 4:锁粗化 5:锁消除
Java中具有通过Synchronized实现的内置锁,和ReentrantLock实现的显⽰锁,内置锁获得锁和释放锁是隐式的,进⼊synchronized修饰的代
码就获得锁,⾛出相应的代码就释放锁。与Synchronized配套使⽤的通信⽅法通常有wait(),notify()。wait()⽅法会⽴即释放当前锁,并进⼊等待
状态,等待到相应的notify并重新获得锁过后才能继续执⾏;notify()不会⽴刻⽴刻释放锁,必须要等notify()所在线程执⾏完synchronized块中
的所有代码才会释放。ReentrantLock是显⽰锁,需要显⽰进⾏ lock 以及 unlock 操作。相⽐于Synchronized要复杂⼀些,⽽且⼀定要记得在
finally中释放锁⽽不是其他地⽅,这样才能保证即使出了异常也能释放锁。
java容器:1)Collection:⼀个独⽴元素的序列,这些元素都服从⼀条或者多条规则。 List必须按照插⼊的顺序保存元素,⽽set不能有java容器:1)Collection:⼀个独⽴元素的序列,这些元素都服从⼀条或者多条规则。
重复的元素。Queue按照排队规则来确定对象产⽣的顺序(通常与它们被插⼊的顺序相同)。
2)Map:⼀组成对的“键值对”对象,允许你使⽤键来查值。
1、List接⼝
List是有序的Collection,使⽤此接⼝能够精确的控制每个元素插⼊的位置。⽤户能够使⽤索引(元素在List中的位置,类似于数组下标)来访问
List中的元素,这类似于Java的数组。
实现List接⼝的常⽤类有LinkedList,ArrayList,Vector和Stack。
1)LinkedList类
LinkedList实现了List接⼝,允许null元素。此外LinkedList提供额外的get,remove,insert⽅法在 LinkedList的⾸部或尾部。这些操作使
LinkedList可被⽤作堆栈(stack),队列(queue)或双向队列(deque)。
注意:LinkedList没有同步⽅法。如果多个线程同时访问⼀个List,则必须⾃⼰实现访问同步。⼀种解决⽅法是在创建List时构造⼀个同步的
注意:LinkedList没有同步⽅法。
List:List
list = Collections.synchronizedList(new LinkedList(…));
2)
类
ArrayList类
ArrayList实现了可变⼤⼩的数组。它允许所有元素,包括null。ArrayList没有同步。size,isEmpty,get,set⽅法运⾏时间为常数。但是add
⽅法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的⽅法运⾏时间为线性。每个ArrayList实例都有⼀个容量(Capacity),即⽤于存
储元素的数组的⼤⼩。这个容量可随着不断添加新元素⽽⾃动增加,但是增长算法并 没有定义。当需要插⼊⼤量元素时,在插⼊前可以调⽤
ensureCapacity⽅法来增加ArrayList的容量以提⾼插⼊效率。
和LinkedList⼀样,ArrayList也是⾮同步的(unsynchronized)。⼀般情况下使⽤这两个就可以了,因为⾮同步,所以效率⽐较⾼。
如果涉及到堆栈,队列等操作,应该考虑⽤List,对于需要快速插⼊,删除元素,应该使⽤LinkedList,如果需要快速随机访问元素,
应该使⽤ArrayList。
3)Vector类
Vector⾮常类似ArrayList,但是Vector是同步的。由Vector创建的Iterator,虽然和ArrayList创建的 Iterator是同⼀接⼝,但是,因为
Vector是同步的,当⼀个 Iterator被创建⽽且正在被使⽤,另⼀个线程改变了Vector的状态(例 如,添加或删除了⼀些元素),这时调⽤
Iterator的⽅法时将抛出 ConcurrentModificationException,因此必须捕获该 异常。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论