个人简介:
📦个人主页:赵四司机
🏆学习方向:JAVA后端开发
📣种一棵树最好的时间是十年前,其次是现在!
🔔博主推荐网站:牛客网 刷题|面试|找工作神器
💖喜欢的话麻烦点点关注喔,你们的支持是我的最大动力。
前言
由于很快到了金九银十的秋招季节,博主最近也在找一些面经资源,但是发现很多都不全,后来我发现了牛客网这个网站,发现里面不仅可以看面经,还能刷题模拟面试,要是你要找各种招聘信息也可以在上面找到,我愿称之为程序员必备网站,下面把它推荐给你们!
链接地址:牛客网
文章目录
65.Java中常见的IO模型
我们知道,为了保证操作系统的稳定性及安全性,一个进程的地址空间分为用户空间和内核空间,像我们平时的应用程序都是运行在用户空间中,只有内核空间才能完成系统级的操作,比如文件管理,进程通信等,我们要进行IO操作,就一定要依赖内核空间的能力。当我们进行I/O操作时候,就会发起系统调用。
BIO(同步阻塞IO)
同步阻塞IO中,当应用程序发起Read调用之后,会一直阻塞,直到内核把数据拷贝到用户空间。当连接数少的时候这样的没问题的,但是当连接数很多时候,这种模式是无法应对如此高的并发量的。
NIO(同步非阻塞IO)
Java的NIO于JDK1.4中被引入,对应java.nio包,它是支持面向缓冲的,基于通道的IO操作方法,对于高负载,高并发的应用,应该使用NIO。
在NIO中,应用程序会一直发起read调用,在这段时间里是不阻塞的,但是在内核准备好数据将数据从内核拷贝到用户空间这段时间之内仍然是阻塞的,直到内核把数据拷贝到用户地址空间。
相比BIO模型,NIO确实有了很大的提升,但是这种通过轮询的策略应用程序不断进行I/O系统调用轮询数据是否准备好的过程十分消耗CPU资源。
这时候I/O多路复用就上场了,在I/O多路复用中,Selector(选择器)会不断轮询注册在上面的channel,如果某个channel为读写事件做好准备,那么就处于就绪状态,通过Selector就可以不断轮询发现出就绪的channel,完成后续的I/O操作。一个Selector能够同时监控多个channel,这样一个线程就可以管理多个channel,不用为每个连接都创建一个线程造成不必要的上下文切换带来的开销。
I/O多路复用
I/O多路复用指的是一个线程/进程就能够处理多个I/O请求。其实现原理是将要监视的文件描述符添加到select/poll/epoll函数中,由内核监视,一旦有文件描述符就绪,或者超时,这时候函数就会返回,进程就可以执行相应的读写操作。在Linux中有三种方式实现I/O多路复用,分别为select、poll、epoll。select和poll只支持水平触发,而epoll支持水平触发和边缘触发,一般来说边缘触发效率要高一些。
**文件描述符:**在Linux中万事万物皆文件,而我们使用文件都需要借助一个号码,这个号码就是文件描述符,因此文件描述符就是一个非负的整数,也即一个索引,指向内核为每一个进程打开的文件的记录表,当程序打开一个现有文件或者新建一个文件时候,内核就会返回一个文件描述符,内核通过文件描述符访问文件。
**水平触发:**只要一个文件描述符就绪就会发起通知,如果这次没有一次性把数据读完,下次还会通知;
**边缘触发:**当文件描述符从未就绪状态变为就绪状态就会触发一次通知,之后不会再触发,直到再次从未就绪状态变为就绪状态。
**select:**应用程序首先进行系统调用,传入需要监听的文件描述符集合,然后内核会遍历传入的fd集合,如果没有就绪的fd则这时候就会进入阻塞状态,让出CPU;如果有就绪的fd则将对应的fd打标记然后返回,最后应用程序遍历fd集合找到就绪的fd进行相应的数据处理。由于每次都需要重新将fd_set从用户态拷贝到内核态,为了减少数据拷贝带来的性能消耗,Linux内核对fd_set集合大小做了限制,规定用户文件描述符不能超过1024个。
**poll:**poll实现方式和select差不多,只不过poll定义了一个pollfd数据结构,包含需要监听的文件描述符、监听的事件、就绪的事件,这样就不用像select那样每次都要新生成一个fd_set。
**epoll:**epoll通过内核和用户空间共享内存空间避免了数据从用户空间到内核空间复制带来的性能损耗。此外epoll是基于事件驱动的,文件描述符就绪时候采用回调机制避免了轮询,而且它支持的同时连接数上限很高,同时支持水平触发和边缘触发两种方式。
AIO(异步I/O)
AIO是在java7中引入的,AIO是基于事件和回调机制实现的,也 就是应用操作之后会直接返回,不会阻塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续操作。
66.产生死锁的四个必要条件
- 互斥条件:该资源任意一时刻只能被一个线程拥有
- 请求与保持条件:一个线程因请求资源被阻塞时,对拥有的资源不释放
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕之后才释放
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
67.Sleep和wait的区别
两者都能暂停线程的执行;
sleep()方法没有释放锁,而wait()方法释放了锁;
wait()通常用于线程之间的通信/交互,而sleep()通常用于暂停线程的执行;
wait()方法执行后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法。sleep()方法执行完毕之后线程会自动苏醒。
sleep()是Thread类的静态本地方法,wait()是Object类的本地方法。
68.为什么wait方法不定义在Thread中
wait()方法是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象都拥有对象锁,既然要释放对象锁并让其进入等待状态自然是操作当前对象而非当前线程。
而sleep()方法是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
69.可以直接调用Thread类的run方法吗
我们创建一个线程的步骤是先new一个线程,然后调用satrt()方法让该线程进入就绪状态,当分配到时间片线程就会自动执行里面的run方法。而直接调用run方法会将该方法当做main线程的一个方法来执行,并不会创建一个新的线程,这并不是多线程工作。
70.volatile关键字
- volatile关键字可以保证变量的可见性,如果变量用该关键字修饰,这就指示JVM,这个变量是共享的且不稳定的,每次使用它都从主存中获取,当该变量发生变化时候其他线程都能马上感知到。
- volatile关键字能够保证数据的可见性,但是不能保证数据的原子性,synchronized关键字两者都能保证。
- volatile关键字能防止指令重排,当我们对该变量进行读写操作时候,会插入特定的内存屏障的方式来禁止指令重排序。
71.synchronized关键字
概述
synchronized翻译成中文是同步的意思,主要解决多个线程之间访问资源的同步问题,可以保证被他修饰的方法或者代码块在任意时刻只能有一个线程执行。
在Java早期版本中,synchronized是重量级锁,效率低下,它是依赖于操作系统的互斥锁来实现的,完成线程的挂起或者唤醒都需要操作系统帮忙完成,这样会发生用户态到内核态的转换,时间成本高。
不过在Java6之后,Java在JVM层面对synchronized进行了优化,所以现在的synchronized锁效率也优化的很好了,加入了自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等关键技术来减少锁操作的开销。
如何使用
修饰实例方法(锁当前对象)
修饰静态方法(锁当前类)
修饰代码块(锁指定对象/类)
//表示进入同步代码块前要获得给定对象的锁 synchronized(object) { } //表示进入同步代码块前要获得给定Class的锁 synchronized(类.class) { }
尽量不要使用synchronized(String a),因为JVM中字符串常量池具有缓存功能。
72.Java6对synchronized的优化
为了减少获得锁和释放锁所带来的性能消耗,JDK1.6引入了偏向锁和轻量级锁,所以在JDK1.6里一共有四种锁状态,无锁状态、偏向锁、轻量级锁、重量级锁,它们会随着竞争的激烈程度逐渐升级,但是不能降级。
偏向锁
如果运行过程中只有一个线程访问,不存在多线程竞争的情况,则线程是不需要触发同步的,这种情况下就会加偏向锁。当线程第二次到达同步代码块时候,此时会判断持有锁的线程是否是自己,是的话则直接进入执行,这里没有锁的释放和申请,因此没有额外的开销。
轻量级锁
如果运行过程中有其他线程抢占锁,这时候持有偏向锁的线程会被挂起,JVM会消除他身上的偏向锁,将锁恢复到标准的轻量级锁(自旋锁)。撤销偏向锁的时候会导致STW(stop the word)操作,即除了GC所需的线程,其他线程都停止工作。
自旋锁原理为当一个线程持有的锁能够被很快释放,则其他等待的线程不需要做用户态和内核态的切换进入阻塞状态,只需要等待锁被释放,避免用户线程和内核的切换的消耗。
重量级锁
在轻量级锁状态下继续竞争,没有抢到锁的线程将自旋,即不停地循环判断锁能否被成功获取。长时间的自旋操作是十分消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了有效任务,这种情况叫忙等。如果锁竞争严重,某个线程达到最大自旋次数,会将轻量级锁升级为重量级锁。
一旦进入重量级锁状态,后面的线程要请求锁时候,会直接将自己挂起,等待被唤醒。
73.乐观锁悲观锁
乐观锁
乐观锁是一种乐观思想,认为读多写少,遇到并发写的可能性较低,每次读取数据时候认为别人不会修改数据,故不加锁,只有在写数据时候才会加锁,加锁过程为先读出当前版本号,比较跟上一次的版本号,如果一样则更新。
Java中的乐观锁是通过CAS操作实现的,CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。CAS机制中使用了三种基本操作数,内存地址V,旧的预期值A,要修改的新值B。只有当预期值A与内存地址中的实际值相同时候才会执行更新操作。
悲观锁
悲观锁则每次读写数据时候都会上锁,别人想读写这个数据就会阻塞直到拿到锁。synchronized是悲观锁,但是也会有CAS,即轻量级锁时候。
74.可重入锁
可重入锁表示自己可以再次获取自己内部的锁,比如一个线程获得了某个对象的锁,此时这个对象锁还没释放,当其再次想要获得这个对象的锁时候还是可以获取的,在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁。
75.ThreadLocal
ThreadLocal的作用就是让每个线程都能拥有自己的值,当我们创建了一个ThreadLocal变量,那么访问这个变量的线程都会拥有这个变量的副本,它们可以采用get()、set()方法来获取默认值或者将其值修改为当前线程所存的副本的值,多个线程之间的数据是不共享的。
ThreadLocal的原理是用ThreadLocalMap实现的,也就是一个key-value结构,这也就能说明前面为什么是用get、set方法了,ThreadLocal只是ThreadLocalMap的封装,ThreadLocalMap的key是弱引用类型的,而value是强引用类型的,所以如果ThreadLocal没有被外部强引用的话在垃圾回收时候key会被清理掉,而value不会被清理掉,这样ThreadLocalMap中就会出现key为null的Entry,假如不做任何措施,value永远无法被GC回收,这样就造成了内存泄漏,因此使用完ThreadLocal之后最好手动调用remove方法。
温馨提示:上面只是我总结的面经知识,如果你想要更全面的可以到网站自行查看喔。
友情链接:牛客网