面试准备-八股【面试准备】

  • Java基础
  • 解决hash冲突的方法
  • try catch finally
  • Exception与Error的包结构
  • OOM你遇到过哪些情况,SOF你遇到过哪些情况
  • 线程有哪些基本状态?
  • Java IO与 NIO的区别
  • 堆和栈的区别
  • 对象分配规则
  • notify()和notifyAll()有什么区别?
  • sleep()和wait() 有什么区别?
  • 为什么wait, notify 和 notifyAll这些方法不在thread类里面?
  • Java中synchronized 和 ReentrantLock 有什么不同?
  • 常用的线程池有哪些?
  • 简述一下你对线程池的理解
  • 反射
  • Java集合
  • ArrayList
  • get查找
  • set修改
  • add添加
  • remove删除
  • elementData 下标获取
  • grow扩容
  • LinkedList
  • get查找
  • set修改
  • node通过下标获取结点
  • add添加
  • remove删除
  • linkBefore前插
  • unlink移除
  • ArrayList和LinkedList对比
  • HashMap
  • get查找
  • remove
  • put插入|修改
  • resize扩容
  • AQS
  • CopyOnWriteArrayList
  • ConcurrentHashMap
  • JUC
  • 多线程
  • Java语言中的线程安全
  • 线程安全的实现方法
  • 线程的创建
  • 线程的状态
  • wait和sleep的区别
  • ThreadLocal
  • synchronize的优化
  • synchronize和Reentrant的对比
  • AQS
  • 线程池
  • ThreadPoolExecutor
  • ThreadPoolExecutor源码
  • ConcurrentHashMap
  • CopyOnWriteArrayList
  • JVM


2024-3-15 16:03:31

公开发布于
2024-5-24 13:23:43

Java基础

解决hash冲突的方法

数据结构中介绍的两种方法:

第八章 查找【数据结构】:8.4.2处理冲突的方法

1.开放定址法
开放定址法也被称为“再散列法”。其基本思想是,当关键字key的初始散列地址h0=H(key)出现冲突时,以h0为基础查找下一个地址h1,如果h1仍然冲突,再以h0为基础,产生另一个散列地址h2…直到找出一个不冲突的地址hi,将相应元素存入其中。这种方法有一个通用的再散列函数形式:
hi=(H(key)+di)%m i=1,2,…,n
其中,H(key)为哈希函数,h,=H(key),m为表长,di为增量序列。增量序列的取值方式不同,对应有不同的再散列方式,主要有以下三种。
(1)线性探测再散列
di=c×i 最简单的情况c=1
这种方法的特点是,冲突发生时,顺序查看表中下一个单元,直到找到一个空单元或查遍全表。值得注意的是,由于这里使用的是%运算,因而整个表成为一个首尾连接的循环表,在查找时类似于循环队列,表尾的后边是表头,表头的前边是表尾。

(2)二次探测再散列
di=12,-12,22,-22,…k2,-k2(k≤m/2)
这种方法的特点是,冲突发生时分别在表的右、左进行跳跃式探测,较为灵话,不易产生聚集,但缺点是不能探查到整个散列地址空间。

(3)随机探测再散列
di=伪随机数
这种方法需要建立一个随机数发生器,并给定一个随机数作为起始点。

2.链地址法
链地址法解决冲突的基本思想是,把所有具有地址冲突的关键字链在同一个单链表中: 若哈希表的长度为m,则可将哈希表定义为一个由m个头指针组成的指针数组。散列地址为i的记录均插入以指针数组第i个单元为头指针的单链表。
应用:HashMap

Java面试复习发现有第3种方法

3.再哈希法
再哈希:又叫双哈希法,有多个不同的Hash函数.当发生冲突时,使用第二个,第三个….等哈希函数计算地址,直到无冲突
应用:布隆过滤器

网上又搜了一下发现有第4种方法
hash冲突的4种解决方案

4.建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

try catch finally

finally语句包含return的情况

//下面是个测试程序
public class FinallyTest  
{
	public static void main(String[] args) {
		 
		System.out.println(new FinallyTest().test());;
	}

	static int test()
	{
		int x = 1;
		try
		{
			x++;
			return x;
		}
		finally
		{
			++x;
		}
	}
}
结果是2。
分析:
	在try语句中,在执行return语句时,要返回的结果已经准备好了,就在此时,程序转到finally执行了。
在转去之前,try中先把要返回的结果存放到不同于x的局部变量中去,执行完finally之后,在从中取出返回结果,
因此,即使finally中对变量x进行了改变,但是不会影响返回结果。
它应该使用栈保存返回值。

Exception与Error的包结构

Java可抛出(Throwable)的结构分为三种类型:被检查的异常(CheckedException),运行时异常(RuntimeException),错误(Error)。

1、运行时异常
定义:RuntimeException及其子类都被称为运行时异常。

特点:Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。

例如:除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fast机制产生的ConcurrentModificationException异常

