Java多线程

1.线程安全的集合有哪些?

Java中提供了多种线程安全的集合类,它们位于java.util.concurrent包下。以下是一些常见的线程安全集合:

  1. ConcurrentHashMap

    • 类似于HashMap,但它是线程安全的,允许并发读写操作,并且在高并发场景下的性能优于同步HashMap。
  2. CopyOnWriteArrayList

    • 线程安全的List实现,当修改集合时(如添加、删除元素),它会创建一个新的数组副本进行修改,而遍历操作则总是看到不变的原数据视图,适用于读多写少的并发场景。
  3. CopyOnWriteArraySet

    • 基于CopyOnWriteArrayList实现的线程安全Set,具有同样的复制更新机制。
  4. ConcurrentLinkedQueue

    • 一个线程安全的FIFO队列,适合用于多生产者-多消费者模式,支持高效的非阻塞并发插入和移除操作。
  5. BlockingQueue接口的实现

    • 如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue等,这些是线程安全的队列,支持阻塞的插入和获取操作,以及容量限制等功能。
  6. ConcurrentSkipListMap和ConcurrentSkipListSet

    • 分别为线程安全的有序映射和有序集,内部采用跳跃表(Skip List)数据结构,支持高效并发访问和排序功能。
  7. ConcurrentLinkedDeque

    • 双向链表实现的线程安全deque(双端队列),支持从两端添加和移除元素。
  8. CyclicBarrier, CountDownLatch, Semaphore

    • 虽然不是传统的集合,但它们是线程协作工具,可以用来控制多个线程之间的同步行为。
  9. ConcurrentHashSet

    • 这是一个不太常见的集合类,可通过Collections.newSetFromMap(new ConcurrentHashMap<>()方式创建一个线程安全的HashSet。

总之,Java通过java.util.concurrent包提供了一系列线程安全的数据结构,能够满足不同场景下的并发需求。

2 hashMap为什么不是线程安全的?底层原理是什么?

HashMap在Java中不是线程安全的原因在于其内部实现的并发控制机制不足。HashMap的设计并没有考虑多线程环境下的同步问题,当多个线程同时对HashMap进行读写操作时,可能会出现数据不一致的问题。

底层原理:

  1. 结构设计: HashMap底层基于数组和链表(或红黑树)实现,通过哈希函数计算key的哈希值,并根据这个值将元素存储到数组的特定位置上。如果哈希冲突,则使用拉链法解决冲突,即将相同哈希值的元素链成一个链表。

  2. 线程不安全性表现:

    • 扩容(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中实现线程主要有以下三种方式:

  1. 继承java.lang.Thread

    • 通过创建一个继承自Thread的子类,并重写run()方法来定义线程执行体。
     Java 
    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不支持多重继承)。
    • 当需要在多个地方复用线程逻辑时,由于单继承限制,这种方式不够灵活。
  2. 实现java.lang.Runnable接口

    • 创建一个实现Runnable接口的类,并实现其run()方法。然后将这个实例传给Thread构造函数来创建并启动线程。
     Java 
    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实例进行操作。
  3. 使用java.util.concurrent.Callable接口和FutureTask

    • Callable接口提供了一个带有返回值的call()方法,而FutureTask则是一个实现了RunnableFuture接口的类,它可以包装Callable对象并将结果存储为Future,从而可以在主线程中获取异步计算的结果。
     Java 
    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操作。

  1. 同步(Synchronous)

    • 同步指的是在发出一个操作请求后,调用者会一直等待这个操作完成并返回结果。比如在Java中,当你调用某个方法时,如果没有采用异步机制,那么调用线程会在该方法返回之前一直被阻塞。
    • 在I/O操作中,同步意味着应用程序主线程发起读写请求后,会一直等待直到数据准备好或操作完成才继续执行后续代码。
  2. 异步(Asynchronous)

    • 异步则是指在发出一个操作请求后,调用者不必等待该操作完成就可以继续执行其他任务。当操作完成后,通常会通过回调函数、事件通知或者Future/Promise等机制告知调用者。
    • 在I/O操作中,异步处理允许主线程发出读写请求后立即返回,操作系统或框架会在后台处理数据准备,完成后触发相应的通知机制。
  3. 阻塞(Blocking)

    • 阻塞是指在执行过程中,如果遇到某种资源不可用(如网络连接未建立、文件未就绪等),调用线程会被挂起,直到资源可用或者超时为止。在此期间,调用线程无法执行其他任何任务。
    • 例如,在进行网络I/O时,若使用了阻塞API,当缓冲区为空而试图读取数据时,线程会进入等待状态,直到有新的数据到来。
  4. 非阻塞(Non-blocking)

    • 非阻塞则是在尝试访问资源时,即使资源暂时不可用,也不会导致调用线程挂起,而是直接返回一个错误信息或告诉调用者当前资源的状态,然后可以立刻执行下一条指令。
    • 在非阻塞I/O中,当没有数据可读时,读操作不会阻塞,而是立即返回并告知当前无数据;当不能立即写入时,写操作也会立即返回,之后可以通过轮询或事件驱动的方式得知何时可以再次尝试写入。

总结来说:

  • 同步/异步关注的是消息通信机制,即发送一个请求后,是否需要等待响应后再进行下一步操作;
  • 阻塞/非阻塞关注的是在等待系统内资源(如I/O操作)时,进程或线程的状态,是否需要等待资源就绪才能继续执行。

结合在一起,同步阻塞模型是最常见的,它表示调用者必须等待操作完成;而异步非阻塞模型则更适用于高并发和高性能场景,因为它允许调用者在等待资源的同时还能处理其他任务。

5 你的程序中用到了哪些锁?是如何实现的?

作为AI助手,我并不能直接编写程序或使用锁机制。但我可以解释在Java等编程语言中常见的几种锁实现方式:

  1. 内置锁(Intrinsic Locks / Monitor Locks)

    • 在Java中,每个对象都有一个内置锁,也称为监视器锁(monitor)。当一个线程调用一个对象的synchronized方法或者同步块时,会自动获取该对象的锁,并在执行完同步代码后释放锁。
     Java 
    1public class MyClass {
    2    public synchronized void method() {
    3        // 同步代码块
    4    }
    5}
  2. 显式Lock(Explicit Locks)

    • Java提供了java.util.concurrent.locks.Lock接口以及其实现类如ReentrantLock,允许开发者更灵活地控制锁的获取和释放。
     Java 
    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}
  3. 读写锁(Read-Write Locks)

    • java.util.concurrent.locks.ReadWriteLock接口及其ReentrantReadWriteLock实现提供了一种允许多个读线程同时访问资源,但只允许一个写线程访问资源的锁机制。
     Java 
    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}
  4. 条件变量(Condition Variables)

    • 配合锁使用,比如ReentrantLock提供了newCondition()方法创建Condition对象,用于线程间的协作与通知唤醒机制。
     Java 
    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
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值