青春有你-Java基础篇(2.3)

2、Java基础知识

→ IO

字符流、字节流、输入流、输出流

输入流:数据从文件流向内存。 输出流:数据从内存流向文件 。输入流和输入流是相对程序而言的 。

输入流输出流说明
字节流streamInputStreamOutputStream处理二进制数据
字符流reader/writerReaderWriterunicode码,处理文本数据

字节输入流:

  • InputStream 字节输入流的抽象基类 read(byte[] b) read(byte[] b, int off, int len) close()
  • FileInputStream 主要用来操作文件输入流 read()
  • BufferedInputStream 【BufferedInputStream不是InputStream的直接实现子类,是FilterInputStream的子类】 可以提高效率 read()

字节输出流:

  • OutputStream 字节输出流的基类 write(byte[] b) write(byte[] b,int off,int len) flush() close()
  • FileOutputStream 写文件的输出流 write(int b)
  • BufferedOutputStream 【BufferedOutputStream不是OutputStream的直接实现子类,是FilterOutputStream的子类】 可以提高效率 write(int b)

字符输入流:

  • Reader 字符输入流的抽象基类 read() read(char[] cbuf) close()
  • InputStreamReader 把InputStream中的 字节流→字符流 read(char[] cbuf, int offset, int length)
  • FileReader 与InputStreamReader 功能类似 read(char[] cbuf, int offset, int length)
  • BufferedReader 可以把字符输入流进行封装,将数据进行缓冲,提高读取效率。 readLine()

字符输出流:

  • Writer 字符输出流的抽象基类 write(int c) write(String str) write(char[] cbuf) write(String str, int off, int len) close() flush()
  • OutputStreamWriter 把字符流→字节流
  • FileWriter 与OutputStreamWriter功能类似
  • BufferedWriter 利用了缓冲区来提高写的效率 newLine()

【参考链接】https://www.cnblogs.com/progor/p/9357676.html

同步、异步、阻塞、非阻塞、Linux 5 种 IO 模型
  1. 阻塞I/O(blocking I/O)
  2. 非阻塞I/O (nonblocking I/O) 区别:应用程序的调用是否立即返回
  3. I/O复用(select 和poll) (I/O multiplexing)
  4. 信号驱动I/O (signal driven I/O (SIGIO))
  5. 异步I/O (asynchronous I/O (the POSIX aio_functions)) 区别: 数据拷贝的时候进程是否阻塞

阻塞IOrecv函数默认是阻塞的,等待数据准备好(没有得到结果之前,不会返回,当前线程被挂起),数据没有准备好时进入等待,准备好了就进行数据拷贝 。

非阻塞IOrecv函数为非阻塞, 即使没有数据到来,recv函数也不会阻塞 ,并且立即返回-1。数据到来之前会反复调用函数并判断返回值,这种循环测试称为忙等待。但是在数据拷贝时线程是阻塞的 。

IO复用 :IO复用多了一个select函数,select函数有一个参数是文件描述符集合,select可以循环监听多个文件描述符是否有数据,从而提高性能。IO复用也是属于阻塞IO。

信号驱动I/O: 在用户进程安装SIGIO信号处理函数,即recv函数。用户进程可以执行其他操作不会被阻塞。当数据准备好时,用户进程会收到SIGIO信号,跳转到信号处理函数中调用recv函数,接收数据。当数据从内核空间拷贝到用户态空间后,recv函数返回。这种方式使异步处理成为可能,信号是异步处理的基础。

异步IO:通过aio_read函数实现,aio_read提交请求,并递交一个用户态空间下的缓冲区。即使内核中没有数据到来,aio_read函数也立刻返回,应用程序就可以处理其他的事情。等待有结果时通过状态、通知和回调来通知调用者。 属于非阻塞状态

阻塞程度:阻塞IO>非阻塞IO>IO复用>信号驱动IO>异步IO,效率是由低到高的。

异步IO和信号驱动IO的不同?

在于信号通知用户态程序时数据所处的位置。异步IO已经把数据从内核空间拷贝到用户空间了;而信号驱动IO的数据还在内核空间,等着recv函数把数据拷贝到用户态空间。 异步IO是一种推数据的机制,相比于信号处理IO拉数据的机制效率更高。 推数据是直接完成的,而拉数据是需要调用recv函数,调用函数会产生额外的开销,故效率低。

【参考链接】https://www.jianshu.com/p/e4768446f7eb