常见的五种运行时异常:
ClassCastException(类转换异常)
IndexOutOfBoundsException(数组越界)
NullPointerException(空指针异常)
ArrayStoreException(数据存储异常,操作数组时类型不一致)
BufferOverflowException(文件流Buffer需要clear)

Redis中出现过ClassCastException

2、被检查异常
定义:Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异常。
特点 : Java编译器会检查它。 此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。

例如,CloneNotSupportedException就属于被检查异常。当通过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出CloneNotSupportedException异常。被检查异常通常都是可以恢复的。

如:
IOException
FileNotFoundException
SQLException

被检查的异常适用于那些不是因程序引起的错误情况,比如:读取文件时文件不存在引发的FileNotFoundException 。
然而,不被检查的异常通常都是由于糟糕的编程引起的,比如:在对象引用时没有确保对象非空而引起的 NullPointerException 。

3、错误
定义 : Error类及其子类。
特点 : 和运行时异常一样,编译器也不会对错误进行检查。
当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这些错误的。例如,VirtualMachineError就属于错误。出现这种错误会导致程序终止运行。
OutOfMemoryError、ThreadDeath。
Java虚拟机规范规定JVM的内存分为了好几块,比如堆,栈,程序计数器,方法区等

我有过内存崩溃,然后虚拟机启动不了
因为我电脑只有8G内存,然后扩展了虚拟内存

OOM你遇到过哪些情况,SOF你遇到过哪些情况

OOM:

1,OutOfMemoryError异常
除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。

Java Heap 溢出:
一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess。java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。

出现这种异常,一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

如果是内存泄漏,可进一步通过工具查看泄漏对象到GCRoots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。

如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。

2,虚拟机栈和本地方法栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
意思是没有到达最大深度,但是因内存空间不足二申请不到了

这里需要注意当栈的大小越大可分配的线程数就越少。

3,运行时常量池溢出
异常信息:java.lang.OutOfMemoryError:PermGenspace

如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。

4,方法区溢出
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置。

异常信息:java.lang.OutOfMemoryError:PermGenspace

方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。

SOF(堆栈溢出StackOverflow):
StackOverflowError 的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。

因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。

栈溢出的原因:递归调用,大量循环或死循环,全局变量是否过多,数组、List、map数据过大。

线程有哪些基本状态?

面试准备-八股【面试准备】_Java


操作系统隐藏 Java虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。

Java IO与 NIO的区别

推荐阅读:
什么是NIO?NIO的原理是什么机制?

堆和栈的区别

栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;
堆是存储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在区域不连续,会有碎片。

1、功能不同
栈内存用来存储局部变量和方法调用,而堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。

2、共享性不同
Young Generation 即图中的Eden + From Space(s0) + To Space(s1)
Eden 存放新生的对象
Survivor Space 有两个,存放每次垃圾回收后存活的对象(s0+s1)
Old Generation Tenured Generation 即图中的Old Space
主要存放应用程序中生命周期长的存活对象

栈内存是线程私有的。
堆内存是所有线程共有的。

3、异常错误不同
如果栈内存或者堆内存不足都会抛出异常。
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.lang.OutOfMemoryError。

4、空间大小
栈的空间大小远远小于堆的

对象分配规则

  • 对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
  • 动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
  • 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。

notify()和notifyAll()有什么区别?

notify可能会导致死锁,而notifyAll则不会

任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行synchronized 中的代码

使用notifyall,可以唤醒所有处于wait状态的线程,使其重新进入锁的争夺队列中,而notify只能唤醒一个

举例:如果是生产者-消费者模型,生产者.notify了一个生产者,就会产生死锁

sleep()和wait() 有什么区别?

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。

当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,获取对象锁进入运行状态。

为什么wait, notify 和 notifyAll这些方法不在thread类里面?

明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需
要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在
等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们
定义在Object类中因为锁属于对象。

Java中synchronized 和 ReentrantLock 有什么不同?

相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一
个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行
线程阻塞和唤醒的代价是比较高的.

区别:

这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需
要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配
合try/finally语句块来完成。

Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指
令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经
拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计
算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释
放为止。

由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,
ReentrantLock类提供了一些高级功能,主要有以下3项:
1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于
Synchronized来说可以避免出现死锁的情况。
2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,
ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性
能不是很好。
3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。

常用的线程池有哪些?

newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。
newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

简述一下你对线程池的理解

(如果问到了这样的问题,可以展开的说一下线程池如何用、线程池的好处、线程池的启动策略)合理利用线程池能够带来三个好处。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

反射

在ArrayList中的构造器中

有个构造包含指定集合的元素的列表

使用到getClass()方法

面试准备-八股【面试准备】_职场和发展_02

Java集合

ArrayList

get查找

public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

set修改

public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

add添加

public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

remove删除

public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

elementData 下标获取

E elementData(int index) {
        return (E) elementData[index];
    }

grow扩容

计算所需的最小长度

例如:add(),minCapacity=size+1

grow():

得到旧容量:oldCapacity=elementData.length;
计算新容量:newCapacity=oldCapacity + (oldCapacity >> 1);

