锁机制
1. synchronized关键字(同步锁):
synchronized关键字可以用来修饰方法或代码块,实现对对象的互斥访问。当一个线程进入synchronized代码块时,会自动获取对象的锁,其他线程则需要等待锁释放后才能进入。
示例:
// 修饰方法:确保同一时间只有一个线程执行该方法
public synchronized void synchronizedMethod() {
// 代码块
}
// 修饰代码块:以缩小锁定的范围,提高并发性能。
// 需要指定一个对象作为锁,多个线程需要使用相同的锁对象才能实现同步。
public void myMethod() {
synchronized (lockObject) {
// 执行需要同步的操作
}
}
// 修饰静态方法:作用范围是该类的所有实例对象和该类的所有静态方法之间的互斥
public static synchronized void myStaticMethod() {
// 执行需要同步的操作
}
注意:
synchronized 锁定的范围应尽量小,避免锁定的时间过长影响性能。
synchronized 需要指定一个对象作为锁,多个线程需要使用相同的锁对象才能实现同步。
synchronized 仅能保证线程安全性,无法保证数据的正确性,需要结合其他机制并根据具体业务需求来进行处理。
2. ReentrantLock类(重入锁):
ReentrantLock是Java提供的可重入锁,它提供了更灵活的锁机制。与synchronized不同,ReentrantLock需要手动获取和释放锁。
示例:
Lock lock = new ReentrantLock();
lock.lock();
try {
// 代码块
} finally {
lock.unlock();
}
3. ReadWriteLock接口(读写锁):
ReadWriteLock接口定义了读锁和写锁,读锁可以被多个线程同时持有,写锁只能被一个线程持有。这种机制可以提高读操作的并发性能。
示例:
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
readLock.lock();
try {
// 读操作
} finally {
readLock.unlock();
}
writeLock.lock();
try {
// 写操作
} finally {
writeLock.unlock();
}
以上是Java中常见的锁机制,根据不同的需求选择合适的锁机制来保证线程安全。
乐观锁
乐观锁是一种乐观的思想,认为竞争情况发生的概率很低,因此不主动加锁,而是通过版本号或时间戳等机制来检查是否发生冲突。当发生冲突时,乐观锁会检测冲突并采取相应的处理方式。以下是几种常见的乐观锁实现方式:
-
版本号/时间戳机制(Version/TimeStamp Mechanism):
- 在数据表中增加一个表示版本号或时间戳的字段。
- 在读取数据时,将当前版本号或时间戳一并读取出来。
- 在更新数据时,根据读取到的版本号或时间戳进行比对,如果一致则执行更新操作,否则认为发生了冲突。
- 冲突处理方式可以是回滚事务、重试操作或提示用户等。
-
CAS(Compare and Swap)算法:
- CAS是一种硬件原语,通过比较内存中的值与期望值,如果相等则进行替换,否则不执行替换操作,同时可以返回比较结果标识是否替换成功。
- 使用CAS可以实现乐观锁的方式,例如AtomicInteger、AtomicLong等原子类。
- 在多线程环境下,通过循环CAS操作,直到成功为止。
-
基于版本号的数据库乐观锁:
- 一些数据库提供了内置的乐观锁机制,通过给表增加版本号字段或使用时间戳来实现。
- 在更新数据时,数据库会自动检查当前版本号与允许更新数据的版本号是否一致,如果不一致则认为发生了冲突。
总的来说,乐观锁的实现方式主要依赖于数据版本号或时间戳的比较,以及CAS算法的应用。这些机制可以避免不必要的加锁操作,提高并发性能,但也需要在冲突发生时进行相应的处理。在实际应用中,选择何种乐观锁实现方式需要根据具体的场景和需求来确定。
sleep和wait区别
并发控制机制
在 Java 中,有多种并发控制机制可供选择,用于处理多线程并发访问共享资源时的同步与互斥。以下是几种常见的 Java 并发控制机制:
-
synchronized 关键字:
synchronized 是 Java 中的关键字,用于实现线程安全的同步机制。它可以修饰方法、代码块和静态方法,对对象或类进行加锁,防止多个线程同时访问共享资源造成的数据不一致性或并发问题。
-
Lock 接口:
Java 提供了 Lock 接口及其实现类,如 ReentrantLock,用于更灵活地实现线程同步。通过 Lock 接口,可以手动控制线程的加锁和解锁操作,相比使用 synchronized 关键字,Lock 接口提供了更多的扩展功能,如可重入锁、公平性等。
-
Condition 接口:
Condition 接口是 Java 提供的一种线程同步机制,通常与 Lock 接口一起使用,用于实现等待/通知模
式。Condition 接口提供了对等待和唤醒线程的支持,可以实现更细粒度的线程同步控制。下面是 Condition 接口的主要方法:
await():
void await() throws InterruptedException
当线程执行 await() 方法时,会释放当前持有的锁并进入等待状态,直到其他线程调用相应 Condition 的 signal() 或 signalAll() 方法唤醒该线程。signal():
void signal()
唤醒一个等待在 Condition 对象上的一个线程。如果有多个线程在等待,只会唤醒其中的一个线程。
Condition 接口可通过 Lock.newCondition() 方法创建,如下所示:Lock lock = new ReentrantLock(); Condition condition = lock.newCondition();
使用 Condition 接口的经典模式是等待/通知模式。例如,一个线程在某个条件满足时进行等待,而另一个线程在满足该条件时进行通知,示例代码如下:
Condition condition = lock.newCondition(); boolean conditionMet = false; // 等待线程 lock.lock(); try { while (!conditionMet) { condition.await(); } // 执行等待满足条件后的操作 } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } // 唤醒线程 lock.lock(); try { conditionMet = true; condition.signal(); } finally { lock.unlock(); }
Condition 接口的使用可以实现更灵活的线程同步和通信,允许线程之间更精细地协调和控制。需要注意的是,在使用 Condition 接口时,对应的 Lock 对象必须进行正确的加锁和释放锁操作,以保证线程同步的正确性。
- AtomicInteger 和 AtomicReference:
Java 提供了一系列的原子类,如 AtomicInteger 和 AtomicReference 等。这些原子类提供了原子性操作,能够在不需要显式使用锁的情况下,实现线程安全的操作,用于处理多个线程修改同一个变量的情况。
AtomicInteger: AtomicInteger是原子整型类,提供了一系列的原子操作方法,例如:
get()、set()、incrementAndGet()、decrementAndGet()等。
通过使用AtomicInteger可以避免多个线程对同一个整型变量进行并发修改导致的数据不一致性问题。示例:
AtomicInteger atomicInt = new AtomicInteger(0); atomicInt.incrementAndGet(); // 自增并返回新值 atomicInt.getAndAdd(5); // 增加指定值并返回旧值 atomicInt.compareAndSet(10, 15); // 当值为10时设置为15
AtomicReference: AtomicReference是原子引用类,允许你在多线程环境下对对象的引用进行原子性更新操作。它提供了一系列的操作方法,如:
get()、set()、compareAndSet()等。
通过使用AtomicReference,可以确保对于给定的对象只有一个线程可以修改它,从而避免了线程安全问题。示例:
AtomicReference<String> atomicRef = new AtomicReference<>(); atomicRef.set("Hello"); // 设置引用对象 atomicRef.compareAndSet("Hello", "World"); // 当引用对象为Hello时设置为World String value = atomicRef.get(); // 获取引用对象的值
这些原子类利用了底层硬件提供的原子性操作,使用无锁算法实现了高效且线程安全的操作。它们在处理多线程并发访问共享资源时非常有用,能够提供更好的性能和可靠性。
需要注意的是,尽管原子类可以解决一些并发访问问题,但不能解决所有的并发问题,因此在设计并发应用时,仍需综合考虑适当的同步机制和正确的数据访问策略,以保证线程安全和数据的一致性。
-
并发集合类:
Java 提供了一系列的并发集合类。这些并发集合类在多线程环境下提供了高效的并发访问能力,避免了手动进行同步的复杂性,常用于解决多个线程间共享数据的问题。
这些并发集合类实现了线程安全的数据结构,并提供了一些额外的功能。以下是几种常见的并发集合类:
- ConcurrentHashMap: ConcurrentHashMap 是一个线程安全的哈希表,它提供了高并发访问的能力。相比于传统的 HashMap,ConcurrentHashMap 使用了分段锁(Segment)的机制,使得多个线程可以同时读取不同的分段,从而提高了并发读取的性能,同时仍然保持线程安全。
它将整个数据结构分成多个段(Segment),每个段维护着一个独立的哈希表(Hash Entry数组)。分段锁的原理如下:
ConcurrentHashMap 将哈希表划分为一系列的段(Segment),每个段拥有自己的锁。
默认情况下,ConcurrentHashMap 初始化时创建16个段,可以通过构造函数参数来指定段的数量。每个段都是一个独立的锁。
当进行读写操作时,ConcurrentHashMap 首先使用给定的键的哈希值找到对应的段。每个段维护一个独立的哈希表,读写操作只会锁定对应的段,不会锁住整个 ConcurrentHashMap。 在进行写操作时,只有涉及到的段被锁住,其他段可以独立并发地进行读操作。
如果多个线程同时访问不同的段,它们可以并发进行操作,互不影响,从而提高了并发性能。
当涉及到对同一个段的并发修改时,该段会通过内部的锁来保证同一时刻只有一个线程进行修改。 通过分段锁的机制,ConcurrentHashMap 在保证线程安全的同时,可以允许多个线程同时读取不同的段,提高了读取操作的并发性能。在进行写操作时,它只需要锁住涉及到的段,而不需要锁住整个 ConcurrentHashMap,从而减小了锁竞争的范围,提高了写操作的并发性能。
需要注意的是,ConcurrentHashMap 仅保证了单个段内的数据一致性和线程安全性,不同段之间的操作并不互斥。因此,在某些情况下,可能需要通过额外的同步机制来保证整个 ConcurrentHashMap 的一致性。
- ConcurrentLinkedQueue:
ConcurrentLinkedQueue 是一个基于链表实现的无界非阻塞并发队列。ConcurrentLinkedQueue 适用于多生产者多消费者的场景,没有容量限制,因此需要根据实际需求进行控制和管理。 另外,由于它是基于链表实现的,对于频繁的插入和删除操作,效率较高,但对于随机访问和索引操作效率较低。 因此,在选择使用 ConcurrentLinkedQueue 时,应根据具体的场景和需求进行评估和选择。
ConcurrentLinkedQueue 的线程安全性是通过基于非阻塞算法、无需加锁和逐节点操作这些特性来实现的。这使得多个线程可以同时对队列进行操作,而不会产生竞态条件或资源争用问题。
CopyOnWriteArrayList: CopyOnWriteArrayList 是一个线程安全的动态数组,它通过实现写时复制(Copy-On-Write)的机制来保证线程安全。在进行修改操作时,会创建一个新的底层数组来拷贝原有的数据,从而保证原有数据的不可变性。由于读取操作不会涉及到锁或复制操作,因此读取性能很高。
ConcurrentSkipListSet: ConcurrentSkipListSet 是一个基于跳表(Skip List)实现的有序集合,并且它是线程安全的。跳表是一种类似于平衡二叉树的数据结构,它通过多级索引来快速定位元素。ConcurrentSkipListSet 支持高效的并发读写操作,并且底层数据结构保证了元素的排序。
这些并发集合类都提供了线程安全的并发访问控制机制,能够在高并发的场景下提供良好的性能和可靠性。在使用这些集合类时,需要注意选择合适的集合类来满足具体的需求,并根据并发访问的特点进行合理的选择。
- 线程池
线程池是一种并发编程的技术,用于管理和复用线程资源,提高并发性能和资源利用率。线程池维护了一个线程池的线程集合,可以根据需要自动创建、销毁和调度线程,以执行提交的任务。
线程池的工作原理如下:
创建线程池:首先,需要创建一个线程池。线程池一般会预先创建一定数量的线程,这些线程处于等待状态,准备接收任务。
提交任务:当有任务需要执行时,可以将任务提交给线程池。任务可以通过 Runnable 或 Callable 接口封装为一个可执行的任务对象。
任务队列:线程池会维护一个任务队列,用于存储尚未执行的任务。当有任务提交时,线程池会将任务添加到任务队列中。
选择线程执行任务:线程池中的空闲线程会从任务队列中获取任务进行执行。线程会循环监听任务队列,当队列中有任务时,线程会依次获取任务进行执行。
执行任务:线程从任务队列中获取任务后,会调用任务对象的 run() 方法或者 call() 方法执行任务。任务可以是耗时的、IO 密集的或者计算密集的操作。
线程池管理:线程池会根据需要动态调整线程数量。如果所有的线程都处于繁忙状态,并且任务队列已满,线程池会考虑创建新的线程来执行任务。如果线程处于空闲状态过长时间,线程池可能会销毁一些线程,以减少资源占用。
使用线程池的好处包括:
- 降低线程创建和销毁的开销,提高系统性能和资源利用率。
- 控制并发线程数量,避免线程过多导致系统负载过高。
- 提供任务调度和管理的功能,包括任务队列、线程池大小控制和任务执行情况监控等。
线程池在多线程编程中有广泛的应用场景,下面列举了一些常见的应用场景:
并发任务处理:当有大量任务需要并发执行时,可以使用线程池来管理和调度线程,以提高并发性能和资源利用率。例如,网络服务器中的请求处理、批量数据处理、并行计算等场景都可以使用线程池来实现。
异步任务执行:当有需要异步执行的任务时,可以使用线程池来提交任务,并在后台线程中执行。例如,异步加载文件、发送邮件、处理消息等场景都可以使用线程池来提高响应性能和并发处理能力。
定时任务调度:在需要定时执行任务的场景下,可以使用线程池的定时调度功能来实现。通过调度器可以周期性地执行指定的任务,例如定时备份、定时清理等。
并行计算:当有需要进行大规模计算或处理的任务时,可以使用线程池来将任务分配到多个线程中进行并行计算,提高计算速度和效率。例如,数据分析、图像处理、模型训练等都可以使用线程池实现并行计算。
线程复用:在需要频繁创建和销毁线程的场景下,使用线程池可以避免频繁的线程创建和销毁开销,提高性能和资源利用率。线程池可以维护一定数量的预创建线程,根据任务需求复用这些线程,减少线程创建开销,提高响应速度。
并发控制:有时候需要在多个线程并发执行的情况下控制并发数量。线程池可以通过控制线程数量和任务队列大小来限制并发数量,避免资源过度占用和系统负载过高。
注意,在使用线程池时需要根据场景和需求来选择恰当的线程池类型和参数配置。不同的应用场景可能需要不同的线程池策略,如固定线程数、可缓存线程、单线程池等。合理使用线程池可以提高并发性能、降低资源消耗,并简化多线程编程的复杂性。
Java 中的 java.util.concurrent 包提供了 Executor 框架和 ThreadPoolExecutor 类,用于创建和管理线程池。在使用线程池时,需要根据实际需求合理地配置线程池的参数,如核心线程数、最大线程数和任务队列大小,以满足系统的并发处理需求。
除了上述机制,Java 还提供了CountDownLatch、Semaphore 等工具类和接口,用于更好地控制和管理多线程的执行。
在选择并发控制机制时,需要根据具体的业务需求和线程安全性的要求,综合考虑各种因素来选择合适的机制。同时,注意使用这些并发机制时要正确处理线程之间的竞争条件和互斥问题,以确保数据的正确性和线程的安全性。
SpringCloud原理
Spring Cloud 是一个基于 Spring 框架的开源微服务架构工具,它提供了一系列工具和组件,用于开发和管理分布式系统中的微服务。Spring Cloud 的核心原理和组件包括:
-
服务注册与发现:Spring Cloud 使用服务注册与发现机制来管理微服务的注册和发现。它基于 Eureka、Consul、Zookeeper 等注册中心实现,微服务通过注册中心将自身的信息注册,并可以通过查询注册中心获取其他微服务的信息,实现服务之间的动态调用与通信。
-
负载均衡:在微服务架构中,服务消费方可能需要调用多个提供同一服务的实例。Spring Cloud 通过负载均衡机制来分发请求到不同的服务实例,提高系统的性能和可用性。它可以使用 Ribbon、Nacos 等组件实现负载均衡。
-
服务熔断:为了保护系统免受故障服务的影响,Spring Cloud 引入了熔断机制。它使用 Hystrix 组件实现服务熔断,当某个微服务出现故障或超时时,Hystrix 会通过断路器拦截请求并执行预设的降级逻辑,防止故障向下游传递。
-
服务网关:Spring Cloud 提供了服务网关组件 Zuul、Gateway 等,用于实现微服务的统一入口和请求转发。服务网关可以处理请求的路由、负载均衡、安全认证等功能,简化了客户端与微服务之间的通信。
-
配置中心:Spring Cloud 提供了配置中心组件 Config,它可以集中管理微服务的配置信息,支持动态刷新配置。通过配置中心,可以实现配置的集中管理和动态变更,减少对微服务的重启和部署。
-
链路追踪:在分布式系统中,调用链路的追踪和监控对于故障排查和性能优化非常重要。Spring Cloud 使用 Sleuth、Zipkin 等组件实现分布式链路追踪,记录每个请求经过的微服务,以及请求的时间和耗时信息。
-
分布式消息:分布式系统中,微服务之间需要进行消息的异步通信。Spring Cloud 使用消息中间件(如 RabbitMQ、Kafka)来实现分布式消息的发送和接收,通过消息队列实现微服务之间的解耦和异步通信。
通过上述组件和机制,Spring Cloud 可以帮助开发人员快速搭建和管理分布式微服务系统,提供了一整套解决方案,大大简化了分布式系统的开发和维护。
HashMap底层实现原理
哈希函数、数组和链表/红黑树
数组+链表;当链表长度超过一定阈值(默认为8)时,会将链表转换为红黑树,以提高查找的效率。
HashMap加载因子为什么是0.75?
浪费的空间,hash冲突的几
而较低的加载因子会减少哈希碰撞的概率,提高查找性能,但也会导致桶数组中有较多的空闲空间,造成空间的浪费。
Spring注解
Spring 框架提供了很多注解,下面是几个常用的 Spring 注解以及它们的意思、用法和示例:
-
@Component
该注解用于标记一个类为组件,通常是普通的 Java 类。被标记为 @Component 的类会被自动扫描并注册为 Spring 的 Bean。
示例:
@Component public class MyComponent { // 类的具体实现 }
-
@Controller
该注解用于标记一个类为控制器,通常用于处理请求的入口。@Controller 常用于 Spring MVC 中。
示例:
@Controller public class MyController { @RequestMapping("/hello") public String hello() { return "Hello, Spring!"; } }
-
@Service
该注解用于标记一个类为服务层组件,通常用于标记处理业务逻辑的类。
示例:
@Service public class MyService { public String generateMessage() { return "Hello, Spring!"; } }
-
@Repository
该注解用于标记一个类为数据访问层组件,通常用于标记访问数据库或其他数据源的类。
示例:
@Repository public class UserRepository { public User getUserById(Long id) { // 从数据库中获取用户 } }
-
@Autowired
该注解用于自动装配 Bean,通过类型自动寻找并注入合适的 Bean。通常与构造器、字段或 setter 方法一起使用。
示例:
@Service public class MyService { private final UserRepository userRepository; @Autowired public MyService(UserRepository userRepository) { this.userRepository = userRepository; } }
这些只是 Spring 框架中一些常用注解的示例,每个注解都有更多的属性和用法。具体使用哪个注解取决于你的需求和应用程序的结构。还可以结合 Spring 文档和相关教程进一步了解每个注解的用法和细节。
SpringBoot注解
Spring Boot 提供了很多注解,下面是几个常用的 Spring Boot 注解以及它们的意思、用法和示例:
-
@SpringBootApplication
该注解用于标记一个类为 Spring Boot 的主启动类,同时也是一个配置类。通常将该注解添加在项目的入口类上。
示例:
@SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }
-
@RestController
该注解用于标记一个类为 RESTful 接口控制器,主要用于处理 HTTP 请求并返回 RESTful 风格的响应结果。
示例:
@RestController public class MyController { @GetMapping("/hello") public String hello() { return "Hello, Spring Boot!"; } }
-
@ControllerAdvice
该注解用于定义全局的异常处理器或全局通用属性的绑定。可以捕获控制器中的异常并进行统一的处理。
示例:
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity<String> handleException(Exception e) { // 异常处理逻辑 } }
-
@Configuration
该注解用于标记一个类为配置类,通常与其他注解一起使用。在配置类中可以定义 Bean、配置属性等。
示例:
@Configuration public class MyConfig { @Bean public UserService userService() { return new UserService(); } }
-
@Value
该注解用于获取配置属性的值,并进行注入。可以用来注入配置文件中的属性值或通过其他方式配置的属性值。
示例:
@RestController public class MyController { @Value("${myapp.message}") private String message; @GetMapping("/message") public String getMessage() { return message; } }
这些只是 Spring Boot 中一些常用注解的示例,每个注解都有更多的属性和用法。具体使用哪个注解也要根据项目的需求和具体场景来定。参阅 Spring Boot 文档和相关教程可以进一步了解每个注解的详细用法和细节。
SpringCloud注解
Spring Cloud 提供了多个注解,每个注解都有不同的用途和功能。下面是几个常用的 Spring Cloud 注解及其用法:
-
@EnableDiscoveryClient
用于启用服务注册与发现功能,将服务注册到注册中心并可以通过服务名发现其他服务。适用于使用 Spring Cloud Netflix Eureka、Consul、Zookeeper 等作为服务注册中心的场景。
示例:
@SpringBootApplication @EnableDiscoveryClient public class ProductServiceApplication { public static void main(String[] args) { SpringApplication.run(ProductServiceApplication.class, args); } }
-
@EnableEurekaServer
用于启用 Eureka 服务器,即将当前应用作为 Eureka 服务注册中心。适用于使用 Eureka 作为服务注册中心的场景。
示例:
@SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
-
@LoadBalanced
用于实现客户端负载均衡,将 RestTemplate 或 WebClient 注册为具有负载均衡能力的 Bean。适用于需要通过服务名进行服务之间的调用的场景。
示例:
@SpringBootApplication public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } @LoadBalanced @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
-
@FeignClient
用于声明一个声明式的 REST 客户端,简化了通过 HTTP 调用服务的代码实现。通过指定服务名和接口定义,Feign 将会自动生成代理对象,并处理负载均衡、服务降级、熔断等功能。适用于依赖其他微服务提供的接口的场景。
示例:
@FeignClient(name = "product-service") public interface ProductServiceClient { @GetMapping("/products/{id}") ProductDTO getProductById(@PathVariable("id") Long id); }
这些只是 Spring Cloud 提供的一些常用注解的示例,每个注解都有更详细的用法和属性,具体的用法可以根据实际需求和文档进行参考和深入学习。
Java类加载器
当JVM运行时,首先会调用启动类加载器来加载Java核心类库,然后再由扩展类加载器和应用程序类加载器来加载其他类。
Java内存管理
Java虚拟机的内存区域主要分为以下几个部分:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区和运行时常量池。
程序计数器是一块较小的内存空间,它的作用是记录当前线程所执行的字节码的行号,即下一个要执行的指令地址。
Java虚拟机栈也是一块较小的内存空间,它用于存储方法调用和返回值等信息。每个线程都有一个独立的Java虚拟机栈,其生命周期与线程相同。
本地方法栈与Java虚拟机栈类似,不同的是它是为native方法服务的。
堆是最大的一块内存空间,它用于存储对象实例。Java虚拟机堆是垃圾回收器管理的主要区域。
方法区也是一块较大的内存空间,它用于存储类信息、常量、静态变量等数据。
IO
输入流Input
字节流:InputStream
字符流:Reader
字节缓冲流:BufferedInputStream
输出流Output
字节流:OutputStream
字符流:Writer
字节缓冲流:BufferOutputStream
写入一个字节数组再复制,性能更高。
IO设计模式
装饰器模式
适配器模式
工厂模式
观察者模式
Map
HashMap:线程不安全,效率高,可以null,扩容16到2n,当链表长度大于默认值则解决冲突时链表转红黑树。
HashTable:线程安全,效率低,不可null,扩容11到2n+1
TreeMap:集合内可搜索排序,可根据键排序
HashMap 多线程操作导致死循环问题
JDK1.7 及之前版本的 HashMap 在多线程环境下扩容操作可能存在死循环问题,
这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。
为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在数据覆盖的问题。并发环境下,推荐使用 ConcurrentHashMap 。
HashMap 为什么线程不安全?
JDK1.7 及之前版本,在多线程环境下,HashMap 扩容时会造成死循环和数据丢失的问题。数据丢失这个在 JDK1.7 和 JDK 1.8 中都存在,这里以 JDK 1.8 为例进行介绍。JDK 1.8 后,在 HashMap 中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对 HashMap 的 put 操作会导致线程不安全,具体来说会有数据覆盖的风险。