ThreadLocal
在Java中用于提供线程局部变量,这些变量在每个线程中都有独立的副本,互不干扰。其底层原理可以简要描述如下:
数据存储: 每个线程中都有一个 ThreadLocalMap
的实例,用于存储线程局部变量。键是ThreadLocal
实例本身,值是线程局部变量的副本。
初始化: 当线程第一次通过 get()
或 set()
方法访问 ThreadLocal
变量时,会触发 ThreadLocal
的初始化流程,为线程创建一个 ThreadLocalMap
实例。
获取值: 在调用 get()
方法时,ThreadLocal
会使用当前线程中的 ThreadLocalMap
,并使用自身作为键来获取对应的值。
设值: 在调用 set()
方法时,同样会使用当前线程的 ThreadLocalMap
,并使用 ThreadLocal
实例作为键来存储值。
防止内存泄漏: 由于使用了线程自身的 ThreadLocalMap
,因此有必要在不再需要线程局部变量时清理它们,以防止潜在的内存泄漏。这通常是通过调用 ThreadLocal
的 remove()
方法来完成的。
双亲委派机制是一种强有力的安全和隔离机制,但在某些情况下,如果需要打破这种层级关系,可以通过自定义类加载器并覆盖其加载类的方法来实现。双亲委派机制是Java中类加载器的一种工作机制,它的主要流程如下:
加载检查: 当一个类加载器需要加载一个类时,它不会首先尝试自己加载这个类,而是委派给它的父加载器。
递归委派: 这个委派过程是递归的,最终会达到最顶层的类加载器(如Bootstrap ClassLoader)。
尝试加载: 如果父加载器能够加载这个类,则使用父加载器的结果。如果父加载器无法加载这个类(因为它没有找到这个类),则调用子加载器自己的findClass
方法尝试加载这个类。
安全性与隔离: 这种机制可以确保Java核心库的类型安全,因为无论哪个类加载器最终加载了java核心库的类,都是同一个类加载器(Bootstrap),确保了被信任的类不会被覆盖。
效率: 这种机制也提高了类加载的效率。因为如果一个类加载器已经加载过某个类,那么它的所有子加载器都无需再次加载这个类。
在Java中,线程池可以通过java.util.concurrent.ExecutorService
接口及其实现类来构造和管理。最常用的实现类是ThreadPoolExecutor
。
线程池7大参数
corePoolSize: 线程池的基本大小,即在没有任务需要执行时线程池的大小,也就是线程池中基本的线程数。
maximumPoolSize: 线程池最大的大小,即线程池中允许的最大线程数。
keepAliveTime: 当线程池中线程数量超过corePoolSize
时,多余的空闲线程能存活的最长时间。
unit: keepAliveTime
的时间单位。
workQueue: 工作队列,用于存储等待执行的任务。
threadFactory: 线程工厂,用于创建新线程。
handler: 拒绝策略,当任务太多来不及处理,如何拒绝任务。
拒绝策略
常见的拒绝策略有以下几种:
RejectedExecutionException
异常。多线程是一种使得多个线程并发执行的编程技术,通常用于提高程序的执行效率和响应速度。Java中的多线程可以通过Thread
类或者实现Runnable
接口来创建和管理。
异步编程是一种编程范式,允许程序在等待某些操作完成的时候继续执行其他任务。在Java中,异步编程可以通过Future
、CompletableFuture
或者使用Reactive编程库如Project Reactor来实现。异步编程的关键是将耗时的操作放在另一个线程或者任务中执行,从而不阻塞当前线程,提高程序的整体效率。
定义:
资源分配和独立性:
创建和管理:
通信:
资源竞争: 多个线程可能会同时访问共享资源,导致资源竞争,需要操作系统进行调度和管理。
上下文切换: 频繁的线程切换会导致大量的CPU时间被用于上下文切换,降低系统效率。
死锁: 线程之间因资源竞争可能出现死锁的情况,需要操作系统介入解决。
稳定性和安全性: 不合理的线程操作可能导致程序崩溃,甚至影响操作系统的稳定性。
HashMap
在Java中是一种基于哈希表的Map接口实现,其主要数据结构包括:
数组: 存储元素的主体是一个数组。
链表: 当哈希冲突时,元素会以链表的形式存储在数组的同一个索引位置。
红黑树: 当链表长度超过一定阈值时,链表会转换为红黑树以提高查找效率。
使用HashMap的注意点:
线程不安全: HashMap
是线程不安全的,多线程操作时需要注意同步。
初始容量和加载因子: 合理设置初始容量和加载因子可以提高HashMap
的性能。
Key的哈希函数: Key对象的哈希函数需要合理设计,以避免大量的哈希冲突。
Key的不可变性: 作为Key的对象最好是不可变的,这样可以确保哈希值的一致性。
Null值: HashMap
允许使用null作为Key和Value,但最好避免这样做以防止出现歧义。
容量扩展: 当元素数量达到容量和加载因子的乘积时,HashMap
会进行扩容操作,这是一个耗时的过程,需要注意。
遍历效率: 如果HashMap的容量远大于实际存储的键值对数量,遍历HashMap的效率会较低,因为需要遍历很多空的桶。
在HashMap
中,桶的定位是通过哈希算法来实现的。具体步骤如下:
计算Key的HashCode: 调用Key对象的hashCode()
方法计算出其哈希值。
扰动函数处理: 为了减少哈希碰撞,将HashCode经过扰动函数处理,充分混合HashCode的高位和低位。
计算索引位置: 使用处理过的HashCode值与数组长度减一进行位与操作,计算出桶的位置。
index = (n - 1) & hash
其中,n
是数组的长度,hash
是处理过的HashCode值。
为了使存储100个Key-Value对的效率最高,可以考虑以下策略:
设置合适的初始容量: 设置一个合适的初始容量,以减少或消除扩容操作。考虑到加载因子默认为0.75,为避免扩容,初始容量应设置为至少134(100 / 0.75)。
设计良好的Key哈希函数: 确保Key的哈希函数设计得足够好,能够均匀地分布哈希值,减少哈希冲突。
避免使用可变对象作为Key: 使用不可变对象(如字符串或整数)作为Key,以确保在HashMap的整个生命周期内,Key的哈希值保持不变。
原理:
扰动函数是对哈希值进行一系列位操作的过程,目的是为了更好地分散原始哈希码的位信息,使得哈希值在桶数组中的分布更加均匀。在Java 8中,HashMap
的扰动函数主要通过hash >>> 16
实现,即将32位的原始哈希码向右无符号移动16位。
作用:
扰动函数的主要作用是为了减少哈希冲突,提高HashMap的查找效率。通过将哈希码的高16位和低16位进行异或操作,能够使得原本在低位分布不均匀的哈希码在数组中的分布更加均匀。
为什么能减少哈希冲突:
扰动函数通过混合哈希码的高位和低位信息,使得即使原始哈希码在低位上分布不均匀,经过扰动函数处理后也能在数组中均匀分布,从而减少了哈希冲突的可能性。这样,即使在遇到较差的哈希函数时,HashMap
的性能也能得到一定程度的保障。
在HashMap进行rehash操作时,它需要重新计算每个存储在HashMap中的元素的桶位置。这是因为当HashMap的容量变化时(通常是翻倍),桶的数量也发生了变化,这就需要重新计算每个元素在新数组中的位置。这个过程通常涉及到重新计算元素键的哈希值,并根据新的数组大小重新定位元素。
定义
接口(Interface): 是一种完全抽象的结构,只能定义方法的签名,不能包含方法的实现。
抽象类(Abstract Class): 是一种可能包含抽象方法(没有实现的方法)的类。
实现
接口: 一个类可以实现多个接口。
抽象类: 一个类只能继承一个抽象类。
方法
接口: Java 8之后,接口可以包含默认方法(有实现的方法)和静态方法。但在此之前,接口中的所有方法都必须是抽象的。
抽象类: 可以包含抽象方法和非抽象方法。
构造器
接口: 不能有构造器。
抽象类: 可以有构造器。
成员变量
接口: 只能定义常量(public static final)。
抽象类: 可以定义变量,可以有成员变量。
访问修饰符
接口: 方法默认是public的。
抽象类: 方法可以有任意访问修饰符。
多态的实现通常依赖于继承和方法重写。在面向对象编程中,多态允许你将子类类型的对象赋值给父类类型的引用变量。这样,你就可以使用父类的引用来调用在子类中重写的方法。
继承: 提供了一种机制,允许子类继承父类的属性和方法。
重写(Overriding): 在子类中提供了一种方法,这种方法与在其父类中定义的方法具有相同的方法签名。
虽然多态的典型实现依赖于继承和重写,但Java也支持接口实现的多态,你可以有一个接口和多个实现该接口的类。在这种情况下,多态是通过实现接口的不同类来实现的,而不是通过继承和重写。
AQS(AbstractQueuedSynchronizer)是Java中提供的一种用于构建锁和同步器的框架。AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。AQS支持两种资源共享方式:
独占模式(Exclusive):
在这种模式下,每次只能有一个线程持有锁。AQS会将尝试获取锁的其他线程放入队列中。
ReentrantLock
是一个典型的独占模式的应用。它确保了每次只有一个线程能够执行被保护的代码区域。共享模式(Shared):
在这种模式下,多个线程可以共同持有锁。具体有多少个线程可以共享锁,取决于同步器的实
Semaphore
:一个计数信号量,允许多个线程同时访问。ReadWriteLock
:它有一个读锁和一个写锁,读锁可以被多个读线程持有,但写锁是独占的。CountDownLatch
:允许一个或多个线程等待其他线程完成操作。Semaphore(信号量)
CountDownLatch(倒计时锁存器)
总的来说,Semaphore更多的是用来控制对特定资源的访问线程数目,而CountDownLatch用于一个线程等待多个线程完成操作后再执行。
Java的类加载过程包括以下几个主要阶段:
加载(Loading)
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。验证(Verification)
准备(Preparation)
解析(Resolution)
初始化(Initialization)
类加载的机制包括:
OOM的常见情况:
堆内存溢出(Java Heap Space):创建的对象过多,超过了堆的最大限制。
栈内存溢出(Stack Overflow):方法调用层次太深,超出了栈的最大限制。
方法区溢出:常量池或类元数据信息太多,超过了方法区的最大限制。
直接内存溢出:使用NIO时,申请的直接内存过大。
线程创建过多:创建的线程数目超过系统承载极限。
解决方法:
分析堆转储文件:使用工具(如Eclipse Memory Analyzer)分析堆转储文件,查找内存泄漏或者大对象。
调整JVM参数:根据具体情况调整JVM启动参数,如-Xms, -Xmx, -Xss等。
代码优化:优化代码逻辑,减少内存占用,避免内存泄漏。
资源清理:确保使用完资源后进行适时的清理,如关闭文件、数据库连接等。
使用Profiler工具:使用Profiler工具对运行时的程序进行分析,找出内存占用高或者线程活动异常的地方进行优化。
通过这些方法,可以有效地避免或解决OOM问题,确保程序的健壮性和稳定性。
线程安全理解
解决线程安全问题的方法
ConcurrentHashMap的线程安全性
ConcurrentHashMap1.7和1.8的区别
分段锁的可重入性
分段锁的可重入性取决于其实现,ConcurrentHashMap中的分段锁是可重入的,因为它使用了ReentrantLock来实现锁的功能。
可重入锁理解
公平锁
非公平锁
非公平锁吞吐量为什么比公平锁大:
非公平锁之所以有更高的吞吐量,是因为它减少了线程切换的次数和等待的时间。在锁被释放时,非公平锁允许新请求锁的线程直接获得锁,而不是把锁分配给之前在队列中等待的线程。这样就省去了唤醒线程、线程切换等开销,提高了执行效率。
JVM内存结构:
为什么把堆区进一步划分:
通过对堆区的划分,JVM能够更精细地管理内存,提升垃圾回收的效率,从而提高整体的系统性能。
MySQL的存储引擎:
B+树:
B树:
B树和B+树的不同点:
创建方式:
在MySQL中,你可以通过CREATE INDEX
或在CREATE TABLE
时定义索引来创建联合索引。
CREATE INDEX index_name ON table_name (column1, column2, ...);
数据结构:
聚簇索引将数据存储与索引放到了一起,表中的数据按照每张表的主键构造一棵B+树,同时叶子节点中存放的就是整条记录的数据。
是否每张表都有
所以,每张InnoDB表都会有聚簇索引,但这个聚簇索引可能不是由显示定义的主键来构建的。
覆盖索引是指一个索引包含(或覆盖)了查询中需要的所有字段的情况。
特点
应用场景
使用场景
数据类型
设计原则
实现方式
SETNX key value
,当且仅当key不存在时,为key设置指定的值,返回1;key已存在,什么也不做,返回0。EXPIRE
命令来为锁设置一个过期时间。GET
命令获取锁的值(客户端标识)进行判断。解锁可以使用DEL
命令。注意事项
SET
命令在2.6.12版本添加了选项,可以确保设置值和设置过期时间的原子操作。SET key value EX seconds NX
。基本原则
实现方法
分布式事务确保在分布式系统中,事务的ACID属性仍然得以保持。
常见的实现方式有:
在处理数据库与缓存一致性问题时,通常追求的是高性能、高可用,而分布式事务在这些方面有一定的劣势,因此一般不会选用分布式事务来解决这类问题。相对来说,采用延时双删、消息队列等策略更加轻量级,更容易保证系统的高性能和高可用。
在以下情况下,通常需要考虑设计索引:
与B+树相比,跳表在一些场景下提供了更好的性能,尤其是在内存数据库和高并发环境下,且实现相对更为简单。这是Redis选择使用跳表而不是B+树的主要原因。
Channel:代表一个网络套接字或者组件,用于处理数据的传输。
NioSocketChannel
:用于TCP网络IO。NioDatagramChannel
:用于UDP网络IO。FileChannel
等。EventLoop:用于处理Channel的I/O操作。
ChannelPipeline:持有一系列ChannelHandler实例,用于处理入站和出站I/O事件。
ChannelHandler
:处理I/O事件的接口,可以自定义处理逻辑。ChannelPipeline
实现了Interceptor模式,可以通过它来实现网络通信中的编码、解码、安全认证等功能。JDK序列化机制是Java提供的一种将对象转换为字节流的方法,也可以从字节流中恢复对象。其主要用于网络传输、本地存储等。
原理:
对象序列化:将对象的状态信息转换为可以存储或传输的形式。
ObjectOutputStream
写出对象。Serializable
接口。对象反序列化:从序列化状态恢复为对象。
ObjectInputStream
读取对象。serialVersionUID
需要匹配。底层原理:
writeObject
和readObject
方法自定义序列化逻辑:如果类中存在这两个方法,JDK序列化机制会调用它们来进行序列化和反序列化。通过使用Spring Session,可以在分布式环境下提供更加强大、灵活的会话管理能力。
TCP协议为了保证数据可靠传输,采用了三次握手和四次挥手的机制。四次挥手主要是为了断开一个TCP连接,其过程如下:
import heapq
def top_k_ips(filename, k):
ip_count = {}
with open(filename, 'r') as f:
for line in f:
ip = line.strip()
if ip in ip_count:
ip_count[ip] += 1
else:
ip_count[ip] = 1
# 使用最小堆找出出现次数最多的top K个IP
min_heap = []
for ip, count in ip_count.items():
if len(min_heap) < k:
heapq.heappush(min_heap, (count, ip))
elif count > min_heap[0][0]:
heapq.heappop(min_heap)
heapq.heappush(min_heap, (count, ip))
# 输出结果
while min_heap:
count, ip = heapq.heappop(min_heap)
print(f"IP: {ip}, Count: {count}")
# 测试
top_k_ips('ips.txt', 3)
上述代码中,我们使用了Python的heapq
模块来维护一个最小堆,从而高效地找到出现次数最多的top K个IP地址。
import threading
def print_odd_even(max_num):
num = 1
odd_even_lock = threading.Lock()
def print_odd():
nonlocal num
while num < max_num:
with odd_even_lock:
if num % 2 == 1:
print("Odd:", num)
num += 1
odd_even_lock.notify()
else:
odd_even_lock.wait()
def print_even():
nonlocal num
while num < max_num:
with odd_even_lock:
if num % 2 == 0:
print("Even:", num)
num += 1
odd_even_lock.notify()
else:
odd_even_lock.wait()
odd_thread = threading.Thread(target=print_odd)
even_thread = threading.Thread(target=print_even)
odd_thread.start()
even_thread.start()
odd_thread.join()
even_thread.join()
print_odd_even(10)
在这个例子中,我们使用了一个锁(threading.Lock
)和Python的with
语句来简化线程同步的逻辑。两个线程“交替”获取锁,根据当前数的奇偶性来决定打印并递增数值。odd_even_lock.notify()
用于唤醒等待在锁上的另一个线程,而odd_even_lock.wait()
则使当前线程等待直到另一个线程调用notify()
。