JavaSE

[TOC]

JavaSE

基础

八种基本数据类型

final关键字

final 是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不能被改变。

重写重载

重载:发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。

重写:重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。

为什么在重写 equals 方法的时候需要重写 hashCode 方法

hashCode() 方法返回对象的散列码,而 equals() 方法用于比较两个对象是否相等

重写 equals() 方法时,我们通常会根据对象的属性或状态来比较两个对象是否相等。如果两个对象相等,那么它们的 hashCode() 方法应该返回相同的值。

String、StringBuffer与StringBuilder的区别

范围:String对象是不可变的,而StringBuffer和StringBuilder是可变字符序列。每次对String的操作相当于生成一个新的String对象,而对StringBuffer和StringBuilder的操作是对对象本身的操作,而不会生成新的对象,所以对于频繁改变内容的字符串避免使用String,因为频繁的生成对象将会对系统性能产生影响。

线程安全:String由于有final修饰,是immutable的,安全性是简单而纯粹的。StringBuilder和StringBuffer的区别在于StringBuilder不保证同步,也就是说如果需要线程安全需要使用StringBuffer,不需要同步的StringBuilder效率更高。

集合类

Collection(集合)类

  • Set

    • TreeSet(logN):基于红黑树实现

    • HashSet(1):基于哈希表实现,支持快速,但不有序

    • LinkedHashSet(1):快速查找,并且内部使用双向链表保证有序

  • List

    • ArrayList:基于动态数组实现,随机访问

    • Vector:跟ArrayList比较多了线程安全

    • LinkedList:与双向链表实现,只能顺序访问

  • Queue

    • LinkedQueue:双向队列

    • PriorityQueue:优先队列,一般算法使用的比较多(最短路径,最大最小值的时候)

ArrayList自动扩容

每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。

数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。

这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。

Map类

  • TreeMap:基于红黑树实现

  • HashMap:1.7基于哈希表实现,1.8过后基于数组+链表+红黑树实现

  • LinkedHashMap:使用双向链表来维护元素的顺序

1.8HashMap如何实现

在Java8中,当链表中的元素达到8个的时候,会将链表自动转换为红黑树(在这之前会先判断。如果当前数组的长度小于64,那么会先将数组扩容,再转换成红黑树),转换成红黑树的原因是减少搜索时间

HashMap和HashTable的区别

  • 线程:HashMap不是线程安全的,HashTable是线程安全的(他的内部的方法基本上都经过synchronized修饰的)

  • 效率:HashMap肯定效率高一点

  • 数据结构:1.8过后HashMap是数组+链表+红黑树;HashTable底层是数组实现,每个数组元素都是一个链表或红黑树

HashMap为什么线程不安全

在1.8过后的HashMap中,多个键值对可能会被分配到同一个桶中。多个线程对HashMap的put操作会造成线程不安全

  • 两个线程1 2 同时进行put操作,并且发生了哈希冲突

  • 不同的线程可能在不同的时间片获得了CPU执行的机会,当前线程1执行完哈希冲突判断后,由于时间片消耗挂起。线程2先完成了插入

  • 后面线程1获得了时间片,由于之前已经进行过hash碰撞的判断,所以此时会直接插入,这就导致线程2插入的数据被线程1给覆盖了

ConcurrentHashMap了解过吗

ConcurrentHashMap也是线程安全的,他和HashTable的实现线程安全的方式不同:

  • ConcurrentHashMap:使用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,加锁是对node进行加锁,synchronized只锁定当前链表或红黑树的首节点,这样是要hash不冲突,就不会产生并发。就不会影响其他Node的读写,效率大福提升

  • HashTable:使用synchronized来保证线程安全,效率非常低下。当一个线程访问同步方法的时候,其他线程也来访问同步方法。就可能会进入阻塞或轮询状态

并发JUC

线程进程

进程是程序的一个执行过程,是系统运行程序的基本单位,所以说进程是动态的。

线程和进程相似,线程是比进程更小的执行单位。一个进程在执行的过程中可以产生多个线程。

线程被称为轻量级进程:同类的多个线程是共享进程的堆和方法区的资源,但是每个线程有自己的程序计数器、虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之前之间切换工作的时候。负担要比进程小得多

线程的堆和方法区

堆和方法区是所有线程共享的资源。其中对是进程中最大的一块内存,只要用存放新创建的对象。方法区主要用于存放各一件被加载在的类信息、常量、静态常量、即时编译器编译后的代码等数据

说说线程的声明周期

线程有6中状态:new、runnable、blocked、waiting、time_waiting、terminaled

说说死锁

死锁就是,多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放。

比如说线程A持有资源2,线程2持有资源1,他们同时想得到对方的资源,这样就会卡死,死锁

