Java 多线程

多线程

1.进程与线程

线程和进程计算机中的两个重要概念,用于管理和执行程序的执行单元

1.1 进程

  • 进程是资源分配的最小单位,通过进程可以创程序的应用。

  • 进程是操作系统的一个执行实例,代表了正在运行的程序的一个实例,其有自己的独立的内存空间,包代码、数据、和堆栈等。

  • 进程之间相互独立,彼此不能直接访问对方的内存,且由操作系统通过分配时间片轮流执行各个进程来实现多任务处理;

1.2 线程

  • 线程是进程的执行路径,是进程内的实体。线程是程序执行(进程)的最小单位,

  • 一个进程可包含多个线程,并且可以同时执行(此时就形成了多线程),而且线程不仅可以单独执行,也可通过协同工作实现并行处理。

  • 同一进程中的线程的内存和资源可进行共享,且线程之间可以通过共享内存直接交流和访问数据,比进程间通信更高效。

1.3 线程和进程的对比

  • 进程是资源分配的最小单位,而线程是程序执行的最小单位。
  • 进程之间资源的切换和通信开销较大,而线程之间切换和通信开销较小。
  • 进程之间相互独立,崩溃一个进程不会影响其他进程,而线程共享进程的资源,一个线程的崩溃可能导致整个进程崩溃。
  • 多线程可以提高程序的并发性和响应性,但也需要注意线程安全的问题。

1.4 并发与并行

并发(Concurrency)和并行(Parallelism)是指计算机中多个任务执行的方式,它们用于实现更高效的程序执行和资源利用,以下是他们的简单解释和区别,也可以参考 如图(1)的图释:

并发(Concurrency):

  • 并发是指多个任务在同一时间段内交替执行的情况。
  • 在并发执行中,任务之间可以通过切换执行权轮流执行,使得多个任务似乎同时进行。
  • 并发可以在单个处理器上实现,通过时间切片或事件驱动等机制,使得任务以快速的速度交替执行。
  • 任务的执行顺序可能不确定,而且任务之间可能相互影响,需要使用同步机制来保证数据一致性。

并行(Parallelism):

  • 并行是指多个任务在同一时间点同时执行的情况。
  • 在并行执行中,可以利用多个处理器或多核处理器等硬件资源,将不同的任务同时分配给多个处理器执行。
  • 并行可以实现更高的性能和吞吐量,特别适用于需要大量计算或处理的任务。
  • 任务之间相互独立,不需要同步操作,可以无需考虑数据一致性问题。

区别:

  • 并发是在单个处理器上交替执行多个任务,而并行是利用多个处理器同时执行多个任务。
  • 并发需要通过任务切换和同步机制来实现任务间的交替执行和数据访问控制,而并行无需考虑同步机制。
  • 并发适用于I/O密集型任务,可以提高系统的响应能力和并发处理能力;并行适用于CPU密集型任务,可以提高任务的执行速度和计算能力。

并发和并行都是为了提高计算机系统的性能和资源利用,但实现方式和应用场景不同。并发通过交替执行多个任务在单个处理器上提供较好的响应能力,而并行利用多个处理器同时执行多个任务提供较高的计算能力。在实际应用中,根据任务的性质和系统的硬件资源选择合适的并发和并行方案

(图一)

如图所示:并发是指两个对列交替的在一台咖啡机上接咖啡

​ 并行是指两个对列在两台咖啡机排队接咖啡

​ 串行是指只有一个对列吗,挨个接咖啡

2.自定义线程实现方式

常见的的方式有两种

  • 1.继承Thread类

  • 2.实现Runnable接口

  • 此外还可可以通过Callable实现

下面我们将逐一讲解:

2.1 继承Thread类

  • 创建一个自定义的线程类,继承自java.lang.Thread类。
  • 重写Thread类的run()方法,定义线程的执行逻辑。
  • 在run()方法中编写线程要执行的代码。
  • 创建该自定义线程类的实例,并调用start()方法启动线程。
public class MyThread extends Thread {
    public void run() {
        // 线程执行的代码逻辑
        for (int i = 0; i < 10; i++) {
            System.out.println("Thread running: " + i);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start(); // 启动线程
    }
}
  • 优点:继承方式,优点明显,编程方式比较简单,直接就可以及性能调用
  • 缺点:单继承,所以希望扩展时比较困难

2.2 实现Runnable接口

  • 创建一个自定义的类,实现java.lang.Runnable接口。
  • 实现Runnable接口的run()方法,定义线程的执行逻辑。
  • 创建Thread类的实例,将自定义的Runnable实例作为参数传递给Thread的构造方法。
  • 调用Thread类的start()方法启动线程。
 public class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的代码逻辑
        for (int i = 0; i < 10; i++) {
            System.out.println("Thread running: " + i);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start(); // 启动线程
    }
}
  • 缺点:先将线程接口实例化,在利用Thread实现线程的调用,稍显复杂。
  • 优点:灵活度更高,可以实现继承的同时,还可以同时实现多个接口,可以实现任务的共享机制。

无论是继承Thread类还是实现Runnable接口,都可以用来创建自定义的线程类,并实现线程的自定义逻辑。推荐使用实现Runnable接口的方式,因为Java支持多重继承接口,而只能单继承类。

2.3 run 方法调用和Start调用的区别

在Java中,线程有两种常用的启动方式:调用run()方法和调用start()方法

run()方法:通过直接调用线程对象的run()方法,线程将在当前线程中以普通的方法调用方式执行,而不会创建新的线程。这种方式没有多线程的效果,代码将按照顺序逐个执行,直到run()方法结束

public class MyThread extends Thread {
    public void run() {
        System.out.println("Thread running");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.run(); // 普通方法调用,不会创建新的线程
    }
}

start()方法:通过调用线程对象的start()方法,系统将创建一个新的线程来执行线程对象的run()方法。这种方式实现了多线程的效果,start()方法将启动一个新的线程,并在新线程中调用线程对象的run()方法。

public class MyThread extends Thread {
    public void run() {
        System.out.println("Thread running");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start(); // 创建新线程并调用run()方法
    }
}

通过调用start()方法来启动线程,系统将会为线程分配资源,使得线程在独立的执行路径中运行。这样可以实现线程的并发执行,提高系统的响应和处理能力。

在实际应用中,推荐使用start()方法来启动线程,以获得多线程的效果。直接调用run()方法在某些场景下也是有用的,例如在当前线程中需要按照特定顺序执行一些操作时(因为这样不会创建新的线程,而是将线程的逻辑作为普通方法在当前线程中执行)。

但是,线程的启动应该使用start()方法而不是直接调用run()方法,这样才能启动线程并创建新的执行线程

3. 线程调度

3.1setPriority(int priority)(优先级调度)

优先级调度:Java中的线程有10个优先级,范围从1到10,其中1是最低优先级,10是最高优先级,默认优先级是5。可以使用Thread类的setPriority(int priority)方法设置线程的优先级,值越高表示优先级越高。例如:

Thread thread = new Thread();
thread.setPriority(Thread.MAX_PRIORITY); // 设置线程的优先级为最高

线程优先级的实际影响因操作系统而异,不同的操作系统可能对优先级的处理方式不同

3.2 sleep(线程休眠)

sleep()方法:Thread类的sleep(long millis)方法可以让当前线程休眠一段时间。通过调用sleep()方法,可以控制线程的暂停时间

try {
    Thread.sleep(1000); // 当前线程休眠1秒
} catch (InterruptedException e) {
    e.printStackTrace();
}

注意,sleep()方法也可以抛出InterruptedException异常,因此需要进行异常处理

3.3 yield(线程礼让)

yield()方法:Thread类中的yield()方法允许当前线程让出CPU资源,使其他线程有机会运行。调用yield()方法将暂停当前线程并将CPU资源让给其他具有相同或更高优先级的线程。例如

Thread.yield(); // 当前线程主动让出CPU资源

yield()方法不能保证其他线程一定能获得CPU资源,它仅作为一种提示,告诉调度器当前线程愿意让出CPU

3.4 join(线程“强制”插入)

通过使用join()方法,可以实现线程之间的协同执行和等待,常用于多线程协作和任务编排等场景。

join()方法用于在一个线程中等待另一个线程完成执行。当调用线程的join()方法时,它将暂停执行,直到被调用的线程执行完毕或超时

join()方法有以下几个重载形式:

  1. join(): 该方法会让当前线程等待被调用线程执行完毕。
  2. join(long millis): 该方法会让当前线程等待被调用线程执行完毕,或者等待指定的时间(以毫秒为单位)。如果指定的时间到期,而被调用的线程仍未执行完毕,当前线程会继续执行。
  3. join(long millis, int nanos): 与上一种方式类似,该方法也会让当前线程等待被调用线程执行完毕,或者等待指定的时间。但是,除了等待指定的毫秒数外,还可以额外指定纳秒数
Thread thread = new Thread(() -> {
    // 执行一些耗时操作
});

thread.start(); // 启动被调用线程

try {
    thread.join(); // 等待被调用线程执行完毕
} catch (InterruptedException e) {
    e.printStackTrace();
}

// 当被调用线程执行完毕后,继续执行其他操作

在上述示例中,调用thread.join()方法会导致当前线程暂停执行,直到thread线程执行完毕。这样可以确保在thread线程执行完成后,才会继续执行主线程中的其他操作。

注意:java的线程调度是由JVM和操作系统共同完成的,具体的行为可能受到底层操作系统的影响。在实际应用中,建议根据具体需求合理设置线程的优先级,并根据需要使用yield()sleep(),join等方法来控制线程的调度。

4. 线程同步

线程同步是指在多个线程访问共享资源时,通过采用某种机制来控制线程的执行顺序,以避免数据不一致或并发访问问题。线程同步的主要目的是保证共享资源的正确性和一致性

如下代码导致的数据不一致的并发问题:

public class SellTicket implements Runnable{

    //卖10张票
    private  int ticket =10;

    //卖票
    @Override
    public void run() {
        sellTicket();
    }

