目录
集合框架
Java迭代器(Iterator)
Java迭代器(Iterator)是 Java 集合框架中的一种机制,是一种用于遍历集合(如列表、集合和映射等)的接口。
它提供了一种统一的方式来访问集合中的元素,而不需要了解底层集合的具体实现细节。
Java Iterator(迭代器)不是一个集合,它是一种用于访问集合的方法,可用于迭代 ArrayList 和HashSet 等集合。
Iterator 是 Java 迭代器最简单的实现,ListIterator 是 Collection API 中的接口, 它扩展了 Iterator 接口。
迭代器接口定义了几个方法,最常用的是以下三个:
next() - 返回迭代器的下一个元素,并将迭代器的指针移到下一个位置。
hasNext() - 用于判断集合中是否还有下一个元素可以访问。
remove() - 从集合中删除迭代器最后访问的元素(可选操作)。
Iterator 类位于 java.util 包中,使用前需要引入它,语法格式如下:
import java.util.Iterator; // 引入 Iterator 类
通过使用迭代器,我们可以逐个访问集合中的元素,而不需要使用传统的 for 循环或索引。这种方式更加简洁和灵活,并且适用于各种类型的集合。
Java集合类
Java集合类主要由两个根接口Collection和Map派生出来的。
Collection派生
1)List
List代表了有序可重复集合,可直接根据元素的索引来访问
2)Set
Set代表无序不可重复集合,只能根据元素本身来访问
3)Queue
Queue是队列集合
Map接口派生:
Map代表的是存储key-value对的集合,可根据元素的key来访问value。
因此Java集合大致也可分为List、Set、Queue、Map四种接口体系。
Java集合List
List代表了有序可重复集合,可直接根据元素的索引来访问。
List接口常用的实现类有:ArrayList、LinkedList、Vector。
List集合特点 集合中的元素允许重复 集合中的元素是有顺序的,各元素插入的顺序就是各元素的顺序 集合中的元素可以通过索引来访问或者设置
ArrayList
ArrayList是一个动态数组,也是我们最常用的集合,是List类的典型实现。
它允许任何符合规则的元素插入甚至包括null,每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小。
随着容器中的元素不断增加,容器的大小也会随着增加,在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。
所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。
ArrayList擅长于随机访问,同时ArrayList是非同步的。
Vector
与ArrayList相似,但是Vector是同步的,它的操作与ArrayList几乎一样。
LinkedList
LinkedList是采用双向循环链表实现,LinkedList是List接口的另一个实现,除了可以根据索引访问集合元素外,LinkedList还实现了Deque接口,可以当作双端队列来使用,也就是说,既可以当作“栈”使用,又可以当作队列使用。
Java List总结
1)ArrayList 优点: 底层数据结构是数组,查询快,增删慢。 缺点: 线程不安全,效率高
2)Vector 优点: 底层数据结构是数组,查询快,增删慢。 缺点: 线程安全,效率低
3)LinkedList 优点: 底层数据结构是链表,查询慢,增删快。 缺点: 线程不安全,效率高
Java集合Set
Set扩展Collection接口,无序集合,不允许存放重复的元素。
Set接口常用的实现类有:HashSet、LinkedHashSet、TreeSet
HashSet
HashSet是Set集合最常用实现类,是其经典实现。
HashSet底层数据结构采用哈希表实现,元素无序且唯一,线程不安全,效率高,可以存储null元素,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
LinkedHashSet
底层数据结构采用链表和哈希表共同实现,链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性。
TreeSet
底层数据结构采用二叉树来实现,元素唯一且已经排好序,唯一性同样需要重写hashCode和equals()方法,二叉树结构保证了元素的有序性。
Java Set总结
1)HashSet
底层其实是包装了一个HashMap实现的 底层数据结构是数组+链表 + 红黑树 具有比较好的读取和查找性能, 可以有null 值 通过equals和HashCode来判断两个元素是否相等 非线程安全
2)LinkedHashSet
继承HashSet,本质是LinkedHashMap实现 底层数据结构由哈希表(是一个元素为链表的数组)和双向链表组成。 有序的,根据HashCode的值来决定元素的存储位置,同时使用一个链表来维护元素的插入顺序 非线程安全,可以有null 值
3)TreeSet
是一种排序的Set集合,实现了SortedSet接口,底层是用TreeMap实现的,本质上是一个红黑树原理 排序分两种:自然排序(存储元素实现Comparable接口)和定制排序(创建TreeSet时,传递一个自己实现的Comparator对象) 正常情况下不能有null值,可以重写Comparable接口 局可以有null值了。
Java集合Queue(队列)
队列是数据结构中比较重要的一种类型,它支持 FIFO(First Input First Output简单说就是指先进先出。),尾部添加、头部删除(先进队列的元素先出队列),跟我们生活中的排队类似。
PriorityQueue(优先级队列)
PriorityQueue保存队列元素的顺序并不是按照加入的顺序,而是按照队列元素的大小进行排序的。
PriorityQueue不允许插入null元素。
Deque
Deque接口是Queue接口的子接口,它代表一个双端队列,当程序中需要使用“栈”这种数据结构时,推荐使用ArrayDeque。
Java集合Map
Map用于保存具有映射关系的数据,Map里保存着两组数据:key和value,它们都可以使任何引用类型的数据,但key不能重复。
1.HashMap
Map接口基于哈希表的实现,是使用频率最高的用于键值对处理的数据类型。
它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,特点是访问速度快,遍历顺序不确定,线程不安全,最多允许一个key为null,允许多个value为null。
可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap类。
2.Hashtable
Hashtable和HashMap从存储结构和实现来讲有很多相似之处,不同的是它承自Dictionary类,而且是线程安全的,另外Hashtable不允许key和value为null,并发性不如ConcurrentHashMap。
Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
3.LinkedHashMap
LinkedHashMap继承了HashMap,是Map接口的哈希表和链接列表实现,它维护着一个双重链接列表,此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
4.TreeMap
TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序(自然顺序),也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。
Map总结
实现类 | 数据结构 | 是否线程安全 | key是否可为null | 是否有序 |
hashmap | 数组+链表+红黑树 | 否 | 是 | 否 |
hashtable | 数组+链表 | 是 | 否 | 否 |
linkedhashmap | 数组+链表+红黑树+双重链接列表 | 否 | 是 | 是 |
treemap | 红黑树 | 否 | 否 | 是 |
多线程
进程
进程就是正在运行中的程序(进程是驻留在内存中的)
- 是系统执行资源分配和调度的独立单位
- 每一进程都有属于自己的存储空间和系统资源
- 注意:进程A和进程B的内存独立不共享。
线程
线程就是进程中的单个顺序控制流,也可以理解成是一条执行路径
-
单线程:一个进程中包含一个顺序控制流(一条执行路径)
-
多线程:一个进程中包含多个顺序控制流(多条执行路径)
-
在java语言中:
线程A和线程B,堆内存和方法区内存共享。
但是栈内存独立,一个线程一个栈。
-
假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。
-
java中之所以有多线程机制,目的就是为了提高程序的处理效率。
-
对于单核的CPU来说,不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是多个事情同时在做。
java中多线程的生命周期
就绪状态:就绪状态的线程又叫做可运行状态,表示当前线程具有抢夺CPU时间片的权力(CPU时间片就是执行权)。当一个线程抢夺到CPU时间片之后,就开始执行run方法,run方法的开始执行标志着线程进入运行状态。
运行状态:run方法的开始执行标志着这个线程进入运行状态,当之前占有的CPU时间片用完之后,会重新回到就绪状态继续抢夺CPU时间片,当再次抢到CPU时间之后,会重新进入run方法接着上一次的代码继续往下执行。
阻塞状态:当一个线程遇到阻塞事件,例如接收用户键盘输入,或者sleep方法等,此时线程会进入阻塞状态,阻塞状态的线程会放弃之前占有的CPU时间片。之前的时间片没了需要再次回到就绪状态抢夺CPU时间片。
锁池:在这里找共享对象的对象锁线程进入锁池找共享对象的对象锁的时候,会释放之前占有CPU时间片,有可能找到了,有可能没找到,没找到则在锁池中等待,如果找到了会进入就绪状态继续抢夺CPU时间片。(这个进入锁池,可以理解为一种阻塞状态)
多线程实现
1.使用实现多线程有四种方式:①继承Thread类;②实现Runnable接口;③使用Callable和FutureTask实现有返回值的多线程;④使用ExecutorService和Executors工具类实现线程池(如果需要线程的返回值,需要在线程中实现Callable和Future接口)
2.继承Thread类的优点:简单,且只需要实现父类的run方法即可(start方法中含有run方法,会创建一个新的线程,而run是执行当前线程)。缺点是:Java的单继承,如果对象已经继承了其他的类则不能使用该方法。且不能获取线程的返回值
3.实现Runnable接口优点:简单,实现Runnable接口必须实现run方法。缺点:创建一个线程就必须创建一个Runnable的实现类,且不能获取线程的返CallabTask优点:可以获取多线程的返回值。缺点:每个多线程都需要创建一个Callable的实现类
4.线程池ExecutorService和工具类Executors优点:可以根据实际情况创建线程数量,且只需要创建一个线程池即可,也能够通过Callable和Future接口得到线程的返回值,程序的执行时间与线程的数量紧密相关。缺点:需要手动销毁该线程池(调用shutdown方法)。
尽量不要使用 继承Thread类 和 实现Runnable接口;尽量使用线程池。否则项目导出都是线程。
关于Concurrent包
concurrent包是在AQS的基础上搭建起来的,AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架。
线程池参数
我们常用的主要有newSingleThreadExecutor、newFixedThreadPool、newCachedThreadPool、调度等,使用Executors工厂类创建。
newSingleThreadExecutor可以用于快速创建一个异步线程,非常方便。而newCachedThreadPool永远不要用在高并发的线上环境,它用的是无界队列对任务进行缓冲,可能会挤爆你的内存。
我习惯性自定义ThreadPoolExecutor,也就是参数最全的那个。
假如我的任务可以预估,corePoolSize,maximumPoolSize一般都设成一样大的,然后存活时间设的特别的长。可以避免线程频繁创建、关闭的开销。I/O密集型和CPU密集型的应用线程开的大小是不一样的,一般I/O密集型的应用线程就可以开的多一些。
threadFactory我一般也会定义一个,主要是给线程们起一个名字。这样,在使用jstack等一些工具的时候,能够直观的看到我所创建的线程。
监控
高并发下的线程池,最好能够监控起来。可以使用日志、存储等方式保存下来,对后续的问题排查帮助很大。
通常,可以通过继承ThreadPoolExecutor,覆盖beforeExecute、afterExecute、terminated方法,达到对线程行为的控制和监控。
阻塞队列
阻塞队列会对当前的线程进行阻塞。当队列中有元素后,被阻塞的线程会自动被唤醒,这极大的提高的编码的灵活性,非常方便。在并发编程中,一般推荐使用阻塞队列,这样实现可以尽量地避免程序出现意外的错误。阻塞队列使用最经典的场景就是socket数据的读取、解析,读数据的线程不断将数据放入队列,解析线程不断从队列取数据进行处理。
ArrayBlockingQueue对访问者的调用默认是不公平的,我们可以通过设置构造方法参数将其改成公平阻塞队列。
LinkedBlockingQueue队列的默认最大长度为Integer.MAX_VALUE,这在用做线程池队列的时候,会比较危险。
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。队列本身不存储任何元素,吞吐量非常高。对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务”。它更像是一个管道,在一些通讯框架中(比如rpc),通常用来快速处理某个请求,应用较为广泛。
DelayQueue是一个支持延时获取元素的无界阻塞队列。放入DelayQueue的对象需要实现Delayed接口,主要是提供一个延迟的时间,以及用于延迟队列内部比较排序。这种方式通常能够比大多数非阻塞的while循环更加节省cpu资源。
另外还有PriorityBlockingQueue和LinkedTransferQueue等,根据字面意思就能猜测它的用途。在线程池的构造参数中,我们使用的队列,一定要注意其特性和边界。比如,即使是最简单的newFixedThreadPool,在某些场景下,也是不安全的,因为它使用了无界队列。
CountDownLatch
假如有一堆接口A-Y,每个接口的耗时最大是200ms,最小是100ms。
我的一个服务,需要提供一个接口Z,调用A-Y接口对结果进行聚合。接口的调用没有顺序需求,接口Z如何在300ms内返回这些数据?
此类问题典型的还有赛马问题,只有通过并行计算才能完成问题。归结起来可以分为两类:
实现任务的并行性
开始执行前等待n个线程完成任务
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
CyclicBarrier与其类似,可以实现同样的功能。不过在日常的工作中,使用CountDownLatch会更频繁一些。
信号量
Semaphore虽然有一些应用场景,但大部分属于炫技,在编码中应该尽量少用。
信号量可以实现限流的功能,但它只是常用限流方式的一种。其他两种是漏桶算法、令牌桶算法
Lock && Condition
在Java中,对于Lock和Condition可以理解为对传统的synchronized和wait/notify机制的替代。concurrent包中的许多阻塞队列,就是使用Condition实现的。
线程控制
方法名 | 说明 |
---|---|
void yield() | 使当前线程让步,重新回到争夺CPU执行权的队列中 |
static void sleep(long ms) | 使当前正在执行的线程停留指定的毫秒数 |
void join() | 等死(等待当前线程销毁后,再继续执行其它的线程) |
void interrupt() | 终止线程睡眠 |
sleep()方法 (谁执行谁就是当前线程)
注意:run()方法中的异常只能try catch,因为父类没有抛出异常,子类不能抛出比父类更多的异常。
合理的终止线程
做一个boolean类型的标记
yield()
暂停当前正在执行的线程对象,并执行其他线程 yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。 yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。 注意:在回到就绪之后,有可能还会再次抢到。
线程的调度
-
线程调度模型
均分式调度模型:所有的线程轮流使用CPU的使用权,平均分配给每一个线程占用CPU的时间。
抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么就会随机选择一个线程来执行,优先级高的占用CPU时间相对来说会高一点点。
Java中JVM使用的就是抢占式调度模型
-
getPriority():获取线程优先级
-
setPriority:设置线程优先级
线程的安全
数据安全问题 是否具备多线程的环境
是否有共享数据
是否有多条语句操作共享数据
例如:我和小明同时取一个账户的钱,我取钱后数据还没返回给服务器,小明又取了,这个时候小明的余额还是原来的。
如何解决?线程排队执行(不能并发),线程同步机制。
变量对线程安全的影响 实例变量:在堆中。
静态变量:在方法区。
局部变量:在栈中。
以上三大变量中: 局部变量永远都不会存在线程安全问题。 因为局部变量不共享。(一个线程一个栈。) 局部变量在栈中。所以局部变量永远都不会共享。 实例变量在堆中,堆只有1个。 静态变量在方法区中,方法区只有1个。 堆和方法区都是多线程共享的,所以可能存在线程安全问题。 局部变量+常量:不会有线程安全问题。 成员变量:可能会有线程安全问题。
线程同步的利弊 好处:解决了线程同步的数据安全问题
弊端:当线程很多的时候,每个线程都会去判断同步上面的这个锁,很耗费资源,降低效率
编程模型
异步编程模型: 线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1, 谁也不需要等谁,这种编程模型叫做:异步编程模型。 其实就是:多线程并发(效率较高。)
同步编程模型: 线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行 结束,或者说在t2线程执行的时候,必须等待t1线程执行结束, 两个线程之间发生了等待关系,这就是同步编程模型。 效率较低。线程排队执行。
线程同步
线程同步方式 同步语句块:synchronized(this){方法体} (synchronized括号后的数据必须是多线程共享的数据,才能达到多线程排队)
普通同步方法:修饰符 synchronized 返回值类型 方法名(形参列表){方法体}
synchronized出现在实例方法上,一定锁的是this(此方法)。不能是其他的对象了。 所以这种方式不灵活。 另外还有一个缺点:synchronized出现在实例方法上, 表示整个方法体都需要同步,可能会无故扩大同步的 范围,导致程序的执行效率降低。所以这种方式不常用。
静态同步方法:修饰符 synchronized static 返回值类型 方法名(形参列表){方法体}
(静态方法中不能使用this)表示找类锁。类锁永远只有1把。
如何解决线程安全问题 是一上来就选择线程同步吗?synchronized 不是,synchronized会让程序的执行效率降低,用户体验不好。 系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择 线程同步机制。
第一种方案:尽量使用局部变量代替“实例变量和静态变量”。 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样 实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象, 对象不共享,就没有数据安全问题了。) 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候 就只能选择synchronized了。线程同步机制。
Lock 应用场景不同,不一定要在同一个方法中进行解锁,如果在当前的方法体内部没有满足解锁需求时,可以将lock引用传递到下一个方法中,当满足解锁需求时进行解锁操作,方法比较灵活。
死锁 形成原因
当两个线程或者多个线程互相锁定的情况就叫死锁
避免死锁的原则
顺序上锁,反向解锁,不要回头
守护线程
java语言中线程分为两大类: 一类是:用户线程 一类是:守护线程(后台线程) 其中具有代表性的就是:垃圾回收线程(守护线程)。
守护线程的特点: 一般守护线程是一个死循环,所有的用户线程只要结束, 守护线程自动结束。 注意:主线程main方法是一个用户线程。 守护线程用在什么地方呢? 每天00:00的时候系统数据自动备份。 这个需要使用到定时器,并且我们可以将定时器设置为守护线程。 一直在那里看着,每到00:00的时候就备份一次。所有的用户线程 如果结束了,守护线程自动退出,没有必要进行数据备份了。
定时器 定时器的作用: 间隔特定的时间,执行特定的程序。
在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。 不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持 定时任务的。
线程与定时器执行轨迹不同
线程与定时器之间互不抢占CPU时间片
线程池
概念 线程池就是首先创建一些线程,他们的集合称之为线程池。线程池在系统启动时会创建大量空闲线程,程序将一个任务传递给线程池,线程池就会启动一条线程来执行这个任务,执行结束后线程不会销毁(死亡),而是再次返回到线程池中成为空闲状态,等待执行下一个任务。
线程池的工作机制 在线程池的编程模式下,任务是分配给整个线程池的,而不是直接提交给某个线程,线程池拿到任务后,就会在内部寻找是否有空闲的线程,如果有,则将任务交个某个空闲线程。
使用线程池的原因 多线程运行时,系统不断创建和销毁新的线程,成本非常高,会过度的消耗系统资源,从而可能导致系统资源崩溃,使用线程池就是最好的选择。
可重用线程
方法名 | 说明 |
---|---|
Executors.newCacheThreadPoll(); | 创建一个可缓存的线程池 |
execute(Runnable run) | 启动线程池中的线程 |
java创建线程池的四种方式:
newCachedThreadPool 创建一个可缓存的线程池,如果线程池长度超过处理需求,可灵活回收空闲线程,若无可回收,则新建线程
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行
newSingleThreadExecutor 创建一个单线程化的线程池,它只会唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行
线程池的优点:
a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。
线程池的创建可以分为两大类:
通过 Executors 创建
通过 ThreadPoolExecutor 创建
以上这两类创建线程池的方式有 7 种具体实现方法,这 7 种方法便是本文要说的创建线程池的七种方式。分别是:
方法 | 含义 |
Executors.newFixedThreadPool() | 创建一个大小固定的线程池,可控制并发的线程数,超出的线程会在队列中等待 |
Executors.newCachedThreadPool() | 创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程 |
Executors.newSingleThreadExecutor() | 创建单个线程的线程池,可以保证先进先出的执行顺序 |
Executors.newScheduledThreadPool() | 创建一个可以执行延迟任务的线程池 |
Executors.newSingleThreadScheduledExecutor() | 创建一个单线程的可以执行延迟任务的线程池 |
Executors.newWorkStealingPool() | 创建一个抢占式执行的线程池 |
ThreadPoolExecutor() | 手动创建线程池,可自定义相关参数 |
Java中常见的四个线程池是:
1. FixedThreadPool:固定大小线程池,线程数量固定,适用于执行长期的任务,可控制线程的最大并发数。当线程池中的线程都处于活动状态时,新任务会进入等待队列中等待执行。
2. CachedThreadPool:缓存线程池,线程数量不固定,根据任务数量动态调整线程数量。适用于执行大量的短期任务,当线程池中的线程空闲时,会重用空闲线程执行新任务,没有空闲线程时,会创建新线程。
3. ScheduledThreadPool:定时任务线程池,适用于需要定时执行任务的场景。可以指定任务的延迟时间和执行周期,线程数量固定。
4. SingleThreadPool:单线程线程池,只有一个工作线程的线程池,适用于需要保证任务按照顺序执行的场景。所有任务按照FIFO的顺序执行,保证了任务的顺序性。
这些线程池都是通过ThreadPoolExecutor类实现的,可以通过Executors工具类来创建这些线程池。线程池的使用可以避免频繁创建和销毁线程的开销,提高了线程的复用和执行效率,同时还可以控制线程的并发数,避免资源过度占用。
线程的状态,线程池满了怎么办
线程池满的处理策略
1.默认–拒绝策略handler
线程池满了之后,一般的处理方式是丢弃某一线程,并且抛出异常。
Handler有四种策略:AbortPolicy:直接抛出异常RejectedExecutionException。
DiscardPolicy:直接丢弃任务,但是不抛出异常( 默认)。
DiscardOldestPolicy:丢弃队列中最旧的任务,然后重新尝试执行任务;
CallerRunsPolicy:由调用线程处理该任务。
IO流
Java IO流
I/O是Input/Output的缩写, I/O技术是非常实用的技术,用于处理设备之间的数据传输:如读/写文件,网络通讯等
java.io包下提供了各种“流”类和接口,以获取不同种类的数据并通过标准的方法输入或输出数据 ●输入input:读取外部数据(磁盘、光盘等存储设备数据)到程序(内存)中。 ●输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中。 Java的IO流共涉及40多个类,实际上非常规则,都是从如下4个抽象基类派生的
InputStream
InputStream 和 Reader 是所有输入流的基类
-
InputStream 其典型实现:FileInputStream
-
FileInputStream用于读取非文本数据之类的原始字节流
-
方法
Reader
InputStream 和 Reader 是所有输入流的基类
-
Reader 其典型实现:FileReader
-
读取字符流,需要使用 FileReader
-
方法
OutputStream
方法
Writer
方法
字节流写数据加异常处理
-
异常处理格式
-
try-catch-finally
try{ 可能出现异常的代码; }catch(异常类名 变量名){ 异常的处理代码; }finally{ 执行所有清除操作; }
-
finally特点
-
被finally控制的语句一定会执行,除非JVM退出
-
-
字节缓冲流构造方法
-
字节缓冲流介绍
-
BufferOutputStream:该类实现缓冲输出流。 通过设置这样的输出流,应用程序可以向底层输出流写入字节,而不必为写入的每个字节导致底层系统的调用
-
BufferedInputStream:创建BufferedInputStream将创建一个内部缓冲区数组。 当从流中读取或跳过字节时,内部缓冲区将根据需要从所包含的输入流中重新填充,一次很多字节
-
-
构造方法:
方法名 说明 BufferedOutputStream(OutputStream out) 创建字节缓冲输出流对象 BufferedInputStream(InputStream in) 创建字节缓冲输入流对象 -
字符流中的编码解码问题
-
字符流中和编码解码问题相关的两个类
-
InputStreamReader:是从字节流到字符流的桥梁
它读取字节,并使用指定的编码将其解码为字符
它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集
-
OutputStreamWriter:是从字符流到字节流的桥梁
是从字符流到字节流的桥梁,使用指定的编码将写入的字符编码为字节
它使用的字符集可以由名称指定,也可以被明确指定,或者可以接受平台的默认字符集
-
-
构造方法
方法名 说明 InputStreamReader(InputStream in) 使用默认字符编码创建InputStreamReader对象 InputStreamReader(InputStream in,String chatset) 使用指定的字符编码创建InputStreamReader对象 OutputStreamWriter(OutputStream out) 使用默认字符编码创建OutputStreamWriter对象 OutputStreamWriter(OutputStream out,String charset) 使用指定的字符编码创建OutputStreamWriter对象
字符流写数据的5种方式
-
方法介绍
方法名 说明 void write(int c) 写一个字符 void write(char[] cbuf) 写入一个字符数组 void write(char[] cbuf, int off, int len) 写入字符数组的一部分 void write(String str) 写一个字符串 void write(String str, int off, int len) 写一个字符串的一部分 -
刷新和关闭的方法
方法名 说明 flush() 刷新流,之后还可以继续写数据 close() 关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据
符缓冲流
-
字符缓冲流介绍
-
BufferedWriter:将文本写入字符输出流,缓冲字符,以提供单个字符,数组和字符串的高效写入,可以指定缓冲区大小,或者可以接受默认大小。默认值足够大,可用于大多数用途
-
BufferedReader:从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取,可以指定缓冲区大小,或者可以使用默认大小。 默认值足够大,可用于大多数用途
-
-
构造方法
方法名 说明 BufferedWriter(Writer out) 创建字符缓冲输出流对象 BufferedReader(Reader in) 创建字符缓冲输入流对象 -
字符缓冲流特有功能
-
方法介绍
BufferedWriter:
方法名 说明 void newLine() 写一行行分隔符,行分隔符字符串由系统属性定义 BufferedReader:
方法名 说明 String readLine() 读一行文字。 结果包含行的内容的字符串,不包括任何行终止字符如果流的结尾已经到达,则为null
【IO多路复用】
IO 多路复用是一种同步 IO 模型,实现一个线程可以监视多个文件句柄。一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出 cpu。IO 是指网络 IO,多路指多个TCP连接(即 socket 或者 channel),复用指复用一个或几个线程。
意思说一个或一组线程处理多个 TCP 连接。最大优势是减少系统开销小,不必创建过多的进程/线程,也不必维护这些进程/线程。IO 多路复用的三种实现方式:select、poll、epoll。
select 机制
1️⃣基本原理:
客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和 exceptfds(异常)。select 会阻塞住监视 3 类文件描述符,等有数据、可读、可写、出异常或超时就会返回;返回后通过遍历 fdset 整个数组来找到就绪的描述符 fd,然后进行对应的 IO 操作。
2️⃣优点:
几乎在所有的平台上支持,跨平台支持性好
3️⃣缺点:
由于是采用轮询方式全盘扫描,会随着文件描述符 FD 数量增多而性能下降。
每次调用 select(),都需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间)。
单个进程打开的 FD 是有限制(通过FD_SETSIZE设置)的,默认是 1024 个,可修改宏定义,但是效率仍然慢。
poll 机制
1️⃣基本原理与 select 一致,也是轮询+遍历。唯一的区别就是 poll 没有最大文件描述符限制(使用链表的方式存储 fd)。
2️⃣poll 缺点
由于是采用轮询方式全盘扫描,会随着文件描述符 FD 数量增多而性能下降。
每次调用 select(),都需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间)。
epoll 机制
1️⃣基本原理:
没有 fd 个数限制,用户态拷贝到内核态只需要一次,使用时间通知机制来触发。通过 epoll_ctl 注册 fd,一旦 fd 就绪就会通过 callback 回调机制来激活对应 fd,进行相关的 io 操作。epoll 之所以高性能是得益于它的三个函数:
epoll_create() 系统启动时,在 Linux 内核里面申请一个B+树结构文件系统,返回 epoll 对象,也是一个 fd。
epoll_ctl() 每新建一个连接,都通过该函数操作 epoll 对象,在这个对象里面修改添加删除对应的链接 fd,绑定一个 callback 函数
epoll_wait() 轮训所有的 callback 集合,并完成对应的 IO 操作
2️⃣优点:
没 fd 这个限制,所支持的 FD 上限是操作系统的最大文件句柄数,1G 内存大概支持 10 万个句柄。效率提高,使用回调通知而不是轮询的方式,不会随着 FD 数目的增加效率下降。内核和用户空间 mmap 同一块内存实现(mmap 是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间)
3️⃣epoll缺点:
epoll 只能工作在linux下。
4️⃣epoll 应用:redis、nginx
epoll 水平触发(LT)与边缘触发(ET)的区别
epoll 有 epoll LT 和 epoll ET 两种触发模式,LT 是默认的模式,ET 是“高速”模式。
1️⃣LT 模式下,只要这个 fd 还有数据可读,每次 epoll_wait 都会返回它的事件,提醒用户程序去操作。
2️⃣ET 模式下,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论 fd 中是否还有数据可读。所以在 ET 模式下,read 一个 fd 的时候一定要把它的 buffer 读完,或者遇到 EAGAIN 错误。
select/poll/epoll 之间的区别
为什么有 IO 多路复用机制
没有 IO 多路复用机制时,有 BIO、NIO 两种实现方式,但有一些问题。
1️⃣同步阻塞(BIO)
服务端采用单线程,当 accept 一个请求后,在 recv 或 send 调用阻塞时,将无法 accept 其他请求(必须等上一个请求 recv 或 send 完),无法处理并发。
服务器端采用多线程,当 accept 一个请求后,开启线程进行 recv,可以完成并发处理,但随着请求数增加需要增加系统线程,大量的线程占用很大的内存空间,并且线程切换会带来很大的开销,10000 个线程真正发生读写事件的线程数不会超过 20%,每次 accept 都开一个线程也是一种资源浪费。
2️⃣同步非阻塞(NIO)
服务器端当 accept 一个请求后,加入 fds 集合,每次轮询一遍 fds 集合 recv(非阻塞)数据,没有数据则立即返回错误,每次轮询所有 fd(包括没有发生读写事件的fd)会很浪费 cpu。
3️⃣IO 多路复用
服务器端采用单线程通过 select/epoll 等系统调用获取 fd 列表,遍历有事件的 fd 进行 accept/recv/send,使其能支持更多的并发连接请求。
理解 IO 多路复用机制
小王在 S 城开了一家快递店,负责同城快送服务。小王因为资金限制,雇佣了一批快递员,然后小王发现资金不够了,只够买一辆车送快递。
1️⃣【经营方式一】
客户每送来一份快递,小王就让一个快递员盯着,然后快递员开车去送快递。慢慢的小王就发现了这种经营方式存在下述问题:
几十个快递员基本上时间都花在了抢车上了,大部分快递员都处在闲置状态,谁抢到了车,谁就能去送快递。
随着快递的增多,快递员也越来越多,小王发现快递店里越来越挤,没办法雇佣新的快递员了。
快递员之间的协调很费时间。
2️⃣【经营方式二】
小王只雇佣一个快递员。然后呢,客户送来的快递,小王按送达地点标注好,然后依次放在一个地方。最后,那个快递员依次去取快递,一次拿一个,然后开着车去送快递,送好了就回来拿下一个快递。
3️⃣【对比】
两种经营方式对比,第二种明显效率更高,更好。在上述比喻中:
每个快递员------------------>每个线程
每个快递-------------------->每个socket(IO流)
快递的送达地点-------------->socket的不同状态
客户送快递请求-------------->来自客户端的请求
小王的经营方式-------------->服务端运行的代码
一辆车---------------------->CPU的核数
4️⃣ 于是有如下结论:
【经营方式一】就是传统的并发模型,每个 IO 流(快递)都有一个新的线程(快递员)管理。
【经营方式二】就是 IO 多路复用。只有单个线程(一个快递员),通过跟踪每个 IO 流的状态(每个快递的送达地点),来管理多个 IO 流。
类比到真实的redis线程模型如图。简言之,就是 redis-client 在操作的时候,会产生具有不同事件类型的 socket。在服务端,有一段 IO 多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。需要说明的是,这个 IO 多路复用机制,redis 还提供了 select、epoll、evport、kqueue 等多路复用函数库。
示例
100 万个连接,里面有 1 万个连接是活跃,可以对比 select、poll、epoll 的性能表现:
1️⃣select:不修改宏定义默认是 1024,则需要100w/1024=977个进程才可以支持 100 万连接,会使得 CPU 性能特别的差。
2️⃣poll:没有最大文件描述符限制,100 万个链接则需要 100 万个 fd,遍历都响应不过来了,还有空间的拷贝消耗大量的资源。
3️⃣epoll:请求进来时就创建 fd 并绑定一个 callback,只需要遍历 1 万个活跃连接的 callback 即可,既高效又不用内存拷贝。
参考:JAVA——文件IO流_至,若春和景明的博客-CSDN博客
容器集合——Collection(单列)、Map(双列)_单列表collection和双列表map_至,若春和景明的博客-CSDN博客