Java基础
- 两句题外话,自己复习整理的知识点,准备发出来分享给大家。有不足之处还望不吝赐教,都是自己手打,难免有错别字,担待,谢谢各位!
JUC
1. 谈谈对volatile的理解
① 保证可见性
② 不保证原子性
③ 禁止指令重排序
④ volatile的原理和实现机制:观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。内存屏障
⑤ volatile修饰的变量如果被修改会被强制立即写入主存,但是当前线程被阻塞的时候可能会发生还没写入主存又被别的线程修改了,最后这个线程被唤醒继续写进去,就导致覆盖写,这就保证不了原子性。
2. 谈谈JMM
① 保证可见性
② 保证原子性
③ 禁止指令重排序
④ 是一套规则,用来屏蔽各个操作系统和硬件访问内存系统的差异。Java内存模型Java memory model 规定所有Java中的变量(主要指静态变量,常量等线程共享的变量,局部变量等私有变量一般存储在栈中,用过即销毁)都放在主存中,每个线程各有各自的工作内存,工作内存中的变量是主内存的副本,线程只能对工作内存的变量直接操作,线程间不能互相访问对方工作内存,只能依靠主内存通信。
3. 谈谈CAS原理
① CompareAndSet,比较并交换
② Unsafe类是它的核心类,在Unsafe类里面大部分都是native修饰的方法,用来操纵操作系统底层硬件资源。CAS实际是系统的一条原语,由几个命令组成。天生连续,不会被打断,没有线程安全问题。
③ 底层去修改的时候就是找到那个对象的内存地址然后获得它的当前值,然后自己操作一番加加减减,最后调用CAS的那个指令去比较,如果当前对象还是自己之前拿的那个值,就把操作过的值替换更新。如果变了,那就循环再获取,再操作,再比较,再交换。直到成功。其底层原语是Atomic::cmpxchg实现比较交换。
④ CAS缺点:循环时间长,不停的比较,浪费性能
4. 聊聊ArrayList 和HashMap相关问题
① ArrayList和Set都是线程不安全的,因为它的add()方法不是原子性的。
② java.util.ConcurrentModificationException(并发修改异常),是一个常发的线程异常。
③ 以下为解决方案:
④ 1.vector类是加锁(synchronized)的集合类线程安全,但是一般不用。
⑤ 2.使用Collections中的synchronizedList(List li),synchronizedSet(Set s),synchronized(Map m)方法
⑥ 3.使用CopyOnWriteArrayList(写时复制), 即:在添加元素的时候不直接添加在原来的list里面,而是先把之前的list复制一份过来,然后扩容一个位置,把需要添加的内容加入尾部,然后再将引用指向现在的list。(读写分离)。Set里面也有CopyOnWriteArraySet ,但是其实其底层也是CopyOnWriteArrayList,一套东西。
⑦ 对了,HashSet底层HashMap,LinkedHashSet底层LinkedHashMap。TreeSet底层。。。你懂的。
⑧ HashMap:
⑨ HashMap会在超过阈值时候扩容,有个负载因子。扩大的容量为旧容量的2倍(旧容量左移一位)。
⑩ HashMap为什么数组长度是2的幂? 当其是2的幂时,二进制表示低位全是1,key位置就是根据的hash值&操作来表示,因为kys的hash值是1的地方才为1,不是1的地方为0. 如果数组的长度不是2的幂,那其二进制表示肯定有0,就导致和数据的key的hash值不是强相关的了,导致两个key虽然hash值不一样,但是位置一样,导致数组的某些位置非常拥挤,某些位置永远用不到。
⑪ hashmap扩容时每个entry需要再计算一次hash吗?jdk1.7的时候会,jdk1.8中如果该桶没有链表只有一个元素,则和jdk1.7一样直接计算下标;如果该元素是一个树节点则使用split()方法进行拆分。如果该桶有链表则采用如下算法拆分,因为扩容容量直接是原来2倍,也是最低位多了个1,所以元素的hash如果之前那个位置是1 做&运算的时候就移动到之前两倍的位置上,如果之前是0,就不动。
因此,我们在扩容的时候不需要像jdk1.7那样重新hash,只需要看看原hash值新增的那个bit位是1还是0就好了,是0的话索引没有变,是1的话索引变成原索引+oldCap(旧数组的大小)下图为reSize()示意图:
⑬ 为啥到8转换红黑树,为啥是8?第一,转换后查的快,第二由于TreeNodes占用空间是普通Nodes的两倍,所以只有当bin包含足够多的节点时才会转成TreeNodes。理想情况下用不到,超过8的概率很小很小,几乎不可能。
⑭ 为啥hashmap在get()的时候会发生死循环? 主要原因还是因为它在扩容的时候导致头尾相连。因为扩容使用头插法,原本1-》2-》3,变成3-》2-》1,但是并发操作的时候,容易导致1-》2-》1,丢数据+死循环。
⑮ ConcurrentHashMap 是一个并发散列映射表,它允许完全并发的读取,并且支持给定数量的并发更新。而HashTable和同步包装器包装的 HashMap,使用一个全局的锁来同步不同线程间的并发访问,同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器,这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。
⑯ ConcurrentHashMap 是Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
⑰ Hashtable的任何操作都会把整个表锁住,是阻塞的。好处是总能获取最实时的更新,比如说线程A调用putAll写入大量数据,期间线程B调用get,线程B就会被阻塞,直到线程A完成putAll,因此线程B肯定能获取到线程A写入的完整数据。坏处是所有调用都要排队,效率较低。
⑱ ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据(Segment,默认是16个,实际上在其内部就是一个个的hashtable),但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。应该根据具体的应用场景选择合适的HashMap。
⑲ HashMap使用数组加链表的形式存储(这是java7,在Java8里)。链表里是一个个的Entry实体,Entry实体里有一个next属性,可以指向下一个Entry类的实体。链表来实现对数据的存储,数组寻址快,插入删除慢。链表正好反过来,所以HashMap是这俩数据结构的集大成者,很优秀。
5. 可重入锁
① 可重入就是加锁进去后,当前线程在用的资源即使也是加锁的,也能进去。举个例子,一个方法加锁,进去处理东西,结果碰到另一个方法也加锁,遇到这种情况,除非你把之前的锁释放掉,否则进不去。但是你又不能释放,因为你得保证原子性,这就必须用到可重入锁了。能进去的叫可重入锁,不能进去的叫不可重入锁。
② 它的底层和不可重入锁的区别就是,不可重入锁只判断当前资源是不是被锁上了,被锁上了,其他线程只能等待。可重入锁不一样,它会判断资源是否被锁上了?谁锁上的?根据这个判断是不是可重入。如果是当前线程锁上的,则可重入,但是锁的数量+1,相应解锁的时候也得有对应的解锁数量。
③ 可重入锁解决了死锁问题,说的就是解决了那种不可重入锁的死锁问题。当前线程访问临界资源上锁,结果这个资源内部要去访问一个内容也需要加锁,这俩就互相等,造成死锁。里面的锁等外面的锁释放,外面的锁等里面的业务处理完,没完没了,无穷尽也,是为死锁。
6. 自旋锁
① 自旋锁就是当一个线程碰到一个锁资源被占用时不会立即挂起进入阻塞状态,而是循环调用的来访问,等待锁资源释放。这么做的好处有:节省系统资源,不用挂起线程来回切换导致的系统资源消耗。坏处是:如果锁资源一直得不到是释放,那么等待的线程就在一直循环,造成CPU消耗严重。联想一下CAS。
② 手写自旋锁
7. 公平锁和非公平锁
① 公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;
② 非公平锁:新的线程来了,上去就抢第一个人的位置,抢不到就排队,抢到就直接进去。
③ 非公平锁导致反转和饥饿现象。有的线程虽然先来,但是可能被新来的抢占,有的线程可能一直抢不过,就处于饥饿状态了。
8. 其他的锁(独占锁,共享锁/互斥锁)
① 独占锁:一个线程独占锁,synchronized,Reentrantlock这都是独占。
② 读写锁,读锁是共享锁,写锁是互斥锁。
9. 有没有用过CountDownLatch/CyclicBarrier/Semaphore?
① CountDownLatch(int i)调用await()方法的线程会阻塞,调用countDown(),i会逐次递减,在减到0之后会唤醒之前的调用await()方法的线程继续执行,不可重复利用,一句话解释:所有人到齐之后主线程才继续往下走,没到齐,继续等。
② CountDownLatch可以配合枚举enum来使用。达到一种类似数据库的效果。
③ CyclicBarrier 和CountDownLatch正好相反,它是加到一定的数量才会放开线程的阻塞,继续进行下一步。先来的线程被阻塞,直到所有线程到齐,用来把线程同步,可重复利用,一句话解释:CyclicBarrier是操作的其他线程,主线程不会等在哪里,早跑完了
④ Semaphore是 synchronized 的加强版,作用是控制线程的并发数量。Semaphore semaphore = new Semaphore(1);参数为1的时候共享资源互斥。多个线程抢多份资源。
⑤ Semaphore 能控制多个线程抢占资源,举个例子,停车场停车,车位有限,但是车子较多,就可以使用Semaphore来控制车位,有车位的时候就让车子吧停进去,有车子走了,车位空出来也可以让新的车辆停进去。
10. 阻塞队列?
① 当阻塞队列为空时,从队列取元素的操作将被阻塞。
② 当阻塞队列为满时,往队列里添加元素的操作将被阻塞。
③ 使用阻塞线程可以不用管线程的挂起和唤醒,阻塞队列一手包办。
④ ArrayBlockingQueue:有数组组成的有界队列。
⑤ LinkedBlockingQueue:由链表组成的有界(最大值integer.MaxValue)队列,最大值太大,和*差不多。
⑥ SynchronousQueue,专属队列,不存储元素,每个put()操作都得等待一个take()操作。有一个就取走一个,取走一个就生产一个,从来不多也不少。