    public void sellTicket() {
        while (true){
            if (ticket>0){
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                System.out.println(Thread.currentThread().getName()+"车票卖出去了"+(10-(ticket--))+"张车车票");
            }else {
                System.out.println("票卖完了");
                break;
            }
        }
    }


    public static void main(String[] args) {
        SellTicket sellTicket=new SellTicket();

        Thread thread1=new Thread(sellTicket,"1");
        Thread thread2=new Thread(sellTicket,"2");
        Thread thread3=new Thread(sellTicket,"3");
        Thread thread4=new Thread(sellTicket,"4");

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

错误图示:

解决方法:

1.synchronized关键字:使用synchronized关键字可以实现对方法或代码块的同步,确保同一时间只有一个线程访问被同步的代码段。当一个线程进入synchronized方法或代码块时,它会锁定相关的对象,其他线程需要等待锁释放后才能执行

public synchronized void synchronizedMethod() {
    // 同步的方法体
}

2.Lock:Lock和synchronized 都Java队锁的定义 其有很多实现类均由 jDK 提供 ,其中 常用的ReentrantLock是JDK提供的一个可重入锁类,它提供了与synchronized关键字相似的功能,但更加灵活和扩展性强。通过使用ReentrantLock,可以实现精确控制锁的获取和释放,支持公平锁和非公平锁等特性

ReentrantLock lock = new ReentrantLock();
lock.lock(); // 获取锁

try {
    // 同步的代码块
} finally {
    lock.unlock(); // 释放锁
}

3.AtomicInteger类:AtomicInteger是Java提供的一个原子类,它提供了一种线程安全的方式来进行原子操作。通过使用AtomicInteger,可以实现对共享的整数变量进行原子操作,如增加、减少、比较和交换等操作

4 volatile关键字:在Java中,volatile关键字用于声明变量,确保在多个线程之间的可见性。当一个变量被声明为volatile时,每个线程都能够获取到最新的值,而不会读取到过期的缓存值。volatile关键字适用于仅进行单纯值的读写操作,不能用于复合操作的原子性控制.

这些线程同步机制可以根据具体的需求和应用场景来选择合适的方式。使用线程同步机制可以避免多线程并发访问共享资源时出现的数据竞争和不一致问题,从而保证程序的正确性和稳定性

下面我们将一 一讲解:

4.1 synchronized

synchronized是Java中的一个关键字,用于实现线程同步。它可以用来修饰方法或代码块,确保在同一时间只有一个线程可以执行被synchronized修饰的代码

synchronized关键字的使用方式有以下几种:

4.1.1修饰实例方法:

synchronized修饰实例方法时,同一实例对象的不同线程会同步执行这个方法。只有当一个线程执行完该方法后,其他要调用该方法的线程才能继续执行。例如:

public synchronized void synchronizedMethod() {
    // 在这里编写需要同步的代码
}
4.1.2修饰静态方法:

synchronized修饰静态方法时,不同实例对象的线程也会同步执行这个方法。只有当一个线程执行完该方法后,其他要调用该方法的线程才能继续执行

  • 当在一个静态的方法上添加synchronized关键字,会采用当前类的.class类信息作为锁对象。
  • 如果是一个静态代码块,可以使用静态的变量进行修饰,但是有可能会发生在外部修改锁对象的值。建议采用类的.class作为锁对象。
  • 线程互斥:线程带有锁后,可能会出现互斥现象。静态的同步方法或方法块,不会出现互斥的现象。
public static synchronized void synchronizedStaticMethod() {
    // 在这里编写需要同步的代码
}
4.1.3 修饰代码块:

除了修饰方法,synchronized还可以修饰代码块,用于更细粒度的同步控制。通过指定一个对象作为锁,确保同一时间只有一个线程可以持有该锁,进入被synchronized修饰的代码块执行。例如:

  • 通过锁对象可以将当前的代码段进行锁定,其他的线程需要排队。
  • 作为同步方法一旦被整体修饰,就代表着整个方法内部都不能够实现异步访问。
  • 作为同步代码块,可以先调用synchronized修饰之前的代码,提升程序的执行性能。
public void synchronizedBlock() {
    // ...
    synchronized (obj) {  // obj为锁对象
        // 在这里编写需要同步的代码
    }
    // ...
}

注意:

当多个线程同时访问同一对象的synchronized方法或代码块时,只有一个线程可以获得锁,并执行同步代码,其他线程需要等待锁的释放才能继续执行

synchronized关键字提供了隐式的锁机制,使得对共享资源的访问变得同步和安全。然而,需要注意的是,使用synchronized可能会引起线程的阻塞,慎重使用以避免死锁或线程饥饿的问题。在实际应用中,可以根据具体的需求和场景,选择适合的同步方式

public class SellTicket implements Runnable{

    //卖10张票
    private  int ticket =10;

    //卖票
    @Override
    public void run() {
        sellTicket();
    }

    public void sellTicket() {
        while (true){
            synchronized (this){
                if (ticket>0){
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                    System.out.println(Thread.currentThread().getName()+"车票卖出去了"+(10-(ticket--))+"张车车票");
                }else {
                    System.out.println("票卖完了");
                    break;
                }
            }
        }
    }


    public static void main(String[] args) {
        SellTicket sellTicket=new SellTicket();

        Thread thread1=new Thread(sellTicket,"1");
        Thread thread2=new Thread(sellTicket,"2");
        Thread thread3=new Thread(sellTicket,"3");
        Thread thread4=new Thread(sellTicket,"4");

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UVIwM8Eh-1689607984740)(C:\Users\yuguiping\AppData\Roaming\Typora\typora-user-images\image-20230717212113943.png)]

5. Lock

通过synchronized都是Java中的锁定义。由于synchronized灵活度较低,没有办法实现锁的监控过程(lock 、unlock)以及公平锁的实现,所以在JDK5的版本中,在java.util.concurrent.locks包中添加Lock接口,同时给了一些实现类

比较常用的实现类——ReentrantLock类。

在Java中,Lock是一种比synchronized更加灵活的锁机制。它提供了显式的加锁和释放锁的控制,并支持更多高级特性。

Lock接口定义了以下几个常用的方法:

  1. void lock(): 获取锁,如果锁不可用,则当前线程会被阻塞,直到获取到锁为止。如果线程已经被中断,将抛出InterruptedException异常。
  2. void lockInterruptibly(): 获取锁,如果锁不可用,则当前线程会被阻塞。如果线程已经被中断,在等待锁的过程中可以响应中断并中断等待,并抛出InterruptedException异常。
  3. boolean tryLock(): 尝试获取锁,如果锁可用,则立即返回true,并获得锁;如果锁不可用,则立即返回false,不会阻塞当前线程。
  4. boolean tryLock(long timeout, TimeUnit unit): 尝试获取锁,在指定的时间内如果获得到锁,则立即返回true,否则等待指定时间。如果在等待期间线程被中断,则抛出InterruptedException异常。
  5. void unlock(): 释放锁。

使用如下:

Lock lock = new ReentrantLock();  // 创建一个Lock对象

lock.lock();  // 获取锁

try {
    // 执行需要同步的代码
} finally {
    lock.unlock();  // 释放锁
}

相比于synchronized关键字,Lock提供了更强大的功能,例如可中断锁、可轮询锁、公平锁和非公平锁等。它能够更细粒度地控制锁的加锁和释放,更好地满足复杂场景下的需求。

需要注意的是,在使用Lock时,必须确保每次获取锁后都会对应地释放锁,以免造成死锁或资源泄漏等问题。此外,由于Lock是一个接口,具体的实现类有多种选择,如ReentrantLockStampedLock等,可以根据具体需求选择适合的实现

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

public class SellTicket implements Runnable{

    //卖10张票
    private  int ticket =10;


    private Lock lock=new ReentrantLock(true);
    //卖票
    @Override
    public void run() {
        sellTicket();
    }

    public void sellTicket() {
        while (true){
                lock.lock(); //上锁
                if (ticket>0){
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                    System.out.println(Thread.currentThread().getName()+"车票卖出去了"+(10-(ticket--))+"张车车票");
                    lock.unlock(); //释放锁
                }else {
                    System.out.println("票卖完了");
                    lock.unlock(); //释放锁
                    break;
                }
            }
        }



    public static void main(String[] args) {
        SellTicket sellTicket=new SellTicket();

        Thread thread1=new Thread(sellTicket,"1");
        Thread thread2=new Thread(sellTicket,"2");
        Thread thread3=new Thread(sellTicket,"3");
        Thread thread4=new Thread(sellTicket,"4");

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XJEZaNRm-1689607984741)(C:\Users\yuguiping\AppData\Roaming\Typora\typora-user-images\image-20230717213453552.png)]

5.1 synchronized与lock的区别

在Java中,synchronizedLock是实现线程同步的两种不同机制,它们有以下几个区别:

  1. 使用方式: synchronized是一种关键字,可以用来修饰方法或代码块,而Lock是一个接口,需要使用具体的实现类来创建一个锁对象。
  2. 锁的获取和释放: 使用synchronized关键字时,锁的获取和释放是隐式的,由Java虚拟机自动控制。而使用Lock接口时,锁的获取和释放是显式的,需要手动调用lock()方法获取锁,调用unlock()方法释放锁。
  3. 灵活性: Lock相比synchronized更加灵活,提供了一些高级特性,例如可中断锁、可轮询锁、公平锁、非公平锁等。这些特性使得Lock能够更好地满足复杂场景下的需求。
  4. 性能: 在低并发情况下,synchronized的性能可能略优于Lock,因为synchronized是由Java虚拟机底层实现的,它能够针对不同的场景进行优化。而Lock则是通过Java代码实现的,它可能会有额外的开销。然而,在高并发情况下,Lock的性能可能会更好,因为它提供了更细粒度的控制和更好的并发性。
  5. 适用范围: synchronized适用于简单的同步场景,可以快速实现线程之间的同步。而Lock适用于更复杂的同步场景,可以提供更灵活和高级的功能。

总的来说,synchronized是Java中的基本线程同步机制,使用简单并且在大多数场景下都能满足需求。而Lock具有更高级的功能和更好的灵活性,适用于更复杂的同步需求。在实际开发中,可以根据具体的需求和场景选择适合的线程同步机制。

2.线程的状态

1.线程状态概述:

1.1线程由生到死的完整过程:

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周

期中,有几种状态呢?在API中 java.lang.Thread.State 这个枚举中给出了六种线程状态:

在Java中,线程可以处于不同的状态,这些状态是由Thread类内部的状态变量来表示的。以下是几种常见的线程状态:

  1. New(新建):当一个线程对象被创建但尚未调用start()方法时,线程处于新建状态。此时线程是不可运行的。
  2. Runnable(可运行):当线程对象调用了start()方法后,线程进入可运行状态。此时线程处于等待CPU调度的状态,一旦获取到CPU时间片,就可以执行线程的run()方法。
  3. Blocked(阻塞):线程可能因为某种原因被阻塞,无法继续执行。常见的情况包括等待锁的释放、等待输入/输出、等待其他线程的通知等。处于阻塞状态的线程不会消耗CPU资源。
  4. Waiting(等待):线程通常进入等待状态是因为调用了Thread类的wait()方法或Object类的wait()join()sleep()等方法。等待状态的线程需要其他线程的特定动作来唤醒。
  5. Timed Waiting(计时等待):线程调用了带有时间限制的等待方法,比如Thread类的sleep()方法或Object类的wait(timeout)方法。在指定的时间内,线程将处于计时等待状态。
  6. Terminated(终止):线程执行完run()方法或因异常等原因导致线程退出时,线程进入终止状态。一旦线程被终止,不可再次启动。

通过使用线程状态,我们可以了解和控制线程的执行状态,并进行相应的处理。在多线程编程中,了解各个线程状态的转换和含义对于进行并发编程和调试非常重要,

我们不需要去研究这几种状态的实现原理,我们只需知道在做线程操作中存在这样的状态。那我们怎么去理解这几个状态呢,新建与被终止还是很容易理解的,我们就研究一下线程从Runnable(可运行)状态与非运行状态之间的转换问题。

1.2 wait与notify(等待与唤醒)

Object类的方法

1.2.1 wait

wait()是Java中Object类的一个方法,用于使当前线程进入等待状态,直到其他线程调用相同对象上的notify()notifyAll()方法来唤醒它。

wait()方法有以下几个重要的特点:

  1. wait()方法必须在synchronized块或方法中使用,因为它要求当前线程释放对象的锁,而只能在持有锁的情况下才能调用。
  2. 当一个线程调用wait()方法后,它会释放持有的锁,并进入等待状态,直到以下情况之一发生:
    • 其他线程调用相同对象上的notify()方法来唤醒当前等待的线程。
    • 其他线程调用相同对象上的notifyAll()方法来唤醒所有等待的线程。
    • 其他线程中断了当前线程。
  3. 被唤醒的线程会尝试重新获得对象的锁,一旦获得锁,它将从wait()方法的调用处继续执行。

使用wait()方法的经典模式如下:

synchronized (obj) {
    while (条件不满足) {  // 使用while循环判断条件,避免虚假唤醒
        obj.wait();  // 当前线程进入等待状态,并释放obj对象的锁
    }
    // 执行等待之后的逻辑
}

需要注意的是,wait()方法必须在同步块中使用,并且通常会使用一个条件来进行循环判断,以防止虚假唤醒(即条件不满足时被误唤醒)。在使用wait()notify()时,确保满足线程间的正确通信和同步需求,以避免出现死锁或活锁等问题。

1.2.2 notify

notify()是Java中Object类的一个方法,用于唤醒在相同对象上调用wait()方法而进入等待状态的线程中的一个线程。它会选择唤醒等待时间最长的线程,并使其从等待状态转换为可运行状态。

notify()方法具有以下几个重要的特点:

  1. notify()方法必须在synchronized块或方法中使用,因为它要求当前线程持有对象的锁,并且只能在持有锁的情况下调用。
  2. notify()方法的调用会唤醒在相同对象上调用wait()方法而进入等待状态的一个线程,并使其从等待状态转换为可运行状态。但是,被唤醒的线程不会立即执行,它需要等待唤醒的线程释放锁之后才能继续执行。
  3. notify()方法只会唤醒一个等待线程,如果有多个线程在等待同一个对象上的锁,则只有其中一个线程会被唤醒。

使用notify()方法可以实现线程间的通信和同步。下面是一个经典的使用示例:

synchronized (obj) {
    // 执行一些操作
    obj.notify();  // 唤醒在obj对象上调用wait()方法而等待的一个线程
}

需要注意的是,notify()方法必须在同步块中使用,并确保在调用notify()之前已经完成了对共享变量的修改,以免唤醒的线程看不到所需的状态更新。

另外,还有一个notifyAll()方法,它唤醒在相同对象上调用wait()方法而进入等待状态的所有线程,而不是唤醒一个线程。通常情况下,应该使用notifyAll()方法来确保所有等待的线程都能被唤醒,以避免死锁或活锁等问题

1.2.3 案例(等待 -唤醒)

定义一个集合,包子铺线程完成生产包子,包子添加到集合中;吃货线程完成购买包子,包子从集合中移除。
1. 当包子没有时(包子状态为false),吃货线程等待.
2. 包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态)
public class BaoZiPu extends  Thread{

