JAVA开发学习-day10

JAVA开发学习-day10

1. Lock锁对象

Lock接口时Java提供的一种线程同步机制,它允许线程以排他性的方式访问共享资源。与synchronized关键字不同,Lock接口提供了更灵活的锁定和解锁操作,以及更多的控制选项。

Lock接口主要的实现类时ReentrantLock,它是一种可重入锁,意味着同一个线程可以多次获取同一把锁,而不会发生死锁。除了ReentrantLock,JAVA还提供了其他类型的锁,如ReentrantReadWriteLock等。

创建Lock对象

Lock lock = new ReentrantLock();

在需要进行同步的代码块中可以通过lock方法来上锁,执行完同步的代码块后调用unlock方法来解锁。也可以使用tryLock方法,返回值为布尔值,如果返回值为true则代表上锁成功,返回值为false则表示上锁失败。

public class EasyThreadB {
    Lock lock = new ReentrantLock();
    //锁对象  Lock
    public void method(){
        //lock.lock(); //加锁
        //lock.tryLock()尝试加锁,加锁成功返回true,失败返回false
        if(lock.tryLock()){
            System.out.println(Thread.currentThread().getName() + "进入方法");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "结束方法");
            lock.unlock(); //解锁
        } else{
            System.out.println(Thread.currentThread().getName() + "未成功加锁,去执行别的代码");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            method();
        }
    }
    public static void main(String[] args) {
        Runnable run = new EasyThreadB()::method;
        Thread a = new Thread(run);
        Thread b = new Thread(run);
        a.start();
        b.start();
    }
}

ReentrantReadWriteLock类不是锁,而是锁容器,这个锁容器中有读锁和写锁。

当使用读锁时,一个线程可以对数据进行读操作,不允许其他线程对数据进行写操作,但是不影响其他的线程进行读操作。

写锁是互斥锁,当一个线程进行写操作时,其余的线程阻塞。

下面的示例中调用读方法的线程同时进行,调用写方法的线程顺序进行

public class EasyThreadC {
    public static ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
    public static ReentrantLock rl = new ReentrantLock();
    public static void method(){
        System.out.println(Thread.currentThread().getName() + "进入读方法");
        Lock lock = rrwl.readLock();
        lock.lock();
        System.out.println(Thread.currentThread().getName() + "读锁---加锁成功");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName() + "方法结束");
        lock.unlock();
    }

    public static void methodWrite(){
        System.out.println(Thread.currentThread().getName() + "进入写方法");
        Lock lock = rrwl.writeLock();
        lock.lock();
        System.out.println(Thread.currentThread().getName() + "写锁---加锁成功");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName() + "方法结束");
        lock.unlock();
    }
    public static void main(String[] args) {
        Runnable run = EasyThreadC::method;
        Runnable runWrite = EasyThreadC::methodWrite;
        Thread a = new Thread(runWrite);
        Thread b = new Thread(runWrite);
        Thread c = new Thread(runWrite);
        a.start();
        b.start();
        c.start();

        Thread d = new Thread(run);
        Thread f = new Thread(run);
        Thread g = new Thread(run);
        d.start();
        f.start();
        g.start();
        System.out.println("main线程结束");
    }
}

在读取数据的方法中使用读锁,修改数据的方法中使用写锁,可以减少被阻塞的线程,提高程序的效率。

如下面的例子所示

public class EasyThreadE {
    public static void main(String[] args) {
        EasyList list = new EasyList();
        Runnable runSize = ()->{
            list.size();
        };
        Runnable runGet = ()->{
            list.get(0);
        };
        Runnable runAdd = ()->{
            list.add(12);
        };
        list.add(1);
        Thread a = new Thread(runSize);
        Thread b = new Thread(runGet);
        Thread c = new Thread(runAdd);
        a.start();
        b.start();
        c.start();

    }
}

class EasyList{
    private int[] value = new int[20];
    private int size = 0;

    ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public int size(){
        Lock readLock = rwLock.readLock();
        readLock.lock();
        System.out.println(Thread.currentThread().getName() + "进入size方法");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName() + "结束size方法");
        readLock.unlock();
        return size;
    }

    public int get(int index){
        Lock readLock = rwLock.readLock();
        readLock.lock();
        System.out.println(Thread.currentThread().getName() + "进入get方法");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        if(index < 0 && index >= size){
            throw new IndexOutOfBoundsException("index is" + index);
        }
        System.out.println(Thread.currentThread().getName() + "结束get方法");
        readLock.unlock();
        return value[index];
    }

    public boolean add(int item){
        Lock writeLock = rwLock.writeLock();
        writeLock.lock();
        System.out.println(Thread.currentThread().getName() + "进入add方法");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        if(size >= value.length){
            return false;
        }
        value[size++] = item;
        System.out.println(Thread.currentThread().getName() + "结束add方法");
        writeLock.unlock();
        return true;
    }
}