如果新容量<所需最小长度
新容量=最小长度

如果minCapacity大于MAX_ARRAY_SIZE,
则新容量则为Interger.MAX_VALUE,
否则,新容量大小则为 MAX_ARRAY_SIZE。

elementData=Arrays.copyof(elementData,newCapacity)

测试代码:
测试1:默认构造器

private static void extracted1() throws NoSuchFieldException {
        //测试默认构造器第一次add,扩容机制
        ArrayList<Integer> arrayList=new ArrayList<>();
        //this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

        //第一次强制进入Integer.valueOf()
        //第二次强制进入ArrayList.add()
//        arrayList.add(1);
        //直接进入add()
        arrayList.add(new Integer(1));
        /*
            add(E e){
                ensureCapacityInternal(size + 1);  // Increments modCount!!{
                    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);//10
                    }
                    ensureExplicitCapacity(minCapacity=10);{
                        modCount++;
                        // overflow-conscious code
                        if (minCapacity - elementData.length > 0)
                            grow(minCapacity=10);{
                                int oldCapacity = elementData.length;//0
                                int newCapacity = oldCapacity + (oldCapacity >> 1);//0
                                if (newCapacity - minCapacity < 0)
                                    newCapacity = minCapacity;//10
                                if (newCapacity - MAX_ARRAY_SIZE > 0)
                                    newCapacity = hugeCapacity(minCapacity);
                                // minCapacity is usually close to size, so this is a win:
                                elementData = Arrays.copyOf(elementData, newCapacity);//10
                            }
                    }
                }
                elementData[size++] = e;
            }
         */

    }

测试2:传入initialCapacity=0

private static void extracted2() {
        //        ArrayList<Integer> arrayList0=new ArrayList<>(-1);
        //  throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);


        ArrayList<Integer> arrayList = new ArrayList<>(0);
        // this.elementData = EMPTY_ELEMENTDATA;
        arrayList.add(new Integer(1));
        /*
            add(E e){
                ensureCapacityInternal(minCapacity=size+1){
                    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
                    }
                    ensureExplicitCapacity(minCapacity);{
                        modCount++;

                        // overflow-conscious code
                        if (minCapacity - elementData.length > 0)
                            grow(minCapacity);{
                                int oldCapacity = elementData.length;
                                int newCapacity = oldCapacity + (oldCapacity >> 1);
                                if (newCapacity - minCapacity < 0)
                                    newCapacity = minCapacity;
                                if (newCapacity - MAX_ARRAY_SIZE > 0)
                                    newCapacity = hugeCapacity(minCapacity);
                                // minCapacity is usually close to size, so this is a win:
                                elementData = Arrays.copyOf(elementData, newCapacity);
                            }
                    }
                }
                elementData[size++] = e;
            }
        */

    }

测试3:传入initialCapacity=1

private static void extracted3() {
        ArrayList<Integer> arrayList1=new ArrayList<>(1);
        // this.elementData = new Object[initialCapacity];
        arrayList1.add(new Integer(1));
        arrayList1.add(new Integer(2));
        // elementData = Arrays.copyOf(elementData, newCapacity);//传入newCapacity=2
    }

LinkedList

get查找

public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

set修改

public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

node通过下标获取结点

Node<E> node(int index) {
        // assert isElementIndex(index);
		//如果小于一半,使用next
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {//如果大于一半,使用prev
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }

add添加

public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

remove删除

public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

linkBefore前插

void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

unlink移除

E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;
		
		//更改next指针
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }
        
		//更改prev指针
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

ArrayList和LinkedList对比

数组和链表的区别

数组:查找更新快(下标获取),插入删除慢(复制数组)
链表:查找更新慢(从前往后,或从后往前),插入删除快(两个指针前驱和后继的操作)

//ArrayList
查找更新---下标获取
get---elementData[index]
set---elementData[index]
插入删除(复制数组)
add---arraycopy
remove---arraycopy
//LinkedList
查找更新----从前到后或从后到前来查
get---node
set---node
插入删除----两个指针前驱和后继操作
add---linkBefore
remove--unlink

HashMap

get查找

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

//---------------------------------------------------------
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //先检查数组是否为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //再检查头部
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //再检查后续结点
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

remove

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
//-----------------------------------------------------------------------------------
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //判断数组是否为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            //先找到,放到node
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //删除node
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

put插入|修改

面试准备-八股【面试准备】_对象锁_03

put

putVal

如果table为空或长度为0 resize()扩容
根据hash&len-1找到槽位
如果为空,直接插入
否则,hash冲突
判断第一个元素是否相等,更新
否则,得到node结点
	判断是否是红黑树TreeNode,红黑树插入
	遍历链表,相等更新,否则插入到最后

【Java面试】说一下HashMap的put方法

