题目来源:https://www.nowcoder.com/discuss/525371909735792640
本期是【捞捞面经】系列文章的第 1 期,持续更新中…。
《捞捞面经》系列正式开始连载啦,据说看了这个系列的朋友都拿到了大厂offer~
ArrayList
和 LinkedList
都是 Java 中常用的 List 实现,但是由于它们内部数据结构的不同,所以在不同的场景下,我们会选择使用不同的List。
总的来说,我们需要根据实际的需求和使用场景来选择合适的 List 实现。
继承Thread类:这是创建线程的最基本方法。我们可以创建一个新的类,继承自 Thread 类,然后重写其 run() 方法,将我们的任务代码写在 run() 方法中。然后创建该类的对象并调用其 start() 方法来启动线程。
实现Runnable接口:我们也可以创建一个新的类,实现 Runnable 接口,然后将我们的任务代码写在 run() 方法中。然后创建该类的对象,将其作为参数传递给 Thread 类的构造器,创建 Thread 类的对象并调用其 start() 方法来启动线程。
实现Callable接口和FutureTask类:Callable 接口与 Runnable 接口类似,但是它可以返回一个结果,或者抛出一个异常。我们可以创建一个新的类,实现 Callable 接口,然后将我们的任务代码写在 call() 方法中。
然后创建该类的对象,将其作为参数传递给FutureTask 类的构造器,创建 FutureTask 类的对象。最后,将FutureTask类的对象作为参数传递给 Thread 类的构造器,创建Thread 类的对象并调用其 start() 方法来启动线程。
使用线程池:Java 提供了线程池 API(Executor框架),我们可以通过 Executors
类的一些静态工厂方法来创建线程池,然后调用其 execute() 或 submit() 方法来启动线程。线程池可以有效地管理和控制线程,避免大量线程的创建和销毁带来的性能开销。
以上四种方式,前两种是最基本的创建线程的方式,但是在实际开发中,我们通常会选择使用线程池,因为它可以提供更好的性能和更易于管理的线程生命周期。
在并发量特别高的情况下,一般推荐使用 ReentrantLock,原因如下:
ReentrantLock
比 synchronized
有更多的功能。例如,ReentrantLoc
k 可以实现公平锁和非公平锁(synchronized只能实现非公平锁);ReentrantLock 提供了一个 Condition 类,可以分组唤醒需要唤醒的线程(synchronized 要么随机唤醒一个线程,要么唤醒所有线程);ReentrantLock 提供了 tryLock
方法,可以尝试获取锁,如果获取不到立即返回,不会像synchronized 那样阻塞。但是,虽然 ReentrantLock 在功能上比 synchronized 更强大,但也更复杂,使用不当容易造成死锁。而 synchronized 由 JVM 直接支持,使用更简单,不容易出错。所以,在并发量不高,对性能要求不高的情况下,也可以考虑使用 synchronized。
ConcurrentHashMap 在 Java 1.7
和 Java 1.8
中的实现方式有所不同,但它们的共同目标都是提供高效的并发更新操作。下面我将分别介绍这两个版本的实现方式。
Java 1.7:在Java 1.7中,ConcurrentHashMap
内部使用一个 Segment 数组来存储数据。每个Segment
对象包含一个 HashEntry 数组,每个 HashEntry 对象就是一个键值对。Segment 对象是可锁定的,每个Segment对象都可以看作是一个独立的 HashMap。
在更新数据时,只需要锁定相关 Segment 对象,而不需要锁定整个HashMap,这样就可以实现真正的并发更新。Segment 的数量默认为 16,这意味着 ConcurrentHashMap 最多支持 16 个线程的并发更新。
Java 1.8:在 Java 1.8 中,ConcurrentHashMap 的实现方式进行了改进。它取消了 Segment 数组,直接使用 Node 数组来存储数据。每个Node对象就是一个键值对。在更新数据时,使用 CAS 操作和 synchronized 来保证并发安全。
具体来说,如果更新操作发生在链表的头节点,那么使用 CAS 操作;如果发生在链表的其他位置,或者发生在红黑树节点,那么使用synchronized。这样,ConcurrentHashMap可以支持更多线程的并发更新。
总的来说,ConcurrentHashMap 通过分段锁(Java 1.7)或 CAS+synchronized(Java 1.8)
的方式,实现了高效的并发更新操作,从而保证了并发安全
这是一个很好的问题。首先,我们需要明确一点,虽然 ReentrantLock
在某些情况下的性能优于synchronized,但这并不意味着在所有情况下都是这样。
实际上,synchronized
在JDK 1.6 及以后的版本中已经进行了大量的优化,例如偏向锁、轻量级锁等,使得在竞争不激烈的情况下,synchronized 的性能已经非常接近甚至超过 ReentrantLock。
在 JDK 1.7的 ConcurrentHashMap中,使用 ReentrantLock(Segment)是为了实现分段锁的概念,即将数据分成多个段,每个段独立加锁,从而实现真正的并发更新。这种设计在当时是非常先进和高效的。
然而,在 JDK 1.8 的 ConcurrentHashMap
中,为了进一步提高并发性能,引入了更复杂的数据结构(如红黑树)和更高效的并发控制机制(如CAS操作)。在这种情况下,使用 synchronized
比 ReentrantLock
更加简单和高效。首先,synchronized 可以直接与JVM进行交互,无需用户手动释放锁,减少了出错的可能性。
其次,synchronized
在 JDK 1.6及以后的版本中已经进行了大量的优化,性能已经非常接近 ReentrantLock。最后,synchronized 可以与其他 JVM 特性(如偏向锁、轻量级锁、锁消除、锁粗化等)更好地配合,进一步提高性能。
总的来说,选择使用 ReentrantLock
还是 synchronized
,需要根据具体的需求和使用场景来决定。在 JDK 1.8
的 ConcurrentHashMap中,使用 synchronized 是一种更加合理和高效的选择。
volatile
关键字可以保证变量的可见性和禁止指令重排序,但不能保证复合操作的原子性。它通常用于标记状态变量。ThreadLocal
可以为每个线程创建一个单独的变量副本,每个线程都只能操作自己的副本,从而实现了线程隔离,保证了线程安全。CountDownLatch
允许一个或多个线程等待其他线程完成操作。Phaser
是 JDK 1.7引入的一个用于解决控制多个线程分阶段共同完成任务的类。以上就是 Java 中常见的一些并发安全的实现方式。
如果不使用 ThreadLocal
来存储用户信息,我会考虑以下几种方式:
以上就是我考虑的几种存储用户信息的方式,具体使用哪种方式,需要根据实际的需求和场景来决定。
对于千万级的数据,如果要判断一个整数是否存在,可以考虑以下几种方法:
O(log n)
,但是它需要数据是有序的。以上就是判断一个整数是否存在的几种方法,具体使用哪种方法,需要根据实际的需求和场景来决定。
布隆过滤器是一种概率型的数据结构,它的主要特点是高效地判断一个元素是否在一个集合中。它的基本原理是通过多个哈希函数将元素映射到一个位数组中。当查询一个元素是否存在时,通过同样的哈希函数计算出位数组中的位置,如果所有的位置都是1,那么就认为元素可能存在;如果有任何一个位置是 0,那么就认为元素一定不存在。
这里的 "可能存在” 和 “一定不存在” 是因为布隆过滤器的特性决定的:
因此,布隆过滤器的一个重要特性就是,它可能会误判一个不存在的元素为存在(假阳性),但是绝不会误判一个存在的元素为不存在(真阴性)。这就是“布隆过滤器说某个元素存在,则大概率在。布隆过滤器说某个元素不在,则一定不在”的含义。
对于千万级的数据,如果直接使用Redis来初始化布隆过滤器确实可能会比较慢。这是因为Redis的操作都是网络IO操作,相比于内存操作,网络IO的速度要慢很多。以下是一些可能的优化方法:
以上就是一些可能的优化方法,具体使用哪种方法,需要根据实际的需求和场景来决定。
多线程间传递值主要有以下几种方式:
以上就是多线程间传递值的几种方式,具体使用哪种方式,需要根据实际的需求和场景来决定。
设计登录黑名单的主要目的是防止恶意用户或者机器人进行暴力破解或者恶意登录。以下是一种可能的设计方案:
IP
地址,记录其登录失败的次数。如果在一定时间内登录失败次数超过某个阈值,那么就将该用户或者 IP
地址加入黑名单。以上就是设计登录黑名单的一种可能的方案,具体的设计可能还需要根据实际的需求和场景来调整。