    private List<String> list;

    public BaoZiPu(String name, List<String> list) {
        super(name);
        this.list = list;
    }

    @Override
    public  void run(){
        int i=0;
        while (true){

            //list作为锁  线程等待
            synchronized (list){
                if (list.size()>0){
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }

                //如果线程没进入等待状态,说明集合中没有元素
                //生产包子
                list.add("包子"+i++);

                System.out.println(list);

                list.notify();
            }

        }
    }
}

public class ChiHuo extends Thread {

    private List<String> list;

    public ChiHuo(String name, List<String> list) {
        super(name);
        this.list = list;
    }

    @Override
    public void run(){
        while (true){
            synchronized (list){
                if (list.size()==0){
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                list.remove(0);
                System.out.println(list);
                list.notify();
            }

        }
    }
}
public class Demo {
    public static void main(String[] args) {
//等待唤醒案例
        List<String> list = new ArrayList<>();
// 创建线程对象
        BaoZiPu bzp = new BaoZiPu("包子铺", list);
        ChiHuo ch = new ChiHuo("吃货", list);
// 开启线程
        bzp.start();
        ch.start();
    }
}

2. volatile关键字

volatile是Java中的一个关键字,用于声明变量,以提供可见性和禁止指令重排序的特性。当一个变量被声明为volatile时,对它的读写操作都具有以下特点:

  1. 可见性: 当一个线程修改一个volatile变量的值时,其他线程可以立即看到最新的值。这是因为volatile保证了变量的更新对其他线程的可见性,即保证了线程之间的通信。
  2. 禁止指令重排序: volatile关键字禁止编译器和处理器对变量的读写重排序,保证了变量的有序性。这意味着volatile变量的赋值操作在指令执行顺序上不会被优化或重排序,即保证了线程按照预期的顺序读写变量。

volatile关键字适用于以下场景:

  1. 状态标志: 当一个变量用于标识多个线程之间的某种状态,且这些线程需要及时获取最新的状态值时,可以使用volatile关键字来确保可见性。
  2. 双重检查锁定(Double-Checked Locking): 在多线程环境下使用双重检查锁定模式时,如果使用的变量没有被声明为volatile,可能会引发线程安全问题。将需要被共享的变量声明为volatile可以解决该问题。

需要注意的是,虽然volatile提供了可见性和禁止指令重排序的特性,但它并不能保证原子性。如果涉及到复合操作(例如自增或自减操作),仍然需要使用其他的同步机制,如synchronized块或Lock接口。

总之,volatile关键字是一种轻量级的同步机制,用于确保变量的可见性和有序性,在多线程环境下使用时需要特别注意其适用的场景和限制。

private volatile boolean flag ;

工作原理:

  1. VolatileThread线程从主内存读取到数据放入其对应的工作内存

  2. 将flag的值更改为true,但是这个时候flag的值还没有写会主内存

  3. 此时main方法main方法读取到了flag的值为false

  4. 当VolatileThread线程将flag的值写回去后,失效其他线程对此变量副本

  5. 再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中

总结: volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。

注意: volatile不保证原子性

volatile****的使用场景

  • 开关控制

    利用可见性特点,控制某一段代码执行或者关闭(比如今天课程的第一个案例)。

  • 多个线程操作共享变量,但是是有一个线程对其进行写操作,其他的线程都是读

2.1看程序说结果

public class VolatileThread extends Thread {
    // 定义成员变量
    private boolean flag = false ;
    public boolean isFlag() { return flag;}
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
// 将flag的值更改为true
        this.flag = true ;
        System.out.println("flag=" + flag);
    }
}
public class VolatileThreadDemo {// 测试类
    public static void main(String[] args) {
// 创建VolatileThread线程对象
        VolatileThread volatileThread = new VolatileThread() ;
        volatileThread.start();
// main方法
        while(true) {
            if(volatileThread.isFlag()) {
                System.out.println("执行了======");
    		}
        }
    }
}

通过观察控制台,我们看到,VolatileThread线程中已经将flag设置为true,但main()方法中始终没有读到,从而没有打印。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3NQC8lCp-1689607984741)(C:\Users\yuguiping\AppData\Roaming\Typora\typora-user-images\image-20230717215436466.png)]

2.2 JMM

JMM(Java内存模型)是Java语言中用于定义多线程并发访问共享内存的规范。它规定了线程如何与主内存和工作内存进行交互,以及操作的可见性、有序性和原子性。JMM确保了多线程程序在不同的平台和虚拟机上的表现是一致的。

