什么是JUC(java.util.concurrent)

java.util工具包 包 分类

业务:普通的线程代码 Thread

Runnable ,没有返回值,效率比Callable相对较低

线程和进程

进程

  • 定义:进程是操作系统中的一个程序执行实例,是系统进行资源分配和调度的基本单位。每个进程都有自己的地址空间、内存、数据栈以及其他用于跟踪进程执行的辅助数据。

  • 特点:进程之间相互独立,各自拥有独立的内存空间,进程间通信需要特殊的机制。

  • 优点:进程之间相互隔离,一个进程崩溃不会影响其他进程。

  • 缺点:创建、销毁进程的开销较大。

线程

  • 定义:线程是进程中的一个执行单元,一个进程可以包含多个线程,共享进程的资源。线程是CPU调度的基本单位。

  • 特点:线程共享进程的地址空间和资源,可以方便地进行通信和数据共享。

  • 优点:线程切换开销小,适合用于多任务并发处理。

  • 缺点:线程间共享资源,需要考虑同步和互斥问题,容易引发死锁等问题。

进程:操作系统中的一个程序执行实例

一个进程往往可以包含多个线程,至少包含一个!

java默认有2个线程 main GC

线程:开了一个进程Typora,写字,保存(线程负责)

对java而言:thread runabble callable

java 无法真正开启线程

只能通过本地方法native调用底层c++,java无法直接操作硬件

在 Java 中,虽然我们可以通过 ThreadRunnableCallable 等类来创建线程,但实际上 Java 本身并不能直接操作硬件,也就是说 Java 无法真正开启线程。这是因为 Java 是一种高级语言,它运行在虚拟机(JVM)上,而 JVM 是运行在操作系统提供的进程中的。

当我们在 Java 中创建一个线程时,实际上是通过 JVM 调用底层的操作系统接口来创建一个操作系统级别的线程。这些操作系统级别的线程由操作系统来管理和调度,Java 线程只是 JVM 中的一个抽象概念,对应着操作系统中的一个实际线程。

因此,Java 中的线程是依赖于操作系统的实际线程来运行的,Java 线程的生命周期、调度等行为都是由操作系统来管理的。Java 线程只是一个逻辑上的线程,它并不直接操作硬件,而是通过 JVM 和操作系统之间的交互来实现线程的创建、调度和管理。

并发,并行

并发(多线程操作统一资源)

  • 定义:并发是指一个系统能够同时处理多个任务,通过任务之间的快速切换,让用户感觉多个任务同时进行。在单处理器系统中,通过时间片轮转的方式实现并发。

  • 特点:并发是指多个任务交替执行的过程,任务之间可能会有一定的时间片差距,但整体上给人的感觉是同时执行。

  • 优点:提高系统资源的利用率,增加系统的吞吐量,提高系统的响应速度。

  • 缺点:需要考虑线程安全、死锁、资源竞争等并发问题,编程复杂度较高。

并行(多个人一起行走)

  • 定义:并行是指系统中同时存在多个任务,这些任务真正同时执行,每个任务都在独立的处理器核心上运行。在多处理器系统中,通过多个处理器核心同时执行多个任务来实现并行。

  • 特点:并行是指多个任务真正同时执行,每个任务都有自己的处理器核心,不存在时间片差距。

  • 优点:能够显著提高系统的计算能力和处理速度,适合处理大规模计算密集型任务。

  • 缺点:需要更多的硬件资源支持,成本较高,对系统架构和设计要求较高。

区别

  • 并发与并行的区别:并发是指多个任务交替执行,通过时间片轮转实现;而并行是指多个任务真正同时执行,每个任务有自己的处理器核心。并发更多用于提高系统资源利用率和响应速度,适合I/O密集型任务;而并行更多用于提高计算能力和处理速度,适合计算密集型任务。

线程的六个状态

1. 初始状态(NEW)

实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

2.1. 就绪状态(RUNNABLE之READY)

就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。

调用线程的start()方法,此线程进入就绪状态。

当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。

当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。

锁池里的线程拿到对象锁后,进入就绪状态。

2.2. 运行中状态(RUNNABLE之RUNNING)

线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。

