JUC并发编程(一)Thread、TreadPoolExecutor、BlockingQueue、Synchronized、Lock、JUC辅助类

Thread

多线程的创建方法

继承Thread

  1. 方法一:继承Thread类,创建步骤:
    • 定义子类MyThread继承Thread,重写run()方法
    • 创建MyThread类对象
    • 调用线程对象的start()方法启动线程(启动后执行的是run()方法)

这种创建方式因为已经继承了Thread类,无法继承其他类,不利于拓展。

  1. 方法二:声明一个实现Runnable接口的类。
    • 定义定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法。
    • 创建MyRunnable对象
    • 把MyRunnable对象交给Thread处理
    • 调用Thread对象的start方法启动

实现runnable接口

方法二只实现接口,可以继续继承类和实现接口,扩展性更强。但缺点是编程多一层包装(runnable对象还需要再传给thread构造thread对象)
在这里插入图片描述
方法一实现:

public class MyThread extends Thread{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 5; ++i) {
    
    
            System.out.println("子线程执行输出:" + i);
        }
    }
}
 Thread t1 = new MyThread();
 t1.start();

方法二实现:

Thread t = new Thread(new Runnable() {
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            System.out.println(i);
        }
    }
});
for (int i = 0; i < 10; i++) {
    
    
    System.out.println("主线程:" + i);
}

当然,可以用lambda表达式简写。

 Thread t = new Thread(() -> {
    
    
     for (int i = 0; i < 10; i++) {
    
    
         System.out.println( "子线程" + i);
     }
 });
 for (int i = 0; i < 10; i++) {
    
    
     System.out.println("主线程:" + i);
 }
  1. 方式3
    前两种创建方式都存在一个问题:
    • 他们重写的run()方法均不能直接返回结果
    • 不适合需要返回线程执行结果的业务场景

定义类实现Callable接口,组装为FutureTask

jdk5利用Callable、FutureTask接口实现上述功能。

创建步骤:

  • 定义类实现Callable接口,重写call方法,封装要做的事情。
  • 用FutureTask把Callable对象封装成线程任务对象。
  • 把线程任务对象交给Thread处理
  • 调用Thread的start方法启动线程,执行任务。
  • 线程执行完毕后,通过FutureTask的get()方法去获取任务执行的结果。

方法三的实现:

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