1.7的put

  1. 我们的put方法会去判断这个hashmap是否为null或者长度是否为0,如果是则调用inflateTable()方法对该hashmap数组初始化;
  2. 判断key是否为null,为null则遍历以table[0]为首的链表,寻找是否存在key==null对应的键值对,若存在,则覆盖旧value;同时返回旧的value值,否则调用addEntry()完成插入;
  3. key不为null.根据key计算数组索引值,循环链表,判断该key是否存在,存在,则覆盖旧value,并返回旧value;
  4. addEntry()先判断是否需要扩容,如果要扩容就进行扩容,如果不用扩容就生成Entry对象,并使用头插法添加到当前位置的链表中;

1.8的put

  1. 我们的put方法会去判断这个hashmap是否为null或者长度是否为0,如果是则对该hashmap数组进行resize()扩容;
  2. put方法根据key通过hash算法与运算得到数组下标;
  3. 如果数组下标元素为空,则将key和value封装成Node<K,V>并放入该位置
  4. 如果数组下标元素不为空:
  • a.判断数组上该位置key是否相同,相同则执行覆盖操作,返回老的value值
  • b.不相同,则判断该节点上Node的类型,判断是是红黑树或者是链表.
  • i.如果是红黑树Node,则将key和value封装为一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value
  • ii.如果是链表节点,则将key和value封装为一个链表Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插入到链表中,插入到链表后,会看当前链表的节点个数,如果大于等于8,(数组长度大于64)那么则会将该链表转成红黑树

面试准备-八股【面试准备】_职场和发展_04


面试准备-八股【面试准备】_Java_05


面试准备-八股【面试准备】_线程池_06


面试准备-八股【面试准备】_职场和发展_07


面试准备-八股【面试准备】_线程池_08

resize扩容

resize()

两个作用

初始化数组,扩容

/**
 *   resize() 有两个模式,1.初始化数组,2.扩容。
 *   整体氛围两大步骤:
 *   1)计算新数组大小,然后创建新数组
 *   初始化 指定容量构造,newCap=threshold
 *   未指定容量构造,newCap=16 扩容:newCap=oldCap * 2
 *
 *   2) 执行扩容策略:逐个遍历所有槽点,根据具体情况进行迁移
 *   情况一:当前槽点只有一个node,直接重新分配到新数组即可newTab[e.hash & (newCap - 1)]
 *   情况二:当前槽点下是红黑树
 *   情况三:当前槽点下是链表:因为链表中所有node的hash和key相同,而现在数组扩容了两倍,所以现在的想法是将当前链等分成两部分
 *   怎么等分成两部分?分成high链与low链,两链关系可近似理解为单双数节点  (e.hash & oldCap) == 0
 *   怎么实现?hiHead、loHead 用来标识高低链,hiTail、loTail用来尾插新节点
 *   两链放在新数组哪里?:low 链置于newTab[ j ],high 链置于newTab[ j + oldCap ]( j 表示在原来数组位置)
 */