BIO、NIO 和 AIO 的区别、三种 IO 的用法与原理、netty

Netty是一个提供异步事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络服务器和客户端程序。换句话说,Netty是一个NIO框架,使用它可以简单快速地开发网络应用程序,比如客户端和服务端的协议。这是官方文档中的描述 。

区别:
BIO:同步并阻塞, 服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理(一客户端一线程)。 该模型缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程数与客户端并发访问数呈1:1的关系,随着并发访问量的继续增加,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致宕机或僵死。

NIO:同步非阻塞 。 服务器实现模式为一个请求一个线程,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

对于NIO,有两点需要强调的:
(1)关于概念有两种理解,New I/O(相对于之前的I/O库是新增的)和Non-block I/O(非阻塞的)。由于NIO的目标就是让java支持非阻塞I/O,所以更多人喜欢用Non-block I/O。
(2)很多人喜欢将NIO称为异步非阻塞I/O,但是,如果按照严格的NUIX网络编程模型和JDK的实现进行区分,实际上它只是非阻塞I/O,不能称之为异步非阻塞I/O。但由于NIO库支持非阻塞读和写,相对于之前的同步阻塞读和写,它是异步的,因此很多人习惯称NIO为异步非阻塞I/O。

AIO(NIO2.0):异步非阻塞。 JDK1.7升级了NIO库,升级后的NIO库被称为NIO2.0,此即AIO。其服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

用法:

BIO适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

NIO适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

AIO适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

BIO是一个连接一个线程。

NIO是一个请求一个线程。

AIO是一个有效请求一个线程。
原理:

Java NIO(New IO)是从JDK1.4新增的IO操作API,是面向通道和缓冲区进行操作。在NIO库中,数据从通道读取到缓冲区中,或者从缓冲区写入到通道中。通过设置通道为非阻塞机制,可以同时进行读写双向操作而不发生阻塞。

NIO中另外一个重要概念是多路复用器(Selector)。Selector通过轮询检查每个注册在其上的NIO通道,如果某个通道上面发生读(SelectionKey.OP_READ)或者写(SelectionKey.OP_WRITE)操作,则这些通道会被Selector检测出来,并且存放在一个Set集合中,进行后续操作。因为JDK底层采用epoll()代替传统select实现,当一个连接创建后,这个连接会被注册到多路复用器上面,所有的连接对应一个线程就可以搞定。因此只需要一个线程负责Selector的轮询,可以管理多个channel,从而管理多个网络连接。 优点是在高并发场景下,可以有效减少服务端起的线程数量和CPU时间片的浪费,降低了内存的消耗。

AIO异步非阻塞是从JDK7新增的IO操作API,使用回调的方式,真正实现了高效异步IO。当进行读写操作时,只须直接调用API的read或write方法即可,不需要再使用Selector。由于这两种方法均为异步的,所以均由底层操作系统进行处理,完成后主动通知应用程序。

【参考链接】https://blog.youkuaiyun.com/qq_36907589/article/details/80689091
借图-NIO知识点

→ 枚举

枚举的用法、枚举的实现、枚举与单例、Enum 类

用法:定义常量、switch判断、enum实例必须在前、只能实现不能再继承、新增方法和覆盖方法、枚举集合 。 https://blog.youkuaiyun.com/testcs_dn/article/details/78604547

实现:枚举本质上是通过普通的类来实现的,只是编译器为我们进行了处理。

每个枚举类型都继承自 java.lang.Enum,并自动添加了 values 和 valueOf 方法。

而每个枚举常量是一个静态常量字段,使用内部类实现,该内部类继承了枚举类。

所有枚举常量都通过静态代码块来进行初始化,即在类加载期间就初始化。

另外通过把 clone、readObject、writeObject 这三个方法定义为 final 的,同时实现是抛出相应的异常。

这样保证了每个枚举类型及枚举常量都是不可变的。可以利用枚举的这两个特性来实现线程安全的单例。

枚举与单例

/**
 * 懒汉式单例模式(适合多线程安全)
 */
public class Singleton {
    private static volatile Singleton singleton = null;
    
    private Singleton(){}
 
    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }    
}

