返回

Java开发知识点简单总结

Java基础

面向对象的特点

封装,继承,多态

包装类型的缓存机制

Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False

接口和抽象类有什么共同点和区别

共同点 :

  • 都不能被实例化。
  • 都可以包含抽象方法。
  • 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。

区别 :

  • 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
  • 一个类只能继承一个类,但是可以实现多个接口。
  • 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

深拷贝和浅拷贝区别?什么是引用拷贝?

  • 引用拷贝就是两个不同的引用指向同一个对象。如 Object a = new Object(); Object b = a;
  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
  • 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。

字符串

String s1 = new String(“abc”);这句话创建了几个字符串对象?

会创建 1 或 2 个字符串对象。
如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建 2 个字符串对象(new String()和"abc")。否则创建一个字符串对象(new String())

集合

Arraylist/Linkedlist的区别

ArrayList:

  • 基于数组。
  • 随机访问速度优于 LinkedList,可以根据下标以 O(1) 时间复杂度对元素进行随机访问。
  • 插入与删除元素的速度劣于 LinkedList,原因是在进行插入与删除操作时,会涉及到底层数组的数据搬移操作。
  • 需要连续的内存块存储数据。

LinkedList:

  • 基于双向链表。
  • 随机访问速度劣于 ArrayList,因为当要访问链表中的某个元素时,只能从头部往后遍历查找。
  • 插入与删除元素的速度优于 ArrayList,只需要更改前后节点的指针指向即可。
  • 需要更多的内存空间来存储的每个节点的前驱节点和后继节点的指针

Hashmap原理

  • 链表散列组成
  • 初始容量默认16
  • 负载因子默认0.75
  • 每次扩容为2倍,容量总是2的整数次幂
  • 链表大小>8,数组大小>=64时,链表会转换为红黑树

Java多线程

线程

线程与进程

  • 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程
  • 线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程
  • 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反

并发与并行

  • 并发:两个及两个以上的作业在同一 时间段 内执行。
  • 并行:两个及两个以上的作业在同一 时刻 执行。

同步与异步

  • 同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
  • 异步:调用在发出之后,不用等待返回结果,该调用直接返回。

线程安全和不安全

  • 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
  • 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失

线程的生命周期和状态

  • NEW: 初始状态,线程被创建出来但没有被调用 start() 。
  • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
  • BLOCKED:阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。

sleep()和wait()

  • sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
  • wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。
    • sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁,所以定义在Thread中
    • wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。是要释放当前线程占有的对象锁并让其进入 WAITING 状态,所以是Object类的本地方法

乐观锁与悲观锁

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。像 Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。
高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。在 Java 中java.util.concurrent.atomic包下面的原子变量类(比如AtomicInteger、LongAdder)就是使用 CAS 实现的。
高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升

使用场景

  • 悲观锁通常用于写比较多的情况下(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如LongAdder),也是可以考虑使用乐观锁的,要视实际情况而定。
  • 乐观锁通常用于写比较少的情况下(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考java.util.concurrent.atomic包下面的原子变量类)

乐观锁实现机制

版本号机制:线程A更新数据时先读取version字段,version字段与之前读取的一致则更新,不一致则说明期间有其他线程更新了数据

CAS机制:Compare And Swap。用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。(CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。原子操作:即最小不可拆分的操作)
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新

  • V:要更新的变量值(Var)
  • E:预期值(Expected)
  • N:拟写入的新值(New)

乐观锁的ABA问题

如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,并不能说明它的值没有被其他线程修改过了,因为在这段时间它的值可能被改为B,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。即 “ABA"问题。

解决思路是在变量前面追加上版本号或者时间戳

乐观锁的其他问题

循环时间长开销大

CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用:
可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
可以避免在退出循环的时候因内存顺序冲而引起 CPU 流水线被清空,从而提高 CPU 的执行效率

只能保证一个共享变量的原子操作

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作

Java中加锁的方式

synchronized关键字与Lock接口

synchronized用法

  • 修饰实例方法
  • 修饰静态方法
  • 修饰代码块

synchronized锁的升级:偏向锁 → 轻量级锁 → 重量级锁

首次执行到synchronized代码块,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)
这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。

在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等

显然,忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。

公平锁与非公平锁

  • 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
  • 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁

读写锁、共享锁、互斥锁

读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)

如果加锁只是为了读取数据,那么加锁时明确地加一个读锁,其他线程如果也要加读锁,不需要等待,可以直接获取

如果我读取值是为了更新它,那么加锁的时候就直接加写锁,我持有写锁的时候别的线程无论读还是写都需要等待

可重入锁

可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。

JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的

线程池

阿里的开发手册中提到禁止使用Executors创建线程池,建议使用ThreadPoolExecutor创建
FixedThreadPool和SingleThreadPool允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM
CachedThreadPool和ScheduledThreadPool允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM

public ThreadPoolExecutor(
  int corePoolSize,   // 核心线程数,核心线程不会被回收,除非设置allowCoreThreadTimeOut(true)
  int maximumPoolSize,// 最大线程数,闲置时会被回收
  long keepAliveTime, // 线程闲置时间
  TimeUnit unit,  // 闲置时间的单位 时分秒
  BlockingQueue<Runnable> workQueue, // 任务队列,核心线程满后进入队列,队列满后创建线程
  ThreadFactory threadFactory, // 线程工厂,用于创建线程执行任务
  RejectedExecutionHandler handler // 拒绝策略,当线程池处于饱和时,使用某种策略来拒绝任务提交
)

核心线程未满则使用核心线程执行,核心线程满则进入任务队列排队,任务队列也满则创建非核心线程执行任务,线程与队列双满则执行拒绝策略。

ThreadPoolExecutor的拒绝策略4种,AbortPolicy,默认策略,抛出异常;DiscardPolicy静默丢弃任务;DiscardOldestPolicy丢弃队列中最老的任务,提交当前任务;CallerRunsPolicy让提交任务的线程来执行任务

IO密集型应设置较多的线程数(2n)(n=cpu核心数),CPU密集型任务应设置较少的线程数(n+1)

Licensed under CC BY-NC-SA 4.0