Java中提供了多种线程安全的集合类,它们位于java.util.concurrent
包下。以下是一些常见的线程安全集合:
ConcurrentHashMap
CopyOnWriteArrayList
CopyOnWriteArraySet
ConcurrentLinkedQueue
BlockingQueue接口的实现
ConcurrentSkipListMap和ConcurrentSkipListSet
ConcurrentLinkedDeque
CyclicBarrier, CountDownLatch, Semaphore
ConcurrentHashSet
Collections.newSetFromMap(new ConcurrentHashMap<>()
方式创建一个线程安全的HashSet。总之,Java通过java.util.concurrent
包提供了一系列线程安全的数据结构,能够满足不同场景下的并发需求。
HashMap在Java中不是线程安全的原因在于其内部实现的并发控制机制不足。HashMap的设计并没有考虑多线程环境下的同步问题,当多个线程同时对HashMap进行读写操作时,可能会出现数据不一致的问题。
底层原理:
结构设计: HashMap底层基于数组和链表(或红黑树)实现,通过哈希函数计算key的哈希值,并根据这个值将元素存储到数组的特定位置上。如果哈希冲突,则使用拉链法解决冲突,即将相同哈希值的元素链成一个链表。
线程不安全性表现:
为了确保线程安全,在多线程环境下通常建议使用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 可以去掉
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.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 {
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 task = new FutureTask<>(callable);
13 Thread thread = new Thread(task);
14 thread.start();
15 System.out.println("Main thread: " + task.get()); // 获取线程执行结果
16 }
17}
优点:
缺点:
总结来说,选择哪种方式取决于具体的应用场景和需求,通常推荐使用Runnable接口,因为它具有更好的灵活性和可扩展性。而在需要获取线程执行结果或者进行更复杂的异步任务管理时,可以考虑使用Callable和FutureTask。
同步、异步和阻塞、非阻塞是描述程序执行方式和线程行为的四种关键概念,它们主要针对的是并发编程中的任务调度和I/O操作。
同步(Synchronous)
异步(Asynchronous)
阻塞(Blocking)
非阻塞(Non-blocking)
总结来说:
结合在一起,同步阻塞模型是最常见的,它表示调用者必须等待操作完成;而异步非阻塞模型则更适用于高并发和高性能场景,因为它允许调用者在等待资源的同时还能处理其他任务。
作为AI助手,我并不能直接编写程序或使用锁机制。但我可以解释在Java等编程语言中常见的几种锁实现方式:
内置锁(Intrinsic Locks / Monitor Locks)
synchronized
方法或者同步块时,会自动获取该对象的锁,并在执行完同步代码后释放锁。1public class MyClass {
2 public synchronized void method() {
3 // 同步代码块
4 }
5}
显式Lock(Explicit Locks)
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}
读写锁(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中常用的几种锁类型和它们的基本使用示例。在实际编程中,选择哪种锁取决于具体的并发需求和性能考量。