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(循环屏障),用来协调多个线程的执行顺序,实现更复杂的同步逻辑。