java&juc多线程锁Lock

synchronized相关的不再说

synchronized相关的不再说

什么是JUC

JUC指的是java.util.concurrent

image-20240922013228236

Lock锁

所有已知实现类:

ReentrantLockReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock

image-20240922013432233

Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition

锁是用于通过多个线程控制对共享资源的访问的工具。 通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。 但是,一些锁可能允许并发访问共享资源,如ReadWriteLock的读锁。

使用synchronized方法或语句提供对与每个对象相关联的隐式监视器锁的访问,但是强制所有锁获取和释放以块结构的方式发生:当获取多个锁时,它们必须以相反的顺序被释放,并且所有的锁都必须被释放在与它们相同的词汇范围内。

虽然synchronized方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。 例如,用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。 所述的实施方式中Lock接口通过允许获得并在不同的范围释放的锁,并允许获得并以任何顺序释放多个锁使得能够使用这样的技术。

随着这种增加的灵活性,额外的责任。 没有块结构化锁定会删除使用synchronized方法和语句发生的锁的自动释放。 在大多数情况下,应使用以下惯用语:

   //创建锁
   Lock l = ...;
   //加锁
   l.lock(); 
   try {
   		// access the resource protected by this lock 
   } finally {
   		//释放锁
   		l.unlock(); 
   } 

当在不同范围内发生锁定和解锁时,必须注意确保在锁定时执行的所有代码由try-finally或try-catch保护,以确保在必要时释放锁定。