这种编写方式被称为“双重检查锁”,主要在getSingleton()方法中,进行两次null检查。这样可以极大提升并发度,进而提升性能。毕竟在单例中new的情况非常少,绝大多数都是可以并行的读操作,因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,也就提高了执行效率。但是必须注意的是volatile关键字,该关键字有两层语义。第一层语义是可见性,可见性是指在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以其它线程会马上读取到已修改的值,关于工作内存和主内存可简单理解为高速缓存(直接与CPU打交道)和主存(日常所说的内存条),注意工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化,我们写的代码(特别是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同,这在单线程并没什么问题,然而一旦引入多线程环境,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题,值得关注的是volatile的禁止指令重排序优化功能在Java 1.5后才得以实现,因此1.5前的版本仍然是不安全的,即使使用了volatile关键字。或许我们可以利用静态内部类来实现更安全的机制,静态内部类单例模式如下:

/**
 * 静态内部类
 */
public class SingletonInner {
    private static class Holder {
        private static SingletonInner singleton = new SingletonInner();
    }

    private SingletonInner(){}

    public static SingletonInner getSingleton(){
        return Holder.singleton;
    }
}

静态内部类实现单例模式,这种方式优于上面两种方式,他即实现了线程安全,又省去了null的判断,性能优于上面两种。

但是以上两种写法是不考虑放射机制和序列化机制的情况下实现的单例模式,举个例子就能知道上面的单例不是很安全,以双重检索的单例模式为例子,我利用放射,能够创建出新的实例:

public static void main(String[] args) throws Exception {
        Singleton s=Singleton.getInstance();
        Singleton sual=Singleton.getInstance();
        Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton s2=constructor.newInstance();
        System.out.println(s+"\n"+sual+"\n"+s2);
        System.out.println("正常情况下,实例化两个实例是否相同:"+(s==sual));
        System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(s==s2));
    }

结果为:

cn.singleton.Singleton@1641e19d
cn.singleton.Singleton@1641e19d
cn.singleton.Singleton@677323b6
正常情况下,实例化两个实例是否相同:true
通过反射攻击单例模式情况下,实例化两个实例是否相同:false

枚举单例(Enum Singleton)是实现单例模式的一种新方式

public enum  EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

检测一下枚举的单例模式,结果会报Exception in thread “main” java.lang.NoSuchMethodException。出现这个异常的原因是因为EnumSingleton.class.getDeclaredConstructors()获取所有构造器,会发现并没有我们所设置的无参构造器,只有一个参数为(String.class,int.class)构造器,而且在反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。所以枚举是不怕发射攻击的。

显然枚举单例模式确实是很不错的选择,因此我们推荐使用它。但是这总不是万能的,对于android平台这个可能未必是最好的选择,在android开发中,内存优化是个大块头,而使用枚举时占用的内存常常是静态变量的两倍还多,因此android官方在内存优化方面给出的建议是尽量避免在android中使用enum。但是不管如何,关于单例,我们总是应该记住:线程安全,延迟加载,序列化与反序列化安全,反射安全是很重要的。

Enum 类: 因为在没有枚举类的时候,我们要定义一个有限的序列,比如星期几,男人女人,春夏秋冬,一般会通过上面那种静态变量的形式,但是使用那样的形式如果需要一些其他的功能,需要写很多奇奇怪怪的代码。所以,枚举类的出现,就是为了简化这种操作。

枚举的特点

【参考链接】https://www.cnblogs.com/saoyou/p/11087462.html 清晰易懂
https://blog.youkuaiyun.com/javazejian/article/details/71333103 比较复杂

使用== 和使用equals方法的执行结果是一样的。 因为在Enum类里面已经重写了equals方法,而方法里面比较就是直接使用 ==来比较两个对象的。所以在外边直接使用也是可以的。

switch 对枚举的支持

switch判断的是枚举类的ordinal方法,即枚举值的序列值。

枚举的序列化如何实现

枚举自己处理序列化

单例模式有一个比较大的问题,就是一旦实现了Serializable接口就不再是单例的了,因为每次调用 readObject()方法返回的都是新创建的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是为了保证枚举类型像Java规范中所说的那样,每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。

在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中。反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。所以enum类型在添加新的enum对象时,如果没有用到新的对象,那么enum的序列化和反序列化是不会有异常的。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、writeReplace和readResolve等方法。 所以,JVM对序列化有保证。
【参考链接】 https://www.cnblogs.com/z00377750/p/9177097.html

枚举的线程安全性问题

我们在深度分析Java的ClassLoader机制(源码级别)Java类的加载、链接和初始化两个文章中介绍过,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是thread-safe(线程安全的) 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值