专栏:JavaEE初阶
标题:Java相关八股文
Java相关八股文
- ==引言==
- ==Callable接口==
- ==ReentrantLock类==
- ==信号量(Semaphore)==
- ==CountDownLatch类==
- ==多线程环境使用ArrayList==
-
- `1.自行加锁`
- [`2.Collections.synchronizedList(new ArrayList);`](#
2.Collections.synchronizedList(new ArrayList);) - [`3.使⽤ CopyOnWriteArrayList`](#
3.使⽤ CopyOnWriteArrayList)
- ==多线程环境使用哈希表==
-
- `1.HashMap`
- `2.Hashtable`
- `图解`
- `3.ConcurrentHashMap`
- [`图解 `](#
图解)
引言
在上一篇的八股文博客中,我们介绍了一些关于锁的相关策略,以及关于原子类CAS的详细介绍;
下面我们进行了解一些八股文方面的其它内容:即在Java的并发工具包(java.util.concurrent)中提供了一些相关类的使用和多线程相关的面试八股文;
下面我们也进行一定的了解,以便与我们更好更容易的通过面试;
Callable接口
简单了解
①:Callable接口和Runnable接口都属于任务相关的类,是并列关系;
②:不同点:
Callable类有返回值,返回类型为泛型;
Runnable无返回值,使用void;
③:Callable类用于定义一个任务类,可以通过线程来完成任务;但是由于线程中没有提供具有返回值任务类的构造方法;所以需要借助FutureTask对象用于中间桥梁进行任务的传递与执行;
④:
FutureTask对象的简单介绍;FutureTask 是 Java 并发编程中连接 Callable 和 Runnable 的重要桥梁,也是实现异步计算的基础组件之一,相当于:我们在去餐厅点餐是给我们的号牌,做好饭后,我们通过号牌进行取餐;这其中的号牌就相当于FutureTask对象;
代码案例
javapublic class Demo40 { public static void main(String[] args) throws ExecutionException, InterruptedException { Callable<Integer> callable=new Callable<Integer>() { @Override public Integer call() throws Exception { int result=0; for(int i=0;i<100;i++){ result+=i; } return result; } } ; FutureTask<Integer> futureTask=new FutureTask<>(callable); Thread thread=new Thread(futureTask); thread.start(); System.out.println("result="+futureTask.get()); 此个体get方法是进行获取furtruretask类的返回值, // 这个返回值来源于:Callable得call方法; //get方法的执行时间:这个线程执行完毕后就获取到值; //当线程没有执行完毕后就会阻塞等待,等到线程的执行完毕; } }
ReentrantLock类
简单介绍RenntrantLock是和synchronized锁是并列的;都是进行加锁操作的;在早期,我们使用ReentrantLock,由于synchronized优化的越来越好,synchronized就成为我们主流使用的了;
ReentrantLock和synchronized的区别;(经典面试题)①含义:
synchronized是一个关键字,内部是JVM通过C++实现的;
ReentrantLock是Java标准库中提供的类;
② 实现:
synchronized是通过代码块控制加解锁操作的;
ReentrantLock是通过lock和unlock方法实现加锁解锁操作的;其次其还提供了一个tryLock()方法;
tryLock方法:这个方法中不会进行阻塞;加锁成功:返回true; 加锁失败:返回false;调用者通过结果值进行判断接下来如何做;并且该方法中也能进行超时时间设置,控制等待时间;非常方便;
③ 功能:
ReentrantLock类实现公平锁,默认情况下是非公平锁;
④:ReentrantLock类搭配的等待机制是Condition类,相比于wait,notify方法的机制性能更高;
代码案例使用ReentrantLock锁类进行加锁操作,完成线程安全问题的解决;
javapublic class Demo42 { private static int count=0; public static void main(String[] args) throws InterruptedException { ReentrantLock reentrantLock=new ReentrantLock(); Thread t1=new Thread(new Runnable() { @Override public void run() { for(int i=0;i<5000;i++){ reentrantLock.lock(); try{ count++; }finally { reentrantLock.unlock(); } } } }); Thread t2=new Thread(new Runnable() { @Override public void run() { for(int i=0;i<5000;i++){ reentrantLock.lock(); count++; reentrantLock.unlock(); } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("count="+count); } }
信号量(Semaphore)
介绍:在Java中的信号量用来描述当前可用资源的个数,可以进行实时观测,进而用此来进行:在多线程情况下完成多线程资源的协调分配;
生活案例信号量类似于我们所用到的计数器,当计数器作用于记录一个车库,当前可以停放车的数量的时候,在有车进入车库的时候,计数器就会减1;在有车出车库的时候,计数器就会加1;当计数器为0的时候,就表示车库的车以及停满了;这些操作也类似于信号量的操作;
功能:信号量用来记录可用资源的个数;
当申请一个资源的时候,就进行一次"p 操作",这就类似于计数器加1;
当释放一个资源的时候,就进行一次"v 操作",这就类似于计数器减1
特殊情况二元信号量
①:当信号量初始值为1的时候,信号量就在0和1之间进行切换;表示一个资源是否被占用,当被占用显示1;反之:显示0;
②:推广:这样的二元信号量就可以应用到加锁操作上;通过0和1的切换来表示锁对象是否被占用的情况;进而完成了锁的基础功能的实现;
代码运用
简单使用信号量(semaphore)类
javapublic class Demo43 { public static void main(String[] args) throws InterruptedException { Semaphore semaphore=new Semaphore(3);//设置计数器的当前容量为3; semaphore.acquire();// 此方法为执行p操作; System.out.println("进行一次P操作"); semaphore.acquire(); System.out.println("进行一次P操作"); semaphore.acquire(); System.out.println("进行一次P操作"); } }利用二元信号量Semaphore类的P与V操作进行线程安全操作的解决;
javapublic class Demo44 { private static int count=0; public static void main(String[] args) throws InterruptedException { Semaphore semaphore=new Semaphore(1);//设置该信号量为二元信号量; Thread t1=new Thread(new Runnable() { @Override public void run() { for(int i=0;i<5000;i++){ try { semaphore.acquire();//进行p操作; count++; semaphore.release();//进行v操作; } catch (InterruptedException e) { throw new RuntimeException(e); } } } }); Thread t2=new Thread(new Runnable() { @Override public void run() { for(int i=0;i<5000;i++){ try { semaphore.acquire();//进行p操作; count++; semaphore.release();//进行v操作; } catch (InterruptedException e) { throw new RuntimeException(e); } } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("count的值为:"+count); } }
代码思路上述代码通过两个线程来完成count++的操作;由于多线程的并发执行的随机调度问题,会出现线程安全问题,即:出现我们想要的结果值出现和预期值不一样的bug;
因此:我们使用信号量semaphore类进行解决此问题;
具体方法:通过使用使用semaphore设置其为二元信号量(即信号量的初始值为1),同时在每个线程中我们进行使用信号量类的P与V操作进行解决此安全问题;
代码执行结果
CountDownLatch类
①:含义:在Java中一个线程可能会遇到接收一个相对于其它任务来说的大任务,这样就会导致这个线程一直占用CPU资源,影响其它线程的执行,这时,我们为了提高效率,于是我们就把这个大任务均衡的分成几个小任务交由不同的线程去完成这个大任务,正所谓:人多力量大;这就会使得我们就能够成功的完成了任务,并且提高了执行效率;这样我们怎么进行任务的分配,以及记录子任务啥时候执行完毕;我们就可以通过这个CountDownLatch类以及其提供的方法进行任务的成功完成;
②:实例代码
javapublic class Demo45 { public static void main(String[] args) throws InterruptedException { CountDownLatch latch=new CountDownLatch(10); ExecutorService executorService=Executors.newFixedThreadPool(4); for(int i=0;i<10;i++){ int id=i; executorService.submit(()->{ System.out.println("子任务开始执行"+id); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("子任务执行完毕"+id); latch.countDown();//记录子任务的执行数量; }); } latch.await();//等待所有的子任务执行完毕; System.out.println("所有的子任务执行完毕"); executorService.shutdown();//关闭线程池; } }代码分析
①:首先我们进行实例化一个CountDownLatch类的对象Latch来完成将大任务分成多个小任务;
②:接着:我们借助线程池来一个个完成这些小的任务,每完成一个小任务,我们利用Lacch对象的countDown方法进行完成执行完小任务的个数记录;
③:接着,我们利用Lathc对象的await方法进行阻塞等待所有的子任务都完成后才能唤醒等待
④:最后,我们进行线程池的关闭,宣告任务的结束;
代码执行结果
多线程环境使用ArrayList
在多线程的情况下,使用到顺序表,肯定也会涉及到相关的线程安全问题:
随着Java语言的发展,也有了几种公认的可以解决线程安全的方法,
但都各有利弊,下面我们一一进行简单了解;
1.自行加锁考虑那些操作需要进行加锁操作,将其设置一个原子类进行打包,根据个人思路来解决;对个人能力要求高;但是切实可行;
2.Collections.synchronizedList(new ArrayList);此方法是将顺序表中的每个方法都进行使用synchronized进行加锁操作;这样可以解决线程安全问题,但是大量方法进行加锁操作也会有很大的资源消耗,大大影响执行效率;
3.使⽤ CopyOnWriteArrayListCopyOnWrite容器即写时复制的容器;
含义:此方法就是当我们往⼀个容器添加元素的时候,不直接往当前容器添加,⽽是先将当前容器进⾏Copy,复制出⼀个新的容器,然后新的容器⾥添加元素,添加完元素之后,再将原容器的引⽤指向新的容器。
当在进行新的容器里添加元素期间,有其他线程进行读取操作的时候,就会让其从原容器中进行读取操作,只有当添加元素成功后才进行原容器的更新;
优点①:在读多写少的场景下,就会性能很⾼,因为进行原容器复制以及迁移的情况少;
②:不需要加锁竞争
缺点:①:占⽤内存较多.
② 新写的数据不能被第⼀时间读取到
应用进行配置文件的更新;
当在进行一些游戏渲染方面的更新时:即游戏画面,游戏声音,以及游戏按键的设置更新时,我们一般必须进行重启服务器操作才能完成更新操作,这样会大大影响客户的正常使用,这时我们就会向服务器中引入配置文件,所谓配置就是在服务器的内存中以数组或哈希表的形式进行存储;并且服务器中的代码进行相关逻辑的执行都会进行读取这些配置,这时当我们进行一下设置的修改的时候就进行采用这种CopyOnWriteArrayList容器进行设置配置,手动进行修改配置,完后进行重加载配置完成设置的修改即可解决这种设置更新问题;
多线程环境使用哈希表
1.HashMap本身不是线程安全的,多线程环境不进行使用;
2.Hashtable这个哈希表类只是简单的将哈希表中的关键方法进行了synchronized锁的使用;
这就相当于直接对Hashtable对象进行加锁;
图解
根据上图我们分析得出:
1.如果多个线程访问同一个Hashtable就会直接造成锁冲突;因为,只有一把锁,这样也会导致两个线程无论访问Hashtable对象的那一个数据都会造成锁冲突;
2.size 属性也是通过 synchronized 来控制同步, 也是⽐较慢的;
3.⼀旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到⼤量的元素拷⻉, 效率会⾮常低.;
3.ConcurrentHashMapConcurrentHashMap是对Hashtable的进一步优化,性能方面比Hashtable更有优势;
优化操作①:
读操作没有加锁(但是使⽤了 volatile 保证从内存读取结果), 只对写操作进⾏加锁. 加锁的⽅式仍然
是⽤ synchronized, 但不是锁整个对象, ⽽是 "锁桶" (⽤每个链表的头结点作为锁对象), ⼤⼤降
低了锁冲突的概率.
②:
充分利⽤ CAS 特性. ⽐如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
③:扩容操作进行了优化:使用"化整为零"的思想;
即扩容操作就意味着需要数组需要进行搬运到新的数组中,一个Hash中的元素是很多的,这样成功搬完需要耗费大量的时间;
而假设正好有一个线程调用ConcurrentHashMap对象的时候触发了扩容操作,这时我们就采用化整为零的操作进行搬运,即通过每次的put操作进行一小部分的搬用,就相当于每个线程搬用一小部分,直到成功搬完结束,将旧哈希表替换成新的哈希表。
注意:当有其它线程访问该对象的时候,可以进行访问新旧哈希表,这样就解决了锁冲突问题了;
图解ConcurrentHashMap中每个哈希桶都有一把锁,这样只用当两个线程同时进行访问同一个哈希桶中的数据才会产生锁冲突,其它情况都不会产生影响;