3. 阻塞状态(BLOCKED)

阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

4. 等待(WAITING)

处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

5. 超时等待(TIMED_WAITING)

处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

6. 终止状态(TERMINATED)

当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。

在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

wait 和sleep 的区别

1. 来自不同的类

在Java中,wait和sleep是两个不同的方法,分别来自不同的类。wait方法是Object类的方法,而sleep方法是Thread类的方法。

2. 关于锁的释放

当调用wait方法时,当前线程会释放它所持有的锁。这意味着其他线程可以获得该锁并继续执行。

然而,当调用sleep方法时,当前线程不会释放它所持有的锁。这意味着其他线程无法获得该锁,直到当前线程醒来并继续执行。

3. 使用的范围不同

wait方法必须在同步代码块中使用。同步代码块是指使用synchronized关键字修饰的代码块。在同步代码块中,当调用wait方法时,当前线程会暂停执行,并释放锁,直到其他线程调用notify或notifyAll方法来唤醒它。

相比之下,sleep方法可以在任何地方使用。它可以在同步代码块中使用,也可以在非同步代码块中使用。当调用sleep方法时,当前线程会暂停执行,但不会释放锁。它会在指定的时间间隔后自动醒来,并继续执行。

Lock

synchronized

public class SynchronizedExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + example.count);
    }
}

Lock接口详解

Lock 接口是 Java 并发包中提供的用于控制多线程访问共享资源的锁机制。与传统的synchronized关键字相比,Lock 接口提供了更灵活、更强大的锁定机制,可以更精细地控制线程的同步访问。

主要接口方法

  1. void lock():获取锁。如果锁不可用,当前线程将一直阻塞,直到获取到锁为止。

  2. void unlock():释放锁。用于释放之前获取的锁。

  3. Condition newCondition():返回一个与该锁相关的条件对象,用于实现线程间的协调和通信。

主要实现类

  1. ReentrantLockReentrantLockLock 接口的主要实现类之一,它提供了与synchronized关键字类似的锁定功能,但更加灵活和可控。

  2. ReentrantReadWriteLockReentrantReadWriteLockLock 接口的另一个实现类,它提供了读写锁的功能,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。

优点

  1. 可中断性Lock 接口提供了可中断的获取锁方式,可以避免线程无限期等待的情况。

  2. 公平性Lock 接口可以实现公平锁,即按照线程请求锁的顺序来获取锁。

  3. 条件等待Lock 接口提供了条件对象,可以实现线程间的协调和通信。

使用示例

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private int count = 0;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        LockExample example = new LockExample();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + example.count);
    }
}

在上面的示例中,通过使用 ReentrantLock 实现了对共享资源 count 的线程安全访问。通过调用 lock()unlock() 方法,确保了在修改共享资源时的线程安全性。

公平锁和非公平锁

公平锁

公平锁是指多个线程按照请求锁的顺序来获取锁,即先来先得的原则。在公平锁的机制下,当一个线程请求锁时,如果锁当前是被其他线程占有的,那么该线程会进入等待队列,按照请求锁的顺序等待获取锁。公平锁的实现通常会保证线程按照先后顺序获取锁,避免线程饥饿的情况。

公平锁的优点在于公平性,能够避免某些线程长时间等待锁的情况,保证每个线程都有机会获取锁。但公平锁的缺点是可能会降低系统的吞吐量,因为线程需要等待的时间较长。

在Java中,ReentrantLock可以实现公平锁,通过在创建ReentrantLock对象时传入true来指定为公平锁。

非公平锁

非公平锁是指多个线程获取锁的顺序是不确定的,不保证按照请求锁的顺序来获取锁。在非公平锁的机制下,一个线程请求锁时,如果锁当前是被其他线程占有的,那么该线程有可能直接获取到锁,而不需要进入等待队列。

非公平锁的优点在于可以提高系统的吞吐量,因为线程获取锁的等待时间较短,不需要严格按照请求锁的顺序等待。但非公平锁的缺点在于可能会导致某些线程长时间等待锁,造成线程饥饿的情况。

在Java中,默认情况下,ReentrantLock是非公平锁。如果在创建ReentrantLock对象时传入false,则表示使用非公平锁。