当size大于threshold
执行模式有三种:初始化 扩容
扩容策略有三种:单点 红黑树 链表

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // threshold > 0 有两种情况
        // 1.指定容量的初始化值,此时 oldCap = 0
        // 2.扩容阈值,此时 oldCap != 0
        int oldThr = threshold;
        int newCap, newThr = 0;
    	// 模式一:oldCap>0表示要扩容
        if (oldCap > 0) {
            // 当老数组容量已达最大值时无法再扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // Cap * 2 , Thr * 2
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
    	// 模式二:初始化,且已指定初始化容量
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
    	// 模式三:初始化,未指定初始化容量
        else {
            // 以默认初始化容量16进行初始化
            newCap = DEFAULT_INITIAL_CAPACITY;
            // 16 * 0.75 = 12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
    	// 用指定容量初始化后,更新其扩容阈值
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        // 更新扩容阈值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
    	// 执行初始化,建立新数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    	// 将新数组附给table
        table = newTab;

    	//-------------------------------扩容策略-----------------------------------------------
    	// 若是执行的初始化,old=null,就不用走这里的扩容策略了,而是直接返回赋值去了
        if (oldTab != null) {
            // 遍历原数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                // 当原数组的当前槽点有值时,将其赋给e
                if ((e = oldTab[j]) != null) {
                    // 释放原数组当前节点内存,帮助GC
                    oldTab[j] = null;
                    // 情况一:若当前槽点只有一个值,直接找到新数组相应位置并赋值
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    // 情况二:若当前节点下是红黑树
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//当做链表处理,最后判断是否退化链表 小于6 否则树化
                    // 情况三:当前节点下是链表
                    else { // preserve order
                        // 将该链表分成两条链,low低位链 和 high高位链
                        // Head标识链头,Tail用来连接
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        // do while((e = next) != null)
                        do {
                            next = e.next;
                            // 低位链连接
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 高位链连接
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 将低位链放置于老链同一下标
                        // eg.原来当前节点位于oldTab[2],则现在低位链还置于newTab[2]
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // 将高位链放置于老链下标+oldCap
                        // eg.原来当前节点位于oldTab[2],则现在高位链置于newTab[2+8=10]
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

AQS

说说你对AQS的理解

它是抽象的队列同步器,

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配

AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。

AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

状态信息通过 protected 类型的 getState , setState , compareAndSetState 进行操作

模板方法模式实现架构

isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;
正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

面试准备-八股【面试准备】_对象锁_09

以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独
占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即
释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的
(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state
是能回到零态的。

再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个
数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会
CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然
后主调用线程就会从 await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现 tryAcquiretryRelease 、 tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器同
时实现独占和共享两种方式,如 ReentrantReadWriteLock 。

Semaphore与CountDownLatch一样,也是共享锁的一种实现。它默认构造AQS的state为
permits。当执行任务的线程数量超出permits,那么多余的线程将会被放入阻塞队列Park,并自旋判断
state是否大于0。只有当state大于0的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执
行release方法,release方法使得state的变量会加1,那么自旋的线程便会判断成功。
如此,每次只有最多不超过permits数量的线程能自旋成功,便限制了执行任务线程的数量。

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到
达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障
拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties) ,其参数
表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前
线程被阻塞。
再来看一下它的构造函数:
其中,parties 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所
有线程通过。

总结:CyclicBarrier 内部通过一个 count 变量作为计数器,cout 的初始值为 parties 属性的初始化
值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后
一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。

高频面试题-Java 并发【java面试】

ReentrantReadWtiteLock
实现
state使用高低位区分读锁 写锁的状态

//因为读锁和写锁使用的是同一个Sync,但是只有一个state,所以应该如何区分
		//高16位存读锁 低16位存写锁
		//统计读锁的个数 无符号右移16位    取出state高16位
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        //统计写锁的个数 按位与16个1 		取出state低16位
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
static final int SHARED_SHIFT   = 16;  //16位划分读锁和写锁的状态
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT); //读锁的单位 状态+-
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;//锁最大的数量
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//取写锁的数量

ReentrantLock的特性:
绑定多个条件:newCondition

举例:ArrayBlockingQueue源码
目标:生产者通知消费者,消费者通知生产者。
避免:生产者通知生产者,消费者通知消费者。

//两个队列 两个Condition 
    private final Condition notEmpty;
    private final Condition notFull;

如果是传统的synchronize,必须signalAll()

2023-7-26 09:27:15

CopyOnWriteArrayList

读读
读写使用COW
写写使用锁

读不加锁
写加锁,并且CopyOnWrite进行复制到新数组,替换引用,
底层数组是volatile修饰,触发volatile语义,使得其他多的线程立即可见


高频面试题-Java 集合【java面试】:01 / CopyOnWriteArrayList

对于CopyOnWriteArrayList集合,正如它的名字所暗示的,它采用复制底层数组的方式来实现写操作。当线程对CopyOnWriteArrayList集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。**当线程对CopyOnWriteArrayList集合执行写入操作时,该集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。**由于对CopyOnWriteArrayList集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。

需要指出的是,由于CopyOnWriteArrayList执行写入操作时需要频繁地复制数组,性能比较差,但由于读操作与写操作不是操作同一个数组,而且读操作也不需要加锁,因此读操作就很快、很安全。由此可见,CopyOnWriteArrayList适合用在读取操作远远大于写入操作的场景中,例如缓存等。

COW技术的举例:
Redis的hash,渐进式rehash
Redis的RDB持久化:bgsave
子进程在写RDB文件时,父进程COW修改数据页

写时复制技术
改数据在复制数组中
读数据在旧数组中

读多写少场景:避免老是复制数组

读写是通过COW规避
读快照,写新数组

写写是通过加锁规避

package java.util.concurrent;

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

	//可重入锁
    final transient ReentrantLock lock = new ReentrantLock();

    //底层是[]
    private transient volatile Object[] array;

    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));//复制的数组
    }

    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }
//--------------------------------------------------------------------------------  
	//读的时候不加锁
    public E get(int index) {
        return get(getArray(), index);
    }

    final Object[] getArray() {
        return array;
    }

//--------------------------------------------------------------------------------  
	//写的时候加锁
	//加锁为了拷贝数组
	//之后添加数据
	//最后修改引用 触发volatile
    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();
        }
    }
//--------------------------------------------------------------------------------  
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }

    static final class COWIterator<E> implements ListIterator<E> {
        /** Snapshot of the array */
        private final Object[] snapshot;//快照 迭代时
        /** Index of element to be returned by subsequent call to next.  */
        private int cursor;

        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }

        public boolean hasNext() {
            return cursor < snapshot.length;
        }

        public boolean hasPrevious() {
            return cursor > 0;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

        @SuppressWarnings("unchecked")
        public E previous() {
            if (! hasPrevious())
                throw new NoSuchElementException();
            return (E) snapshot[--cursor];
        }

        public int nextIndex() {
            return cursor;
        }

        public int previousIndex() {
            return cursor-1;
        }

        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; {@code remove}
         *         is not supported by this iterator.
         */
        public void remove() {
            throw new UnsupportedOperationException();
        }

        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; {@code set}
         *         is not supported by this iterator.
         */
        public void set(E e) {
            throw new UnsupportedOperationException();
        }

        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; {@code add}
         *         is not supported by this iterator.
         */
        public void add(E e) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void forEachRemaining(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            Object[] elements = snapshot;
            final int size = elements.length;
            for (int i = cursor; i < size; i++) {
                @SuppressWarnings("unchecked") E e = (E) elements[i];
                action.accept(e);
            }
            cursor = size;
        }
    }


}

