javaEE初阶————多线程进阶(2)

今天来继续带大家学习多线程进阶部分啦,今天是最后一期啦,下期带大家做一些多线程的题,我们就可以开始下一个环节啦;

1,JUC(java.util.concurrent)的常见类

1)Callable 接口

我们之前学过Runnable接口,它是一个任务,我们可以在创建线程的时候把任务丢给线程使用匿名内部类等方法来完成创建对象,现在我们有了一个新的方法来创建任务,并且执行这个任务,就是我们的Callable接口,Runnable的run方法是没有返回值的,但是Callable提供了返回值,支持泛型,我们就能获取到我们想要的参数,

我们来看看是怎么用的;

java 复制代码
 Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return null;
            }
        };

我们使用匿名内部类的方法创建一个Callable对象,并且重写call方法,就相当与重写Runnable的run方法, 我们是不能把这个对象直接放到线程的构造方法中的,因为Thread没有提供传入Callable的版本,我们要使用另一个类FutureTask来拿到结果,在把创建的futureTask对象放到线程创建时的构造方法中去;

java 复制代码
  FutureTask<Integer> task = new FutureTask<>(callable);
        Thread t1 = new Thread(task);
        t1.start();
        System.out.println(task.get());

这里的task.get方法会阻塞main线程结束,直到t1线程正确计算出结果;

2)ReentrantLock

这个是上古时期的锁,现在有更智能,更好的替代synchronized,那我们还学它干嘛呢,它还活着就一定是有原因的,

1,synchronized是关键字,是由JVM内部通过C++实现的,而ReentrantLock是一个类;

2,synchronized是通过进出代码块来实现的,ReentrantLock需要Lock和UnLock方法来辅助;

java 复制代码
  ReentrantLock reentrantLock = new ReentrantLock();
                reentrantLock.lock();
                a++;
                reentrantLock.unlock();

3,ReentrantLock除了提供Lock和unLock之外还提供了一个不会造成阻塞的tryLock()

它会根据是否加锁成功返回true或者false;

4,synchronized是非公平锁,而ReentrantLock是默认是非公平锁,但是也提供了公平锁的实现;

5,ReentrantLock的等待通知机制是Condition类,比synchronized的wait和notify功能更强

3)线程池

博主博主,咱们之前不是讲过线程池了吗,怎么又来一遍呀,确实嗷,上次虽然给大家详细讲过了,但是我们还没有用呀,哈哈哈哈哈,我直接上代码;

我们先来简单的版的;

java 复制代码
public class Demo2 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(1111);
                }
            }
        };
        ExecutorService executorService = Executors.newFixedThreadPool(1);//创建固定数目的线程池
        //ExecutorService executorService1 = Executors.newSingleThreadExecutor();    创建单线程池
        //ExecutorService executorService2 = Executors.newCachedThreadPool();        创建线程动态增长的线程池
        //ScheduledExecutorService service = Executors.newScheduledThreadPool(1);    创建定时线程池
        //executorService.submit(runnable);
        executorService.shutdown();

    }
}

我们还可以通过execute来提交任务

java 复制代码
executorService.execute(runnable);

都是官方给提供的现成的,我们这会来自己创建;

java 复制代码
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,20,
                10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10),
                Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        

这就是我们自己创建的线程池,我们要把所有的参数都填上;

1. 任务队列类型
队列类型 特点
ArrayBlockingQueue 有界队列,需指定容量
LinkedBlockingQueue 无界队列(默认使用,可能 OOM)
SynchronousQueue 不存储任务,直接提交给线程
PriorityBlockingQueue 支持优先级排序
2. 拒绝策略
策略类 行为
AbortPolicy(默认) 抛出 RejectedExecutionException
CallerRunsPolicy 由提交任务的线程直接执行任务
DiscardPolicy 静默丢弃新任务
DiscardOldestPolicy 丢弃队列中最旧的任务,然后重试提交

工厂模式那个也是官方给提供的现成的哈哈哈哈,太懒了我;

4)信号量 Semaphore

一种计数器,可以表示可用资源的个数;

信号量的P操作,申请资源,计数器加一;

信号量的V操作,释放资源,计数器减一;

如果此时计数器为零,再尝试申请资源就会进入阻塞等待;

有一点点像锁;

我们使用acquire来申请资源,使用release来释放资源,

我们来试试写代码;