Lock实现提供了使用synchronized方法和语句的附加功能,通过提供非阻塞尝试来获取锁( tryLock() ),尝试获取可被中断的锁( lockInterruptibly() ,以及尝试获取可以超时( tryLock(long, TimeUnit) )。

一个Lock类还可以提供与隐式监视锁定的行为和语义完全不同的行为和语义,例如保证排序,非重入使用或死锁检测。 如果一个实现提供了这样的专门的语义,那么实现必须记录这些语义。

请注意, Lock实例只是普通对象,它们本身可以用作synchronized语句中的目标。 获取Lock实例的监视器锁与调用该实例的任何lock()方法没有特定关系。 建议为避免混淆,您不要以这种方式使用Lock实例,除了在自己的实现中。

除非另有说明,传递任何参数的null值将导致NullPointerException被抛出。

synchronized与 Lock的区别

  • synchronized 是内置的Java关键字,而Lock是Java类
  • synchronized 无法判断获取锁的状态,Lock可以判断获取了锁
  • synchronized 会自动释放锁,Lock必须要手动释放锁!不释放锁就死锁!
  • synchronized 锁遇到阻塞就会一直等待,而Lock锁不一定会一直等待,会尝试获取锁 image-20240922015108813
  • synchronized 可重入锁,非公平的,不可中断的;Lock,可重入的,可以判断锁,可以自己设置公平锁&非公平锁
  • synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码

生产者消费者问题

一般写法:(等待、业务、通知

java
class Data{
    private int num=0;
    public void increment(){
        if(num!=0){
            //等待
        }
        num++;
        //唤醒其他线程
    }
    public void decrement(){
        if(num==0){
            //等待
        }
        num--;
        //唤醒其他线程
    }
}
老版本
synchronized	wait	notify/notifyAll
JUC版本
lock			await	signal/signalAll

Condition因素出Object监视器方法( wait , notify和notifyAll )成不同的对象,以得到具有多个等待集的每个对象,通过将它们与使用任意的组合的效果Lock个实现。 Lock替换synchronized方法和语句的使用, Condition取代了对象监视器方法的使用。 

老版

java
class Data{
    private int num=0;
    public synchronized void increment(){
        if(num!=0){
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        num++;
        notifyAll();
    }
    public synchronized void idecrement(){
        if(num==0){
            //等待
        }
        num--;
        notifyAll();
    }
}

问题:两个生产者,两个消费者的时候就线程不安全了!

原因分析:两个生产者都在等待,此时一个抢到锁,执行操作,结果唤醒的是另一个生产者,而这里if只判断一次,他就也执行操作去了,这就是虚假唤醒问题

线程也可以被唤醒,而不会被通知,中断或超时,即所谓虚假唤醒。虽然很少发生,但程序必须通过测试应该使线程被唤醒的条件来防范,而且如果条件不满足就继续等待。换句话说,等待应该总是出现在循环中

  synchronized (obj) {
         while (<condition does not hold>)
             obj.wait();
         ... // Perform action appropriate to condition
     } 

使用while代替if就解决虚假唤醒问题

新版

java
class Data{
    Lock lock = new ReentrantLock();
    Condition condition =lock.newCondition();
    private int num=0;
    public void increment(){
        lock.lock();
        while (num!=0){
            try {
                condition.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        num++;
        condition.signal();
        lock.unlock();
    }
    public void decrement(){
        lock.lock();
        while(num==0){
            try {
                condition.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        num--;
        condition.signal();
        lock.unlock();
    }
}

await理解

Lock 锁的就是他自己,当他执行 lock() 锁住自己的时候
其他进程在想执行	lock() 方法就不行,进入等待(类似死循环)
只有当获得了锁的线程 使用 unlock()	或者 await() 才会释放锁
使用 await 之后需要其他进程来通知 才能继续执行 争抢锁 ,抢到了才再继续执行
所以我通知以后,只要我还没有释放锁,那么他们尽管接到了通知,但是要等我所释放锁之后才能争抢锁并运行
只要在我释放锁之前做完状态更改就好了,通知啥时候都可以

Condition优势

精准的通知和唤醒线程

同步监视器可以创建多个,一个监视一个!

java
public class Main {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {for (int i = 0; i < 10; i++) data.printA();}).start();
        new Thread(() -> {for (int i = 0; i < 10; i++) data.printB();}).start();
        new Thread(() -> {for (int i = 0; i < 10; i++) data.printC();}).start();
        new Thread(() -> {for (int i = 0; i < 10; i++) data.printA();}).start();
        new Thread(() -> {for (int i = 0; i < 10; i++) data.printB();}).start();
        new Thread(() -> {for (int i = 0; i < 10; i++) data.printC();}).start();
    }
}

/**
 * A执行完调用B,B执行完调用C,C执行完调用A
 */
class Data {
    //一个判断的标志位,1A,2B,3C
    private int num=1;
    Lock lock = new ReentrantLock();
    Condition condition1 =lock.newCondition();
    Condition condition2 =lock.newCondition();
    Condition condition3 =lock.newCondition();
    public void printA(){
        lock.lock();
        try {
            while (num!=1){
                condition1.await();
            }
            System.out.println("A");
            //唤醒指定的人
            condition2.signal();
            num=2;
        } catch (Exception e) {
        } finally {
            lock.unlock();
        }
    }
    public void printB(){
        lock.lock();
        try {
            while (num!=2){
                condition2.await();
            }
            System.out.println("B");
            //唤醒指定的人
            condition3.signal();
            num=3;
        } catch (Exception e) {
        } finally {
            lock.unlock();
        }
    }
    public void printC(){
        lock.lock();
        try {
            while (num!=3){
                condition3.await();
            }
            System.out.println("C");
            //唤醒指定的人
            condition1.signal();
            num=1;
        } catch (Exception e) {
        } finally {
            lock.unlock();
        }
    }
}

公平锁和非公平锁

向可重入锁的构造中传入true就是公平锁,传入false就是非公平锁(默认)

image-20240922013750104

  • 公平锁可以先来后到,一定要排队

  • 非公平锁可以插队

可重入锁

在下面例子中,加锁了两次,已经获得锁的对象可以继续给他加锁而不会发生死锁,这就是可重入锁

而且这里可重入锁必须是配套的,有几个加锁就要有几个解锁,不然就可能发生死锁

java
public class Main {
    public static void main(String[] args) {
        A a = new A();
        new Thread(()->{
           a.methodA();
        }).start();
    }
}
class A{
    private Lock lock = new ReentrantLock();
    public void methodA(){
        lock.lock();
        //做一些业务
        methodB();
        lock.unlock();
    }
    public void methodB(){
        lock.lock();
        //做一些业务
        lock.unlock();
    }
}

自旋锁

java
public class MyLock {
    //原子引用的是线程
    private AtomicReference<Thread> lock=new AtomicReference<>(null);
    
    public void lock(){
        //期望值是空的,所以第一次能通过,后面就不能通过了,在这里自旋
        //这个我写的锁貌似是不了可以重入的,只能锁一次
        while(!lock.compareAndSet(null,Thread.currentThread())){
        }
    }
    public void unlock(){
        //锁的时候是啥线程,现在就只能啥线程来解锁
        lock.compareAndSet(Thread.currentThread(),null);
    }
}
//至于感觉好像用if判断也可以实现?
//错!if操作不满足原子性,在并发场景下不安全,所以这里才采用原子引用(满足原子性)

测试

java
public class Main {
    public static void main(String[] args) {
        A a = new A();
        new Thread(()->{
           a.run();
        }).start();
        new Thread(()->{
            a.run();
        }).start();
    }
}
class A{
    private MyLock lock = new MyLock();
    public void run(){
        System.out.println(Thread.currentThread().getName()+"   beforeLock");
        lock.lock();
        System.out.println(Thread.currentThread().getName()+"run");
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        lock.unlock();
    }
}

集合类不安全

java
//并发下list不安全
List list = new ArrayList();
for (int i = 0; i < 10; i++) {
    new Thread(()->{
        list.add(UUID.randomUUID().toString().substring(0, 5));
        System.out.println(list);
    }).start();
}

并发修改异常

image-20240922233049170

怎么变安全?

  • Vector取代ArrayList,因为它是线程安全的 缺点:Vector是古老类,效率不太行

  • 使用工具类转换为线程安全的

    image-20240922233554199

  • JUC下有一些线程安全的类

  • image-20240922233903568

  • image-20240922233938772

    //CopyOnWrite	写入时复制 COW	一种优化策略
    /多线程调用的时候,list,读取的时候,固定的,写入(覆盖)
    //再写入的时候避免覆盖造成数据问题
    //读写分离
    List list = new CopyOnWriteArrayList();
    // CopyOnWriteArrayList	使用了 lock 锁,所以效率比使用了 synchronized 的 vector 效率更高
    transient 不序列化
    volatile	禁止指令重排

set也是不安全的

对应的JUC解决类是

CopyOnWriteSet

JUC类描述
ConcurrentHashMap<K,V>支持检索的完全并发性和更新的高预期并发性的哈希表。
CopyOnWriteArrayList的一个线程安全的变体ArrayList ,其中所有可变操作( addset ,等等)通过对底层数组的最新副本实现。
CopyOnWriteArrayList一个Set使用内部CopyOnWriteArrayList其所有操作。
CyclicBarrier允许一组线程全部等待彼此达到共同屏障点的同步辅助。
CountDownLatch允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。

CallAble

Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的。 然而,一个 Runnable不返回结果,也不能抛出被检查的异常。

1、可以有返回值

2、可以抛出异常

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

但是由于Thread构造只接受Runnable接口,所以我们需要它的子类FeatureTask来转换,下面是基本使用

java
public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> task = new FutureTask<>(new Data());
        new Thread(task).start();
        task.get();
    }
}
class Data implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Hello World";
    }
}

常见的辅助类

CountDownLatch

允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。

A CountDownLatch用给定的计数初始化。 await方法阻塞,直到由于countDown()方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的await 调用立即返回。 这是一个一次性的现象 - 计数无法重置。

java
public static void main(String[] args) throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(6);
    for (int i = 0; i < 6; i++) {
        new Thread(countDownLatch::countDown).start();
    }
    countDownLatch.await();//等待计数器归零,然后才能向下执行
    System.out.println("aaa");
}