死锁产生的条件

  • 互斥:对于资源X和Y,只能被一个线程占用

  • 占有且等待:假设一个线程A占用了X,然后A去申请Y,而在申请Y的过程中,A仍占用Y

  • 不可抢占:假设线程A占有了共享资源X,则其他线程无法强制获得A占有的资源

  • 循环等待:即线程A等待线程B占有的资源,而线程B又在等待线程A占有的资源

如何解决(避免)死锁

  • 一次性申请所有的资源

  • 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源

知道sleep()wait()的区别吗

  • sleep()没有释放锁,wait()释放了锁的

  • wait()通常被用于线程之间交互/通信,sleep()通常被使用来暂停执行

  • sleep()Thread的静态本地方法;wait()Object类的本地方法

  • sleep()方法调用过后,可以自动苏醒;wait()方法需要其他线程notify通知才苏醒

volatile

volatile关键字能保证数据的可见性,有序性。但是不能保证数据的原子性

有序性

volatile关键字有个作用就是防止JVM的指令重排序,就如果我们将一个变量声明为volatile的话,在对这个变量进行读写操作的时候,会通过内存屏障的方式禁止指令重排

原子性(不保证)

因为一个变量进行++的时候(inc++),他实际上是个符合操作:

  1. 读取inc的值

  2. inc进行加1

  3. 写回inc

而volatile无法保证这三个操作是在一个原子操作下

CAS

CAS(Campare And Swap),用于实现乐观锁。它的思想就是用一个预期值和要更新的变量值进行比较。两只相等才会进行更新。

在这个过程当中涉及到三个操作数:

  1. V:要更新的变量值(Var)

  2. E:预期值(Expected)

  3. N:准备写入的值(New)

整个过程是当且仅当V==E的时候,CAS通过原子方式(CPU底层的原子指令)用N来更新V;如果不相等,就说明已经有其他线程更新了V了,当前线程放弃更新

CAS有什么缺点呢

CAS操作是通过空旋/自旋来进行重试的,如果没有成功更新的话,就会一直在那里空旋,时间长的话CPU负载爆满

ABA问题

ABA问题就是:如果一个变量V初次读取的时候是A,在准备赋值的时候我们检查V==E的结果,发现还是A,自认为正确。但是我们其实不能认为A没有被其他线程修改过。因为这个过程当中允许被改成了B而后又被改回了A。使得CAS操作就误认为它没有被修改过。

这就是所谓的ABA问题

如何处理ABA问题

解决思路就是,类似于乐观锁,在变量前面追加版本号或者时间戳

  • 先判断当前值是否==预期值

  • 再判断当前标志是否-==预期标志

synchronized

它主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块一直都是只有一个线程在执行

在Java6过后,synchronized引入了大量的优化:自旋锁,锁消除,锁粗化,偏向锁,轻量级锁等技术来减少锁的操作开销

synchronized底层实现

synchronized底层……那个好像要看class字节码文件,目前还没看过

synchronized和volatile的区别

这两个关键字是两个互补的存在

  • volatile关键字是线程同步的轻量级实现,所以volatile性能好得多

  • volatile只能修饰变量;而synchronized可以修饰方法以及代码块

  • volatile不能保证原子性

  • volatile主要解决多个线程之间的可见性synchronized关键字解决的是多个线程之间访问资源的同步性

Lock锁

ReentrantLock

ReentrantLock是实现了Lock接口的,可重入独占锁

它和synchronized类似,不过比它更灵活更强大,添加了轮询、超时、中断、公平锁、非公平锁等高级功能

ReentrantLock和synchronized的区别

底层依赖来说

  • synchronized依赖于JVM实现,jdk6为synchronized做的优化都是在虚拟机层面的,没有直接暴露出来

  • ReentrantLock依赖于JDK层面实现,也就是API层面,他需要lock(),unlock(),try-catch-final来结合完成

ReentransLock相比synchronized的高级功能

  • 等待可中断:R提供了一种能够中断等待所的线程的机制,通过lock.lockInterruptibly()来实现这个机制

  • 公平锁:R可以指定是公平锁还是非公平锁,默认是非公平锁

  • 选择性通知:swait()notify()/ notifyAll()方法相结合可以实现等待/通知机制

关于选择性通知,ReentrantLock也可以实现,通过Conditoin实现,Condition是JDK1.5过后出现的。可以实现多路通知功能,也就是在一个Lock对象中可以创建多个Condition实例——对象监视器 线程对象可以注册在指定了Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。

ThreadLocal

后续再补……

线程池

顾名思义,池化思想。当有任务要处理的时候,直接从池子里面拿线程来执行任务。执行完了过后不会被立即销毁,继续等待后续任务的执行。