JMM的主要特性如下:

  1. 主内存(Main Memory):主内存是所有线程共享的内存区域,包含了所有的变量(包括对象的实例变量和静态变量)。
  2. 工作内存(Working Memory):工作内存是每个线程独有的内存区域,用于保存线程需要使用的变量的副本。线程对变量的所有操作都是在工作内存中进行的。
  3. 内存间交互操作:线程之间要进行数据的读取和写入必须通过主内存来完成。一个线程对变量的写操作在其他线程中可能不可见,直到写入操作被刷新到主内存。类似地,线程中对变量的读操作可能读取到的是工作内存中已有的副本,而不是主内存中的最新值。
  4. 原子性、可见性和有序性:JMM通过一系列的规则来保证操作的原子性、可见性和有序性。其中,原子性指操作不可被线程中断,可见性指线程对变量的修改对其他线程可见,有序性指程序的执行顺序符合预期。

JMM的设计使得开发者可以通过编写符合JMM规范的代码来保证多线程程序的正确性。同时,JMM也给出了一些关于synchronized、volatile和final等关键字的语义规定,以便开发者正确地使用这些关键字来实现线程安全。

需要注意的是,JMM只保证了共享变量的可见性和有序性,而不保证代码的逻辑正确性。所以,在编写多线程程序时,仍需要合理地使用同步机制、锁和并发容器等工具来保证线程安全性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9cokPkgH-1689607984742)(C:\Users\yuguiping\AppData\Roaming\Typora\typora-user-images\image-20230717215634382.png)]

2.3. 问题分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F5QzFZoN-1689607984742)(C:\Users\yuguiping\AppData\Roaming\Typora\typora-user-images\image-20230717215848436.png)]

  1. VolatileThread线程从主内存读取到数据放入其对应的工作内存

  2. 将flag的值更改为true,但是这个时候flag的值还没有写会主内存3. 此时main方法读取到了flag的值为false

  3. 当VolatileThread线程将flag的值写回去后,但是main函数里面的while(true)调用的是系统比较底层的代码,速度快,快到没有时间再去读取主存中的值,所以while(true)读取到的值一直是false。(如果有一个时刻main线程从主内存中读取到了主内存中flag 的最新值,那么if语句就可以执行,main线程何时从主内存中读取最新的值,我们无法控制)

2.4问题处理

// main方法
while(true) {
    synchronized (volatileThread) {
        if(volatileThread.isFlag()) {
            System.out.println("执行了");
		}
	}
}

某一个线程进入synchronized代码块前后,执行过程入如下:

a.线程获得锁

b.清空工作内存

c.从主内存拷贝共享变量最新的值到工作内存成为副本

d.执行代码

e.将修改后的副本的值刷新回主内存中

f.线程释放锁

volatilesynchronized

volatilesynchronized都是Java中用于处理多线程并发访问共享数据的关键字,但它们在实现上有一些重要的区别。

  1. 可见性和有序性
    • volatile关键字用于确保被修饰的变量对所有线程的可见性和操作的有序性。当一个线程修改一个volatile变量的值时,其他线程可以立即看到最新的值。此外,volatile关键字禁止编译器和处理器对volatile变量的读写指令重排序。
    • synchronized关键字不仅提供了可见性和有序性,还提供了原子性的保证。synchronized关键字用于实现互斥访问,确保同时只有一个线程能够持有某个对象的锁。当一个线程获得对象的锁时,它会将自己的工作内存同步到主内存,以便其他线程可以看到修改的值。
  2. 原子性
    • volatile关键字并不能保证变量操作的原子性。例如,对一个volatile int变量执行自增操作时,如果多个线程同时执行自增操作,可能会出现竞争条件和结果不确定性。因此,volatile适用于对单个变量的读写操作,但不适用于复合操作。
    • synchronized关键字提供了对代码块或方法的原子性操作,确保多个语句的执行在同一时刻只能由一个线程执行。在synchronized块中的所有语句都是原子操作,即要么全部执行完毕,要么完全不执行。
  3. 锁的获取方式
    • volatile关键字是通过标记变量的内存位置来达到可见性和有序性的目的,并没有锁的概念。它的主要作用是告诉编译器和处理器,对该变量的读写操作需要直接与主内存进行交互。
    • synchronized关键字使用锁来确保原子性和互斥访问。只有获得了对象的锁,其他线程才能等待或者进入临界区。

综上所述,volatile关键字用于保证变量的可见性和有序性,而synchronized关键字则提供了更强大的功能,即保证变量的可见性、有序性和原子性,并且允许多个线程同步地访问共享资源。**注:**在Java 5及以后的版本中,volatile关键字的语义得到了修复,也提供了更多的应用场

3.原子性

原子性(Atomicity)是指一个操作是不可中断的,要么全部执行成功,要么全部不执行。在多线程环境中,原子性是保证数据一致性和线程安全性的一个重要属性。

在Java中,原子性可以通过以下方式来保证:

  1. synchronized关键字:使用synchronized关键字可以实现对代码块或方法的原子性操作。当一个线程获得了对象的锁,其他线程就无法进入临界区,从而保证了操作的原子性。
  2. Lock接口:使用Lock接口提供的锁机制,如ReentrantLock,可以通过获取和释放锁来实现对代码块的原子操作。
  3. Atomic类:Java提供了一系列的原子操作类,如AtomicInteger、AtomicLong等,它们使用了底层的CAS(Compare and Swap)算法,保证了操作的原子性。

原子性的重要性体现在以下几个方面:

  1. 数据一致性:当多个线程同时访问共享数据时,如果没有原子性保证,可能会出现数据不一致的情况,导致程序的逻辑错误或异常。
  2. 线程安全性:原子性是确保多线程程序安全性的重要手段,能够避免多线程之间的竞争条件,减少并发错误的发生。

需要注意的是,并非所有的操作都需要保证原子性。在一些非关键的操作中,可以通过其他的同步机制来保证线程安全,或者根据实际需求进行权衡和优化。

总的来说,原子性是指一个操作不会被线程中断或并发干扰,要么全部执行成功,要么全部不执行。在多线程环境中,确保操作的原子性是保证数据的一致性和线程安全性的重要基础。

count++操作包含3个步骤:

从主内存中读取数据到工作内存

对工作内存中的数据进行++操作

将工作内存中的数据写回到主内存

count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打

断。

1)假设此时x的值是100,线程A需要对改变量进行自增1的操作,首先它需要从主内存中读取变量x的值。由

于CPU的切换关系,此时CPU的执行权被切换到了

B线程。A线程就处于就绪状态,B线程处于运行状态

2)线程B也需要从主内存中读取x变量的值,由于线程A没有对x值做任何修改因此此时B读取到的数据还是100

3)线程B工作内存中x执行了+1操作,但是未刷新之主内存中

4)此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程

工作内存中的变量值还是100,没有失效。

A线程对工作内存中的数据进行了+1操作

5)线程B将101写入到主内存

6)线程A将101写入到主内存

虽然计算了2次,但是只对A进行了1次修改。

解决问题

我们可以给count++操作添加锁,那么count++操作就是临界区的代码,临界区只能有一个线程去执行,所以count++就变成了原子操作。

public class VolatileAtomicThread implements Runnable {
    // 定义一个int类型的变量
    private volatile int count = 0 ;
    private static final Object obj = new Object();
    @Override
    public void run() {
// 对该变量进行++操作,100次
        for(int x = 0 ; x < 100 ; x++) {
            synchronized (obj) {
                count++ ;
                System.out.println("count =========>>>> " + count);
            }
        }
    }
}
原子类

概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。

原子类(Atomic classes)是Java中提供的一组特殊类,用于对共享变量进行原子操作,保证多线程环境下的线程安全性。

在Java中,原子类位于java.util.concurrent.atomic包下,提供了一系列的原子操作类,如AtomicBooleanAtomicIntegerAtomicLongAtomicReference等。

这些原子类通过使用底层的CAS(Compare and Swap)算法来实现线程安全的操作,保证了对共享变量的读取、写入和修改等操作的原子性。CAS算法是一种无锁算法,它通过比较当前值和期望值是否相等来判断共享变量是否被其他线程修改过,如果没有修改,则可以原子地修改共享变量的值。

原子类提供了一系列的原子操作方法,例如:

  • get():获取当前值。
  • set():设置新的值。
  • getAndSet():获取当前值并设置新值。
  • compareAndSet():如果当前值等于预期值,则原子地设置新值。

除了基本的原子类型(如整型、长整型、布尔型等),原子类还提供了更高级的操作,如getAndIncrement()getAndDecrement()getAndAdd()等,可以用于原子地进行自增、自减、加法等操作。

使用原子类可以简化多线程环境下对共享变量的操作,避免了使用锁或synchronized关键字的复杂性和开销。然而,需要注意的是,原子类的操作虽然保证了单个操作的原子性,但对于复合操作,仍然需要使用合适的同步机制来确保线程安全

AtomicInteger

原子型Integer,可以实现原子更新操作

AtomicInteger是Java中的一个原子类型,用于原子性地操作整型变量。它位于java.util.concurrent.atomic包下,使用了底层的CAS(Compare and Swap)算法来实现线程安全的自增、自减、比较和设置等操作。

AtomicInteger提供了一系列的方法来对整型变量进行原子操作,包括:

  • incrementAndGet():原子地将当前值加1,并返回结果。
  • decrementAndGet():原子地将当前值减1,并返回结果。
  • getAndIncrement():原子地将当前值返回,并将值加1。
  • getAndDecrement():原子地将当前值返回,并将值减1。
  • addAndGet(int delta):原子地将delta值加到当前值上,并返回结果。
  • getAndAdd(int delta):原子地将delta值加到当前值上,并返回当前值。
  • compareAndSet(int expect, int update):如果当前值等于expect,原子地将当前值设置为update,并返回操作结果。

这些方法保证了对AtomicInteger对象的操作是原子的,即在多线程并发访问时能够保证线程安全。

AtomicInteger还提供了其他一些方法,如get()用于获取当前值,set(int newValue)用于直接设置新的值等。

使用AtomicInteger可以简化多线程环境下对整型变量的操作,并避免了使用synchronized关键字的开销和复杂性。但需要注意的是,AtomicInteger只能保证单个操作的原子性,对于多个操作的组合,仍需要其他的同步机制来确保线程安全。

原子类 CAS 机制实现线程安全