public class ThreadDemo3 {
    
    
    public static void main(String[] args) {
    
    
        Callable<String> call1 = new MyCallable(100000);
        FutureTask<String> f1 = new FutureTask<>(call1);
        Thread t1 = new Thread(f1);
        t1.start();

        Callable<String> call2 = new MyCallable(100000);
        FutureTask<String> f2 = new FutureTask<>(call2);
        Thread t2 = new Thread(f2);
        t2.start();

        try {
    
    
            // 如果f1没有执行完毕,那么get这里会等待,直至完成
            String r1 =  f1.get();
            System.out.println("第一个结果:" + r1);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
            // 如果f2没有执行完毕,那么get这里会等待,直至完成
        try {
    
    
            String r2 =  f2.get();
            System.out.println("第二个结果:" + r2);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

class MyCallable implements Callable<String> {
    
    
    private int n;
    public MyCallable (int n) {
    
    
        this.n = n;
    }
    @Override
    public String call() throws Exception {
    
    
        int sum = 0;
        for (int i = 0; i <= n; i++) {
    
    
            sum += i;
        }
        return "子线程执行的结果是:" + sum;
    }
}

常用 API

在这里插入图片描述
在这里插入图片描述

Native API

sleep

当调用线程的sleep方法,使当前执行的线程休眠(暂时停止执行)指定的毫秒数,取决于系统计时器和调度程序的精度和准确性。如果任何线程中断了当前线程,会抛出InterruptedException异常时清除当前线程的中断状态。

interrupt

  • 当一个线程调用interrupt()时,如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已,被设置中断标志的线程将继续正常运行,不受影响。即interrupt()仅是设置中断标识,并不实际中断线程,需要被调用的线程进行配合。就像你让一个人闭嘴,但最终那人是否闭嘴,需要它的配合。
  • 如果线程处于被阻塞的状态(例如sleep、wait、join等状态),在别的线程中调用当前线程对象的interrupt()方法,那么线程将立即退出被阻塞状态,中断状态标志位将被清除,并抛出一个InterruptedException异常。
  • 对于不活动的线程,调用interrupt()没有任何影响。

join

join方法让一个线程加入到另一个线程之前执行,在此线程执行期间,其他线程进入阻塞状态,当然也可以指定join入参(指定执行等待的超时时间),最多等待几毫秒让该线程终止,超时0意味着永远等待。

public class demo1 {
    
    
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            try {
    
    
                TimeUnit.SECONDS.sleep(2);
                System.out.println("+++++++++++++");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        });
        t1.start();
        try {
    
    
            t1.join();
            System.out.println("ok");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

yield

yield的字面意思是退让。调用该方法会向调度程序提示当前线程愿意放弃其当前对处理器的使用,调度程序可以随意忽略此提示。

yield是一种启发式尝试,使用它可以改善线程之间的相对进展,否则会过度使用 CPU。在使用yield方法时通常有下面两种使用场景:

  • yield的使用应与详细的分析和基准测试相结合,以确保实际上具有预期的效果,但很少使用这种方法。对于调试或测试目的可能很有用,它可能有助于重现由于竞争条件导致的错误
  • 在设计并发控制结构(例如 java.util.concurrent.locks 包中的结构)时,它也可能很有用
public class TestYield {
    
    
    public static void main(String[] args) {
    
    
        MyThread thread1 = new MyThread("thread-1");
        MyThread thread2 = new MyThread("thread-2");
        thread1.start();
        thread2.start();
    }
 
    private static class MyThread extends Thread {
    
    
        public MyThread(String name) {
    
    
            super(name);
        }
        @Override
        public void run() {
    
    
            for (int i = 1; i <= 5; i++) {
    
    
                if (i % 2 == 0) {
    
    
                    Thread.yield();
                    System.out.println(getName() + ":" + i);
                }
            }
        }
    }
}

TreadPoolExecutor

线程池是一个可以复用线程的技术。因为创建新线程的开销很大,使用线程池可以重复利用线程,可提高程序性能。
在这里插入图片描述

获取线程池对象

JDK5.0起提供了代表线程池的接口:ExecutorService
如何获取线程池对象?

  • 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象,这种方法最为灵活。

  • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

Executors内置线程池

1、newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种类型的线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。

如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统OOM。

2、newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

3、newSingleThreadExecutor
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
4、newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。

ThreadPoolExecutor

在这里插入图片描述
临时线程什么时候创建?

  • 新任务提交时,核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
    什么时候会开始拒绝任务?
  • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来时才会开始拒绝任务。

在这里插入图片描述

线程池处理runnable任务

        ExecutorService pool = new ThreadPoolExecutor(3, 5, 6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        Runnable target = () -> {
    
    
            try {
    
    
                Thread.sleep(100000);
                System.out.println(Thread.currentThread().getName() + "输出");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        };
        // 三个核心线程
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        // 五个在等待队列
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        // 等待队列满了,新加两个临时线程
        pool.execute(target);
        pool.execute(target);
        
        // 拒绝任务,抛出异常
        pool.execute(target);

    }

线程池处理callable任务

在这里插入图片描述
execute 执行runnable类任务,submit处理callable任务。

BlockingQueue阻塞队列

BlockingQueue是FIFO(先进先出)型队列,他很好的解决了多线程中,如何高效安全“传输”数据的问题。
在这里插入图片描述
阻塞队列有四种添加元素和移出元素的API:

  • 非阻塞,返回布尔值
    • 会抛出异常:add()、remove()、element()
    • 不会抛出异常:offer()、poll()、peek()
  • 阻塞
    • 一直阻塞:put()、take()
    • 可以设置等待时间,超时返回:offer(e, timeout, unit),poll(timeout, unit)

在这里插入图片描述

LinkedBlockingQueue链表阻塞队列

双向链表阻塞队列。

对于 FixedThreadPool 和 SingleThreadExector 而言,它们使用的阻塞队列是容量为Integer.MAX_VALUE 的 LinkedBlockingQueue,可以认为是无界队列。

由于 FixedThreadPool 线
程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务。

这里需要注意,由于线程池的任务队列永远不会放满,所以线程池只会创建核心线程数量的线程,所以此时的最大线程数对线程池来说没有意义,因为并不会触发生成多于核心线程数的线程。

SynchronousQueue同步队列

SynchronousQueue同步队列不存储元素,只要向里边put一个元素,就需要取出一个元素,take。

对应的线程池是 CachedThreadPool。线程池CachedThreadPool 的最大线程数是 Integer 的最大值,可以理解为线程数是可以无限扩展的。
CachedThreadPool 和上一种线程池 FixedThreadPool 的情况恰恰相反,FixedThreadPool 的情况是阻塞队列的容量是无限的,而这里CachedThreadPool 是线程数可以无限扩展,所以CachedThreadPool 线程池并不需要一个任务队列来存储任务,因为一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们。

SynchronousQueue<String> bq = new SynchronousQueue();
new Thread(() -> {
    
    
    try {
    
    
        bq.put("1");
        System.out.println(Thread.currentThread().getName());
        bq.put("1");
        System.out.println(Thread.currentThread().getName());
        bq.put("1");
        System.out.println(Thread.currentThread().getName());
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
}, "A").start();

new Thread(() -> {
    
    
    try {
    
    
        TimeUnit.SECONDS.sleep(2);
        bq.take();
        System.out.println(Thread.currentThread().getName());
        bq.take();
        System.out.println(Thread.currentThread().getName());
        bq.take();
        System.out.println(Thread.currentThread().getName());
    } catch (Exception e){
    
    
        e.printStackTrace();
    }
}, "B").start();
}

DelayedWorkQueue 延迟阻塞队列

第三种阻塞队列是DelayedWorkQueue,它对应的线程池分别是 ScheduledThreadPool 和SingleThreadScheduledExecutor,这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。

DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构。之所以线程池 ScheduledThreadPool 和SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。

Synchronized

1、并发就是多线程操作同一个资源。

2、在Java中,线程就是一个单独的资源类,没有任何附属操作。资源中包含并发操作的属性、方法。

并发案例——多线程买票的例子:
在我的例子中,公共资源类为Ticket,left为剩余票数,cnt是记录的卖出的票数,sale()方法在有余票时,打印谁买了一张票,并显示当前余票和共卖出的票。

class Ticket {
    
    
    int left;
    int cnt = 0;
    public int getLeft() {
    
    
        return left;
    }

    public void setLeft(int left) {
    
    
        this.left = left;
    }

    public Ticket(int n) {
    
    
        this.left = n;
    }
    public void sale() {
    
    
        try {
    
    
            Thread.sleep(100); // 模拟卖票耗时
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        if (left > 0) {
    
    
            ++cnt;
            --left;
            System.out.println("线程:" + Thread.currentThread().getName() + "剩余:" + left + ",共卖出:" + cnt);
        }
    }
}

在main函数中,设定票数,买票的人数,每人买票的机会尝试数:

public static void main(String[] args) {
    
    
    int ticketNum = 8, people = 4, chance = 5;
    Ticket t = new Ticket(ticketNum);
    System.out.println("开售前:---------" + t.getLeft());
    for (int i = 0; i < people; ++i) {
    
    
        new Thread(() -> {
    
    
            for (int j = 0; j < chance; ++j) {
    
    
                t.sale();
            }
        }, Integer.toString(i)).start();
    }
    try {
    
    
        Thread.sleep(3000);
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
}

结果:
在这里插入图片描述

synchronized解决

synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式:
1. 对于普通同步方法,锁是当前实例对象。
2. 对于静态同步方法,锁是当前类的Class对象。
3. 对于同步方法块,锁是Synchonized括号里配置的对象。

直接让sale()变成同步方法,即每个线程访问sale()方法时,会给当前方法所在实例(也就公共资源)对象加锁,那么一次只能有一个线程能够操作,其他线程必须等待:
在这里插入图片描述
在这里插入图片描述

Lock锁

lock锁使用三部曲:

  1. 创建锁: Lock lk = new ReentrantLock();
  2. 获取锁 :lk.lock
  3. try - catch - finally,其中try中写需要并发控制的业务逻辑,finally中释放锁,确保异常产生时,锁正常被释放lk.lock()。
class Ticket2 {
    
    
    int left;
    int cnt = 0;
    Lock lk = new ReentrantLock();
    public int getLeft() {
    
    
        return left;
    }

    public void setLeft(int left) {
    
    
        this.left = left;
    }

    public Ticket2(int left) {
    
    
        this.left = left;
    }

    public void sale() {
    
    
        lk.lock();
        try {
    
    
            if (left > 0) {
    
    
                ++cnt;
                --left;
                System.out.println("线程:" + Thread.currentThread().getName() + "剩余:" + left + ",共卖出:" + cnt);
            }
        }
        catch (Exception e) {
    
    
            e.printStackTrace();
        }finally {
    
    
            lk.unlock();
        }
    }
}

ReentrantLock也能保障并发安全。
在这里插入图片描述

synchronized锁与Lock锁的区别

  1. synchronized是Java内置的关键字而Lock锁是一个Java类。
  2. synchronized无法获取锁的状态,Lock可以判断是否获取到了锁。
  3. synchronized会自动释放锁,lock必须手动锁,如果不释放锁,会发生死锁。
  4. synchronized,没有获取到锁的线程会一直等待,Lock锁,则有尝试获取锁的机制,不一定会一直等待。
  5. synchronized 可重入,非公平;Lock锁,可重入,可以设置公平锁,即一个是内置关键字不可修改,一个是自己定制。
  6. synchronized适合锁少量的代码同步问题,Lock适合锁大量的同步代码。

生成者消费者问题

synchronized实现

线程 A、B操作同一个变量num,A让num + 1,
B让num - 1,两个交替使用。

这里A操作完了,需要通知B,B操作完了需要通知A,从而实现线程的同步,相当于A生产了后,交给B消费,B消费完毕,再通知A。

完成这种生产-消费模型编程三部曲:

  • 等待:当不满足条件时,while循环等待
  • 业务:当条件满足后,执行业务。
  • 通知:业务完毕后,通知其他线程。

构建一个资源类Data,有一个成员变量num,构建两个同步方法,一个执行+1,一个执行-1,在main方法中,起两个线程A和B,分别尝试操作+1和-1:

class Data {
    
    
    private int num = 0;

    public synchronized void increase() {
    
    
        while (num != 0) {
    
    
            try {
    
    
                this.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        ++num;
        System.out.println(Thread.currentThread().getName() +":->"+ num);
        this.notifyAll();
    }

    public synchronized void decrease() {
    
    
        while (num != 1) {
    
    
            try {
    
    
                this.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        --num;
        System.out.println(Thread.currentThread().getName() +":->"+ num);
        this.notifyAll();
    }
}

    public static void main(String[] args) {
    
    
        Data d = new Data();
        new Thread(() -> {
    
    
            for (int i = 0; i < 5; i++) {
    
    
                d.increase();
            }
        }, "A").start();

        new Thread(() -> {
    
    
            for (int i = 0; i < 5; i++) {
    
    
                d.decrease();
            }
        }, "B").start();
    }
}

lock版实现- condition

在这里插入图片描述
可以利用lock构建condition条件变量,condition提供await()方法和signal()和signalAll()方法,类似wait(),notify()和notifyAll;

要点步骤:

  • 1、创建ReentrantLock() lk,并获取lk的condition
  • 2、加锁:lk.lock()
  • 3、try - catch - final
    • try中编写业务逻辑:
      • 等待:当不满足条件时,while循环等待:condition.await();
      • 业务:当条件满足后,执行业务
      • 通知:业务完毕后,通知其他线程:condition.signalAll();
    • final中释放锁:lk.unlock();
class Data2 {
    
    
    private int num = 0;
    Lock lk = new ReentrantLock();
    Condition condition = lk.newCondition();
    
    public  void increase() {
    
    
        lk.lock();
        try {
    
    
            while (num != 0) condition.await();
            ++num;
            System.out.println(Thread.currentThread().getName() +":->"+ num);
            condition.signalAll();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lk.unlock();
        }
    }

    public  void decrease() {
    
    
        lk.lock();
        try {
    
    
            while (num != 1) condition.await();
            --num;
            System.out.println(Thread.currentThread().getName() +":->"+ num);
            condition.signalAll();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lk.unlock();
        }
    }
}

condition实现精准通知唤醒

在这里插入图片描述

Callable

用于有返回值的异步请求,获取结果。
第一步,构建自己的Callable对象,实现Callable接口,它需要一个泛型参数,标识期望返回的结果类型。

class  MyCall implements Callable<Integer> {
    
    
    int a, b;

    public MyCall(int a, int b) {
    
    
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer call() throws Exception {
    
    
        TimeUnit.SECONDS.sleep(2);
        return a + b;
    }
}

Callable对象与FutureTask包装使用,即封装为一个未来将要执行的任务。

MyCall call = new MyCall(3, 4);
FutureTask future = new FutureTask(call);

在这里插入图片描述
FutureTask 中实现了RunnableFuture复合接口,即有Runnable的实现,因此可以放入Thead中启动:

        new Thread(future).start();
        Integer a = 0;
        try {
    
    
            a = (Integer) future.get();
        } catch (InterruptedException | ExecutionException e) {
    
    
            e.printStackTrace();
        }
        System.out.println(a.toString());

这个 future.get()会阻塞!

JUC常用辅助类

CountDownLatch(倒计时器)

CountDownLatch允许count个线程阻塞在一个地方,直至所有线程的任务都执行完毕。

模拟一个场景,教室中有6个学生,所有学生都离开后才可以关门!

public static void main(String[] args) {
    
    
    // 1、统计num个线程的倒计时器
    int num = 6;
    CountDownLatch cn = new CountDownLatch(num);
    for (int i = 0; i < num; ++i) {
    
    
        new Thread(()->{
    
    
            try {
    
    
                Thread.sleep(100);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "离开");
            // 2、线程结束前,倒数一下
            cn.countDown();
        }, String.valueOf(i)).start();
    }
    try {
    
    
        // 3、等待所有线程结束
        cn.await();
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
    System.out.println("关门!");
}

在这里插入图片描述

CyclicBarrier(循环栅栏)

CyclicBarrier和CountDownLatch非常类似,它也可以实现线程间的技术等待,它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

现在的场景反过来,假设教师前到达的人数达到指定个数,才允许开门:

第一步:创建CyclicBarrier,指定满足的线程数和所有线程都到达后要执行的runnable对象。

CyclicBarrier cb = new CyclicBarrier(num, () -> {
    
    
   System.out.println("开门!");
});

第二步:每个线程执行结束前,使用cb.await();等待其他线程同步。

public static void main(String[] args) {
    
    
    // 1、等待的人数到达num后,才开门!
    int num = 6;
    CyclicBarrier cb = new CyclicBarrier(num, () -> {
    
    
        System.out.println("开门!");
    });
    for (int i = 0; i < num; ++i) {
    
    
        new Thread(()->{
    
    
            System.out.println(Thread.currentThread().getName() + "到达");
            try {
    
    
                // 2、线程结束前,需等待其他线程同步
                cb.await();
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
        }, String.valueOf(i)).start();
    }
}

在这里插入图片描述

Semaphore 信号量 - 允许多个线程同时访问

synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

Semaphore有两种模式:公平模式和非公平模式。

  • 公平模式:调用acquire的顺序就是获取许可证的顺序,遵循FIFO
  • 非公平模式:抢占式

构造方法:

public Semaphore(int permits) {
    
    
        sync = new NonfairSync(permits);
    }
public Semaphore(int permits, boolean fair) {
    
    
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。

在这里插入图片描述

最常用的场景就是在资源有限的情况下,只允许指定数量的线程,同时访问某个资源,比如模拟一个抢车位的场景:

第一步:
模拟资源情况:num个车位,而用户有10个
int num = 3, total = 6;
Semaphore semaphore = new Semaphore(num);

第二步:
try -catch -final :
try:semaphore.acquire(); // 获取到资源
finally:semaphore.release(); // 释放资源

public static void main(String[] args) {
    
    
    // 1、num个车位,而用户有total 个
    int num = 3, total = 6;
    Semaphore sm = new Semaphore(num);
    for (int i = 0; i < total; ++i) {
    
    
        new Thread(()->{
    
    
            try {
    
    
                sm.acquire();
                System.out.println(Thread.currentThread().getName() + "抢到车位");
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + "离开车位");
            } catch (Exception e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                sm.release();
            }
        }, String.valueOf(i)).start();
    }
}

同一时刻只能有3个用户占用车位。
在这里插入图片描述

ReadWriteLock读写锁

利用读写锁实现自定义缓存,写的时候只允许有一个操作:

第一步:定义读写锁:
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
第二步:定义读操作函数:
读前加读锁
readWriteLock.readLock().lock();
读完释放锁
第三步:定义写操作函数()
写时加写锁,写完释放锁。

class MyCache {
    
    
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Map<String, Object> mp = new HashMap<>();

    public void put(String s, Object o) {
    
    
        readWriteLock.writeLock().lock();
        try {
    
    
            mp.put(s, o);
            System.out.println(Thread.currentThread().getName() + "插入:" + s);
        } catch (Exception exception) {
    
    
            exception.printStackTrace();
        } finally {
    
    
            readWriteLock.writeLock().unlock();
        }
    }

    public Object get(String s) {
    
    
        Object ans = null;
        readWriteLock.readLock().lock();
        try {
    
    
            ans = mp.get(s);
            System.out.println(Thread.currentThread().getName() + "查询:" + s);
        } catch (Exception exception) {
    
    
            exception.printStackTrace();
        } finally {
    
    
            readWriteLock.readLock().unlock();
        }
        return ans;
    }
}

四大函数接口

函数式接口是指只定义了一个抽象方法的接口,或者加了 @FunctionalInterface 注解的接口。可以有默认方法。
四大函数式接口分别是:

  • 功能型函数接口(Function):接口输入一个是函数的输入一个是函数的输出
  • 消费型函数接口(Consumer):接口输入是函数的输入 函数的返回是一个布尔值
  • 供给型函数式接口(Supplier)
  • 断言型函数式接口(Predicate)

功能 / 函数型

函数型接口:接口输入一个是函数的输入一个是函数的输出

@FunctionalInterface
public interface Function<T, R> {
    
    

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
    // ... 两个默认函数和一个静态函数
}

断言型

判定型接口:接口输入是函数的输入 函数的返回是一个布尔值

public interface Predicate<T> {
    
    

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
    // ...三个默认函数和一个静态函数
}

消费型

消费型接口:只有输入,没有输出

@FunctionalInterface
public interface Consumer<T> {
    
    

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
    // ...一个默认方法
}

供给型

供给型接口:只有输出,没有输入

@FunctionalInterface
public interface Supplier<T> {
    
    

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}
        Consumer consumer = (str) -> {
    
    
            System.out.println(str);
        };
        consumer.accept("Happy");
    }

使用案例-Stream流式编程

    /**
    有5个用户,筛选
     1、ID 必须是偶数
     2、年龄必须大于23
     3、用户名转大写字母
     4、倒序排序
     5、只需要一个用户
     **/
    public static void main(String[] args) {
    
    
        List<User> list = new ArrayList<>();
        Collections.addAll(list,
                new User(0, 22, "lzy"),
                new User(1, 20, "blzy"),
                new User(2, 25, "azy"),
                new User(3, 24, "czy"),
                new User(4, 24, "dzy"),
                new User(5, 24, "ezy"),
                new User(6, 24, "fzy"),
                new User(7, 24, "gsy"));
        list.stream().filter(e -> {
    
    return e.getId() % 2 == 1;})
                     .filter(e -> {
    
    return e.getAge() > 23;})
                     .map(e -> {
    
    return e.getUsername().toUpperCase();})
                     .sorted(Comparator.reverseOrder())
                     .limit(1)
                     .forEach(System.out::println);

    }

猜你喜欢

转载自blog.csdn.net/baiduwaimai/article/details/132083149