Java 内存模型(JMM)其实就是一套规则,目的是解决多线程下内存访问不一致的问题。简单说,它把内存分成了主内存和线程工作内存,主内存是所有线程共享的,存着对象实例、静态变量这些;每个线程还有自己的工作内存,会拷贝主内存里的变量到自己这,线程操作的都是自己工作内存里的副本,操作完再同步回主内存。这样设计就是为了平衡性能和一致性,避免多线程直接操作主内存导致效率低,同时通过 volatile、synchronized 这些关键字,规范副本和主内存的同步时机,保证线程安全,比如 volatile 能让变量修改后立即同步到主内存,其他线程能及时看到最新值。
这俩都是用来解决多线程同步、避免线程安全问题的,但用法和特点不一样。首先,synchronized 是 Java 自带的关键字,用起来特别简单,加在方法、代码块上就行,自动加锁、自动释放锁,不用手动管,适合简单的同步场景。而 ReentrantLock 是个类,需要手动调用 lock()加锁、unlock() 释放锁,一般要放在 try-finally 里,防止忘记释放。另外,ReentrantLock 更灵活,能设置公平锁(按线程等待顺序获取锁),还能中断等待锁的线程,支持条件变量,而 synchronized 只能是非公平锁,不能中断。还有,synchronized 在 JDK1.8 后优化了,性能和 ReentrantLock 差不多,但复杂场景还是 ReentrantLock 更实用。
垃圾回收(GC)就是 Java 自动帮我们清理内存里“没用”的对象,不用手动释放内存,避免内存泄漏。首先它得判断哪些对象是垃圾,常用的方法是可达性分析,就是从 GC Roots(比如主线程、静态变量)出发,能找到的对象就是有用的,找不到的就是垃圾。然后针对不同的垃圾,用不同的回收算法:年轻代用复制算法,把存活对象复制到另一个区域,清理掉死亡对象,效率高;老年代用标记 - 清除或标记 - 整理算法,标记出垃圾后要么直接清除,要么整理内存碎片,避免内存浪费。整个过程是自动的,我们不用手动触发,但可以通过 System.gc() 建议 JVM 执行,不过 JVM 不一定会听。
反射机制简单说,就是 Java 程序运行的时候,能获取到一个类的所有信息(比如类名、属性、方法、构造器),还能动态操作这些信息,比如创建类的实例、调用类的方法、修改类的属性,哪怕这个类在编译的时候我们不知道。比如我们常用的 Spring 框架,就是靠反射来创建 Bean 实例的。举个例子,通过 Class.forName("类的全路径"),就能获取到这个类的 Class 对象,然后通过 Class 对象就能拿到它的方法,再用 invoke() 方法调用这个方法。反射很灵活,但也有缺点,会破坏封装性,而且效率比直接调用低一点,平时开发除非必要,不用频繁用。
Java 异常处理,就是用来处理程序运行中出现的错误(比如空指针、数组越界),避免程序直接崩溃,让程序能优雅地处理异常、继续运行。核心就是 try-catch-finally,还有 throw 和 throws。try 块里放可能出现异常的代码,catch 块用来捕获指定的异常,然后处理(比如打印错误信息),一个 try 可以有多个 catch,捕获不同类型的异常。finally 块里的代码不管有没有异常都会执行,一般用来释放资源(比如关闭流、关闭数据库连接)。throw 是手动抛出一个异常,throws 是在方法上声明这个方法可能会抛出哪些异常,告诉调用者要处理这些异常。异常分 checked 异常(必须处理)和 unchecked 异常(比如 RuntimeException,可处理可不处理)。
单例模式就是保证一个类只有一个实例,线程安全的单例,就是多线程环境下也不会创建多个实例。常用的有四种方式,最推荐的是静态内部类和双重检查锁。第一种是饿汉式,类加载的时候就创建实例,天生线程安全,但可能浪费内存,因为不管用不用都会创建。第二种是懒汉式加 synchronized,第一次调用的时候创建实例,加锁防止多线程同时创建,但效率低,每次调用都要加锁。第三种是双重检查锁,先判断实例是否存在,不存在再加锁,加锁后再判断一次,既保证线程安全,又提高效率,要给实例加 volatile 关键字,防止指令重排。第四种是静态内部类,内部类加载的时候才创建实例,利用类加载机制保证线程安全,简洁又高效。
HashMap 就是用来存键值对(key-value)的容器,底层是数组 + 链表(JDK1.8 后加了红黑树)。工作原理很简单,首先根据 key 的 hash 值,计算出它在数组中的索引位置,然后把键值对存到这个位置。如果两个 key 的 hash 值一样,就会发生哈希冲突,这时就把第二个键值对放到第一个的后面,形成链表。当链表的长度超过 8,并且数组长度大于 64 时,链表会转成红黑树,这样查询效率更高(红黑树是有序的,查询时间复杂度更低)。HashMap 的默认初始容量是 16,负载因子是 0.75,当元素个数超过 16*0.75=12 时,就会扩容,容量翻倍。另外,HashMap 是线程不安全的,多线程环境下可以用 ConcurrentHashMap。
死锁就是两个或多个线程,互相持有对方需要的锁,都等着对方释放,导致所有线程都卡住,无法继续运行。避免死锁其实有几个简单的方法,记好就行。第一,统一锁的获取顺序,比如所有线程都先获取锁 A,再获取锁 B,这样就不会出现你拿 A 等 B、我拿 B 等 A 的情况。第二,给锁设置超时时间,比如用 ReentrantLock 的 tryLock() 方法,指定超时时间,超过时间没拿到锁就放弃,避免一直等待。第三,减少锁的持有时间,拿到锁后尽快执行完相关代码,赶紧释放锁,别拿着锁做无关的操作。第四,避免嵌套锁,尽量不要在一个锁的代码块里,再去获取另一个锁,减少死锁的概率。
泛型简单说,就是给集合、类、方法指定一个“类型参数”,比如 List<String>,就是指定这个 List 只能存 String 类型的数据,编译的时候就会检查类型,避免存错数据。它的优势特别明显,第一,提高代码复用性,比如写一个通用的工具方法,不用针对 String、Integer 分别写,用泛型就能适配多种类型。第二,保证类型安全,编译时就能发现类型错误,比如往 List<String> 里存 Integer,编译就会报错,不用等到运行时才出现异常。第三,避免强制类型转换,以前没有泛型的时候,从集合里取数据要手动强转,容易出错,有了泛型,取出来的就是指定类型,不用强转。比如 List<User> 取出来的直接是 User 对象,特别方便。
拷贝就是创建一个和原对象一样的新对象,浅拷贝和深拷贝的区别,就是看是否拷贝对象里的引用类型属性。浅拷贝很简单,让类实现 Cloneable 接口,重写 clone()方法,默认的 clone() 就是浅拷贝,它只会拷贝基本类型属性,引用类型属性还是指向原对象的地址,比如原对象里有个 List,浅拷贝后新对象的 List 和原对象的 List 还是同一个,改一个另一个也会变。深拷贝就是不仅拷贝基本类型,还要拷贝引用类型,常用两种方法:一是让引用类型也实现 Cloneable 接口,在 clone() 方法里手动拷贝引用类型;二是用序列化和反序列化,把对象序列化到流里,再反序列化出来,得到的就是深拷贝的对象,这种方法不用手动处理每个引用类型,更简单。
动态代理就是在程序运行的时候,动态创建一个代理类,用来代理目标类,在不修改目标类代码的前提下,给目标类的方法增加额外功能(比如日志、事务)。常见的实现方式有两种:JDK 动态代理和 CGLIB 动态代理。JDK 动态代理是 Java 自带的,需要目标类实现一个接口,然后通过 Proxy.newProxyInstance()方法,传入类加载器、接口数组、InvocationHandler,InvocationHandler 里重写 invoke() 方法,在这里写增强逻辑,调用目标方法。CGLIB 动态代理不需要目标类实现接口,它是通过继承目标类,生成子类来作为代理类,底层用的是字节码技术,同样可以在方法调用前后增加增强逻辑。Spring 的 AOP,就是用这两种动态代理实现的。
处理并发问题,核心就是保证多线程操作共享资源时,数据一致、不出现线程安全问题,常用的方法有几种。第一,用同步机制,比如 synchronized 加锁,或者 ReentrantLock,保证同一时间只有一个线程能操作共享资源。第二,用线程安全的容器,比如 ConcurrentHashMap、CopyOnWriteArrayList,这些容器内部已经做好了同步,不用我们手动加锁。第三,用 volatile 关键字,对于简单的共享变量,比如标记位,用 volatile 保证可见性和禁止指令重排,避免多线程读取到旧值。第四,用原子类,比如 AtomicInteger、AtomicLong,这些类是线程安全的,能实现原子性操作,不用加锁。另外,还可以用线程池管理线程,避免线程创建过多导致资源耗尽,合理控制并发数。
泛型擦除就是 Java 的泛型只在编译阶段有效,运行阶段会把泛型的类型参数“擦掉”,比如 List<String> 和 List<Integer>,在运行时都是 List 类型,JVM 不知道它们的泛型类型。这是因为 Java 为了兼容以前没有泛型的版本,采用了擦除机制。它的影响主要有几个:第一,不能用泛型类型创建实例,比如 new T()是不行的,因为运行时 T 的类型已经被擦除了。第二,不能用泛型类型作为重载方法的参数,比如两个方法 public void test(List<String> list) 和 public void test(List<Integer> list),编译会报错,因为运行时它们的参数类型都是 List。第三,不能获取泛型的 Class 对象,比如 List<String>.class 是不合法的,只能用 List.class。
volatile 是 Java 中用来保证线程安全的关键字,主要有两个核心作用,特别好记。第一,保证可见性,就是一个线程修改了 volatile 修饰的变量,这个修改会立即同步到主内存,其他线程读取这个变量时,能立即看到最新的值,避免了线程读取到自己工作内存里的旧副本。第二,禁止指令重排,JVM 为了提高效率,会对代码指令进行重排,但 volatile 会禁止这种重排,保证代码的执行顺序和我们写的一致,比如单例模式的双重检查锁,必须给实例加 volatile,否则可能出现指令重排,导致创建出不完整的实例。但要注意,volatile 不能保证原子性,比如 i++ 这种操作,即使 i 用 volatile 修饰,多线程下还是会出错,需要配合锁或者原子类使用。
Java NIO 就是非阻塞 I/O,和传统的 IO(BIO,阻塞 I/O)不一样,主要用于处理高并发、大量连接的场景,比如服务器开发。它的主要特点有三个:第一,非阻塞,传统 IO 是阻塞的,比如读取数据时,线程会一直等着,直到有数据可读,而 NIO 的线程在等待数据时,可以去做其他事情,不用一直阻塞。第二,基于缓冲区(Buffer),传统 IO 是面向流的,只能逐个字节读取,NIO 是面向缓冲区的,先把数据读到缓冲区,再从缓冲区读取,效率更高。第三,基于通道(Channel),通道是双向的,既能读又能写,而传统 IO 的流是单向的(输入流只能读,输出流只能写)。另外,NIO 还有选择器(Selector),一个线程可以管理多个通道,大大提高了并发处理能力。
处理大量数据时,主要是避免内存溢出、提高处理速度,常用的优化方法有这几个。第一,用分批处理,不要一次性把所有数据加载到内存,比如从数据库查大量数据,用分页查询,每次查 1000 条,处理完再查下一批,避免 OOM。第二,用高效的容器,比如 ArrayList 比 LinkedList 适合随机访问,HashMap 适合查找,避免用效率低的容器。第三,多线程并行处理,把大量数据分成多个任务,用线程池分配线程并行处理,比如用 Fork/Join 框架,适合分治处理大量数据,提高处理速度。第四,减少 IO 操作,IO 是很慢的,比如频繁读写文件、数据库,尽量批量读写,比如批量插入数据库,减少连接次数。另外,还可以用缓存,把常用的数据缓存起来,避免重复计算或查询。
Java 类加载机制,就是 JVM 把.class 文件(编译后的字节码文件)加载到内存,然后解析、初始化,最终形成可以使用的类对象的过程,主要分三个阶段:加载、链接、初始化。加载就是找到.class 文件,把它的字节码读入内存,创建一个 Class 对象。链接又分三步:验证(检查字节码是否合法,避免恶意代码)、准备(给类的静态变量分配内存,设置默认值,比如 int 默认 0)、解析(把符号引用转成直接引用,比如把类名转成内存地址)。初始化是最后一步,执行静态代码块、给静态变量赋值,只有当类被主动使用时才会初始化(比如创建实例、调用静态方法、访问静态变量)。类加载器有三层:引导类加载器(加载 JDK 核心类)、扩展类加载器、应用类加载器,采用双亲委派机制,先让父加载器加载,父加载器加载不了再自己加载。
优化垃圾回收(GC),核心就是减少 GC 的频率和 GC 的耗时,避免 GC 影响程序运行。常用的方法有几个。第一,合理设置 JVM 参数,比如调整年轻代、老年代的大小,根据程序的内存使用情况,设置合适的初始容量和最大容量,避免频繁扩容导致 GC。第二,减少对象的创建,尤其是短生命周期的对象,比如在循环里创建对象,尽量复用对象(比如用对象池),减少垃圾的产生,从而减少 GC 次数。第三,避免内存泄漏,内存泄漏会导致对象无法被回收,堆积在老年代,频繁触发 Full GC,比如忘记关闭流、静态集合持有对象引用,这些都要避免。第四,选择合适的 GC 收集器,比如年轻代用 Parallel Scavenge(注重吞吐量),老年代用 CMS(注重响应时间),根据程序的需求选择。
注解就是给 Java 代码(类、方法、属性)添加的“标签”,用来传递额外信息,比如 @Override、@Autowired,都是常用的注解。注解本身不影响代码的执行,它的作用是让程序(比如编译器、框架)读取这些信息,做相应的处理。注解的工作原理很简单:首先,我们定义一个注解(用 @interface 关键字),指定它的保留策略(比如保留到运行时)和作用目标(比如作用在方法上)。然后,在代码上使用这个注解。最后,通过反射机制,获取到注解的信息,然后执行相应的逻辑。比如 Spring 的 @Autowired,Spring 在启动时,会通过反射扫描带有这个注解的属性,自动注入对应的 Bean 实例;@Override 是给编译器用的,编译器会检查这个方法是否真的重写了父类的方法。
异常和错误都属于 Throwable 类的子类,但两者的区别很大,简单说就是:异常是可以处理的,错误是无法处理的。首先说异常,它分 checked 异常和 unchecked 异常,都是程序运行中出现的可预期的问题,比如空指针、数组越界、文件找不到,我们可以用 try-catch 捕获,处理后程序还能继续运行。然后说错误(Error),它是 JVM 层面的问题,比如 OutOfMemoryError(内存溢出)、StackOverflowError(栈溢出),这些都是严重的问题,超出了程序的控制范围,我们无法通过 try-catch 处理,出现错误后,JVM 会终止程序,比如内存溢出,就算捕获了,也没法解决内存不足的问题,只能通过优化程序、调整 JVM 参数来避免。
Java NIO 是非阻塞 I/O,传统 IO 是阻塞 I/O(BIO),两者的核心区别的就是“阻塞”与否,还有底层实现的不同。首先,阻塞 vs 非阻塞:传统 BIO 中,线程读取数据时,会一直阻塞,直到有数据可读,或者连接关闭,这段时间线程什么都做不了;而 NIO 的线程在等待数据时,不会阻塞,可以去处理其他连接或任务,效率更高。其次,面向流 vs 面向缓冲区:BIO 是面向流的,只能逐个字节读取,不能回头读;NIO 是面向缓冲区的,先把数据读到缓冲区,再从缓冲区读取,可重复读取,效率更高。另外,通道 vs 流:BIO 的流是单向的,输入流只能读,输出流只能写;NIO 的通道是双向的,既能读又能写。还有,NIO 有选择器,一个线程能管理多个通道,适合高并发场景,而 BIO 一个线程只能处理一个连接。
泛型擦除就是 Java 泛型只在编译期有效,运行期会擦掉泛型的类型参数,比如 List<String> 运行时就是 List,导致一些限制。解决泛型擦除带来的问题,有几个常用方法。第一,通过反射获取泛型类型,比如在父类中保存泛型的 Class 对象,子类继承时传入具体类型,父类通过反射获取,比如 Spring 的 ParameterizedTypeReference,就是这么实现的。第二,手动传递类型参数,比如写一个方法,额外传入 Class<T> clazz 参数,用来获取具体的泛型类型,避免擦除后无法获取。第三,使用泛型通配符(?),比如 List<?>,可以接收任意类型的 List,解决泛型擦除后的类型兼容问题。第四,避免在运行时依赖泛型类型,比如不要用 new T(),可以用 clazz.newInstance() 来创建实例,通过反射绕开擦除的限制。
Java 设计模式主要分三大类:创建型、结构型、行为型,每类都有常用的模式,举几个最常见的。第一,创建型模式,负责对象的创建,比如单例模式(保证一个类只有一个实例)、工厂模式(通过工厂创建对象,不用直接 new)、建造者模式(一步步构建复杂对象,比如 StringBuilder)。第二,结构型模式,负责类和对象的组合,比如代理模式(给对象加代理,增强功能)、装饰器模式(动态给对象加功能,比如 IO 的 BufferedReader)、适配器模式(把一个类的接口转换成另一个接口,比如 List 转 Enumeration)。第三,行为型模式,负责类和对象之间的交互、职责分配,比如观察者模式(一个对象变化,通知其他依赖它的对象,比如 Spring 的事件机制)、策略模式(不同场景用不同策略,比如排序算法)、迭代器模式(遍历集合,比如 Iterator)。
Stream API 是 Java 8 新增的,用来处理集合的工具,它能以声明式的方式,对集合进行过滤、映射、排序、聚合等操作,代码更简洁、易读。简单说,Stream 就是把集合变成一个“流”,然后对这个流进行一系列操作,最终得到想要的结果。它的优势很明显:第一,代码简洁,不用写繁琐的 for 循环,比如过滤集合中大于 10 的元素,用 Stream 的 filter()方法一行就能搞定,比传统 for 循环简洁多了。第二,支持链式调用,多个操作可以连在一起写,比如 filter() 过滤后,再用 map()映射,再用 collect() 收集结果,逻辑清晰。第三,支持并行处理,用 parallelStream() 就能实现并行操作,自动利用多线程处理集合,提高处理大量数据的效率。第四,可读性强,声明式编程,一看就知道要做什么,不用关心怎么做。
优化数据库访问性能,核心就是减少数据库的压力,提高查询和操作的速度,常用方法有这几个。第一,使用索引,给查询频繁的字段(比如 where 条件里的字段)建立索引,能大幅提高查询速度,但要注意索引不要建太多,否则会影响插入、更新的速度。第二,优化 SQL 语句,避免写低效 SQL,比如避免 select *(只查需要的字段)、避免子查询嵌套过深、避免 where 条件里用函数(会导致索引失效)。第三,使用缓存,比如用 Redis 缓存常用的查询结果,下次查询先查缓存,缓存没有再查数据库,减少数据库访问次数。第四,批量操作,比如批量插入、批量更新,减少数据库连接次数,比如用 MyBatis 的批量插入,比单次插入效率高很多。第五,合理使用连接池,比如 Druid、HikariCP,管理数据库连接,避免频繁创建和关闭连接,节省资源。
反射机制就是程序运行时,能获取类的所有信息(属性、方法、构造器),并动态操作这些信息。实际开发中,反射用得很多,尤其是框架开发。比如 Spring 框架,我们用 @Autowired 注入 Bean,Spring 就是通过反射扫描带有注解的类,创建实例并注入;Spring MVC 接收请求时,通过反射调用 Controller 里的方法。再比如 MyBatis,我们写的 Mapper 接口,MyBatis 通过反射动态生成接口的实现类,执行 SQL 语句。还有,常用的 JSON 工具(比如 Jackson),把 JSON 字符串转成 Java 对象,就是通过反射获取类的属性,然后给属性赋值。另外,在写通用工具类时,用反射能适配多种类型,比如写一个通用的对象拷贝工具,不用针对每个类写拷贝方法,用反射就能实现。
Java 常用的垃圾回收算法有四种,分别对应不同的内存区域,各有特点。第一,复制算法,主要用在年轻代,年轻代的对象大多是短生命周期的,存活率低。工作原理是把年轻代分成两个区域(Eden 区和 Survivor 区),每次只使用 Eden 区和一个 Survivor 区,回收时把存活的对象复制到另一个 Survivor 区,然后清空 Eden 区和用过的 Survivor 区,效率高,没有内存碎片。第二,标记 - 清除算法,主要用在老年代,先标记出所有存活的对象,然后把未标记的垃圾对象直接清除,优点是不用移动对象,缺点是会产生内存碎片,影响后续对象分配。第三,标记 - 整理算法,也是用在老年代,标记存活对象后,把存活对象往一端移动,然后清空另一端的垃圾,解决了标记 - 清除的内存碎片问题,但需要移动对象,效率稍低。第四,分代收集算法,不是单独的算法,是结合前面三种,年轻代用复制算法,老年代用标记 - 清除或标记 - 整理算法,是目前 JVM 默认的收集算法。
序列化就是把 Java 对象转换成字节序列,反序列化就是把字节序列再转成 Java 对象。过程很简单:序列化时,让类实现 Serializable 接口(标记接口,不用重写方法),然后用 ObjectOutputStream 的 writeObject()方法,把对象写入流中,转换成字节序列;反序列化时,用 ObjectInputStream 的 readObject() 方法,从流中读取字节序列,转成原来的对象。它的重要性主要体现在两个方面:第一,数据持久化,把对象序列化后写入文件、数据库,下次可以通过反序列化读取出来,比如保存用户会话信息。第二,网络传输,在分布式系统中,两个服务之间传递对象,只能传递字节序列,需要先序列化,传输到对方后再反序列化,比如 RPC 调用、Socket 通信,都是通过序列化传递对象的。另外,要注意,静态变量不会被序列化,transient 修饰的属性也不会被序列化。
Lambda 表达式是 Java 8 新增的,用来简化匿名内部类的写法,尤其是函数式接口(只有一个抽象方法的接口),比如 Runnable、Comparator。使用方法很简单,格式是(参数)->{代码块},如果参数只有一个,可以省略括号;如果代码块只有一行,可以省略大括号和 return。比如创建 Runnable 线程,以前要写 new Runnable(){},用 Lambda 就是 ()->{System.out.println("线程执行");},简洁很多。它的优势:第一,简化代码,减少冗余代码,把匿名内部类的代码压缩成一行,提高可读性。第二,让代码更简洁优雅,尤其是处理集合的 Stream API 时,结合 Lambda 表达式,能写出简洁的链式调用,比如 list.stream().filter(s->s.length()>5).collect(Collectors.toList())。第三,支持函数式编程,让 Java 能更方便地进行函数式编程,提高开发效率。
内存泄漏就是 Java 对象已经没用了(应该被 GC 回收),但因为有其他对象持有它的引用,导致它无法被回收,长期堆积在内存中,最终导致内存溢出(OOM)。常见的内存泄漏场景:比如静态集合持有对象引用(static List<User> list,添加对象后不清理)、忘记关闭流(InputStream、OutputStream)、监听器未移除、线程池核心线程持有对象引用。防止内存泄漏的方法:第一,避免静态集合长期持有对象,用完后及时清理(list.clear())。第二,用完流、数据库连接、Socket 等资源后,一定要在 finally 块中关闭,释放资源。第三,移除不需要的监听器、回调函数,避免它们持有对象引用。第四,合理使用弱引用(WeakReference),对于不需要强引用的对象,用弱引用,GC 会自动回收弱引用指向的对象,比如 WeakHashMap,当 key 没有强引用时,会自动被回收。第五,定期检查代码,排查可能的内存泄漏,比如用 JProfiler 工具分析内存使用情况。
依赖注入(DI)就是不用我们手动创建对象,而是由框架(比如 Spring)自动创建对象,并把对象注入到需要它的地方,比如把 Service 注入到 Controller,不用我们 new Service(),框架会自动处理。它的核心是“控制反转(IOC)”,以前创建对象的控制权在我们自己手里,现在交给了框架。工作原理很简单:首先,我们在配置文件(或用注解)中告诉框架,哪些类需要被管理(比如用 @Component、@Service 注解)。然后,框架在启动时,会扫描这些类,创建它们的实例(Bean),并把这些 Bean 存放在容器中。最后,当某个类需要依赖另一个类时,框架会从容器中取出对应的 Bean,注入到这个类中(比如用 @Autowired 注解)。比如 Controller 里需要 Service,用 @Autowired 注解标注 Service 属性,Spring 就会自动把 Service 的实例注入到 Controller 中,我们直接用就行,不用手动创建。
Java 处理日期时间,以前用的是 Date 和 Calendar 类,但这两个类有很多问题,比如 Date 是可变的(线程不安全)、月份从 0 开始(1 月是 0)、API 设计混乱。Java 8 新增了一套日期时间 API(java.time 包),解决了这些问题。以前处理日期:比如 new Date()获取当前时间,用 Calendar.getInstance() 来加减日期,很繁琐。Java 8 的改进:第一,提供了不可变的日期时间类,比如 LocalDate(日期)、LocalTime(时间)、LocalDateTime(日期时间),线程安全,不会被意外修改。第二,API 设计更简洁,比如 LocalDate.now()获取当前日期,plusDays(1) 加一天,minusMonths(2) 减两个月,不用像 Calendar 那样繁琐。第三,提供了 DateTimeFormatter 类,专门用来格式化日期时间,比以前的 SimpleDateFormat 更安全(SimpleDateFormat 是线程不安全的)。第四,新增了 ZoneId、ZonedDateTime,支持时区处理,解决了跨时区的问题。
接口和抽象类都是用来抽象的,不能直接实例化,但两者的区别很大,主要有这几点。第一,关键字不同:抽象类用 abstract class,接口用 interface。第二,方法修饰符不同:抽象类可以有抽象方法(没有实现),也可以有非抽象方法(有实现);接口在 Java 8 前只能有抽象方法,Java 8 后可以有默认方法(default)和静态方法,但不能有非抽象的实例方法。第三,继承方式不同:一个类只能继承一个抽象类(单继承),但可以实现多个接口(多实现)。第四,属性不同:抽象类的属性可以是各种修饰符(public、private、protected),可以有普通属性;接口的属性默认是 public static final(常量),不能有普通属性。第五,用途不同:抽象类适合抽取多个类的公共属性和方法,体现继承关系;接口适合定义类的行为规范,体现多态,比如 Comparable 接口,定义了比较的行为。
多线程同步机制,就是为了避免多线程同时操作共享资源,导致数据不一致、线程安全问题,常用的同步方式有几种。第一,synchronized 关键字,最常用的,可加在方法、代码块上,保证同一时间只有一个线程能执行同步代码,自动加锁、释放锁,简单易用。第二,ReentrantLock 类,手动加锁、释放锁,比 synchronized 灵活,支持公平锁、中断锁、条件变量,适合复杂的同步场景。第三,volatile 关键字,保证变量的可见性和禁止指令重排,适合简单的共享变量(比如标记位),但不能保证原子性。第四,原子类,比如 AtomicInteger、AtomicReference,底层用 CAS 机制,实现原子性操作,不用加锁,效率高,适合简单的计数、赋值操作。第五,锁机制的补充,比如 CountDownLatch(倒计时器)、CyclicBarrier(循环屏障),用来协调多个线程的执行顺序,实现更复杂的同步逻辑。