CAS的全称是: Compare And Swap(比较再交换); 是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。也 是一种常见的并发算法,用于实现无锁同步。它是在多线程并发环境中保证数据一致性和线程安全的一种策略。

CAS可以将read-modify-check-write转换为原子操作,这个原子操作直接由处理器保证。

CAS操作包含三个操作数:内存地址 V(或称为变量值的地址)、期望值 A 和新值 B。它的执行过程如下:

  1. 比较内存地址中的值是否等于期望值;
  2. 如果相等,则将新值赋给内存地址中的值;
  3. 如果不等,则说明其他线程已经修改了内存地址中的值,操作失败。

CAS是一种无锁算法,它不会阻塞线程等待锁的释放,而是通过原子性地对比和交换来保证数据一致性和线程安全。由于CAS不使用锁,因此它避免了锁竞争带来的开销和问题,提高了并发性能。

在Java中,Atomic系列的原子类(如AtomicIntegerAtomicLong等)就是使用CAS算法实现的。这些原子类提供了一系列的原子操作方法,使用了底层的CAS指令来保证对共享变量的原子操作。通过使用CAS操作,可以避免使用锁或synchronized关键字,从而提升多线程并发的性能和可伸缩性。

需要注意的是,虽然CAS能够提供无锁的线程安全,但它也有一些限制。当多个线程同时执行CAS操作时,只有一个线程能够成功,其他线程可能需要重试,这可能会导致自旋等待的效率降低。此外,CAS操作对于复杂的操作,例如涉及多个变量的操作,可能无法保证原子性,此时仍需要使用其他的同步机制来保证线程安全。

  1. 在内存地址V当中,存储着值为10的变量

  1. 此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11

  1. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

  1. 线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交

失败。

  1. 线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。

这个重新尝试的过程被称为自旋。

  1. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等

的。

  1. 线程1进行SWAP,把地址V的值替换为B,也就是12。

CAS与Synchronized:乐观锁,悲观锁。

CAS(Compare and Swap)和synchronized是两种并发编程中常用的同步机制,它们代表了乐观锁和悲观锁的不同思想。

CAS和Synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?

CAS与乐观锁:
  • 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!

  • CAS是一种乐观锁的实现方式。它认为在大多数情况下,并发访问不会产生冲突,因此不使用锁而是尝试执行操作,然后检查操作是否成功。如果操作成功,则继续执行;如果操作失败,则需要回退重试。

  • CAS操作利用底层的原子性指令,比较当前值与期望值,如果相等则更新值,并认为操作成功。如果在比较与更新之间,有其他线程对该值做了修改,则CAS会失败。

  • 乐观锁是一种乐观地认为并发访问很少冲突的思想,适用于多读少写的场景。使用CAS操作可以避免锁的竞争,提高并发性能。但是,CAS并不适用于涉及多个变量的复合操作,因为无法保证这些操作的原子性。

synchronized与悲观锁
  • Synchronized是从悲观的角度出发(悲观锁)

  • 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。性能较差!!

  • synchronized是一种悲观锁的实现方式。它认为并发访问会产生冲突,因此使用互斥锁来确保同一时间只有一个线程可以执行关键代码块,其他线程需要等待锁的释放才能执行。

  • 使用synchronized关键字可以保证关键代码块的原子性和线程安全性,但也会引入锁竞争和线程阻塞的开销。悲观锁适用于多写少读或写多读少的场景,在访问共享资源时保证数据一致性和线程安全。

  • 需要注意的是,synchronized关键字会自动释放锁,而CAS操作不会自动释放,需要在操作失败时进行回退和重试。

在实际应用中,选择CAS还是synchronized取决于具体的场景和需求。如果并发访问较少且并发冲突较少,可以考虑使用CAS进行乐观锁的实现;如果并发冲突较多或需要保证复合操作的原子性,可以使用synchronized进行悲观锁的实现。

4.并发包

在JDK的并发包里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用

并发包(Concurrent package)是Java中提供的一组用于多线程编程的工具和类库。它位于java.util.concurrent包下,提供了一系列的线程安全的集合类、同步器、原子类和线程池等工具,用于简化并发编程的开发。

下面是并发包中主要的类和工具:

  1. 线程安全集合类:并发包提供了一系列的线程安全的集合类,如ConcurrentHashMap(线程安全的哈希表)、ConcurrentLinkedQueue(线程安全的链表队列)、CopyOnWriteArrayList(线程安全的数组列表)等。这些集合类能够在多线程并发访问时保证线程安全性。

  2. 同步器:并发包提供了一些同步器类,如Semaphore(信号量)、CountDownLatch(倒计时门闩)、CyclicBarrier(循环屏障)等。这些同步器可以用于线程间的协同操作,控制线程的执行顺序和同步。

  3. 原子类:并发包提供了一系列的原子类,如AtomicIntegerAtomicLongAtomicReference等。这些原子类使用CAS(Compare and Swap)算法实现线程安全的原子操作,简化了多线程环境下对共享变量的操作。

  4. Executor框架:并发包提供了Executor框架,包括ExecutorExecutorServiceThreadPoolExecutor等类,用于管理线程池和执行多线程任务。通过使用线程池可以避免频繁的线程创建和销毁开销,提高并发性能。

并发包提供的类和工具可以简化多线程编程的复杂性,提供了一系列的线程安全的工具和数据结构,帮助开发人员处理并发访问的问题,提高多线程程序的性能和可伸缩性。它是进行高效、安全的并发编程的重要支持库之一。

4.1 ConcurrentHashMap

ConcurrentHashMap是Java并发包中提供的线程安全的哈希表实现,是对传统的HashMap的线程安全改进版本。

HashMap不同,ConcurrentHashMap在内部使用了一种名为“分段锁(Segment)”的机制来确保线程安全性。具体来说,ConcurrentHashMap被分成多个小的“段”,每个段都有自己的锁。多个线程可以同时访问不同的段,从而在多线程并发访问时提供了更好的并发性能。

下面是ConcurrentHashMap的一些特点和使用方式:

  1. 线程安全性ConcurrentHashMap是线程安全的,多个线程可以安全地并发访问和修改其中的数据,而无需使用额外的同步机制。(为了保证线程安全,可以使用****Hashtable。注意:线程中加入了计时

  2. 高效性能:由于使用了分段锁机制,ConcurrentHashMap在多线程并发访问时能够提供较好的性能。不同的线程可以同时访问不同的段,从而避免了大部分的线程竞争。

    (HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。)

  3. 可伸缩性ConcurrentHashMap的多个段可以根据需要进行动态调整,可以根据并发访问的需求来增加或减少段的数量。

​ 4.迭代器支持ConcurrentHashMap的迭代器是弱一致性的(Weakly Consistent),它不能保证在迭代期间反映出最新的修改。这是为了在并发场景下提高性能和可伸缩性。

使用ConcurrentHashMap的方式与使用HashMap类似,它提供了与HashMap类似的方法,如put()get()remove()等。需要注意的是,虽然ConcurrentHashMap是线程安全的,但仍然需要根据具体的需求进行合理的并发控制和同步。

ConcurrentHashMap在多线程环境下是一个常用且高效的数据结构,适用于需要同时进行读写操作的场景,具备较好的并发性能和线程安全性。

为什么要使用ConcurrentHashMap:

  1. HashMap线程不安全,会导致数据错乱

  2. 使用线程安全的Hashtable效率低下

基于以上两个原因,便有了ConcurrentHashMap的登场机会。

下面是一个简单的示例,展示了如何使用`ConcurrentHashMap`进行线程安全的并发操作:
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个线程安全的ConcurrentHashMap
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // 创建并启动5个线程向map中添加数据
        for (int i = 0; i < 5; i++) {
            int finalI = i;
            new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    // 向map中添加数据,使用put()方法
                    map.put("Key-" + finalI + "-" + j, finalI + j);
                }
            }).start();
        }

        // 等待所有子线程执行完成
        Thread.sleep(1000);

        // 打印map中的数据
        map.forEach((key, value) -> System.out.println(key + " -> " + value));
    }
}

在这个示例中,我们创建了一个ConcurrentHashMap对象,并启动了5个线程,每个线程向map中添加10个数据。由于ConcurrentHashMap是线程安全的,多个线程可以同时进行并发访问和修改操作。

最后,我们等待所有子线程执行完成,然后打印出ConcurrentHashMap中的数据。由于ConcurrentHashMap保证了线程安全性,可以看到最终打印的结果是正确且完整的。

需要注意的是,虽然ConcurrentHashMap是线程安全的,但在复合操作(例如putIfAbsent())时仍需要根据需求进行合理的同步控制。

这只是一个简单的示例,实际应用中你可以根据具体需求和业务逻辑来使用ConcurrentHashMap进行并发操作。

4.2 CountDownLatch

CountDownLatch是Java并发包中的一个同步工具类,用于控制线程的执行顺序和同步。

CountDownLatch的主要作用是让一个或多个线程等待其他线程完成某些操作后再继续执行。它通过一个计数器来实现,计数器的初始值可以设定,每当一个线程完成了一部分工作后,计数器的值就会减1。当计数器的值变为0时,等待中的线程就会被唤醒,继续执行。

下面是CountDownLatch的一些特点和用法:

  1. 计数器CountDownLatch使用一个计数器来表示需要等待的线程数量。计数器的初始值可以通过构造器进行设置,每当有一个线程完成了一部分工作后,可以调用countDown()方法来减少计数器的值。

  2. 等待和唤醒:调用await()方法的线程会进入等待状态,直到计数器的值变为0时才会被唤醒继续执行。可以通过调用await(long timeout, TimeUnit unit)方法来设置等待超时时间。

  3. 主线程等待子线程CountDownLatch通常被用于主线程等待多个子线程完成任务后再继续执行。主线程可以通过调用await()方法来等待子线程的完成。

  4. 多个线程等待一次CountDownLatch还可以多个线程同时等待一次,只要计数器的值变为0,所有等待中的线程都会被唤醒。

CountDownLatch是一个很有用的并发工具,常用于需要等待其他线程执行完成后再继续的场景,如并发测试、线程池等待任务完成等。它帮助实现线程之间的协调和同步,提高多线程编程的可控性和可靠性。