java 复制代码
public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3);//3个可用资源
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("申请资源");
                    semaphore.acquire();
                    System.out.println("获取到了资源");
                    Thread.sleep(10000);
                    semaphore.release();
                    System.out.println("释放资源");
               

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };

        Thread t1= new Thread(runnable);
        Thread t2= new Thread(runnable);
        Thread t3= new Thread(runnable);
        t1.start();
        t2.start();
        t3.start();
        Thread t4= new Thread(runnable);
        t4.start();
        t4.join();
    }
}

我通过运行这个代码可以看到t1, t2,t3线程获取申请资源之后不释放,t4申请资源就要等着,直到10s之后,t4线程才开始工作;

5)CountDownLatch

也类似一个计数器,我们传入构造方法的参数就是需要完成的任务个数,完成一个任务就调用countDown()方法,主线程中使用await方法,等待所有任务完成主线程才结束;

java 复制代码
public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(111);
                countDownLatch.countDown();
            }
        };

        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(runnable);
            t.start();
        }
        countDownLatch.await();
    }
}

2,线程安全的集合类

我们之前学习的数据结构大部分是不安全的,我们还想使用之前的数据结构就要做相应的修改;

1)多线程环境使用ArrayList

1,使用ArrayList的第一种方式就是自己加锁,使用synchronized或者ReentrantLock,来对容易引发线程安全的地方来加以限制;

2,就是套壳

collections.synchronized(new ArrayList)

对于public的方法都加上synchronized;

3,使用CopyOnWriteArrayList

这个方法是不去加锁的,我们知道,读操作是不影响线程安全的,那么我们在使用ArrayList的时候,我们修改了,我们就再复制一个数组,我们读取的时候只能读到旧的数据或者是已经修改完成的数据,不存在读取修改一半的情况,但是,如果我们的数据很大很大呢,难道我们要一下复制所有的元素吗,是的,就是这么难受,并且多个线程修改数据的时候也可能会发生问题,那我们干嘛要用它,这个是存在特定的使用场景的,服务器如果修改配置了的话是需要重新启动的,我们玩游戏的时候,如果我们要修改设置,比如打开声音,或者设置按键等,难道我们还要关掉游戏吗,我们这时候就是我们给出指令,根据新的设置,服务器就会创建新的哈希数组,来代替旧的数组,完成配置文件的修改,而不是服务器的重启;

2)多线程环境下使用队列

  1. ArrayBlockingQueue 基于数组实现的阻塞队列

  2. LinkedBlockingQueue 基于链表实现的阻塞队列

  3. PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列

  4. TransferQueue 最多只包含⼀个元素的阻塞队列

3)多线程环境下使用哈希表

哈希表,查找时间复杂度O(1)啊,这必选得拿到多线程中,我们之前讲过,Hashtable是线程安全的,但它只是对HashMap的所有方法加锁,效率肯定是不高的,我们有一个完美的替代品就是ConcurrentHashMap;

ConcurrentHashMap是对桶级别加锁,和HashTable不一样,更高效;

大家还记不记得的哈希表是咋样的了, 我们要解决哈希冲突,我们通常是在每个下标中构建链表或者是红黑树;如果链表太长了,我们还涉及到扩容操作;

ConcurrentHashMap是对每个下标都加锁的,锁对象就使用表头,当两个线程在不同的下标是,就不会发生锁竞争,当两个线程修改同一个下标时,就存在线程安全性问题了,因为有表头锁的存在就会发生竞争,成功避免了线程安全问题;另外,记录的元素个数size怎么办呢,两个线程同时增加数据,size也会有线程安全问题,还有加锁吗,忘了我们的AtomicIngter了吗,这个原子类也是很好用的呀,大家不要忘了;

还有最后一个哈希扩容问题,如果发生扩容就意味着和CopyOnWriteArrayLIst一样了,我们要把原来的数距全部复制过来,那肯定需要很多的时间,所以我们不会一次就把所有元素复制过去,我们会把每次put一些数据的过程中偷偷复制一些数据到新哈希表,就意味着我们把100%的任务分三开,每次执行别的操作都完成一点点的任务,直到扩容完全完毕;

相关推荐
浮游本尊5 分钟前
Java学习第22天 - 云原生与容器化
java
渣哥2 小时前
原来 Java 里线程安全集合有这么多种
java
间彧2 小时前
Spring Boot集成Spring Security完整指南
java
间彧2 小时前
Spring Secutiy基本原理及工作流程
java
Java水解3 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆6 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学6 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole6 小时前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端
华仔啊6 小时前
基于 RuoYi-Vue 轻松实现单用户登录功能,亲测有效
java·vue.js·后端