所谓的池化技术——线程池、数据库连接池、http连接池等等都是这个思想的应用。主要是为了减少每次获取资源的小号,提高对资源的利用率

  • 降低资源的消耗:重复利用线程,不用反复的创建和销毁

  • 提高响应速度:等到任务来的时候,不需要等待线程创建的时间,立即执行

  • 提高线程的可管理性:有池子统一分配和管理

创建

方式一:推荐通过ThreadPoolExecutor构造函数来创建

方式二:不推荐通过Executors来创建

原因:通过这个工具来来创建

FixThreadPoolSingleThreadExecutor:使用的是无界阻塞队列的LinkedBlockingQueue,任务队列最大长度为Integer.MAX_VALUE,所以会堆积大量的请求,从而导致OOM

CachedThreadPool:使用的是同步队列SynchronousQueue,允许创建的线程数同样是Integer.MAX_VALUE,会造成创建大量的线程,导致OOM

ScheduledThreadPoolSingleThreadScheduledExecutor:使用的是无界的延迟阻塞队列,任务队列最大为Integer.MAX_VALUE导致OOM

七大参数

  • corePoolSize:最大可以同时运行的线程数量

  • maximumPoolSize:当存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程的线程数

  • workQueue:当任务来的时候先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被先存放到该队列

  • keepAliveTime:闲置线程的最大存活时间

  • unit:keepAliveTime的单位

  • threadFactory:池子中创建线程所使用的工厂类

  • handler:饱和策略/拒绝策略

处理任务的流程

  1. 如果当前线程数 < corePoolSize,则创建新线程执行任务

  2. 如果当前线程数 >= corePoolSize,但是 < maximumPoolSize,就会吧任务丢进workQueue

  3. 如果任务丢不进workQueue(任务队列满了),同时当前线程数 < maximumPoolSize,则创建新线程执行任务

  4. 如果上述条件都难以满足:当前线程数快满了,任务队列也满了。这个时候就会执行拒绝策略

你会如何设定线程池大小

首先我觉得中国人嘛,从古至今都是遵守的中庸之道。就类比于现实世界中人们通过合作做某件事情的时候,人数肯定是不要过多或者过小,合适才是最好的。

  • 如果设置的线程池数量过小的话,假设同一时间有大量的请求需要处理,就会导致大量的请求在workQueue里面排队等待执行,甚至会出现任务队列满了过后请求无法处理,或者是大量的请求堆积在workQueue,导致OOM。CPU根本没有充分利用到

  • 如果设置的线程池数量过大的话,大量的线程可以会同时在争取CPU的资源,到只大量的上下文切换,影响整体执行效率

上下文切换:

就是说CPU为了让多个线程都能够有效的执行,它会为每个线分配时间片轮询。然后当前任务在执行完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次在切换回这个任务的时候可以直接加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

回到问题本身:

需要先判断任务是CPU密集(计算密集)任务还是IO密集任务

  • 如果是CPU密集任务,这种任务主要是消耗CPU资源,所以只需要设置N+1就行了

  • 如果是IO密集任务,这种任务可以多配置一些线程,因为线程在处理IO的时间段内是不占用CPU的,设置2N就可以

AQS(AbstractQueuedSynchronizer)

TODO 真吉尔🐔多,后续再补

AQS即抽象队列同步器,它其实就是一个通过维护一个线程队列(CLH)的一个抽象类,采用的是模板设计模式,父类将公共代码部分抽象出来,通过子类对其扩展进行不同类型的锁的实现(如:ReentrantLock、CountDownLatch、ReentrantReadWriteLock...)。而该CLH队列将每条将要去抢占资源的线程封装成一个Node节点来实现锁的分配,有一个int类变量表示持有锁的状态(private volatile int state),通过CAS完成对status值的修改(0表示没有,1表示阻塞)

19:44:57-1689853496904.png

通过Node节点维护一个双向链表,其中子类又有独占节点、共享节点、条件节点(作为标识类)

19:47:33-1689853653919.png
20:05:54-1689854749731.png

在代码中的使用:当有线程占用锁时,后面的线程开始进入队列,队列采用来加载的方式,当有线程需要排队时,开始初始化队列

20:09:14-1689854949569.png
20:11:36-1689855096046.png

该独占节点主要用于区别于共享节点,用于共享节点实现自己的逻辑

20:14:51-1689855285959.png

通过ReentrantLock了解AQS

//todo


自我介绍

(技术面)

面试官你好,我叫杨晨,目前就读于重庆邮电大学软件工程专业,现在是大二。我是从大一入学的时候接触的Java

(HR面)


心识宇宙——上海

竹云科技——广州

运去哪——上海

悬镜——北京

元启星辰——北京

北京特征曲线——北京

浙江省北大信息技术高等研究院——杭州

维纳——成都

同程旅行——苏州

奔步——广州

复深蓝——上海

畅风通信——武汉

凯易迅——南京

上海鸿笛——杭州

浙江中控技术——杭州

山东依智——杭州

Last updated