总的来说,公平锁和非公平锁各有优缺点,选择使用哪种锁取决于具体的应用场景和需求。公平锁适合对线程执行顺序有严格要求的场景,而非公平锁适合对系统吞吐量要求较高的场景。

synchronized 和 Lock 的区别

1. 来源

  • synchronizedsynchronized 是 Java 中的关键字,用于实现同步代码块或同步方法,提供了对对象的锁定和解锁机制。

  • LockLock 是 Java 并发包中的接口,提供了更灵活、更强大的锁定机制,可以替代 synchronized 关键字来实现线程同步。

2. 使用方式

  • synchronizedsynchronized 可以用于同步代码块或同步方法,通过对对象的锁定和解锁来实现线程同步。

  • LockLock 接口提供了更多的锁定和解锁方法,可以实现更灵活的线程同步控制,如可中断锁、公平锁等。

3. 锁的释放

  • synchronized:当使用 synchronized 关键字时,线程在退出同步代码块或同步方法时会自动释放锁。

  • Lock:使用 Lock 接口时,需要手动调用 unlock() 方法来释放锁,确保在适当的时候释放锁,避免死锁等问题。

4. 可中断性

  • synchronizedsynchronized 关键字不支持可中断锁,即线程无法在等待锁的过程中被中断。

  • LockLock 接口提供了可中断的获取锁方式,可以避免线程无限期等待的情况,支持线程中断。

5. 公平性

  • synchronizedsynchronized 关键字是非公平锁,线程获取锁的顺序不确定。

  • LockLock 接口可以实现公平锁,按照线程请求锁的顺序来获取锁,避免线程饥饿的情况。

6. 条件等待

  • synchronizedsynchronized 关键字不支持条件等待,无法实现线程间的协调和通信。

  • LockLock 接口提供了 Condition 接口,可以实现线程间的协调和通信,等待和唤醒操作更加灵活。

7. 适合代码量

  • synchronized:适合锁少量代码 同步问题

  • Lock:适合锁大量的同步代码

综上所述,synchronized 是 Java 中的关键字,提供了简单的线程同步机制;而 Lock 接口提供了更灵活、更强大的锁定机制,可以实现更复杂的线程同步控制。

虚假唤醒

虚假唤醒是指在多线程编程中,一个线程在没有收到通知或信号的情况下被唤醒的现象。虽然在理论上,线程应该只有在收到通知或信号时才会被唤醒,但在某些情况下,线程可能会出现虚假唤醒的情况。

虚假唤醒通常发生在使用wait()notify()notifyAll()等方法进行线程间通信时。在Java中,这些方法通常与synchronized关键字一起使用,用于实现线程间的协调和同步。

造成虚假唤醒的原因主要有两个:

  1. 竞争条件:当多个线程在等待同一个条件时,某个线程被唤醒后可能会发现条件并未满足,这可能是因为其他线程已经改变了条件的状态。这种情况下,线程被唤醒是“虚假”的,因为条件并未真正满足。

  2. Spurious Wakeups:在某些操作系统或JVM实现中,可能会存在虚假唤醒的问题。即使没有调用notify()notifyAll()方法,线程也可能会在没有明显原因的情况下被唤醒。这种情况下,线程被唤醒是“虚假”的,因为并没有实际的通知或信号触发唤醒。

为了避免虚假唤醒问题,通常建议在使用wait()方法时,总是在循环中检查条件是否满足,而不是简单地等待唤醒。例如:

synchronized (lock) {
    while (!condition) {
        lock.wait();
    }
}

通过在循环中检查条件,即使发生虚假唤醒,线程也会重新检查条件是否满足,从而避免出现问题。在实际编程中,应该始终考虑虚假唤醒问题,并采取适当的措施来确保线程间通信的正确性和可靠性。

JUC版生产者消费者问题

JUC中的await和signal

在Java并发编程中,JUC(java.util.concurrent)提供了一些高级的并发工具类,其中包括Condition接口,它提供了类似于waitnotify的功能,但比传统的synchronized关键字更加灵活和强大。

await方法