CyclicBarrier

允许一组线程全部等待彼此达到共同屏障点的同步辅助。循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。屏障被称为循环 ,因为它可以在等待的线程被释放之后重新使用。

A CyclicBarrier支持一个可选的Runnable命令,每个屏障点运行一次,在派对中的最后一个线程到达之后,但在任何线程释放之前。 在任何一方继续进行之前,此屏障操作对更新共享状态很有用。

java
public static void main(String[] args) throws Exception {
    CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {
        public void run() {
            System.out.println("Barrier called");
        }
    });
    for (int i = 0; i < 3; i++) {
        new Thread(
                ()->{
                    try {
                        barrier.await();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
        ).start();
    }
}

Semaphore

信号量

一个计数信号量。在概念上,信号量维持一组许可证。如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。每个release()添加许可证,潜在地释放阻塞获取方。但是,没有使用实际的许可证对象;Semaphore只保留可用数量的计数,并相应地执行。

信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源。 例如,这是一个使用信号量来控制对一个项目池的访问的类:

在获得项目之前,每个线程必须从信号量获取许可证,以确保某个项目可用。 当线程完成该项目后,它将返回到池中,并将许可证返回到信号量,允许另一个线程获取该项目。 请注意,当调用acquire()时,不会保持同步锁定,因为这将阻止某个项目返回到池中。 信号量封装了限制对池的访问所需的同步,与保持池本身一致性所需的任何同步分开。

信号量被初始化为一个,并且被使用,使得它只有至多一个允许可用,可以用作互斥锁。 这通常被称为二进制信号量 ,因为它只有两个状态:一个许可证可用,或零个许可证可用。 当以这种方式使用时,二进制信号量具有属性(与许多Lock实现不同),“锁”可以由除所有者之外的线程释放(因为信号量没有所有权概念)。 这在某些专门的上下文中是有用的,例如死锁恢复。

此类的构造函数可选择接受公平参数。 当设置为false时,此类不会保证线程获取许可的顺序。 特别是, 闯入是允许的,也就是说,一个线程调用acquire()可以提前已经等待线程分配的许可证-在等待线程队列的头部逻辑新的线程将自己。 当公平设置为真时,信号量保证调用acquire方法的线程被选择以按照它们调用这些方法的顺序获得许可(先进先出; FIFO)。 请注意,FIFO排序必须适用于这些方法中的特定内部执行点。 因此,一个线程可以在另一个线程之前调用acquire ,但是在另一个线程之后到达排序点,并且类似地从方法返回。 另请注意, 未定义的tryAcquire方法不符合公平性设置,但将采取任何可用的许可证。

通常,用于控制资源访问的信号量应该被公平地初始化,以确保线程没有被访问资源。 当使用信号量进行其他类型的同步控制时,非正常排序的吞吐量优势往往超过公平性。

本课程还提供了方便的方法, 一次acquirerelease多个许可证。 当没有公平地使用这些方法时,请注意增加无限期延期的风险。

内存一致性效应:在另一个线程中成功执行“获取”方法(如acquire()之前,调用“释放”方法之前的线程中的操作,例如release() happen-before

java
public static void main(String[] args) throws Exception {
    Semaphore semaphore = new Semaphore(2);
    for (int i = 0; i < 6; i++) {
        new Thread(()->{
            //得到许可
            try {
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName()+"得到许可");
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //释放
            System.out.println(Thread.currentThread().getName()+"释放许可");
            semaphore.release();
        }).start();
    }
}

限流使用

读写锁

Interface ReadWriteLock

所有已知实现类:

ReentrantReadWriteLock


public interface ReadWriteLock

A ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。(排他锁和共享锁)

read lock可以由多个阅读器线程同时进行,只要没有作者。

write lock是独家的。

所有ReadWriteLock实现必须保证的存储器同步效应writeLock操作(如在指定Lock接口)也保持相对于所述相关联的readLock 。 也就是说,一个线程成功获取读锁定将会看到在之前发布的写锁定所做的所有更新。

读写锁允许访问共享数据时的并发性高于互斥锁所允许的并发性。 它利用了这样一个事实:一次只有一个线程( 写入线程)可以修改共享数据,在许多情况下,任何数量的线程都可以同时读取数据(因此读取器线程)。 从理论上讲,通过使用读写锁允许的并发性增加将导致性能改进超过使用互斥锁。 实际上,并发性的增加只能在多处理器上完全实现,然后只有在共享数据的访问模式是合适的时才可以。

读写锁是否会提高使用互斥锁的性能取决于数据被读取的频率与被修改的频率相比,读取和写入操作的持续时间以及数据的争用 - 即是,将尝试同时读取或写入数据的线程数。 例如,最初填充数据的集合,然后经常被修改的频繁搜索(例如某种目录)是使用读写锁的理想候选。 然而,如果更新变得频繁,那么数据的大部分时间将被专门锁定,并且并发性增加很少。 此外,如果读取操作太短,则读写锁定实现(其本身比互斥锁更复杂)的开销可以支配执行成本,特别是因为许多读写锁定实现仍将序列化所有线程通过小部分代码。 最终,只有剖析和测量将确定使用读写锁是否适合您的应用程序。

虽然读写锁的基本操作是直接的,但是执行必须做出许多策略决策,这可能会影响给定应用程序中读写锁定的有效性。 这些政策的例子包括:

  • 在写入器释放写入锁定时,确定在读取器和写入器都在等待时是否授予读取锁定或写入锁定。 作家偏好是常见的,因为写作预计会很短,很少见。 读者喜好不常见,因为如果读者经常和长期的预期,写作可能导致漫长的延迟。 公平的或“按顺序”的实现也是可能的。
  • 确定在读卡器处于活动状态并且写入器正在等待时请求读取锁定的读取器是否被授予读取锁定。 读者的偏好可以无限期地拖延作者,而对作者的偏好可以减少并发的潜力。
  • 确定锁是否可重入:一个具有写锁的线程是否可以重新获取? 持有写锁可以获取读锁吗? 读锁本身是否可重入?
  • 写入锁可以降级到读锁,而不允许插入写者? 读锁可以升级到写锁,优先于其他等待读者或作者吗?

在评估应用程序的给定实现的适用性时,应考虑所有这些问题。

  1. 允许多个读操作并发: Java中的读锁是共享的,这意味着多个线程可以同时获得读锁,只要没有线程持有写锁(排他锁)。这允许多个读操作并发执行,提高了程序的并发性能。
  2. 防止写操作同时进行: 当一个或多个线程持有读锁时,其他线程不能获得写锁。这防止了在读取数据时数据被修改,从而保证了数据的一致性。
  3. 锁的升级和降级: Java中的ReentrantReadWriteLock支持锁的降级(从写锁降级为读锁),但不支持锁的升级(从读锁升级为写锁)。这是因为如果允许从读锁升级到写锁,可能会导致死锁。
java
public class Main {
    public static void main(String[] args) throws Exception {
        Data data = new Data();
        for (int i = 0; i < 5; i++) {
            new Thread(data::put).start();
        }
        for (int i = 0; i < 5; i++) {
            new Thread(data::get).start();
        }
    }
}

class Data {
    private volatile Map map=new HashMap<>();
    private ReadWriteLock lock=new ReentrantReadWriteLock();
    public void put(){
        lock.writeLock().lock();
        System.out.println(Thread.currentThread().getName()+"正在进行写入");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName()+"写入完成");
        lock.writeLock().unlock();
    }
    public void get(){
        lock.readLock().lock();
        System.out.println(Thread.currentThread().getName()+"正在进行读取");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName()+"读取完成");
        lock.readLock().unlock();
    }
}