CountDownLatch允许一个或多个线程等待其他线程完成操作,再执行自己。

例如:线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。CountDownLatch构造方法

public CountDownLatch(int count)// 初始化一个指定计数器的CountDownLatch对象

CountDownLatch重要方法:

public void await() throws InterruptedException// 让当前线程等待
public void countDown() // 计数器进行减1

示例

1). 制作线程1

public class ThreadA extends Thread {
    private CountDownLatch down;

    public ThreadA(CountDownLatch down) {
        this.down = down;
    }

    @Override
    public void run() {
        System.out.println("A");
        try {
            down.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("C");
    }
}

2). 制作线程2:

public class ThreadB extends Thread {
    private CountDownLatch down ;
    public ThreadB(CountDownLatch down) {
        this.down = down;
    }
    @Override
    public void run() {
        System.out.println("B");
        down.countDown();
    }
}

3).制作测试类:

public class Demo {
    public static void main(String[] args) {
        CountDownLatch down = new CountDownLatch(1);//创建1个计数器
        new ThreadA(down).start();
        new ThreadB(down).start();
    }
}

4). 执行结果:

会保证按:A B C的顺序打印。说明:

CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。

CountDownLatch是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()方法让计数器-1,当计数器到达0时,调用CountDownLatch。

await()方法的线程阻塞状态解除,继续执行。

示例:

下面是一个简单的示例,展示了如何使用CountDownLatch进行线程的等待和唤醒:

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个CountDownLatch,初始计数器的值为3
        CountDownLatch latch = new CountDownLatch(3);

        // 创建并启动三个线程
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println("线程 " + Thread.currentThread().getName() + " 执行");
                // 模拟线程执行任务的耗时
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程 " + Thread.currentThread().getName() + " 执行完成");
                // 减少计数器的值
                latch.countDown();
            }).start();
        }

        System.out.println("等待子线程执行完成...");
        // 主线程等待计数器变为0,即所有子线程执行完成
        latch.await();
        System.out.println("所有子线程执行完成,继续执行主线程");
    }
}

在这个示例中,我们创建了一个CountDownLatch对象,初始计数器的值为3。然后,我们创建并启动了三个线程,在每个线程中模拟执行任务的耗时,执行完成后通过countDown()方法减少计数器的值。

主线程在创建并启动子线程后输出等待信息,并调用await()方法进行等待。主线程会一直等待,直到计数器的值变为0,即所有子线程都执行完毕。最后,主线程输出所有子线程执行完成的信息,继续执行自己的任务。

运行这个示例会看到如下输出:

等待子线程执行完成...
线程 Thread-0 执行
线程 Thread-1 执行
线程 Thread-2 执行
线程 Thread-2 执行完成
线程 Thread-1 执行完成
线程 Thread-0 执行完成
所有子线程执行完成,继续执行主线程

可以看到,主线程在三个子线程都执行完成后才继续执行。CountDownLatch通过计数器的方式实现了线程的等待和唤醒,能够很方便地控制线程的执行顺序和同步。

4.3 CyclicBarrier

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

CyclicBarrier是Java并发包中的一种同步工具,它可以让一组线程相互等待,直到所有线程都准备好后再一起继续执行任务。

CountDownLatch不同,CyclicBarrier的计数器可以循环使用。当初始设定的线程数量都到达屏障点时,所有线程将被释放,并且计数器会被重置,然后可以继续使用。

下面是一个简单的示例,展示了CyclicBarrier的基本用法:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        // 创建一个CyclicBarrier,计数器的值为3
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            // 所有线程都到达屏障点后执行的任务
            System.out.println("所有线程都到达屏障点,执行最后任务");
        });

        // 创建并启动三个线程
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                System.out.println("线程 " + Thread.currentThread().getName() + " 执行");
                // 模拟线程执行任务的耗时
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                try {
                    // 线程到达屏障点,等待其他线程到达
                    barrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }

                System.out.println("线程 " + Thread.currentThread().getName() + " 继续执行后续任务");
            }).start();
        }
    }
}

在这个示例中,我们创建了一个CyclicBarrier对象,初始计数器的值为3,并指定了一个任务,当所有线程都到达屏障点时,该任务将被执行。

然后,我们创建并启动了三个线程,在每个线程中模拟执行任务的耗时。然后,每个线程在执行完任务后调用await()方法,等待其他线程到达屏障点。

运行这个示例会看到如下输出:

线程 Thread-0 执行
线程 Thread-1 执行
线程 Thread-2 执行
所有线程都到达屏障点,执行最后任务
线程 Thread-2 继续执行后续任务
线程 Thread-1 继续执行后续任务
线程 Thread-0 继续执行后续任务

可以看到,当所有线程都到达屏障点时,最后的任务会被执行,然后线程继续执行后续任务。

CyclicBarrier在某些并发场景中非常有用,例如多线程计算某个问题,需要等待所有线程计算完毕后进行聚合结果。也可以用于多个线程同时执行某项任务,等待所有线程都准备好后再一起执行后续操作。

使用场景

使用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。

需求:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。

4.4 Semaphore

Semaphore(发信号)的主要作用是控制线程的并发数量。

synchronized可以起到"锁"的作用,但某个时间段内,只能有一个线程允许执行。

Semaphore可以设置同时允许几个线程执行。

Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。

Semaphore构造方法

Semaphore是Java并发包中的一种同步工具,它可以控制对某个资源的访问线程数目。

Semaphore内部维护了一个许可的数量,线程需要在访问资源之前获取许可,如果还有可用的许可,线程就可以继续执行,否则就需要等待其他线程释放许可。

下面是一个简单的示例,展示了Semaphore的基本用法:

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        // 创建一个Semaphore,初始许可数量为2
        Semaphore semaphore = new Semaphore(2);

        // 创建并启动5个线程
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    System.out.println("线程 " + Thread.currentThread().getName() + " 正在获取许可");
                    // 调用acquire()方法获取许可
                    semaphore.acquire();
                    System.out.println("线程 " + Thread.currentThread().getName() + " 获取到许可,开始访问资源");

                    // 模拟线程访问资源的耗时
                    Thread.sleep(2000);

                    System.out.println("线程 " + Thread.currentThread().getName() + " 访问资源完成,释放许可");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 调用release()方法释放许可
                    semaphore.release();
                }
            }).start();
        }
    }
}

在这个示例中,我们创建了一个Semaphore对象,初始许可数量为2,表示最多只能有2个线程同时访问资源。

然后,我们创建并启动了5个线程,在每个线程中,首先尝试调用acquire()方法来获取许可,如果还有可用的许可,就可以继续执行。然后,模拟线程访问资源的耗时。最后,线程会调用release()方法来释放许可。

运行这个示例会看到如下输出:

线程 Thread-0 正在获取许可
线程 Thread-0 获取到许可,开始访问资源
线程 Thread-1 正在获取许可
线程 Thread-1 获取到许可,开始访问资源
线程 Thread-0 访问资源完成,释放许可
线程 Thread-2 正在获取许可
线程 Thread-2 获取到许可,开始访问资源
线程 Thread-1 访问资源完成,释放许可
线程 Thread-3 正在获取许可
线程 Thread-3 获取到许可,开始访问资源
线程 Thread-2 访问资源完成,释放许可
线程 Thread-4 正在获取许可
线程 Thread-4 获取到许可,开始访问资源
线程 Thread-3 访问资源完成,释放许可
线程 Thread-4 访问资源完成,释放许可

可以看到,每次只有两个线程可以获取到许可并访问资源,其余的线程需要等待其他线程释放许可才能继续执行。

Semaphore可以应用于各种场景,例如控制数据库连接数、限制同时执行某个任务的线程数等。它能够有效地控制并发访问的数量,从而保护共享资源的安全性。

4.5 Exchanger

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。

这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

Exchanger是Java并发包中的一种同步工具,它允许两个线程在指定的同步点交换数据。

Exchanger提供一个exchange()方法,当两个线程都调用exchange()方法时,它们会阻塞,直到对方也调用了exchange()方法。然后,两个线程交换数据,并继续执行。

下面是一个简单的示例,展示了Exchanger的基本用法:

import java.util.concurrent.Exchanger;

