Java面试题字节流字符流
String 编码UTF-8 和GBK的区别
GBK编码:是指中国的中文字符,其实它包含了简体中文与繁体中文字符,另外还有一种字符
“gb2312”,这种字符仅能存储简体中文字符。
UTF-8编码:它是一种全国家通过的一种编码,如果你的网站涉及到多个国家的语言,那么建议你
选择UTF-8编码。
GBK和UTF8有什么区别?
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
//do something with socketChannel...
}
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
if(socketChannel != null){
//do something with socketChannel...
}
}UTF8编码格式很强大,支持所有国家的语言,正是因为它的强大,才会导致它占用的空间大小要比GBK
大,对于网站打开速度而言,也是有一定影响的。
GBK编码格式,它的功能少,仅限于中文字符,当然它所占用的空间大小会随着它的功能而减少,打开
网页的速度比较快。
什么时候使用字节流、什么时候使用字符流,二者的区别
先来看一下流的概念:
在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,
而当程序需要将一些数据保存起来的时候,就要使用输出流完成。
InputStream 和OutputStream,两个是为字节流设计的,主要用来处理字节或二进制对象,
Reader和 Writer.两个是为字符流(一个字符占两个字节)设计的,主要用来处理字符或字符串.
字符流处理的单元为2个字节的Unicode字符,操作字符、字符数组或字符串,
字节流处理单元为1个字节,操作字节和字节数组。
所以字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的,
所以它对多国语言支持性比较好!
如果是音频文件、图片、歌曲,就用字节流好点,
如果是关系到中文(文本)的,用字符流好点
所有文件的储存是都是字节(byte)的储存,在磁盘上保留的并不是文件的字符而是先把字符编码成字
节,再储存这些字节到磁盘。在读取文件(特别是文本文件)时,也是一个字节一个字节地读取以形成
字节序列
字节流可用于任何类型的对象,包括二进制对象,而字符流只能处理字符或者字符串;
字节流提供了处理任何类型的IO操作的功能,但它不能直接处理Unicode字符,而字符流就可以
字节流是最基本的,所有的InputStrem和OutputStream的子类都是,主要用在处理二进制数据,它是按
字节来处理的
但实际中很多的数据是文本,
又提出了字符流的概念,
它是按虚拟机的encode来处理,也就是要进行字符集的转化
这两个之间通过 InputStreamReader,OutputStreamWriter来关联,
实际上是通过byte[]和String来关联
在实际开发中出现的汉字问题实际上都是在字符流和字节流之间转化不统一而造成的
Reader类的read()方法返回类型为int :作为整数读取的字符(占两个字节共16位),范围在 0 到
65535 之间 (0x00-0xffff),如果已到达流的末尾,则返回 -1
inputStream的read()虽然也返回int,但由于此类是面向字节流的,一个字节占8个位,所以返回 0 到
255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。因此对于不能用
0-255来表示的值就得用字符流来读取!比如说汉字.
字节流和字符流的主要区别是什么呢?
**一.字节流在操作时不会用到缓冲区(内存),是直接对文件本身进行操作的。而字符流在操作时使用了
缓冲区,通过缓冲区再操作文件。** 上面两点能说明什么呢?
递归读取文件夹下的文件,代码怎么实现
**二.在硬盘上的所有文件都是以字节形式存在的(图片,声音,视频),而字符值在内存中才会形成。**
针对第一点,
我们知道,如果一个程序频繁对一个资源进行IO操作,效率会非常低。此时,通过缓冲区,先把需要操作的数
据暂时放入内存中,以后直接从内存中读取数据,则可以避免多次的IO操作,提高效率
针对第二点,
真正存储和传输数据时都是以字节为单位的,字符只是存在与内存当中的,所以,字节流适用范围更为宽广
/**
* 递归读取文件夹下的 所有文件
*
* @param testFileDir 文件名或目录名
*/
private static void testLoopOutAllFileName(String testFileDir) {
if (testFileDir == null) {
//因为new File(null)会空指针异常,所以要判断下
return;
}
File[] testFile = new File(testFileDir).listFiles();
if (testFile == null) {
return;
}
for (File file : testFile) {
if (file.isFile()) {
System.out.println(file.getName());
} else if (file.isDirectory()) {
System.out.println("-------this is a directory, and its files are as
follows:-------");
testLoopOutAllFileName(file.getPath());
} else {
System.out.println("文件读入有误!");
}
}SynchronousQueue实现原理
前言
SynchronousQueue是一个比较特别的队列,由于在线程池方面有所应用,为了更好的理解线程池的实
现原理,笔者花了些时间学习了一下该队列源码(JDK1.8),此队列源码中充斥着大量的CAS语句,理解起
来是有些难度的,为了方便日后回顾,本篇文章会以简洁的图形化方式展示该队列底层的实现原理。
SynchronousQueue简单使用
经典的生产者-消费者模式,操作流程是这样的:
有多个生产者,可以并发生产产品,把产品置入队列中,如果队列满了,生产者就会阻塞;
有多个消费者,并发从队列中获取产品,如果队列空了,消费者就会阻塞;
如下面的示意图所示:
SynchronousQueue
也是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put的时
候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线
程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传
递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)。
我们用一个简单的代码来验证一下,如下所示:
}
package com.concurrent;import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueDemo {
public static void main(String[] args) throws InterruptedException {
final SynchronousQueue<Integer> queue = new SynchronousQueue<Integer>();
Thread putThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("put thread start");
try {
queue.put(1);
} catch (InterruptedException e) {
}
System.out.println("put thread end");
}
});
Thread takeThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("take thread start");
try {
System.out.println("take from putThread: " + queue.take());
} catch (InterruptedException e) {
}
System.out.println("take thread end");
}
});
putThread.start();
Thread.sleep(1000);
takeThread.start();
}
}
一种输出结果如下:
put thread start
take thread start
take from putThread: 1
put thread end
take thread end
从结果可以看出,put线程执行queue.put(1) 后就被阻塞了,只有take线程进行了消费,put线程才可以
返回。可以认为这是一种线程与线程间一对一传递消息的模型。SynchronousQueue实现原理
不像ArrayBlockingQueue、LinkedBlockingDeque之类的阻塞队列依赖AQS实现并发操作,
SynchronousQueue直接使用CAS实现线程的安全访问。由于源码中充斥着大量的CAS代码,不易于理
解,所以按照笔者的风格,接下来会使用简单的示例来描述背后的实现模型。
队列的实现策略通常分为公平模式和非公平模式,接下来将分别进行说明。
公平模式下的模型:
公平模式下,底层实现使用的是TransferQueue这个内部队列,它有一个head和tail指针,用于指向当
前正在等待匹配的线程节点。
初始化时,TransferQueue的状态如下:
接着我们进行一些操作:
1、线程put1执行 put(1)操作,由于当前没有配对的消费线程,所以put1线程入队列,自旋一小会后睡
眠等待,这时队列状态如下:
2、接着,线程put2执行了put(2)操作,跟前面一样,put2线程入队列,自旋一小会后睡眠等待,这时队
列状态如下:3、这时候,来了一个线程take1,执行了
take操作,由于tail指向put2线程,put2线程跟take1线程配对了(一put一take),这时take1线程不需要
入队,但是请注意了,这时候,要唤醒的线程并不是put2,而是put1。为何?
大家应该知道我们现在讲的是公平策略,所谓公平就是谁先入队了,谁就优先被唤醒,我们的例子明显
是put1应该优先被唤醒。至于读者可能会有一个疑问,明明是take1线程跟put2线程匹配上了,结果是
put1线程被唤醒消费,怎么确保take1线程一定可以和次首节点(head.next)也是匹配的呢?其实大家可
以拿个纸画一画,就会发现真的就是这样的。
公平策略总结下来就是:队尾匹配队头出队。
执行后put1线程被唤醒,take1线程的 take()方法返回了1(put1线程的数据),这样就实现了线程间的一
对一通信,这时候内部状态如下:
4、最后,再来一个线程take2,执行take操作,这时候只有put2线程在等候,而且两个线程匹配上了,
线程put2被唤醒,
take2线程take操作返回了2(线程put2的数据),这时候队列又回到了起点,如下所示:以上便是公平模式下,SynchronousQueue的实现模型。总结下来就是:队尾匹配队头出队,先进先
出,体现公平原则。
非公平模式下的模型:
我们还是使用跟公平模式下一样的操作流程,对比两种策略下有何不同。非公平模式底层的实现使用的
是TransferStack,
一个栈,实现中用head指针指向栈顶,接着我们看看它的实现模型:
1、线程put1执行 put(1)操作,由于当前没有配对的消费线程,所以put1线程入栈,自旋一小会后睡眠
等待,这时栈状态如下:
2、接着,线程put2再次执行了put(2)操作,跟前面一样,put2线程入栈,自旋一小会后睡眠等待,这时
栈状态如下:3、这时候,来了一个线程take1,执行了take操作,这时候发现栈顶为put2线程,匹配成功,但是实现
会先把take1线程入栈,然后take1线程循环执行匹配put2线程逻辑,一旦发现没有并发冲突,就会把栈
顶指针直接指向 put1线程
4、最后,再来一个线程take2,执行take操作,这跟步骤3的逻辑基本是一致的,take2线程入栈,然后
在循环中匹配put1线程,最终全部匹配完毕,栈变为空,恢复初始状态,如下图所示:
可以从上面流程看出,虽然put1线程先入栈了,但是却是后匹配,这就是非公平的由来。
总结
SynchronousQueue由于其独有的线程一一配对通信机制,在大部分平常开发中,可能都不太会用到,
但线程池技术中会有所使用,由于内部没有使用AQS,而是直接使用CAS,所以代码理解起来会比较困
难,但这并不妨碍我们理解底层的实现模型,在理解了模型的基础上,有兴趣的话再查阅源码,就会有
方向感,看起来也会比较容易,希望本文有所借鉴意义。