Java中的ReentrantReadWriteLock包含两种锁:读锁(ReadLock)和写锁(WriteLock)。这两种锁是互斥的,具体来说:

  • 读锁(ReadLock):是共享锁,多个线程可以同时持有读锁,只要没有线程持有写锁。
  • 写锁(WriteLock):是排他锁,只有一个线程可以持有写锁,并且当有线程持有写锁时,其他线程既不能获取读锁也不能获取写锁。

以下是它们互斥性的详细说明:

  • 没有线程持有写锁时,多个线程可以同时获取读锁进行读取操作。
  • 有线程持有写锁时,其他线程不能获取读锁或写锁,必须等待持有写锁的线程释放锁。
  • 有线程持有读锁时,其他线程可以获取读锁(因为读锁是共享的),但不能获取写锁,因为写锁需要独占访问。
  • 有线程持有读锁并且有其他线程尝试获取写锁时,尝试获取写锁的线程将等待,直到所有持有读锁的线程释放它们的锁。 这种设计确保了在写操作进行时不会有读操作干扰,而在读操作进行时不会有写操作干扰,从而保证了数据的一致性和完整性。同时,它也允许多个读操作并发进行,提高了并发读取的性能。

阻塞队列

写入:如果队列满了,就必须阻塞等待

读取:如果队列是空的,必须阻塞等待生产