await方法是Condition接口中的一个方法,用于使当前线程等待,直到接收到一个信号或被中断。当调用await方法时,当前线程会释放锁,并进入等待状态,直到其他线程调用signalsignalAll方法来唤醒它。

signal方法

signal方法是Condition接口中的一个方法,用于唤醒一个等待在该条件上的线程。当调用signal方法时,会选择一个等待在该条件上的线程进行唤醒,使其从等待状态转为就绪状态,等待获取锁。

使用示例

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private boolean isSignal = false;

    public void awaitMethod() {
        lock.lock();
        try {
            while (!isSignal) {
                condition.await();
            }
            System.out.println("Received signal");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void signalMethod() {
        lock.lock();
        try {
            isSignal = true;
            condition.signal();
            System.out.println("Signal sent");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ConditionExample example = new ConditionExample();

        Thread thread1 = new Thread(() -> {
            example.awaitMethod();
        });

        Thread thread2 = new Thread(() -> {
            example.signalMethod();
        });

        thread1.start();
        thread2.start();
    }
}

在上面的示例中,通过使用Condition接口的awaitsignal方法实现了线程之间的等待和唤醒。当awaitMethod方法中的isSignalfalse时,线程会调用await方法进入等待状态;当signalMethod方法被调用时,会将isSignal设置为true,并调用signal方法唤醒等待的线程。

通过Condition接口的awaitsignal方法,可以实现更加灵活和精细的线程协作机制,避免了传统synchronized关键字的局限性。

Condition 接口是 Lock 接口提供的用于线程间通信和协调的工具,可以实现精准通知线程的功能。通过 Condition,我们可以在某个条件满足时通知等待的线程,从而实现更灵活的线程协作。

Condition精准通知某一个线程

Condition 接口是 Lock 接口提供的一种条件对象,用于实现线程间的精准通知。与传统的 wait()notify() 方法相比,Condition 接口提供了更灵活、更精确的线程通信机制。

主要方法

  1. void await():当前线程等待,并释放锁,进入等待状态。

  2. void signal():唤醒一个等待在该条件上的线程,使其从等待状态进入就绪状态。

  3. void signalAll():唤醒所有等待在该条件上的线程。

使用示例

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    private int count = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void increment() {
        lock.lock();
        try {
            count++;
            condition.signal(); // 唤醒一个等待在该条件上的线程
        } finally {
            lock.unlock();
        }
    }

    public void awaitMethod() {
        lock.lock();
        try {
            while (count < 10) {
                condition.await(); // 等待条件满足
            }
            System.out.println("Count reached 10!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ConditionExample example = new ConditionExample();

        Thread thread1 = new Thread(() -> {
            example.awaitMethod();
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                example.increment();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在上面的示例中,通过使用 Condition 接口实现了一个线程等待条件满足的功能。当 count 达到 10 时,唤醒等待在条件上的线程,输出 "Count reached 10!"。这样可以实现线程间的精准通知,避免了传统的 wait()notify() 方法可能出现的信号丢失问题。

8锁现象

一、标准情况下,两个线程谁先打印?

public class Lock8 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(()->{
            phone.sendSms();
        },"A").start();

        TimeUnit.SECONDS.sleep(2);

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}
class Phone extends Thread {
    public synchronized void sendSms(){
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}

先发短信,一秒后打电话。

二、sendSms延迟4s,谁先打印?

public class Lock8 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.sendSms();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone.call();
        },"B").start();
    }
}
class Phone extends Thread {
    public synchronized void sendSms() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}

先发短信再打电话

通过一、二我们可以知道,锁的存在影响了结果,一、二中锁的对象是方法的调用者,由于sendSms()和call()的调用者是同一个phone,故谁先拿到锁谁先执行。(一次只能有一个线程访问该类的方法

三、添加一个普通方法,谁先打印?

public class Lock8 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(()->{
            try {
                phone.sendSms();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone.hello();
        },"B").start();
    }
}
class Phone extends Thread {
    public synchronized void sendSms() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
    public void hello(){
        System.out.println("hello");
    }
}

hello先调用,sendSms后调用

hello()没有synchronized关键字修饰,故不参与锁的进程,无需参与锁的等待。它会随着线程的启动立刻执行。

四、两个对象两种方法,谁先打印?

public class Lock8 {
    public static void main(String[] args) throws InterruptedException {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(()->{
            try {
                phone1.sendSms();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            phone2.call();
        },"B").start();
    }
}
class Phone{
    public synchronized void sendSms() throws InterruptedException {
        TimeUnit.SECONDS.sleep(4);
        System.out.println("发短信");
    }
    public synchronized void call(){
        System.out.println("打电话");
    }
}

先打电话后发短信

两个对象->两个调用者->两把锁

因此两个线程互不干扰,谁睡眠得少谁先打印。

五、增加两个静态的同步方法,只有一个对象,谁先打印?

public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendSms();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            Phone.call();
        }, "B").start();
    }
}