ConcurrentHashMap

1.7是一个HashMap拆为几个子HashMap,称为一个段
并发的就是初始的段的数量

1.8引入红黑树
每一个数组槽位是一个单位


高频面试题-Java 集合【java面试】:02 / ConcurrentHashMap

JDK 7中的实现方式:
为了提高并发度,在JDK7中,一个HashMap被拆分为多个子HashMap。每一个子HashMap称作一个Segment,多个线程操作多个Segment相互独立。在JDK 7中的分段锁,有三个好处:

  1. 减少Hash冲突,避免一个槽里有太多元素。
  2. 提高读和写的并发度,段与段之间相互独立。
  3. 提高了扩容的并发度,它不是整个ConcurrentHashMap一起扩容,而是每个Segment独立扩容。

分而治之思想

面试准备-八股【面试准备】_职场和发展_10

  1. 使用红黑树,当一个槽里有很多元素时,其查询和更新速度会比链表快很多,Hash冲突的问题由此得到较好的解决。
  2. 加锁的粒度,并非整个ConcurrentHashMap,而是对每个头节点分别加锁,即并发度,就是Node数组的长度,初始长度为16,和在JDK 7中初始Segment的个数相同。
  3. 并发扩容,这是难度最大的。在JDK 7中,一旦Segment的个数在初始化的时候确立,不能再更改,并发度被固定。之后只是在每个Segment内部扩容,这意味着每个Segment独立扩容,互不影响,不存在并发扩容的问题。但在JDK 8中,相当于只有1个Segment,当一个线程要扩容Node数组的时候,其他线程还要读写,因此处理过程很复杂。

面试准备-八股【面试准备】_线程池_11

初始化时
容量 cap=tableSizeFor(传入参数的1.5倍+1)
sizeCtl = cap;

public ConcurrentHashMap(int initialCapacity) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));//1.5倍 2的n次方
    this.sizeCtl = cap;
}

sizeCtl的两个作用
调整控件
扩容的阈值:sizeCtl = (n << 1) - (n >>> 1)

//控制信号量
//阈值
/**
*表初始化和调整大小控件。
当为负值时表正在初始化或调整大小:
	-1用于初始化,
	else-(1+活动的调整大小线程的数量)。
否则
*	当table为null时,保留要使用的初始表大小创建,或默认为0。
初始化后,保持用于调整表大小的下一个元素计数值。
*/
private transient volatile int sizeCtl;

//transfer()    中
//sizeCtl = (n << 1) - (n >>> 1);

ConcurrentHashMap机制小结:

  • 初始操作:以CAS方式初始化数组和头节点;
  • 插入节点:在某位置插入节点时做加锁处理;
  • 扩容操作:每个线程负责扩容一部分数据,扩容时做加锁处理。并且扩容时依然支持读写操作,若该位置的节点不是fwd则直接读写,否则就访问访问新数组进行读写。

JUC

多线程

单个进程中同时运行多个线程
好处:提高CPU利用率,避免等待网络IO或磁盘IO
举例:Tomcat并行处理多个请求
局限:太多的线程导致CPU上下文切换的开销增大
线程安全性问题:操作临界资源,数据不一致;涉及到锁可能会引起死锁。

Java语言中的线程安全

不可变类:String、Integer

绝对线程安全

相对线程安全

线程兼容

线程对立

ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据,Map<Thread,资源>

线程安全的实现方法

互斥同步
synchronize,ReentrantLock
非阻塞同步
CAS
争用共享资源,成功;失败就不停重试;
避免了线程的阻塞和唤醒带来的开销

无同步方案
可重入代码
线程本地存储

线程的创建

继承Thread,重写run()方法

实现Runnable,重写run()方法

实现Callable,重写call()方法

Thread FutureTask Callable

线程池创建:submit、execute

线程的状态

操作系统中:
NEW Ready Running BLOCKED TERMINATED
新建 就绪 运行 阻塞 结束
Java中:
NEW RUNNABLE WAIT TIME-WAIT BLOCKED TERMINATED
新建 运行 无限期等待 限期等待 阻塞 结束

wait和sleep的区别

wait是Object类中的方法,sleep是Thread类中的静态方法
sleep属于TIMED_WAITING,自动被唤醒、wait属于WAITING,需要手动唤醒。
wait会放弃锁资源,sleep仍占用锁锁资源
sleep可以在持有锁或者不持有锁时,执行。 wait方法必须在持有锁时(同步代码块中|临界区)才可以执行。
抛出llegalMonitorStateException

