java面试(十一)
1、你对HTTP的了解有哪些?
HTTP:超文本传输协议。使用的是可靠的数据传输协议,在传输的过程中不会被损坏或产生混乱。
常见的HTTP方法/请求方式有:GET,POST,PUT,DELETE,HEAD等。
- GET:从服务器向客户端发送命名资源
- PUT:将来自客户端的数据存储到一个命名的服务器资源中去
- DELETE从服务器中删除命名资源
- POST:将客户端数据发送到一个服务器网关应用程序
- HEAD:仅发送命名资源响应中的HTTP首部
2、IO复用了解吗?怎么实现线程安全?IO阻塞?
I/O复用就是单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流.select, poll, epoll 都是I/O多路复用的具体的实现,
select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
· select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍。
· select 只能监视1024个链接,linux 定义在头文件中的,参见FD_SETSIZE。
· select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现,尼玛,这个sock不用,要收回。
对不起,这个select 不支持的,如果你丧心病狂的竟然关掉这个sock, select的标准行为是。。呃。。不可预测的, 这个可是写在文档中的哦.
“If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
poll
- poll 去掉了1024个链接的限制,于是要多少链接呢, 你开心就好。
- · poll 从设计上来说,不再修改传入数组,不过这个要看你的平台了,
但是poll仍然不是线程安全的, 这就意味着,不管服务器有多强悍,你也只能在一个线程里面处理一组I/O流。你当然可以那多进程来配合了,不过然后你就有了多进程的各种问题。
epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:
· epoll 现在是线程安全的。
· epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。
epoll 当年的patch,现在还在,下面链接可以看得到:
/dev/epoll Home Page
可是epoll 有个致命的缺点。。只有linux支持。比如BSD上面对应的实现是kqueue。
而ngnix 的设计原则里面, 它会使用目标平台上面最高效的I/O多路复用模型咯,所以才会有这个设置。一般情况下,如果可能的话,尽量都用epoll/kqueue吧
实现线程安全
在java中,提供了两种方式,synchronized和Lock。
synchronized:
在java中,每一个对象都拥有一个锁标记,monitor,称为监视器,当多个线程同时访问对象时,线程只有获得了对象的锁才能访问。
在java中,synchronized可以用来修饰方法和代码块。当某个线程调用对象的synchronized方法和访问synchronized方法时,必须要先获得对象的锁才可以继续访问,当该线程获得锁时,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。
在java 5中,java.util.concurrent.locks包下提供了另外一种方式来实现线程同步,就是Lock。
synchronized和lock区别:
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
在JDK1.4中引入了一个NIO的类库,使得Java涉及IO的操作拥有阻塞式和非阻塞式两种
在阻塞模式下,若从网络流中读取不到指定大小的数据量,阻塞IO就在那里阻塞着。比如,已知后面会有10个字节的数据发过来,但是我现在只收到8个字节,那么当前线程就在那傻傻地等到下一个字节的到来,对,就在那等着,啥事也不做,直到把这10个字节读取完,这才将阻塞放开通行。
在非阻塞模式下,若从网络流中读取不到指定大小的数据量,非阻塞IO就立即通行。比如,已知后面会有10个字节的数据发过来,但是我现在只收到8个字节,那么当前线程就读取这8个字节的数据,读完后就立即返回,等另外两个字节再来的时候再去读取。
从上面可以看出,阻塞IO在性能方面是很低下的,如果要使用阻塞IO完成一个Web服务器的话,那么对于每一个请求都必须启用一个线程进行处理。而使用非阻塞IO的话,一到两个线程基本上就够了,因为线程不会产生阻塞,好比一下接收A请求的数据,另一下接收B请求的数据,等等,就是不停地东奔西跑,直接到把数据接收完了。
虽然说,非阻塞IO比阻塞IO有更高的性能,但是对于开发来的,难度就成数倍递增了。由于是有多少数据就读取多少数据,这样在读取完整之前需要将已经读取到的数据保存起来,而且需要与其他地方来的数据隔离开来不能混在一起,否则就不知道这数据是谁的了
3、NIO底层实现了解吗?
Java NIO是在jdk1.4开始使用的,它既可以说成“新I/O”,也可以说成非阻塞式I/O。下面是java NIO的工作原理:
- 由一个专门的线程来处理所有的 IO 事件,并负责分发。
- 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
- 线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。
(注:每个线程的处理流程大概都是读取数据、解码、计算处理、编码、发送响应。)
Java NIO的服务端只需启动一个专门的线程来处理所有的 IO 事件,这种通信模型是怎么实现的呢?
java NIO采用了双向通道(channel)进行数据传输,而不是单向的流(stream),在通道上可以注册我们感兴趣的事件。一共有以下四种事件:
服务端和客户端各自维护一个管理通道的对象,我们称之为selector,该对象能检测一个或多个通道 (channel) 上的事件。我们以服务端为例,如果服务端的selector上注册了读事件,某时刻客户端给服务端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在selector中添加一个读事件。服务端的处理线程会轮询地访问selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。下面是我理解的java NIO的通信模型示意图:
二.java NIO服务端和客户端代码实现
为了更好地理解java NIO,下面贴出服务端和客户端的简单代码实现。
服务端:
Java代码
- package cn.nio;
- import java.io.IOException;
- import java.net.InetSocketAddress;
- import java.nio.ByteBuffer;
- import java.nio.channels.SelectionKey;
- import java.nio.channels.Selector;
- import java.nio.channels.ServerSocketChannel;
- import java.nio.channels.SocketChannel;
- import java.util.Iterator;
- /**
- * NIO服务端
- * @author 小路
- */
- public class NIOServer {
- //通道管理器
- private Selector selector;
- /**
- * 获得一个ServerSocket通道,并对该通道做一些初始化的工作
- * @param port 绑定的端口号
- * @throws IOException
- */
- public void initServer(int port) throws IOException {
- // 获得一个ServerSocket通道
- ServerSocketChannel serverChannel = ServerSocketChannel.open();
- // 设置通道为非阻塞
- serverChannel.configureBlocking(false);
- // 将该通道对应的ServerSocket绑定到port端口
- serverChannel.socket().bind(new InetSocketAddress(port));
- // 获得一个通道管理器
- this.selector = Selector.open();
- //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
- //当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
- serverChannel.register(selector, SelectionKey.OP_ACCEPT);
- }
- /**
- * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
- * @throws IOException
- */
- @SuppressWarnings("unchecked")
- public void listen() throws IOException {
- System.out.println("服务端启动成功!");
- // 轮询访问selector
- while (true) {
- //当注册的事件到达时,方法返回;否则,该方***一直阻塞
- selector.select();
- // 获得selector中选中的项的迭代器,选中的项为注册的事件
- Iterator ite = this.selector.selectedKeys().iterator();
- while (ite.hasNext()) {
- SelectionKey key = (SelectionKey) ite.next();
- // 删除已选的key,以防重复处理
- ite.remove();
- // 客户端请求连接事件
- if (key.isAcceptable()) {
- ServerSocketChannel server = (ServerSocketChannel) key
- .channel();
- // 获得和客户端连接的通道
- SocketChannel channel = server.accept();
- // 设置成非阻塞
- channel.configureBlocking(false);
- //在这里可以给客户端发送信息哦
- channel.write(ByteBuffer.wrap(new String("向客户端发送了一条信息").getBytes()));
- //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
- channel.register(this.selector, SelectionKey.OP_READ);
- // 获得了可读的事件
- } else if (key.isReadable()) {
- read(key);
- }
- }
- }
- }
- /**
- * 处理读取客户端发来的信息 的事件
- * @param key
- * @throws IOException
- */
- public void read(SelectionKey key) throws IOException{
- // 服务器可读取消息:得到事件发生的Socket通道
- SocketChannel channel = (SocketChannel) key.channel();
- // 创建读取的缓冲区
- ByteBuffer buffer = ByteBuffer.allocate(10);
- channel.read(buffer);
- byte[] data = buffer.array();
- String msg = new String(data).trim();
- System.out.println("服务端收到信息:"+msg);
- ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
- channel.write(outBuffer);// 将消息回送给客户端
- }
- /**
- * 启动服务端测试
- * @throws IOException
- */
- public static void main(String[] args) throws IOException {
- NIOServer server = new NIOServer();
- server.initServer(8000);
- server.listen();
- }
- }
客户端:
Java代码
- package cn.nio;
- import java.io.IOException;
- import java.net.InetSocketAddress;
- import java.nio.ByteBuffer;
- import java.nio.channels.SelectionKey;
- import java.nio.channels.Selector;
- import java.nio.channels.SocketChannel;
- import java.util.Iterator;
- /**
- * NIO客户端
- * @author 小路
- */
- public class NIOClient {
- //通道管理器
- private Selector selector;
- /**
- * 获得一个Socket通道,并对该通道做一些初始化的工作
- * @param ip 连接的服务器的ip
- * @param port 连接的服务器的端口号
- * @throws IOException
- */
- public void initClient(String ip,int port) throws IOException {
- // 获得一个Socket通道
- SocketChannel channel = SocketChannel.open();
- // 设置通道为非阻塞
- channel.configureBlocking(false);
- // 获得一个通道管理器
- this.selector = Selector.open();
- // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
- //用channel.finishConnect();才能完成连接
- channel.connect(new InetSocketAddress(ip,port));
- //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
- channel.register(selector, SelectionKey.OP_CONNECT);
- }
- /**
- * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
- * @throws IOException
- */
- @SuppressWarnings("unchecked")
- public void listen() throws IOException {
- // 轮询访问selector
- while (true) {
- selector.select();
- // 获得selector中选中的项的迭代器
- Iterator ite = this.selector.selectedKeys().iterator();
- while (ite.hasNext()) {
- SelectionKey key = (SelectionKey) ite.next();
- // 删除已选的key,以防重复处理
- ite.remove();
- // 连接事件发生
- if (key.isConnectable()) {
- SocketChannel channel = (SocketChannel) key
- .channel();
- // 如果正在连接,则完成连接
- if(channel.isConnectionPending()){
- channel.finishConnect();
- }
- // 设置成非阻塞
- channel.configureBlocking(false);
- //在这里可以给服务端发送信息哦
- channel.write(ByteBuffer.wrap(new String("向服务端发送了一条信息").getBytes()));
- //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
- channel.register(this.selector, SelectionKey.OP_READ);
- // 获得了可读的事件
- } else if (key.isReadable()) {
- read(key);
- }
- }
- }
- }
- /**
- * 处理读取服务端发来的信息 的事件
- * @param key
- * @throws IOException
- */
- public void read(SelectionKey key) throws IOException{
- //和服务端的read方法一样
- }
- /**
- * 启动客户端测试
- * @throws IOException
- */
- public static void main(String[] args) throws IOException {
- NIOClient client = new NIOClient();
- client.initClient("localhost",8000);
- client.listen();
- }
- }
4、说一下公平锁和非公平锁你怎么理解的
并发包中ReentrantLock的创建可以指定构造函数的布尔类型来得到公平锁和非公平锁,默认是非公平锁。
ReentrantLock lock = new ReentrantLock(true);
两者的区别
公平锁:就是很公平,在并发环境中,每个线程在获取锁时会查看此锁维护的等待队列,如果为空,并且当前线程是等待队列的第一个,就占有锁。否则就会加入到等待队列中,以后会按照FIFO规则在队列中取到自己。
非公平锁:上来就直接占有锁,如果尝试失败,就再采用类似公平锁的那种方式。非公平锁的优点在于吞吐量比公平锁大。
Synchronized也是一种非公平锁。
5、Spring AOP的理解,哪里用到过?除了日志你还有什么地方用到过?
AOP适合于那些具有横切逻辑的应用:如性能监测,访问控制,事务管理、缓存、对象池管理以及日志记录。
AOP使用场景
AOP用来封装横切关注点,具体可以在下面的场景中使用
Authentication 权限
Caching 缓存
Context passing 内容传递
Error handling 错误处理
Lazy loading 懒加载
Debugging 调试
logging, tracing, profiling and monitoring 记录跟踪 优化 校准
Performance optimization 性能优化
Persistence 持久化
Resource pooling 资源池
Synchronization 同步
Transactions 事务
6、说一下你对volatile的理解?
在多线程并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized。
特性
通俗的来讲就是,两个线程,线程A和线程B 共享同一变量,volatile保证了当线程A对共享变量的值进行修改后,线程B也会得到最新的修改结果。
代码验证:
定义的变量没有被volatile修饰是没有可见性的
package volatiledemo;
public class VolatileVisibleDemo {
private int num = 0;
public void addNum() {
num = num + 60;
}
public static void main(String[] args) {
VolatileVisibleDemo volatileVisibleDemo = new VolatileVisibleDemo();
// t1线程对num就行更改操作
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
// 模拟num更改操作耗时3m,并保证其他线程读取了num变量
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
volatileVisibleDemo.addNum();
System.out.println(Thread.currentThread().getName() + "\t" + "num值已经被更改为:" + volatileVisibleDemo.num);
}, "t1").start();
while (volatileVisibleDemo.num == 0) {
// main线程一直等待,直到num不等于0
}
System.out.println(Thread.currentThread().getName() + "\t mission is over");
}
}
结果:
t1 come in
t1 num值已经被更改为:60
在上面的结果中并没有看到输出“mission is over” ,说明while条件一直满足条件,也就是说主线程main并不知道 t1 已经把值修改了。导致了数据不一致。
再来看看给变量num加上volatile关键字后的效果:volatile private int num = 0;
t1 come in
t1 num值已经被更改为:60
main mission is over
这个时候主线程已经获得了最新的值。
volatile是如何来保证可见性的呢?
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令。
将这个变量所在缓存行的数据写回到系统内存。
这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
volatile为什么不保证原子性?
number++被拆分成3个指令:
getfield 从主内存中拿到原始值
iadd 在线程工作内存中进行加1操作
putfield 把累加后的值写回主内存
如果第二个线程在第一个线程读取旧值和写回新值期间读取number的值,
那么第二个线程就会与第一个线程看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败。
如何解决原子性问题
CAS机制:AtomicInteger number = AtomicInteger(0)
锁机制:synchronized、Lock
-
禁止指令重排序
volatile通过内存屏障来保证指令的有序性。
volatile的写-读与锁的释放-获取有相同的内存效果。
volatile写-读的内存语义:
当写一个volatile变量时,JMM会把线程A对应的本地内存中的共享变量值刷新到主内存。
当读一个volatile变量时,JMM会把线程B对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
线程A写一个volatile变量,随后线程B读这个volatile变量,实质上是线程A通过主内存向线程B发送消息。
求职面试题&解析