BlockingQueue是一个接口

image-20240923005820165

四组API

1、抛出异常

2、不会抛出异常,有返回值

3、阻塞等待

4、超时等待

操作方式抛出异常不会抛出异常,有返回值阻塞等待超时等待
添加addoffer(),失败返回falseputoffer(E e, long timeout, TimeUnit unit)
移除removepoll(),没有就返回nulltakepoll(long timeout, TimeUnit unit)
判断队列首element检索,但不删除,这个队列的头部。此方法与{@link #peek peek}的不同之处在于,如果此队列为空,它将抛出异常。peek检索但不删除此队列的头部,如果此队列为空则返回null。
java
public static void main(String[] args) throws Exception {
    BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
    queue.add("a");
    queue.add("b");
    queue.add("c");
    queue.add("d");
}

抛出异常image-20240923010446268

SynchronousQueue

这个队列没有容量(容量是1),必须要等里面的元素去除之后才能存储

  • 存储 put
  • take

池化技术(线程池,重点)

好处:

1、降低资源的消耗

2、提高响应速度

3、方便管理

Executors类是线程池的工具类,用于创建各种线程池

但是我们不应该用它来创建,都应该使用ThreadPoolExecutor类来创建线程池,他更加灵活

Executors三大方法:

java
Executors.newCachedThreadPool();//遇强则强,遇弱则弱
Executors.newFixedThreadPool();//固定大小的线程池
Executors.newSingleThreadExecutor();//创建的大小是单个线程

//线程池用完要关闭
threadExecutor.shutdown();

测试三大方法

java
public static void main(String[] args) throws Exception {
    ExecutorService threadExecutor1 = Executors.newSingleThreadExecutor();
    ExecutorService threadExecutor2 = Executors.newCachedThreadPool();
    ExecutorService threadExecutor3 = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 10; i++) {
        threadExecutor3.execute(()->{
            System.out.println(Thread.currentThread().getName());
        });
    }
}