class Phone {
    public static synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public static synchronized void call() {
        System.out.println("打电话");
    }
}

先发短信后打电话

由于两个方法都用static修饰,故二者类Class一加载就存在了,锁的是Class模板,而Phone只有唯一一个Class模板,二者用的是同一把锁。

六、两个对象,两个静态同步方法,谁先打印?

public class Lock8 {
    public static void main(String[] args) {
        Phone phone1 = new Phone();
        Phone phone2=new Phone();
        new Thread(() -> {
            phone1.sendSms();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            phone2.call();
        }, "B").start();
    }
}

class Phone {
    public static synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public static synchronized void call() {
        System.out.println("打电话");
    }
}

先发短信后打电话

由于加了static是类锁,而两个对象出自同一个类,两个对象的本质是一样的,用的是同一把锁。

七、一个对象,一个同步方法,一个静态同步方法,谁先打印?

public class Lock8 {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendSms();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            phone.call();
        }, "B").start();
    }
}

class Phone {
    public static synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call() {
        System.out.println("打电话");
    }
}

先打电话后发短信

sendSms()是静态同步方法,锁的是Class模板;call()是同步方法,锁的是调用者。因此这里是双线程。

八、两个对象,一个同步方法,一个静态同步方法,谁先打印?

public class Lock8 {
    public static void main(String[] args) {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone1.sendSms();
        }, "A").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            phone2.call();
        }, "B").start();
    }
}

class Phone {
    public static synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发短信");
    }

    public synchronized void call() {
        System.out.println("打电话");
    }
}

先打电话后发短信

这里和问题七差不多,本质还是两把锁,故两个线程互不干扰,谁时间慢,谁先打印。

总结

Synchronized 静态同步方法使用的锁是类锁(Xxxx.class),非静态同步方法使用的是对象锁(this)

多个线程中同一对象的非静态同步方法使用的是同一把对象锁,同一个类的静态方法使用的是同一把类锁

类锁和对象锁相互无效

锁对普通方法无效

判断多个线程是否使用的同一把锁,若是同一把锁,根据获取锁的顺序执行,若不是同一把锁,线程之间互不影响

集合类不安全

在多线程环境下,Java 中的一些集合类是不安全的,即在多个线程同时对集合进行读写操作时可能会出现数据不一致或异常情况。这种不安全性主要是由于集合类的实现不是线程安全的,没有对并发访问进行同步处理。

主要原因

  1. 线程安全性:集合类的实现通常不考虑多线程并发访问的情况,没有进行同步处理,导致多个线程同时对集合进行读写操作时可能会出现数据不一致的情况。

  2. 迭代器失效:在遍历集合时,如果在迭代过程中有其他线程对集合进行了修改,可能会导致迭代器失效或抛出异常。

  3. 原子性:某些集合操作并非原子操作,例如在 HashMap 中的 put 操作,可能会出现覆盖或丢失数据的情况。

常见的不安全集合类

  1. ArrayList:在多线程环境下,对 ArrayList 进行并发的添加或删除操作可能会导致数组越界或数据丢失。

  2. HashMap:HashMap 不是线程安全的,多线程并发操作可能导致链表环形、数据丢失等问题。

  3. HashSet:HashSet 也不是线程安全的,多线程并发操作可能导致数据丢失或重复。

  4. LinkedList:LinkedList 在多线程环境下,对链表进行并发的添加或删除操作可能会导致链表结构混乱或数据丢失。

