<JAVAEE>多线程6-面试八股文之juc中的组件

juc中的组件:


引言:

在之前我们学习了java的多线程,我们知道java的线程是被封装在java.util.current包中,当然这个包中不仅有Thread 类,还有一些其他的类,下面我们就是来学习一下这些其他的组件。


1.callable( ):

什么是callable?

callable是java中的一个执行并发任务的一个类,他和Runnable是类似的但是又有所不同。

  • callable里面的是call()方法。
  • call()方法是带有泛型的返回值。
  • callable(),任务不能直接给线程执行需要封装成FutureTask任务,返回的结果也是通过FutureTask获取。
  • callable(),能直接通过submit提交给线程池(submit在提交的过程中,会自己封装成FutureTask()),返回Future对象。
  • 可以直接传入多个callable对象,但是接收结果的时候可以使用一个数组或接收。

代码1:FutureTask

java 复制代码
package Thread.JUC;

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

public class callable1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger();
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                for(int i = 0;i<1000;i++){
                    atomicInteger.getAndIncrement();
                }
                return Integer.valueOf(atomicInteger.get());
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t1 = new Thread(futureTask);
        t1.start();
        System.out.println(futureTask.get());
    }
}

注意:

  • 线程我们不能忘记start否则会没有结果
  • 此外call( ) 方法的返回值要和他的泛型类型是一样的。
  • 获取结果的get方法,是带有堵塞的效果的,如果执行到这里且任务没有执行完的话,他会进行堵塞等待任务结束。
    代码2:Future
java 复制代码
package Thread.JUC;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class callable2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger();
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                for(int i = 0;i<1000;i++){
                    atomicInteger.getAndIncrement();
                }
                return Integer.valueOf(atomicInteger.get());
            }
        };
        ExecutorService service = Executors.newFixedThreadPool(1);
        Future<Integer> future = service.submit(callable);
        System.out.println(future.get());
    }
}

总结:

  • future或者futurefask他们相当于一个号码牌,有了这个东西我们才能找到他的返回结果。
  • 此外get方法,还有一个超时等待版本,如果,超过等待时间还没有结束,就不在堵塞等待了。

代码:

java 复制代码
package Thread.JUC;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class callable1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger();
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                for(int i = 0;i<1000;i++){
                    atomicInteger.getAndIncrement();
                    Thread.sleep(1000);
                }
                return Integer.valueOf(atomicInteger.get());
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t1 = new Thread(futureTask);
        t1.start();
        try {
            Integer result = futureTask.get(500, TimeUnit.MILLISECONDS);
            System.out.println("执行成功");
        } catch (TimeoutException e) {
            System.out.println("超时等待");
            throw new RuntimeException(e);
        }
    }
}

2.ReentrantLock :

ReentrantLock是什么?

ReentrantLock是一个可重入锁,他和synchronized并列的,他是java当中的一个类,通过lock unlock进行加锁,解锁。

代码案例:

java 复制代码
package Thread.JUC;

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLock1 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock locker = new ReentrantLock();
        Thread t1 = new Thread(()->{
            for(int i = 0;i<50000;i++){
                locker.lock();
                try{
                    count++;
                }finally{
                    locker.unlock();
                }
            }
        });
        Thread t2 = new Thread(()->{
            for(int i = 0;i<50000;i++){
                locker.lock();
                try{
                    count++;
                }finally{
                    locker.unlock();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();t2.join();
        System.out.println(count);
    }
}

代码分析:

  • 这是一个会出现线程安全的问题,如果但是我们使用了ReentrantLock进行加锁,和解锁。避免了线程安全问题。

ReentrantLock和 synchronized的区别

  • ReentrantLock是java的一个类 ,内部是由java实现的。 synchronized是一个关键字,主要实现是由JVM通过c++实现的。

  • ReentrantLock 是通过方法lock和 unlock进行加锁和解锁的时候,我们要注意对unlock进行解锁,通常我们会用try finally代码包裹避免忘记解锁。synchronized是通过代码块进行加锁和解锁的,不需要考虑忘记解锁等。

  • ReentrantLock 除了lock和 unlock方法以外还有trylock方法

    • 加锁成功饭返回true,加锁失败返回false,调用者根据返回结果做出下一步操作,不像synchronized关键字一样,加锁失败就一直堵塞,具有更多的操作空间。
    • 此外trylock还提供了带时间参数的版本,他不会立即返回结果,而是等待,超出时间之后才会返回结果。
  • ReentrantLock 提供了公平锁的选项,默认是非公平的(即不分先来后到,概率均等)

java 复制代码
	ReentrantLock reentrantLock1 = new ReentrantLock(false);//非公平
    ReentrantLock reentrantLock2 = new ReentrantLock(true);//公平
  • ReentrantLock搭配的等待机制是condition相比synchroized的wait和 notify功能更强大。

总结:

关于ReentrantLock,我们主要了解他是如何使用的以及他和synchroized的区别


信号量 3.semaphore

什么是信号量?

在多线程当中,信号量是用来控制线程对共享资源的访问权限的,本质是由一个计数器+堵塞队列控制的。

  • 计数器:记录当前还有多少个可用的资源
    • 取资源计数器--;
    • 放资源:计数器++;
  • 堵塞队列:当计数器为0的时候,会堵塞线程,等待资源的释放。
  • 类比 :信号量可类比为带固定车位的停车场:车位总数是计数器初始值(共享资源总量) ,车主进停车场 对应线程执行 P 操作**(申请资源)------ 有空位则计数器减 1 后放行,无空位则进入等待区排队(线程阻塞);车主离开对应线程执行 V 操作(释放资源)------ 计数器加 1,若有排队车主则唤醒一位进入(线程唤醒)**;其中 1 个车位的充电桩对应二进制信号量(互斥锁,仅允许 1 个线程独占),10 个车位的普通停车场对应计数信号量(允许 N 个线程同时访问),入口管理员则保证进出场操作的原子性(避免计数错乱),完美匹配信号量 "通过计数器 + 等待队列控制共享资源并发访问" 的核心逻辑。

代码:资源足够的

java 复制代码
package Thread.JUC;

import java.util.concurrent.Semaphore;

public class semaphore {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(4);
        for(int i = 0;i<4;i++){
            semaphore.acquire();
            System.out.println("获得资源"+(i+1));
        }

    }

}

代码:资源不够,发生堵塞

java 复制代码
 public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3);
        for(int i = 0;i<4;i++){
            semaphore.acquire();
            System.out.println("获得资源"+(i+1));
        }

    }
  • 运行代码,在四次循环的时候,我们尝试获取,却发现资源不够,因此就进行堵塞。

代码:释放资源

java 复制代码
	

public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3);
        for(int i = 0;i<4;i++){
            semaphore.acquire();
            System.out.println("获得资源"+(i+1));
            semaphore.release();
        }

    }	 
  • java的v操作对应于relase操作,释放资源。,我们使用之后就进行释放,就不会发生堵塞了。

二元信号量

如果一个信号量的初始值是1 那么他的取值不是1就是0 ,这个时候这个二元信号量有锁的作用了。
代码演示:

java 复制代码
private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(1);
        Thread t1 = new Thread(()->{
            for(int i = 0;i<50000;i++){
                try {
                    semaphore.acquire();
                    count++;
                    semaphore.release();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread t2 = new Thread(()->{
            for(int i = 0;i<50000;i++){
                try {
                    semaphore.acquire();
                    count++;
                    semaphore.release();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
  • 上述获得资源释放资源 的过程中,起到了一个锁的作用

4.CountDownLatch

在多线程中,我们通常是把一个大任务分成多个小任务去执行,但是在线程并发执行的过程中,我们是如何知道,当前线程执行完毕了,当前的子任务已经全部执行完了呢?这个时候我们就需要使用CountDownLatch来记录当前的子任务执行的数量。

使用方法:

  • 构造CountDownLatch实例 latch,注意这个时候CountDownLatch里面的参数要和将执行的任务的数量相等。
  • 每完成一次任务,我们都调用一次latch.countDownLatch,此时就会在CountDownLatch里面完成自减操作。
  • 在主线程调用latch.await( )等待所有任务执行完毕。

代码:

java 复制代码
	package Thread.JUC;

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

public class myCountDownLatch {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);
        ExecutorService service = Executors.newFixedThreadPool(4);
        for(int i = 0;i<10;i++){
            int id = i+1;
            service.submit(()->{
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("执行到任务"+id);
                latch.countDown();
            });
        }
        latch.await();
        System.out.println("任务全部执行完毕");
    }
}
  • CountDownLatch的参数列表和任务数目相同
  • 每次执行完成一个任务我们都需要执行一次countDownLatch方法。
  • await方法的话,会堵塞调用线程,只有CountDownLatch里面的任务数目执行完之后才继续执行调用线程。
  • CountDownLatch只针对个别场景下才有用。

5.多线程下ArrayList的使用

ArrayList是一个线程不安全的类,在多线程中使用会出现线程安全问题,那么我们应该如何做,才能在多线程中安全的使用ArrayList呢?

1.自主加锁:

我们针对我们的代码逻辑,把可能出现线程安全问题的代码打包加锁,组装成原子类
2.使用synchronized进行套壳

collection.synchronizedList( new ArrayList) ,返回一个list对象,这个list对象的所有方法都会被synchronized修饰。
3.CopyOnWriteArrayList:
赋值修改思想:

多线程执行到修改的时候,会首先复制一份数据,在修改的过程中,进行读取,引用指向旧的数据,等到修改之后,引用就执行修改之后的数据。

  • 这样写避免了读取到修改一般的数据
  • 但是这样写存在一些弊端:
    *
    1. 如果数组非常大的话,就会导致资源的大量浪费
    • 2.此外,如果同时多个线程同时修改数据,那么最终结果的引用指向哪个,这个我们也没办法确定。

总结:

上述虽然讲了三种方法,但是实际当中,我们更倾向于使用第一种,第二种对于所有方法都加了锁,会造成性能的浪费,第三种在多线程同时修改的时候会出现问题,此外如果数组非常大,会造成空间极大的浪费。


6.多线程下哈希表的使用

HashMap和HashTable

在之前的学习当中,我们知道HashMap是一个线程不安全的类,HashTable是一个线程安全的类。

  • HashTable;
    • 他本身被synchroized修饰,相当于对this加锁,因此他的冲突概率比较大。
    • 哈希表产生概率的情况比较小,对不同的链表进行修改,且同一链表上不同修改的两个数据的位置不相邻的时候,都不会产生线程安全问题。
    • 因此,哈希表产生冲突的情况很低,但是this针对任意操作都进行加锁,因此就会影响性能。

  • CurrentHashTable:
    • CurrentHashTable相对于HashTable,他并不是针对this加锁,而是针对每一个的链表的头节点,就避免了无效冲突的发生,大大降低了冲突的概率,提升了性能。
    • 这个时候我们又有了一个新的问题:
      • 我们针对每个链表进行加锁,但是如果我们在多线程条件下在两个链表里面都添加一个元素,他没有线程冲突,但是他的size存在线程安全的情况,这就会让代码出现问题。
      • 如何处理呢?我们对size使用一个原子类,这样就避免了size的线程安全问题。

总结:

  • 1.我们说了callable的类的使用
  • 2.ReentrantLock的使用;
  • 3.信号量
  • 4.CountDownLatch
  • 5.多线程下对ArrayList以及哈希表的使用。
相关推荐
DJ斯特拉34 分钟前
日志技术Logback
java·前端·logback
悟能不能悟35 分钟前
springboot的controller中如何拿到applicatim.yml的配置值
java·spring boot·后端
0和1的舞者36 分钟前
《SpringBoot 入门通关指南:从 HelloWorld 到问题排查全掌握》
java·spring boot·后端·网络编程·springboot·开发·网站
SamDeepThinking36 分钟前
88MB Excel文件导致系统崩溃?看我如何将内存占用降低
java·excel
小小8程序员41 分钟前
iOS开发的面试经验
ios·面试·cocoa
Jul1en_41 分钟前
【Spring DI】Spring依赖注入详解
java·spring boot·后端·spring
Unstoppable2243 分钟前
八股训练营第 35 天 | volatile 关键字的作用有那些?volatile 与synchronized 的对比?JDK8 有哪些新特性?
java·八股·volatile
二宝1521 小时前
黑马商城day10-Redis面试篇
数据库·redis·面试
Lisonseekpan1 小时前
HTTP请求方法全面解析:从基础到面试实战
java·后端·网络协议·http·面试