public class ExchangerExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个Exchanger,用于交换String类型的数据
        Exchanger<String> exchanger = new Exchanger<>();

        // 创建并启动两个线程
        Thread thread1 = new Thread(() -> {
            try {
                System.out.println("线程1准备交换数据");
                String data = "Hello from Thread1";
                // 调用exchange()方法来交换数据,并将自己的数据传递给对方线程
                String receivedData = exchanger.exchange(data);
                System.out.println("线程1收到的数据:" + receivedData);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread thread2 = new Thread(() -> {
            try {
                System.out.println("线程2准备交换数据");
                String data = "Hello from Thread2";
                // 调用exchange()方法来交换数据,并将自己的数据传递给对方线程
                String receivedData = exchanger.exchange(data);
                System.out.println("线程2收到的数据:" + receivedData);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

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

        thread1.join();
        thread2.join();
    }
}

在这个示例中,我们创建了一个Exchanger<String>对象,用于交换String类型的数据。

然后,我们创建并启动了两个线程,每个线程都准备交换数据。在thread1中,我们创建了一个data字符串并调用exchange()方法来交换数据。在thread2中也做了同样的操作。当两个线程都调用了exchange()方法时,它们会等待对方的数据,并将自己的数据传递给对方线程。

运行这个示例会看到如下输出:

线程1准备交换数据
线程2准备交换数据
线程2收到的数据:Hello from Thread1
线程1收到的数据:Hello from Thread2

可以看到,两个线程成功地交换了数据。

Exchanger可以用于解决某些对等线程之间的数据交换问题,例如线程A需要某个数据,而线程B需要线程A提供另外一些数据。它提供了一个同步点来确保数据在两个线程之间正确地交换。

需要注意的是,Exchanger只适用于成对的线程之间交换数据,并且必须有线程之间的配对关系。如果配对关系不正确,那么线程可能会永久阻塞。

exchange方法的阻塞特性

Exchangerexchange()方法有阻塞特性,它会使调用该方法的线程阻塞,直到另一个线程也调用了exchange()方法。

具体来说,当一个线程调用exchange()方法时,如果没有其他线程已经在等待交换数据,那么当前线程会被阻塞,直到另一个线程调用exchange()方法。一旦另一个线程调用了exchange()方法,两个线程就会开始交换数据,然后继续执行。

在交换数据的过程中,如果其中一个线程比另一个线程更快地调用了exchange()方法,那么它会等待另一个线程的到来。如果线程在等待期间被中断,将抛出InterruptedException异常。

需要注意的是,Exchanger.exchange()方法只能用于两个线程之间的交换,如果存在更多的线程参与交换,那么只有两个线程会成功交换数据,其他线程可能会一直阻塞。

下面是一个示例,演示了Exchanger的阻塞特性:

import java.util.concurrent.Exchanger;

public class ExchangerBlockingExample {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();

        Thread thread1 = new Thread(() -> {
            try {
                System.out.println("线程1准备交换数据");
                String data = exchanger.exchange("Data from Thread1");
                System.out.println("线程1收到的数据:" + data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("线程2准备交换数据");
                String data = exchanger.exchange("Data from Thread2");
                System.out.println("线程2收到的数据:" + data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

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

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

在这个示例中,thread1线程首先调用了exchange()方法,然后阻塞等待另一个线程的到来;thread2线程睡眠2秒后,调用了exchange()方法来交换数据。

运行这个示例会看到如下输出:

线程1准备交换数据
线程2准备交换数据
线程1收到的数据:Data from Thread2
线程2收到的数据:Data from Thread1

可以看到,当第一个线程调用exchange()方法时,它被阻塞,然后等待另一个线程的到来,直到另一个线程调用了exchange()方法后,两个线程才会进行数据交换。最后,两个线程分别收到了对方的数据。

exchange方法的超时

Exchangerexchange()方法可以通过传递超时参数来设置阻塞的最大等待时间。如果在指定的时间内没有另一个线程调用exchange()方法,则当前线程会解除阻塞并继续执行。

exchange()方法可以使用以下两种形式之一来设置超时:

  1. exchange(V x, long timeout, TimeUnit unit): 当前线程尝试与其他线程进行数据交换,如果在指定的超时时间内没有其他线程调用exchange()方法,则当前线程解除阻塞并返回。
  2. exchange(V x, Timeout timeout): 与第一种形式类似,这里使用了Timeout对象来指定超时时间。

超时参数可以是任何支持java.util.concurrent.TimeUnit枚举的时间单位,例如TimeUnit.SECONDSTimeUnit.MILLISECONDS等。

下面是一个示例,演示了Exchanger的超时特性:

import java.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class ExchangerTimeoutExample {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();

        Thread thread1 = new Thread(() -> {
            try {
                System.out.println("线程1准备交换数据");
                String data = exchanger.exchange("Data from Thread1", 2, TimeUnit.SECONDS);
                System.out.println("线程1收到的数据:" + data);
            } catch (InterruptedException | TimeoutException e) {
                e.printStackTrace();
            }
        });

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(3000);
                System.out.println("线程2准备交换数据");
                String data = exchanger.exchange("Data from Thread2", 2, TimeUnit.SECONDS);
                System.out.println("线程2收到的数据:" + data);
            } catch (InterruptedException | TimeoutException e) {
                e.printStackTrace();
            }
        });

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

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

在这个示例中,我们使用exchange()方法的带超时参数的形式,并设置超时时间为2秒。在thread1线程中调用exchange()方法,然后等待另一个线程;thread2线程睡眠3秒后才调用了exchange()方法。由于超时时间设置为2秒,thread1在等待2秒后会解除阻塞并抛出java.util.concurrent.TimeoutException异常。

运行这个示例会看到如下输出:

线程1准备交换数据
线程2准备交换数据
java.util.concurrent.TimeoutException
线程2收到的数据:Data from Thread1

可以看到,thread1由于超时而解除阻塞,并抛出了TimeoutException异常。而thread2在完成睡眠后成功进行了数据交换。

使用超时参数可以对exchange()方法的阻塞时间进行控制,

exchange方法执行交换

Exchangerexchange()方法在进行交换时,会阻塞调用该方法的线程,直到另一个线程也调用了exchange()方法。一旦两个线程都调用了exchange()方法,它们就会交换数据并继续执行。

exchange()方法有两种执行交换的方式:

  1. 线程A调用exchanger.exchange(dataA)方法,线程B调用exchanger.exchange(dataB)方法。此时,两个线程都会阻塞,直到对方线程也调用了exchange()方法。然后,线程A会将dataA传递给线程B,而线程B会将dataB传递给线程A。交换完成后,两个线程都解除阻塞并继续执行。

  2. 线程A调用exchanger.exchange(dataA)方法,线程B调用exchanger.exchange(dataB)方法。在这种情况下,两个线程交换数据的顺序是相反的。线程A会等待线程B的到来,并将自己的dataA传递给线程B。而线程B会将自己的dataB传递给线程A。交换完成后,两个线程都解除阻塞并继续执行。

下面是一个示例,演示了Exchanger的执行交换的特性:

import java.util.concurrent.Exchanger;

public class ExchangerExchangeExample {
    public static void main(String[] args) {
        Exchanger<String> exchanger = new Exchanger<>();

        Thread thread1 = new Thread(() -> {
            try {
                System.out.println("线程1准备交换数据");
                String dataA = "Data from Thread1";
                String receivedData = exchanger.exchange(dataA);
                System.out.println("线程1收到的数据:" + receivedData);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread thread2 = new Thread(() -> {
            try {
                System.out.println("线程2准备交换数据");
                String dataB = "Data from Thread2";
                String receivedData = exchanger.exchange(dataB);
                System.out.println("线程2收到的数据:" + receivedData);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

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

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

在这个示例中,thread1线程首先调用了exchange(dataA)方法来交换数据;然后,thread2线程调用了exchange(dataB)方法来交换数据。由于两个线程都调用了exchange()方法,它们会交换数据并继续执行。

运行这个示例会看到如下输出:

线程1准备交换数据
线程2准备交换数据
线程2收到的数据:Data from Thread1
线程1收到的数据:Data from Thread2

可以看到,两个线程成功地交换了数据。

Exchanger提供了一种简单而高效的线程间数据的交换机制。

补充知识

高效的线程间数据交换的机制。通过`exchange()`方法,两个线程可以安全地交换数据,这种交换是同步的,确保在交换完成之前,两个线程都会被阻塞。

Exchanger的特点如下:

1. 同步交换:Exchanger确保在两个线程都调用`exchange()`方法之前,交换操作是同步进行的。这意味着,两个线程会在交换点处被阻塞,直到对方线程也调用了`exchange()`方法。

2. 数据传递:当两个线程进行交换时,它们会同时传递数据。每个线程都将自己的数据传递给对方线程,并且通过`exchange()`方法的返回值接收对方线程传递的数据。

3. 一对一交换:Exchanger的设计初衷是支持一对一的数据交换。也就是说,只有两个线程可以参与到交换操作中。不适用于多于两个线程之间的数据交换。

4. 高效性:Exchanger的实现方式经过优化,具有高效性能。在交换数据时,线程间的上下文切换开销较小,因此可以获得较高的并发性能。

需要注意的是,Exchanger在进行数据交换时是阻塞的,并且在线程之间隐式地进行数据传递。如果只需要在一方向上传递数据,或者需要更复杂的交换模式,可以考虑使用其他线程同步工具,如`CountDownLatch`或`CyclicBarrier`等。

Exchanger的使用场景包括但不限于以下情况:

- 线程间数据交换:当两个线程需要进行数据交换时,可以使用Exchanger提供的同步机制。

- 对等协作:Exchanger适用于需要对等线程之间的协作和通信的场景,每个线程都能够在数据交换中扮演主动的和被动的角色。

- 线程间协调:Exchanger可用于在两个线程之间协调操作的序列,确保在指定点上线程之间达成同步。

总之,Exchanger是一个简单而高效的工具,可以实现两个线程之间的数据交换和同步。通过`exchange()`方法,线程可以安全地交换数据,并在交换点处进行同步。

5.线程池

线程池(Thread Pool)是一种用于管理和调度线程的机制,它可以有效地重用线程,避免频繁地创建和销毁线程,从而减少系统开销和提高性能。

线程池通常由以下组件组成:

  1. 任务队列(Task Queue):用于存储待执行的任务。当线程池中的线程空闲时,会从任务队列中获取任务进行执行。

  2. 线程管理器(Thread Manager):负责创建、管理和销毁线程池中的线程。它决定何时创建新线程,何时销毁空闲线程,以及如何调度任务。

  3. 线程池大小(Pool Size):指定线程池中的最大线程数量。线程池根据需要动态地调整线程数量来处理任务,但不会超过指定的最大线程数量。

  4. 线程工厂(Thread Factory):用于创建新线程的工厂。可以自定义线程工厂来配置线程的属性,如线程名、优先级等。

  5. 拒绝策略(Rejected Execution Policy):当任务队列已满且线程池无法继续创建新线程时,拒绝策略定义了如何处理新提交的任务。常见的拒绝策略包括丢弃任务、丢弃最旧的任务、调用线程池所在线程来执行该任务,或抛出异常等。

线程池的优点包括:

  • 提高性能:线程池可以避免频繁地创建和销毁线程,减少了线程创建和上下文切换的开销,从而提高了系统的整体性能。

  • 提高资源利用率:线程池能够按照需求动态地调整线程数量,充分利用系统资源,避免线程过多导致资源耗尽的问题。

  • 提高响应速度:线程池中的线程处于就绪状态,可以立即执行任务,减少了任务开始执行的等待时间,提高了响应速度。

  • 提供线程管理和监控:线程池可以提供线程的创建、销毁、状态监控等功能,方便管理线程的生命周期。

Java提供了java.util.concurrent.Executor接口和java.util.concurrent.ExecutorService接口以及它们的实现类来支持线程池的使用,例如ThreadPoolExecutorScheduledThreadPoolExecutor

使用线程池时,可以根据业务需求合理地配置线程池的大小、任务队列的大小和拒绝策略,以及线程的属性,从而优化系统的性能和资源利用率。

在java中线程池的使用

在Java中,可以使用java.util.concurrent包提供的ExecutorService接口和相关实现类来使用线程池。

以下是使用线程池的步骤:

  1. 创建线程池:可以使用Executors类的静态方法创建线程池。常用的方法包括:

    • newFixedThreadPool(int nThreads):创建一个固定大小的线程池,线程数量为nThreads
    • newCachedThreadPool():创建一个可根据需要自动调整大小的线程池。
    • newSingleThreadExecutor():创建一个只有一个线程的线程池。
    ExecutorService executor = Executors.newFixedThreadPool(5);
    
  2. 提交任务:使用ExecutorServicesubmit()方法提交任务给线程池执行。

    executor.submit(new Runnable() {
        public void run() {
            // 在这里编写具体的任务逻辑
        }
    });
    

    或者使用Lambda表达式:

    executor.submit(() -> {
        // 在这里编写具体的任务逻辑
    });
    
  3. 关闭线程池:在任务完成后,应该关闭线程池来释放资源。

    executor.shutdown();
    

完整的示例代码如下:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 提交任务给线程池执行
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("执行任务:" + taskId);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("任务完成:" + taskId);
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

这个示例创建了一个固定大小为5的线程池,并提交了10个任务给线程池执行。每个任务都会打印执行内容,并在执行结束后打印任务完成信息。最后,线程池会被关闭。

需要注意的是,在提交任务之后并不会立即执行,线程池会根据实际情况来调度任务的执行。可以根据需要调整线程池的大小和任务提交方式来满足实际需求。

另外,还可以使用execute()方法提交任务给线程池执行,它与submit()方法类似,但没有返回值。

Callable接口

java.util.concurrent.Callable接口是一个泛型接口,定义了一个可以返回结果并可能抛出异常的任务,与Runnable接口类似。但是,Callable接口的call()方法可以返回一个结果,并且可以抛出异常。

Callable接口定义了如下的方法:

  • V call() throws Exception:执行任务的方法,返回一个结果(泛型类型为V),可能抛出异常。

call()方法是一个抽象方法,需要在实现Callable接口的类中重写并提供具体的任务逻辑。该方法可以返回一个结果,也可以抛出一个异常。任务的执行结果可以通过Future对象来获取。

下面是一个使用Callable接口的示例代码:

import java.util.concurrent.Callable;

public class MyCallable implements Callable<Integer> {
    private int taskId;

    public MyCallable(int taskId) {
        this.taskId = taskId;
    }

    public Integer call() throws Exception {
        System.out.println("开始执行任务:" + taskId);
        Thread.sleep(5000);
        System.out.println("任务完成:" + taskId);
        return taskId;
    }
}

在以上示例中,自定义了一个MyCallable类实现了Callable接口,并指定泛型类型为Integer。在call()方法中,执行了一个模拟的耗时任务,线程睡眠5秒,并返回了任务的ID号。

在使用Callable接口时,可以通过ExecutorServicesubmit()方法提交Callable任务,并返回一个Future对象,用于获取任务的执行结果。

以下是一个使用MyCallable的示例代码:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableExample {
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 提交Callable任务给线程池执行
        Future<Integer> future1 = executor.submit(new MyCallable(1));
        Future<Integer> future2 = executor.submit(new MyCallable(2));
        Future<Integer> future3 = executor.submit(new MyCallable(3));

        try {
            // 等待任务执行完成,并获取结果
            Integer result1 = future1.get();
            Integer result2 = future2.get();
            Integer result3 = future3.get();

            System.out.println("任务1的结果:" + result1);
            System.out.println("任务2的结果:" + result2);
            System.out.println("任务3的结果:" + result3);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 关闭线程池
        executor.shutdown();
    }
}

在以上示例中,创建了一个固定大小为5的线程池。然后使用executor.submit()方法分别提交了三个MyCallable任务,并分别返回了对应的Future对象。

最后,使用future.get()方法等待任务执行完成,并分别获取了任务的结果。注意,如果任务未完成,get()方法会阻塞当前线程。最后输出任务的结果,并关闭线程池。

使用Callable接口可以更灵活地处理有返回结果的任务,并且可以捕获任务执行过程中抛出的异常。

示例:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableExample {
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService executor = Executors.newSingleThreadExecutor();

        // 创建Callable任务
        Callable<String> callableTask = new Callable<String>() {
            public String call() throws Exception {
                return "Hello, World!";
            }
        };

        // 提交Callable任务给线程池执行
        Future<String> future = executor.submit(callableTask);

        try {
            // 等待任务执行完成,并获取结果
            String result = future.get();
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 关闭线程池
        executor.shutdown();
    }
}

此示例中,创建了一个ExecutorService,使用Executors.newSingleThreadExecutor()创建一个只有一个线程的线程池。然后,创建了一个Callable任务并实现了call()方法,在call()方法中返回了一个字符串"Hello, World!"。

接下来,使用executor.submit(callableTask)提交Callable任务给线程池执行,并返回一个Future对象。Future对象表示了任务的未来结果。

然后,在future.get()方法调用处等待任务执行完成,并获取结果。如果任务未完成,get()方法会阻塞当前线程。最后,输出结果并关闭线程池。

需要注意的是,通过Future对象可以监控任务的执行状态、获取任务执行结果、取消任务等操作。在实际应用中,可以根据具体需求进行处理。

6.死锁

死锁是指两个或多个线程互相等待对方释放资源,导致它们都无法继续执行的情况。在Java中,死锁通常发生在多个线程之间竞争资源的时候。(简单的说 在多线程程序中,使用了多把锁,造成线程之间相互等待.程序不往下走了)

死锁常常涉及以下四个条件的同时满足:

  1. 互斥条件(Mutual Exclusion):至少有一个资源被持有并且正在被一个线程使用,此时其他线程无法访问该资源,只能等待。
  2. 不可剥夺条件(Hold and Wait):一个线程持有至少一个资源,并且在等待获取其他资源的同时,不释放已持有的资源。
  3. 占有且等待条件(No Preemption):一个线程持有至少一个资源,并且在等待获取其他资源的同时,不释放已持有的资源。
  4. 循环等待条件(Circular Wait):多个线程之间形成一个循环等待资源的链路。

简单的说:java中死锁产生的原因如下:

1.有多把锁

2.有多个线程

3.有同步代码块嵌套

下面是一个简单的死锁示例:

public class DeadlockExample {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1: Acquired resource 1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource2) {
                    System.out.println("Thread 1: Acquired resource 2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2: Acquired resource 2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource1) {
                    System.out.println("Thread 2: Acquired resource 1");
                }
            }
        });

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

在以上示例中,有两个线程thread1thread2,两个线程都试图获取resource1resource2两个共享资源。

thread1获取到resource1之后,它会休眠一段时间。同时,thread2获取到resource2之后也会休眠一段时间。

由于thread1thread2都在等待对方释放资源,它们陷入了死锁状态,无法继续执行,程序最终会被阻塞。

要解决死锁问题,可以采取一些策略,例如:

  • 避免循环等待条件:对共享资源的获取顺序进行统一,按照相同的顺序获取资源,可以减少死锁的概率。
  • 加载超时机制:在获取资源时设置一个超时时间,在一定时间内没有获取到资源,则放弃当前的资源请求,防止无限等待。
  • 使用资源分级:将资源划分为不同的优先级,尽量避免不同优先级的线程之间产生死锁。

,输出结果并关闭线程池。

需要注意的是,通过Future对象可以监控任务的执行状态、获取任务执行结果、取消任务等操作。在实际应用中,可以根据具体需求进行处理。

6.死锁

死锁是指两个或多个线程互相等待对方释放资源,导致它们都无法继续执行的情况。在Java中,死锁通常发生在多个线程之间竞争资源的时候。(简单的说 在多线程程序中,使用了多把锁,造成线程之间相互等待.程序不往下走了)

死锁常常涉及以下四个条件的同时满足:

  1. 互斥条件(Mutual Exclusion):至少有一个资源被持有并且正在被一个线程使用,此时其他线程无法访问该资源,只能等待。
  2. 不可剥夺条件(Hold and Wait):一个线程持有至少一个资源,并且在等待获取其他资源的同时,不释放已持有的资源。
  3. 占有且等待条件(No Preemption):一个线程持有至少一个资源,并且在等待获取其他资源的同时,不释放已持有的资源。
  4. 循环等待条件(Circular Wait):多个线程之间形成一个循环等待资源的链路。

简单的说:java中死锁产生的原因如下:

1.有多把锁

2.有多个线程

3.有同步代码块嵌套

下面是一个简单的死锁示例:

public class DeadlockExample {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1: Acquired resource 1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource2) {
                    System.out.println("Thread 1: Acquired resource 2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2: Acquired resource 2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (resource1) {
                    System.out.println("Thread 2: Acquired resource 1");
                }
            }
        });

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

在以上示例中,有两个线程thread1thread2,两个线程都试图获取resource1resource2两个共享资源。

thread1获取到resource1之后,它会休眠一段时间。同时,thread2获取到resource2之后也会休眠一段时间。

由于thread1thread2都在等待对方释放资源,它们陷入了死锁状态,无法继续执行,程序最终会被阻塞。

要解决死锁问题,可以采取一些策略,例如:

  • 避免循环等待条件:对共享资源的获取顺序进行统一,按照相同的顺序获取资源,可以减少死锁的概率。
  • 加载超时机制:在获取资源时设置一个超时时间,在一定时间内没有获取到资源,则放弃当前的资源请求,防止无限等待。
  • 使用资源分级:将资源划分为不同的优先级,尽量避免不同优先级的线程之间产生死锁。

避免死锁问题需要仔细设计和编写多线程代码,并且对资源的使用和竞争进行合理的规划和管理。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值