wait方法会将持有锁的线程从owner扔到WaitSet集合中,这个操作是在修改ObjectMonitor对象,如果没有持有synchronized锁的话,是无法操作ObjectMonitor对象的。

ThreadLocal

Java中的四种引用类型

Java中的使用引用类型分别是强,软,弱,虚

User user = new User();

在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它始终处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。

SoftReference

其次是软引用,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中,作为缓存使用。

然后是弱引用,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。可以解决内存泄漏问题,ThreadLocal就是基于弱引用解决内存泄漏的问题。

最后是虚引用,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。不过在开发中,我们用的更多的还是强引用。

ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据

代码实现

static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();

public static void main(String[] args) {
    tl1.set("123");
    tl2.set("456");
    Thread t1 = new Thread(() -> {
        System.out.println("t1:" + tl1.get());
        System.out.println("t1:" + tl2.get());
    });
    t1.start();

    System.out.println("main:" + tl1.get());
    System.out.println("main:" + tl2.get());
}

ThreadLocal实现原理:

  • 每个Thread中都存储着一个成员变量,ThreadLocalMap
  • ThreadLocal本身不存储数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap
  • ThreadLocalMap本身就是基于Entry[]实现的,因为一个线程可以绑定多个ThreadLocal,这样一来,可能需要存储多个数据,所以采用Entry[]的形式实现。
  • 每一个现有都自己独立的ThreadLocalMap,再基于ThreadLocal对象本身作为key,对value进行存取
  • ThreadLocalMap的key是一个弱引用,弱引用的特点是,即便有弱引用,在GC时,也必须被回收。这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法被回收

ThreadLocal内存泄漏问题:

  • 如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法被获取到。
  • 只需要在使用完毕ThreadLocal对象之后,及时的调用remove方法,移除Entry即可

面试准备-八股【面试准备】_职场和发展_12

synchronize的优化

锁消除:没有操作临界资源

锁膨胀:循环内获取锁->循环外获取锁

锁升级:无锁->偏向锁->轻量级锁->重量级锁

  • 无锁、匿名偏向:当前对象没有作为锁存在。
  • 偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断,当前指向的线程是否是当前线程 。
  • 如果是,直接拿着锁资源走。
  • 如果当前线程不是我,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争的情况)
  • 轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁
  • 如果成功获取到,拿着锁资源走
  • 如果自旋了一定次数,没拿到锁资源,锁升级。
  • 重量级锁:就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程。(用户态&内核态)

synchronize和Reentrant的对比

JVM语义,API层面

monitorenter monitorexit

基于AQS实现,有一个基于CAS维护的state变量来实现锁的操作。

都是可重入锁

1.6优化synchronize,可以锁升级

额外功能
等待可中断:ReentrantLock可以指定等待锁资源的时间。
public boolean tryLock(long timeout, TimeUnit unit) 公平锁:ReentrantLock支持公平锁和非公平锁
public ReentrantLock(boolean fair) 锁绑定多个条件
public Condition newCondition()


ReentrantLock和synchronized的区别

废话区别:单词不一样。。。

核心区别:

  • ReentrantLock是个类,synchronized是关键字,当然都是在JVM层面实现互斥锁的方式

效率区别:

  • 如果竞争比较激烈,推荐ReentrantLock去实现,不存在锁升级概念。而synchronized是存在锁升级概念的,如果升级到重量级锁,是不存在锁降级的。

底层实现区别:

  • 实现原理是不一样,ReentrantLock基于AQS实现的,synchronized是基于ObjectMonitor

功能向的区别:

  • ReentrantLock的功能比synchronized更全面。
  • ReentrantLock支持公平锁和非公平锁
  • ReentrantLock可以指定等待锁资源的时间。

选择哪个:如果你对并发编程特别熟练,推荐使用ReentrantLock,功能更丰富。如果掌握的一般般,使用synchronized会更好


AQS

说说你对AQS的理解

它是抽象的队列同步器,

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且
将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒
时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实
例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的
一个结点(Node)来实现锁的分配

AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。

AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

状态信息通过 protected 类型的 getState , setState , compareAndSetState 进行操作

模板方法模式实现架构

isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;
正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

面试准备-八股【面试准备】_对象锁_09

以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独
占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即
释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的
(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state
是能回到零态的。

再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个
数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会
CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然
后主调用线程就会从 await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现 tryAcquiretryRelease 、 tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器同
时实现独占和共享两种方式,如 ReentrantReadWriteLock 。

Semaphore与CountDownLatch一样,也是共享锁的一种实现。它默认构造AQS的state为
permits。当执行任务的线程数量超出permits,那么多余的线程将会被放入阻塞队列Park,并自旋判断
state是否大于0。只有当state大于0的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执
行release方法,release方法使得state的变量会加1,那么自旋的线程便会判断成功。
如此,每次只有最多不超过permits数量的线程能自旋成功,便限制了执行任务线程的数量。

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到
达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障
拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties) ,其参数
表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前
线程被阻塞。
再来看一下它的构造函数:
其中,parties 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所
有线程通过。

