1.线程安全的集合有哪些?
Java中提供了多种线程安全的集合类,它们位于java.util.concurrent
包下。以下是一些常见的线程安全集合:
-
ConcurrentHashMap
- 类似于HashMap,但它是线程安全的,允许并发读写操作,并且在高并发场景下的性能优于同步HashMap。
-
CopyOnWriteArrayList
- 线程安全的List实现,当修改集合时(如添加、删除元素),它会创建一个新的数组副本进行修改,而遍历操作则总是看到不变的原数据视图,适用于读多写少的并发场景。
-
CopyOnWriteArraySet
- 基于CopyOnWriteArrayList实现的线程安全Set,具有同样的复制更新机制。
-
ConcurrentLinkedQueue
- 一个线程安全的FIFO队列,适合用于多生产者-多消费者模式,支持高效的非阻塞并发插入和移除操作。
-
BlockingQueue接口的实现
- 如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue等,这些是线程安全的队列,支持阻塞的插入和获取操作,以及容量限制等功能。
-
ConcurrentSkipListMap和ConcurrentSkipListSet
- 分别为线程安全的有序映射和有序集,内部采用跳跃表(Skip List)数据结构,支持高效并发访问和排序功能。
-
ConcurrentLinkedDeque
- 双向链表实现的线程安全deque(双端队列),支持从两端添加和移除元素。
-
CyclicBarrier, CountDownLatch, Semaphore
- 虽然不是传统的集合,但它们是线程协作工具,可以用来控制多个线程之间的同步行为。
-
ConcurrentHashSet
- 这是一个不太常见的集合类,可通过
Collections.newSetFromMap(new ConcurrentHashMap<>()
方式创建一个线程安全的HashSet。
- 这是一个不太常见的集合类,可通过
总之,Java通过java.util.concurrent
包提供了一系列线程安全的数据结构,能够满足不同场景下的并发需求。
2 hashMap为什么不是线程安全的?底层原理是什么?
HashMap在Java中不是线程安全的原因在于其内部实现的并发控制机制不足。HashMap的设计并没有考虑多线程环境下的同步问题,当多个线程同时对HashMap进行读写操作时,可能会出现数据不一致的问题。
底层原理:
-
结构设计: HashMap底层基于数组和链表(或红黑树)实现,通过哈希函数计算key的哈希值,并根据这个值将元素存储到数组的特定位置上。如果哈希冲突,则使用拉链法解决冲突,即将相同哈希值的元素链成一个链表。
-
线程不安全性表现:
- 扩容(resize):当HashMap的容量达到阈值时,会自动扩容并重新哈希所有元素。在这个过程中,如果没有适当的同步控制,多个线程可能同时触发扩容,导致元素丢失或重复。
- put、remove等操作:在添加或删除元素时,需要修改链表结构或者调整数组大小,这些操作涉及到了共享变量的修改,未加锁的情况下可能会导致数据不一致,例如死循环、元素丢失等问题。
- 并发读写:在读取的同时有其他线程正在进行写操作,可能导致读取到的数据是正在被修改而尚未完成的状态,即脏读现象。
为了确保线程安全,在多线程环境下通常建议使用java.util.concurrent
包下的ConcurrentHashMap
类,它采用了分段锁(Segment Lock)技术来保证在并发访问时的安全性,从而解决了HashMap的线程安全问题。在JDK 8及以后版本中,ConcurrentHashMap进一步优化了锁机制,使用了一种粒度更细的CAS+Synchronized的方式来提高并发性能。
3 请用代码实现同时开启一个线程打印1~52,开启另一个线程打印A~Z,最终交替打印1A2B3C...这样的数据
这是一个典型的多线程同步问题,可以使用Java的synchronized
关键字和条件变量(如wait()
、notify()
或java.util.concurrent.locks.Condition
)来实现。以下是一个简单的示例:
Java
1import java.util.concurrent.locks.Condition;
2import java.util.concurrent.locks.Lock;
3import java.util.concurrent.locks.ReentrantLock;
4
5public class PrintAlternating {
6 private static final int MAX_NUM = 52;
7 private static final char START_CHAR = 'A';
8 private static final Lock lock = new ReentrantLock();
9 private static final Condition condition1 = lock.newCondition();
10 private static final Condition condition2 = lock.newCondition();
11 private static int number = 1;
12 private static char letter = START_CHAR;
13
14 public static void main(String[] args) {
15 Thread thread1 = new Thread(() -> printNumbers());
16 Thread thread2 = new Thread(() -> printLetters());
17
18 thread1.start();
19 thread2.start();
20
21 try {
22 thread1.join();
23 thread2.join();
24 } catch (InterruptedException e) {
25 e.printStackTrace();
26 }
27 }
28
29 private static void printNumbers() {
30 while (number <= MAX_NUM) {
31 lock.lock();
32 try {
33 while (letter != START_CHAR) { // 等待字母打印完
34 condition1.await();
35 }
36 System.out.print(number++);
37 System.out.flush(); // 确保立即输出到控制台
38 letter++; // 让字母线程开始打印
39 condition2.signal(); // 唤醒字母线程
40 } catch (InterruptedException e) {
41 e.printStackTrace();
42 } finally {
43 lock.unlock();
44 }
45 }
46 }
47
48 private static void printLetters() {
49 while (letter <= 'Z') {
50 lock.lock();
51 try {
52 while (number <= 1 || number > MAX_NUM) { // 等待数字打印完
53 condition2.await();
54 }
55 System.out.print(letter++);
56 System.out.flush();
57 number += 2; // 让数字线程开始打印下一个偶数
58 condition1.signal(); // 唤醒数字线程
59 } catch (InterruptedException e) {
60 e.printStackTrace();
61 } finally {
62 lock.unlock();
63 }
64 }
65 }
66}
注意:在实际执行时,由于线程调度机制的影响,可能不会严格地按照1A2B...的顺序交替打印,但上述代码确保了在逻辑上是交替进行的。为了使输出更加清晰,在每个打印操作后添加了System.out.flush()
以确保数据立即输出到控制台。同时,根据题目描述,这里假设只打印到Z,即总共52个元素,因此数字线程每次增加2以匹配字母线程的进度。
3 请用代码实现同时开启一个线程打印1~52,开启另一个线程打印A~Z,最终交替打印1A2B3C...这样的数据
package main
import (
"fmt"
"sync"
)
func printNumbers(wg *sync.WaitGroup, numbers chan<- rune) {
defer wg.Done()
for i := 1; i <= 52; i++ { // 确保总共发送52个数,因为是交替打印,所以数字也翻倍
num := rune(i)
numbers <- num
}
close(numbers)
}
func printLetters(wg *sync.WaitGroup, letters chan<- rune, numbers <-chan rune) {
defer close(letters) // 在函数结束时关闭letters通道
lettersToSend := 'A'
for number := range numbers {
fmt.Printf("%d", number)
fmt.Printf("%c", lettersToSend)
lettersToSend++
if lettersToSend > 'Z' {
lettersToSend = 'A'
}
}
wg.Done() // 当numbers channel关闭时,表明已处理完所有数据,此时调用Done
}
func main() {
var wg sync.WaitGroup
numbers := make(chan rune)
letters := make(chan rune)
wg.Add(2)
go printNumbers(&wg, numbers)
go printLetters(&wg, letters, numbers)
wg.Wait()
}
ASCII码对照表
上面的leffter 可以去掉
3 Java里实现线程的方式有哪些?各自的优缺点是什么?
Java中实现线程主要有以下三种方式:
-
继承
java.lang.Thread
类- 通过创建一个继承自
Thread
的子类,并重写run()
方法来定义线程执行体。
1class MyThread extends Thread { 2 public void run() { 3 // 线程任务代码 4 } 5 6 public static void main(String[] args) { 7 MyThread thread = new MyThread(); 8 thread.start(); // 启动线程 9 } 10}
优点:
- 实现简单直观,如果需要直接访问当前线程或获取线程状态时非常方便。
缺点:
- 如果要重用现有类并且该类已经继承了其他类,则不能使用继承的方式来创建线程(因为Java不支持多重继承)。
- 当需要在多个地方复用线程逻辑时,由于单继承限制,这种方式不够灵活。
- 通过创建一个继承自
-
实现
java.lang.Runnable
接口- 创建一个实现
Runnable
接口的类,并实现其run()
方法。然后将这个实例传给Thread
构造函数来创建并启动线程。
1class MyRunnable implements Runnable { 2 public void run() { 3 // 线程任务代码 4 } 5 6 public static void main(String[] args) { 7 Thread thread = new Thread(new MyRunnable()); 8 thread.start(); 9 } 10}
优点:
- 更符合面向对象的设计原则,允许线程逻辑与具体的线程类解耦,可以方便地应用于多种上下文。
- 允许一个类同时实现多个接口,因此可以在不影响原有类结构的情况下实现多线程。
- 可以方便地应用于线程池中,提高资源利用率和系统性能。
缺点:
- 不可以直接操作线程相关的方法(如
interrupt()
、setName()
等),必须通过Thread实例进行操作。
- 创建一个实现
-
使用
java.util.concurrent.Callable
接口和FutureTask
Callable
接口提供了一个带有返回值的call()
方法,而FutureTask
则是一个实现了RunnableFuture
接口的类,它可以包装Callable对象并将结果存储为Future,从而可以在主线程中获取异步计算的结果。
1import java.util.concurrent.Callable; 2import java.util.concurrent.FutureTask; 3 4class MyCallable implements Callable<String> { 5 public String call() throws Exception { 6 // 执行有返回值的任务 7 return "Callable result"; 8 } 9 10 public static void main(String[] args) throws ExecutionException, InterruptedException { 11 MyCallable callable = new MyCallable(); 12 FutureTask<String> task = new FutureTask<>(callable); 13 Thread thread = new Thread(task); 14 thread.start(); 15 System.out.println("Main thread: " + task.get()); // 获取线程执行结果 16 } 17}
优点:
- 支持有返回值的任务,并且可以通过Future获取异步执行的结果。
- 适用于处理更复杂的并发场景,结合Future可以实现更灵活的线程控制。
缺点:
- 相对于前两种方式来说,实现过程稍显复杂,API较为抽象,对初学者理解起来可能有一定难度。
总结来说,选择哪种方式取决于具体的应用场景和需求,通常推荐使用Runnable接口,因为它具有更好的灵活性和可扩展性。而在需要获取线程执行结果或者进行更复杂的异步任务管理时,可以考虑使用Callable和FutureTask。
4 你如何理解同步、异步、阻塞、非阻塞?
同步、异步和阻塞、非阻塞是描述程序执行方式和线程行为的四种关键概念,它们主要针对的是并发编程中的任务调度和I/O操作。
-
同步(Synchronous)
- 同步指的是在发出一个操作请求后,调用者会一直等待这个操作完成并返回结果。比如在Java中,当你调用某个方法时,如果没有采用异步机制,那么调用线程会在该方法返回之前一直被阻塞。
- 在I/O操作中,同步意味着应用程序主线程发起读写请求后,会一直等待直到数据准备好或操作完成才继续执行后续代码。
-
异步(Asynchronous)
- 异步则是指在发出一个操作请求后,调用者不必等待该操作完成就可以继续执行其他任务。当操作完成后,通常会通过回调函数、事件通知或者Future/Promise等机制告知调用者。
- 在I/O操作中,异步处理允许主线程发出读写请求后立即返回,操作系统或框架会在后台处理数据准备,完成后触发相应的通知机制。
-
阻塞(Blocking)
- 阻塞是指在执行过程中,如果遇到某种资源不可用(如网络连接未建立、文件未就绪等),调用线程会被挂起,直到资源可用或者超时为止。在此期间,调用线程无法执行其他任何任务。
- 例如,在进行网络I/O时,若使用了阻塞API,当缓冲区为空而试图读取数据时,线程会进入等待状态,直到有新的数据到来。
-
非阻塞(Non-blocking)
- 非阻塞则是在尝试访问资源时,即使资源暂时不可用,也不会导致调用线程挂起,而是直接返回一个错误信息或告诉调用者当前资源的状态,然后可以立刻执行下一条指令。
- 在非阻塞I/O中,当没有数据可读时,读操作不会阻塞,而是立即返回并告知当前无数据;当不能立即写入时,写操作也会立即返回,之后可以通过轮询或事件驱动的方式得知何时可以再次尝试写入。
总结来说:
- 同步/异步关注的是消息通信机制,即发送一个请求后,是否需要等待响应后再进行下一步操作;
- 阻塞/非阻塞关注的是在等待系统内资源(如I/O操作)时,进程或线程的状态,是否需要等待资源就绪才能继续执行。
结合在一起,同步阻塞模型是最常见的,它表示调用者必须等待操作完成;而异步非阻塞模型则更适用于高并发和高性能场景,因为它允许调用者在等待资源的同时还能处理其他任务。
5 你的程序中用到了哪些锁?是如何实现的?
作为AI助手,我并不能直接编写程序或使用锁机制。但我可以解释在Java等编程语言中常见的几种锁实现方式:
-
内置锁(Intrinsic Locks / Monitor Locks)
- 在Java中,每个对象都有一个内置锁,也称为监视器锁(monitor)。当一个线程调用一个对象的
synchronized
方法或者同步块时,会自动获取该对象的锁,并在执行完同步代码后释放锁。
1public class MyClass { 2 public synchronized void method() { 3 // 同步代码块 4 } 5}
- 在Java中,每个对象都有一个内置锁,也称为监视器锁(monitor)。当一个线程调用一个对象的
-
显式Lock(Explicit Locks)
- Java提供了
java.util.concurrent.locks.Lock
接口以及其实现类如ReentrantLock
,允许开发者更灵活地控制锁的获取和释放。
1import java.util.concurrent.locks.ReentrantLock; 2 3public class MyClass { 4 private final ReentrantLock lock = new ReentrantLock(); 5 6 public void method() { 7 lock.lock(); // 获取锁 8 try { 9 // 临界区代码 10 } finally { 11 lock.unlock(); // 无论是否发生异常,确保锁最终会被释放 12 } 13 } 14}
- Java提供了
-
读写锁(Read-Write Locks)
java.util.concurrent.locks.ReadWriteLock
接口及其ReentrantReadWriteLock
实现提供了一种允许多个读线程同时访问资源,但只允许一个写线程访问资源的锁机制。
1import java.util.concurrent.locks.ReentrantReadWriteLock; 2 3public class MyClass { 4 private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 5 private final Lock readLock = lock.readLock(); 6 private final Lock writeLock = lock.writeLock(); 7 8 public void readOperation() { 9 readLock.lock(); 10 try { 11 // 读取操作 12 } finally { 13 readLock.unlock(); 14 } 15 } 16 17 public void writeOperation() { 18 writeLock.lock(); 19 try { 20 // 写入操作 21 } finally { 22 writeLock.unlock(); 23 } 24 } 25}
-
条件变量(Condition Variables)
- 配合锁使用,比如
ReentrantLock
提供了newCondition()
方法创建Condition
对象,用于线程间的协作与通知唤醒机制。
1import java.util.concurrent.locks.Condition; 2import java.util.concurrent.locks.ReentrantLock; 3 4public class MyClass { 5 private final ReentrantLock lock = new ReentrantLock(); 6 private final Condition condition = lock.newCondition(); 7 8 public void waitForSignal() { 9 lock.lock(); 10 try { 11 while (!isReady()) { 12 condition.await(); // 当条件不满足时,线程等待 13 } 14 // 条件满足,执行相应逻辑 15 } catch (InterruptedException e) { 16 Thread.currentThread().interrupt(); 17 } finally { 18 lock.unlock(); 19 } 20 } 21 22 public void signalAll() { 23 lock.lock(); 24 try { 25 // 设置条件为真并唤醒所有等待的线程 26 setReady(true); 27 condition.signalAll(); 28 } finally { 29 lock.unlock(); 30 } 31 } 32}
- 配合锁使用,比如
以上是Java中常用的几种锁类型和它们的基本使用示例。在实际编程中,选择哪种锁取决于具体的并发需求和性能考量。
synchronized与Lock两者区别:
1:Lock是一个接口,而Synchronized是关键字。
2:Synchronized会自动释放锁,而Lock必须手动释放锁。
3:Lock可以让等待锁的线程响应中断,而Synchronized不会,线程会一直等待下去。
4:通过Lock可以知道线程有没有拿到锁,而Synchronized不能。
5:Lock能提高多个线程读操作的效率。
6:Synchronized能锁住类、方法和代码块,而Lock是块范围内的
synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类
作为对象才能保证锁的生效。
且在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。
而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低
Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,
如果因为冲突失败就重试,直到成功为止。
乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,
会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令
作者:凉风拂面秋挽月
链接:https://www.jianshu.com/p/20e1997bd5e7
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。