解决方法

  1. 使用线程安全的集合类:Java 提供了一些线程安全的集合类,如 ConcurrentHashMapCopyOnWriteArrayListCopyOnWriteArraySet 等,可以替代不安全的集合类。

  2. 使用同步控制:对于不安全的集合类,可以通过在多线程访问时使用同步控制(如 synchronized 关键字或 Lock 接口)来保证线程安全。

  3. 使用并发工具类:Java 并发包中提供了一些并发工具类,如 ConcurrentHashMapConcurrentLinkedQueue 等,可以更安全地处理多线程并发访问。

  4. 避免迭代器问题:在遍历集合时,可以使用迭代器的安全删除方法,或者使用并发集合类提供的迭代器来避免迭代器失效问题。

  5. 使用原子操作:对于需要保证原子性的操作,可以使用原子类(如 AtomicIntegerAtomicReference 等)来替代普通的集合操作。

总的来说,在多线程环境下,应当尽量避免使用不安全的集合类,而是选择线程安全的集合类或通过同步控制来保证数据的一致性和安全性。

import java.util.ArrayList;
import java.util.List;

public class UnsafeCollectionExample {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();

        // 创建并启动10个线程,每个线程向集合中添加100个元素
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    list.add(j);
                }
            }).start();
        }

        // 等待所有线程执行完毕
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出集合的大小
        System.out.println("List size: " + list.size());
    }
}

解决不安全的集合类方法

在多线程环境下,使用不安全的集合类方法可能会导致线程安全问题,例如数据不一致、并发修改异常等。为了解决这些问题,可以采用以下几种方式:

1. 使用同步集合类

Java 提供了一些同步集合类,如 Collections.synchronizedList()Collections.synchronizedMap() 等,可以将不安全的集合类转换为线程安全的集合类。这些同步集合类在每个方法上都进行了同步,保证了多线程环境下的安全访问。

List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());

2. 使用并发集合类

Java 并发包中提供了一些高效的并发集合类,如 ConcurrentHashMapCopyOnWriteArrayList 等,这些集合类在设计上考虑了多线程并发访问的情况,提供了更好的性能和线程安全性。

ConcurrentMap<String, String> concurrentMap = new ConcurrentHashMap<>();
List<String> copyOnWriteList = new CopyOnWriteArrayList<>();

3. 使用锁机制

通过显式地使用锁机制,如 ReentrantLock,可以保证在临界区内的操作是原子的,从而避免多线程并发访问时的数据不一致问题。

Lock lock = new ReentrantLock();

lock.lock();
try {
    // 在临界区内进行操作
} finally {
    lock.unlock();
}

4. 使用线程安全的数据结构

有些数据结构本身就是线程安全的,如 AtomicIntegerAtomicReference 等,它们提供了原子性操作,可以保证在多线程环境下的线程安全性。

AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();

通过以上方式,可以有效地解决不安全的集合类方法在多线程环境下可能引发的线程安全问题,保证程序的正确性和稳定性。

CopyOnWrite

CopyOnWrite 是一种并发策略,通常用于读多写少的场景。在 CopyOnWrite 策略中,当需要对数据进行写操作时,不直接在原始数据上进行修改,而是先复制一份数据副本,然后在副本上进行修改,最后再将修改后的副本替换原始数据。这样可以保证在写操作期间不影响读操作的进行,从而实现读写分离,提高了读操作的性能。

主要特点和优点:

  • 写操作不影响读操作:由于写操作是在数据副本上进行的,因此写操作不会影响读操作的进行,读操作可以在不加锁的情况下进行,提高了读操作的性能。

  • 写操作安全性:CopyOnWrite 策略通过复制数据副本的方式来保证写操作的安全性,避免了写操作对读操作的影响,保证了数据的一致性。

  • 适用于读多写少的场景:CopyOnWrite 适用于读操作频繁、写操作较少的场景,例如配置信息、缓存等数据的读取和更新。

然而,CopyOnWrite 也存在一些缺点:

  • 内存占用较大:由于每次写操作都需要复制一份数据副本,因此会消耗额外的内存空间,对于数据量较大的情况,可能会导致内存占用较大。

  • 写操作延迟:由于写操作需要复制数据副本并替换原始数据,因此写操作的延迟较高,特别是在数据量较大时,写操作的性能会受到影响。