七大参数

ThreadPoolExecutor(int corePoolSize,				//核心线程池大小
                              int maximumPoolSize,	//最大线程池大小
                              long keepAliveTime,	//超时没人调用释放事件(释放回核心线程池大小)
                              TimeUnit unit,		//超时单位
                              BlockingQueue<Runnable> workQueue,	//阻塞队列
                              ThreadFactory threadFactory,			//线程工厂,创建线程的,不用动
                              RejectedExecutionHandler handler)		//拒绝策略

image-20240923014248414

上面四种拒绝策略的含义

拒绝策略类名含义
AbortPolicyThreadPoolExecutor.AbortPolicy当线程池和任务队列都满时,如果还尝试提交新任务,将抛出RejectedExecutionException异常,直接丢弃任务。
CallerRunsPolicyThreadPoolExecutor.CallerRunsPolicy当线程池无法接受新任务时,由调用者线程(提交任务的线程)直接执行该任务。
DiscardPolicyThreadPoolExecutor.DiscardPolicy当线程池无法接受新任务时,简单地丢弃这个任务,不进行任何通知。
DiscardOldestPolicyThreadPoolExecutor.DiscardOldestPolicy当线程池无法接受新任务时,会丢弃任务队列中最旧的任务,然后尝试重新提交当前任务。
java
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
        5,5,200,TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(5),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

什么时候线程池满? 当线程数 > maximumPoolSize + 阻塞队列大小

的时候就应该执行拒绝策略了

最大线程数到底应该怎么定

1、CPU密集型 CPU几个线程就定义几,通过Runtime.getRuntime().availableProcessors()来获取

2、IO密集型 定为大于程序中十分废IO的线程数

异步回调

CompletableFuture

Future可以明确地完成(设定其值和状态),并且可以被用作CompletionStage ,支持有关的功能和它的完成时触发动作。

当两个或多个线程试图completecompleteExceptionally ,或cancel一个CompletableFuture,只有一个成功。