总结:CyclicBarrier 内部通过一个 count 变量作为计数器,cout 的初始值为 parties 属性的初始化
值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后
一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。

高频面试题-Java 并发【java面试】

ReentrantReadWtiteLock
实现
state使用高低位区分读锁 写锁的状态

//因为读锁和写锁使用的是同一个Sync,但是只有一个state,所以应该如何区分
		//高16位存读锁 低16位存写锁
		//统计读锁的个数 无符号右移16位    取出state高16位
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        //统计写锁的个数 按位与16个1 		取出state低16位
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
static final int SHARED_SHIFT   = 16;  //16位划分读锁和写锁的状态
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT); //读锁的单位 状态+-
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;//锁最大的数量
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//取写锁的数量

ReentrantLock的特性:
绑定多个条件:newCondition

举例:ArrayBlockingQueue源码
目标:生产者通知消费者,消费者通知生产者。
避免:生产者通知生产者,消费者通知消费者。

//两个队列 两个Condition 
    private final Condition notEmpty;
    private final Condition notFull;

如果是传统的synchronize,必须signalAll()

2023-7-26 09:27:15

线程池

固定线程池 n,n

单例线程池 1,1

可缓存线程池:0 max

定时任务线程池:DelayQueue

ThreadPoolExecutor

查看一下ThreadPoolExecutor提供的七个核心参数

public ThreadPoolExecutor(
    int corePoolSize,           // 核心工作线程(当前任务执行结束后,不会被销毁)
    int maximumPoolSize,        // 最大工作线程(代表当前线程池中,一共可以有多少个工作线程)
    long keepAliveTime,         // 非核心工作线程在阻塞队列位置等待的时间
    TimeUnit unit,              // 非核心工作线程在阻塞队列位置等待时间的单位
    BlockingQueue<Runnable> workQueue,   // 任务在没有核心工作线程处理时,任务先扔到阻塞队列中
    ThreadFactory threadFactory,         // 构建线程的线程工作,可以设置thread的一些信息
    RejectedExecutionHandler handler) {  // 当线程池无法处理投递过来的任务时,执行当前的拒绝策略
    // 初始化线程池的操作
}

JDK提供的几种拒绝策略:

  • AbortPolicy:当前拒绝策略会在无法处理任务时,直接抛出一个异常
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
      throw new RejectedExecutionException("Task " + r.toString() +
                                           " rejected from " +
                                           e.toString());
  }
  • CallerRunsPolicy:当前拒绝策略会在线程池无法处理任务时,将任务交给调用者处理
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
      if (!e.isShutdown()) {
          r.run();
      }
  }
  • DiscardPolicy:当前拒绝策略会在线程池无法处理任务时,直接将任务丢弃掉
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
  }
  • DiscardOldestPolicy:当前拒绝策略会在线程池无法处理任务时,将队列中最早的任务丢弃掉,将当前任务再次尝试交给线程池处理
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
      if (!e.isShutdown()) {
          e.getQueue().poll();
          e.execute(r);
      }
  }
  • 自定义Policy:根据自己的业务,可以将任务扔到数据库,也可以做其他操作。
private static class MyRejectedExecution implements RejectedExecutionHandler{
      @Override
      public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
          System.out.println("根据自己的业务情况,决定编写的代码!");
      }
  }

ThreadPoolExecutor源码

ctl变量
高三位:代表线程池的5种状态
低29位:工作线程的个数

ConcurrentHashMap

ConcurrentHashMap是线程安全的HashMap

ConcurrentHashMap在JDK1.8中是以CAS+synchronized实现的线程安全

CAS:在没有hash冲突时(Node要放在数组上时)

synchronized:在出现hash冲突时(Node存放的位置已经有数据了)

存储的结构:数组+链表+红黑树

sizeCtl:是数组在初始化和扩容操作时的一个控制变量
-1:代表当前数组正在初始化
小于-1:低16位代表当前数组正在扩容的线程个数(如果1个线程扩容,值为-2,如果2个线程扩容,值为-3)
0:代表数组还没初始化
大于0:代表当前数组的扩容阈值,或者是当前数组的初始化大小

CopyOnWriteArrayList

CopyOnWriteArrayList是一个线程安全的ArrayList。

CopyOnWriteArrayList是基于lock锁和数组副本的形式去保证线程安全。

在写数据时,需要先获取lock锁,需要复制一个副本数组,将数据插入到副本数组中,将副本数组赋值给CopyOnWriteArrayList中的array。

因为CopyOnWriteArrayList每次写数据都要构建一个副本,如果你的业务是写多,并且数组中的数据量比较大,尽量避免去使用CopyOnWriteArrayList,因为这里会构建大量的数组副本,比较占用内存资源。

CopyOnWriteArrayList是弱一致性的,写操作先执行,但是副本还有落到CopyOnWriteArrayList的array属性中,此时读操作是无法查询到的。

JVM