1.1 线程通讯(wait, notify, notifyAll)

由于线程之间是抢占式执行的,不能确定线程的执行顺序,在实际开发过程中我们希望控制线程的执行顺序,从而实现线程的通讯,这就用到了wait,notify,notifyAll方法。

  • wait: 使线程进入等待状态,并进入锁对象的等待池
  • notify: 随机唤醒等待池中等待的一个线程,唤醒后的线程会继续运行wait后的代码
  • notifyAll: 唤醒等待池中的所有线程
public class EasyThreadD {
    public static final Object OBJ = new Object();
    public static void method(){
        System.out.println(Thread.currentThread().getName() + "进入方法");
        synchronized (OBJ){
            OBJ.notify(); //唤醒一条被该锁对象wait的线程
            //OBJ.notifyAll(); //唤醒全部被锁对象wait的线程
            System.out.println(Thread.currentThread().getName() + "进入同步代码块");
            try {
                Thread.sleep(1000);
                try {
                    System.out.println(Thread.currentThread().getName() + "线程进入等待状态");
                    OBJ.wait(); //使执行到该行代码的线程进入等待状态 (等待池)
                    System.out.println(Thread.currentThread().getName() + "线程被唤醒,重新运行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "结束同步代码块");
            OBJ.notify();
        }
    }
    public static void main(String[] args) {
        Runnable run = EasyThreadD::method;
        Thread a = new Thread(run);
        Thread b = new Thread(run);
        Thread c = new Thread(run);
        Thread d = new Thread(run);
        Thread e = new Thread(run);
        a.start();
        b.start();
        c.start();
        d.start();
        e.start();
    }
}

wait,notify,notifiyAll方法为什么要用在synchronized代码块中?

wait,notify,notifiyAll方法需要一个线程在具有某个对象的监视锁时才能调用,这个监视锁通常是通过synchronized关键字获得的,如果不在synchronized代码块中调用wait,notify,notifiyAll方法会报异常。wait方法会使线程休眠并释放对象锁,等待其他线程

调用notify或notifyAll唤醒;如果wait,notify,notifiyAll方法不在synchronized代码块中等待,唤醒的语义就变得模糊,其他线程肯无法正常的释放或获取锁。

1.2 死锁

死锁描述的是这样一种情况:多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释
放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程
就会互相等待而进入死锁状态。
在这里插入图片描述

产生死锁必须具备以下四个条件:

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才
    释放资源。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
    注意,只有四个条件同时成立时,死锁才会出现。

2. 线程池

池就是为了资源的重复利用,减少内存占用,提高性能。我们可以使用线程池来实现线程的创建,管理和销毁工作。

线程池有以下三个优点:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的真正实现类是ThreadPoolExecutor,其构造方法的参数有7个:

  • corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  • maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
  • keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  • unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
  • workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
  • threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
  • handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。
public ThreadPoolExecutor(int corePoolSize,
                          //核心线程数(必须)
                          int maximumPoolSize,
                          //最大线程数(必须)
                          long keepAliveTime,
                          //存活时间(必须)
                          TimeUnit unit,
                          //存活时间的时间单位(必须)
                          BlockingQueue<Runnable> workQueue,
                          //工作队列(必须)
                          ThreadFactory threadFactory,
                          //创建线程的工厂(非必须)
                          RejectedExecutionHandler handler)
                          //工作队列满后和达到最大线程数后的回绝策略(非必须)

通过调用ThreadPoolExecutor对象的executor方法来调用线程执行任务

public class EasyExecutors {
    //线程池是为了完成线程创建和管理,销毁工作
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        BlockingQueue queue = new ArrayBlockingQueue(12);
        ThreadPoolExecutor tpe = new ThreadPoolExecutor(
                5,
                10,
                10,
                TimeUnit.SECONDS,
                queue,
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        //线程任务  Runnable
        Runnable run = EasyExecutors::method;
        tpe.execute(run);
        //线程池对象需要关闭
        tpe.shutdown();
    }

    public static void method(){
        System.out.println(Thread.currentThread().getName() + "执行代码");
    }

}

除了实现Runnable接口中的run方法定义线程要执行的任务,还可以实现Callable接口的call方法来定义线程要执行的任务。

比起Runnable,Callable中的方法可以有返回值,可以抛出异常,也支持泛型返回值

运行Callable任务会返回一个Future对象,通过Future对象可以了解任务执行情况

public interface Future<V> {
    //cancel:取消任务的执行
    boolean cancel(boolean mayInterruptIfRunning);
    //isCancelled:如果此任务在正常完成之前被取消,则返回true。
    boolean isCancelled();
    //isDone:如果此任务完成,则返回true。
    boolean isDone();
    //get:等待任务完成,然后返回其结果。是一个阻塞方法
    V get() throws InterruptedException, ExecutionException;
    //get(long timeout, TimeUnit unit):等待任务完成,然后返回其结果。
    //如果在指定时间内,还没获取到结果,就直接返回null。
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
/*cancel:用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。
*参数mayInterruptIfRunning表示是否取消正在执行却没有执行完毕的任务,如果设置true,
*则表示可以取消正在执行过程中的任务。
*如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,
*若mayInterruptIfRunning设置为false,不会取消任务,返回false;如果任务还没有执行则无论*mayInterruptIfRunning为true还是false,肯定返回true。
*如果任务已经完成则无论mayInterruptIfRunning为true还是false,一定会返回false;*/

线程池运行Callable对象需要使用submit方法,submit也可以执行Runnable对象设定的线程任务

public class EasyExecutors {
    //线程池是为了完成线程创建和管理,销毁工作
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        BlockingQueue queue = new ArrayBlockingQueue(12);
        ThreadPoolExecutor tpe = new ThreadPoolExecutor(
                5,
                10,
                10,
                TimeUnit.SECONDS,
                queue,
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        //线程任务  Runnable  Callable
        Runnable run = EasyExecutors::method;
        tpe.execute(run);
        Callable<String> call = EasyExecutors::methodCall;
        Future<String> f = tpe.submit(call);
        //f.cancel(true);
        System.out.println(f.get()); //会等待线程执行完毕
        //tpe.submit(run);
        //线程池对象需要关闭
        tpe.shutdown();
    }

    public static void method(){
        System.out.println(Thread.currentThread().getName() + "执行代码");
    }

    public static String methodCall() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "执行代码call");
        Thread.sleep(1000);
        return "callResult";
    }
}

2.1 线程池的工作原理

线程池的工作原理

  • <1>池中是否有空闲的线程,如果有让该线程执行该任务
  • <2>如果没有空闲的线程,判断池中的线程数量有没有达到核心线程数
  • <3>没有达到核心线程数,创建新的线程,执行新的任务。如果已经达到,就将任务放入工作队列
  • <4>如果工作队列已经填满,就判断是否达到最大线程数,如果没有就创建新的线程
  • <5>如果已经达到最大线程数,工作队列也已经填满,没有空闲的线程,就执行回绝策略

在这里插入图片描述

线程池中的线程达到或超过核心线程数,超出的数量会根据存活时间进行销毁,直到线程数量为核心线程数

线程数小于等于核心线程数时,线程不会消亡

2.2 线程池的回绝策略(handler)

ThreadPoolExecutor类为我们提供了四种回绝策略,分别为AbortPolicy,CallerRunsPolicy,DiscardOldestPolicy,DiscardPolicy。

(默认策略)AbortPolicy 放弃该任务,并抛出一个异常RejectedExecutionException

CallerRunsPolicy 调用者执行,让传递任务的线程执行超出的任务

DiscardOldestPolicy 放弃队列中时间最长的任务,不会抛出异常

DiscardPolicy 直接放弃任务,不会抛出异常

2.3 功能线程池

Executors封装好了 4 种常见的功能线程池,如下:

  • 定长线程池(FixedThreadPool)
  • 定时线程池(ScheduledThreadPool)
  • 可缓存线程池(CachedThreadPool)
  • 单线程化线程池(SingleThreadExecutor)

定长线程池(FixedThreadPool)

定长线程池只有核心线程,线程数量固定,任务执行完成后立即回收线程,可以用于控制线程的最大并发数

// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
fixedThreadPool.execute(task);

定时线程池(ScheduledThreadPool)

核心线程数量固定,非核心线程数量无限,任务执行完成后闲置10ms回收线程,适用于定时或周期性任务

// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务

可缓存线程池(CachedThreadPool)

核心线程数量为0,非核心线程数量无限,执行任务后闲置60s后回收,适用于数量大,耗时小的任务

// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);

单线程化线程池(SingleThreadExecutor)

核心线程数量为1.非核心线程数量为0,执行任务后立即回收,可以保障任务的顺序执行,适用于数据库操作,文件操作等。

// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
  public void run() {
     System.out.println("执行任务啦");
  }
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);

在这里插入图片描述

java枚举类学习:https://blog.csdn.net/weixin_36755535/article/details/130831969

线程池参考: https://blog.csdn.net/u013541140/article/details/95225769

Callable参考:https://blog.csdn.net/m0_61160520/article/details/131636644

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值