//无返回值
CompletableFuture<Void> f1 = CompletableFuture.runAsync(()->{});
//有返回值
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "hello");
future.whenComplete((r,e)->{}).exceptionally((e)->{return e.getMessage();});

JMM(Java内存模型)

image-20240923021859735

八种操作:除了上述六种,还有lock,unlock

Volatile

1、保证可见性

2、不保障原子性

3、禁止指令重排

保证可见性:

内存可见性,指的是线程之间的可见性,当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值

java
//没有加volatile关键字,不可见,那个线程一直无法关闭
public class Main {
    public static Integer i=1;
    public static void main(String[] args) throws Exception {
        new Thread(()->{while (i.equals(1));}).start();
        TimeUnit.SECONDS.sleep(5);
        i=0;
        System.out.println("i=0");
    }
}
//加了关键字之后可以正常了
public class Main {
    public volatile static Integer i=1;
    public static void main(String[] args) throws Exception {
        new Thread(()->{while (i.equals(1));}).start();
        TimeUnit.SECONDS.sleep(5);
        i=0;
        System.out.println("i=0");
    }
}
//但是,如果使用了volatile关键字就不一样了:

*第一,valatile关键字会强制将修改过后的值立即写入主存;
*第二,使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
那么这样的话,线程1读取到的肯定就是最新的值了。

缓存一致性的两种解决办法

  • 总线加LOCK锁的方式

早期的cpu中是通过在总线加锁来解决缓存不一致的,因为计算机cpu通信是通过总线来执行的,如果在总线上面加锁的话就阻塞来其他cpu对该变量的访问,如上面的代码在程序执行时总线发出lock指令,那么只有在这段代码执行完毕后其他cpu才能读取变量执行相应的指令,这样就解决了缓存一致性问题。

由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

  • 缓存一致性协议

?所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

image-20240923022651330

重排序

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排。他的意义就在于尽可能的提高CPU的处理性能。

1a = b + c;
2d = e - f ;

先加载b、c(注意,即有可能先加载b,也有可能先加载c),但是在执行add(b,c)的时候,需要等待b、c装载结束才能继续执行,也就是增加了停顿,那么后面的指令也会依次有停顿,这降低了计算机的执行效率。

为了减少这个停顿,我们可以先加载e和f,然后再去加载add(b,c),这样做对程序(串行)是没有影响的,但却减少了停顿。既然add(b,c)需要停顿,那还不如去做一些有意义的事情。

指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。所以在多线程下,指令重排序可能会导致一些问题。

并发编程的三个重要特性

  • 原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized 可以保证代码片段的原子性。
  • 可见性 :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
  • 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。举个列子

1x = 10; //语句1
2y = x; //语句2
3x++; //语句3
4x = x + 1; //语句4

那么大家分析下以上语句是否是原子性操作呢?咋一看,有些朋友可能会说上面的4个语句中的操作都是原子性操作。其实只有语句1是原子性操作,其他三个语句都不是原子性操作。

语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。

语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。

所以上面4个语句只有语句1的操作具备原子性。 也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

可见性

对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

CAS

java
AtomicInteger integer = new AtomicInteger(0);
integer.compareAndSet(0,0);

cas就是compareAndSet,期望值达到了就更新,否则就不更新

CAS是CPU的并发原语,他是原子性操作!

其他操作,例如
integer.getAndAdd(1);

其中深挖有一点是包装的(只有CAS是CPU原语,其他类似的是自旋封装的),比较当前工作内存的值和主内存的值,如果这个值是期望的,那么则执行操作!如果不是就循环

image-20240923024248389

缺点:

  • 底层是自旋锁,循环会耗时
  • 一次性只能保证一个共享变量的原子性
  • ABA问题(乐观锁)

原子引用

带版本号的原子操作!

基本原子引用:AtomicReference<>(初始值)

带版本号的原子引用AtomicStampedReference<>(初始值,初始版本)

每次执行修改都增加版本号,这样如果版本号和最开始的不一样,就执行失败

stampedReference.compareAndSet(1, 2, stampedReference.getStamp(), stampedReference.getStamp()+1);

死锁排查

jps -l定位进程号

使用jstack 进程号查看死锁

评论

评论加载中……