总的来说,CopyOnWrite 是一种适用于读多写少场景的并发策略,通过读写分离的方式提高了读操作的性能,但也需要权衡内存占用和写操作延迟等因素。在选择使用 CopyOnWrite 策略时,需要根据具体的业务场景和需求进行评估和选择。

CopyOnWriteArrayList 比 Vector 好在哪里

1. 线程安全性

  • CopyOnWriteArrayList 是线程安全的集合类,通过在写操作时复制整个数组来实现线程安全,读操作不需要加锁,适合读多写少的场景。

  • Vector 是传统的线程安全的集合类,通过在方法级别加锁来实现线程安全,读写操作都需要加锁,性能较低。

2. 迭代安全性

  • CopyOnWriteArrayList 上进行迭代操作时,不会抛出 ConcurrentModificationException 异常,因为迭代器遍历的是一个快照,不会受到写操作的影响。

  • Vector 上进行迭代操作时,如果在迭代过程中有其他线程对集合进行了修改,就会抛出 ConcurrentModificationException 异常。

3. 性能

  • CopyOnWriteArrayList 在写操作时需要复制整个数组,因此写操作的性能较低,适合读多写少的场景。

  • Vector 在方法级别加锁,读写操作都需要加锁,性能相对较低。

为什么不用 Vector 代替 ArrayList

1. 线程安全性

  • ArrayList 是非线程安全的集合类,适合在单线程环境下使用,如果在多线程环境下使用,需要自行保证线程安全。

  • Vector 是线程安全的集合类,通过在方法级别加锁来实现线程安全,但由于性能较低,不推荐在高并发场景下使用。

2. 性能

  • ArrayList 在单线程环境下性能较好,不需要额外的线程同步开销,适合读多写少的场景。

  • Vector 在多线程环境下性能较低,因为需要在方法级别加锁,读写操作都需要加锁,会影响整体性能。

3. 扩展性

  • ArrayList 是 Java 集合框架中的一部分,提供了更多的扩展性和灵活性,可以结合其他集合类和接口进行更多操作。

  • Vector 是较为古老的集合类,功能相对较为单一,扩展性和灵活性不如 ArrayList。

综上所述,虽然 Vector 是线程安全的集合类,但由于性能较低和扩展性较差,不推荐在现代 Java 开发中直接使用 Vector,而是可以选择更适合当前场景的线程安全集合类,如 CopyOnWriteArrayList 或使用 ArrayList 结合线程安全措施来保证线程安全。

Callable

  1. 可以有返回值

  2. 可以抛出异常

  3. 方法不同,run() call()

Callable 接口详解

Callable 接口是 Java 并发包中提供的用于支持返回结果和抛出异常的多线程任务的接口。与 Runnable 接口不同,Callable 接口的 call() 方法可以返回结果,并且可以抛出受检查异常。

主要方法

  1. V call() throws Exceptioncall() 方法用于执行多线程任务的逻辑,并返回一个结果。可以在方法声明中指定抛出的异常。

使用示例

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableExample implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 10; i++) {
            sum += i;
        }
        return sum;
    }

    public static void main(String[] args) {
        CallableExample callableExample = new CallableExample();
        FutureTask<Integer> futureTask = new FutureTask<>(callableExample);

        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            int result = futureTask.get();
            System.out.println("Sum of 1 to 10: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

在上面的示例中,通过实现 Callable 接口,定义了一个计算 1 到 10 的和的多线程任务。通过 FutureTaskCallable 对象包装成一个可获取结果的 Future 对象,然后通过 Thread 启动线程执行任务,并通过 get() 方法获取任务的结果。

使用 Callable 接口可以更方便地获取多线程任务的返回结果,并且可以在 call() 方法中处理异常情况。

示例代码

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableExample implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Hello, Callable!";
    }

    public static void main(String[] args) {
        CallableExample callableExample = new CallableExample();
        FutureTask<String> futureTask = new FutureTask<>(callableExample);

        Thread thread = new Thread(futureTask);
        thread.start();

        try {
            String result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}