第二章 流
关闭流之前,应当立即刷新输出所有流:flush()
,否则关闭流时,留在缓冲区中的数据可能会丢失。
释放模式
- try外声明流
- try内初始化流
- finally中先判断流是否为null,再close
Java 7的try with resources
写法:
过滤器流
过滤器流用于附加到原始流中,在原始字节和各种格式之间进行转换。
将过滤器串在一起的几种方法:
链式过滤器的原则:只在最后一个过滤器中读取或写入数据。所以你看方法三,把前面几个过滤器都不放引用了,防止乱写。
缓冲流
阅读器、书写器
Reader
和Writer
可以实现字节到字符的转换,并提供了多种编码、解码方式。InputStreamReader
可以将字节转为特定编码的字符;OutputStreamWriter
接受字符,转为字节再输出。
书写器在写入字节时,由于编码方式不同,写出多少字节、哪些字节也不同:
阅读器、书写器的过滤器
如何串联阅读器、书写器的过滤器呢?
基本流程是inputstream
—>inputstreamReader
—>其他过滤器,这种装饰方式可以将字节流转为字符流,再进行其他过滤器的装饰。
其他过滤器包括:有缓冲区的BufferReader
等。
如果要使用缓冲区的BufferReader
,只要将Reader
的变量指向BufferReader
对象即可,调用的read
方法就是BufferReader
重写的方法了:
BufferWriter:
自己的新方法:readLine()
newLine():
注意:自动写入换行的方法在网络编程中少用,应该使用明确指明换行的方法,避免平台适配问题。
第三章 线程
服务器对每个请求开启一个进程处理时,会导致性能下降,解决方法有:
- 固定进程数,用队列存放请求,重用进程而不是创建新的进程
- 用轻量级的线程来处理请求,再加上线程池,服务器每分钟可以用不到100个线程处理数千个短链接
线程的问题:
- 增加程序的复杂性
- 不同线程间由于共享内存而存在可能破坏变量和数据结构的安全性问题
- 资源竞争问题,可能导致死锁
开启线程的方法:
- 继承Thread类,重写run方法
- 向Thread构造函数传入一个实现了Runnable接口的对象
如何从线程中返回信息?
- 存于Thread的私有变量中
- 轮询
- 回调
静态方法的回调:
在run方法最后调用一个静态方法。
实例对象的回调:
构造函数中传入回调对象,保存引用,在run方法最后调用改回调对象的回调方法。
如果有多个类的实例对线程处理结果感兴趣,可以注册并保存一个interface的回调列表,元素均实现该回调的interface。在run方法最后遍历执行列表元素的回调方法。
该种注册观察者-保存观察者-回调的方法被称之为观察者模式。
Java5新特性:Future、Callable、Executor
调用future.get()方法时,如果两个future没有都完成,那会阻塞,直到都完成后返回,替我们实现了同步的问题。。
同步
synchronized关键字可以同步当前对象(this)的整个方法。
但是直接这样同步有很多缺点:
- VM性能下降严重
- 增加了死锁的可能
- 对方法加同步可能不能保护真正需要保护的对象
同步的替代方式
- 使用局部变量而不是字段,局部变量是线程安全的,一个局部变量不可能由两个不同的线程共享,每个线程都有自己单独的一组局部变量
- 基本类型是值传递,是线程安全的;对象是引用传递,是非线程安全的;但是如果对象是不可变的,那就是安全的,如String;
- 创建一个不可变对象只要让所有字段为private或final,并不设置set方法,只允许在构造函数中初始化字段,即可创建不可变对象,因为构造函数通常情况下是线程安全的。
- 第三种技术,将非安全的类作为安全的类的一个私有字段,不要让非安全对象的引用泄漏就行
Java中提供的线程安全也可变的类:
- java.util.concurrent.atomic中的原子数据类型对象
- Collections.synchronizedSet等转换集合的方法,返回一个线程安全的视图
死锁
同步会导致死锁,因此需要尽可能避免同步,多采取上述方法(如使用不可变对象),或设计程序逻辑:
线程调度
抢占
让线程暂停有几种方式:
阻塞
线程等待资源而停止,常见是I/O阻塞。
注:线程阻塞时,不会释放已拥有的锁
放弃
线程显式放弃控制权,如果有另一个线程准备运行,可以运行改线程。
通过调用Thread.yield()方法实现。
注:不会释放已拥有的锁
休眠
更有力的放弃方式,不管有无线程准备运行,休眠线程都会暂停
注:不会释放已拥有的锁
唤醒
一些线程唤醒休眠的线程。一个线程可以调用休眠线程的Thread对象interrupt()方法,从而唤醒它。
连接线程
一个线程可能需要另一个线程的执行结果。使用join()方法,允许一个线程在继续执行前等待另一个线程结束。
第八行,当排序线程结束时,main线程才会继续执行下去。
等待一个对象
线程可以等待(wait)一个它锁定的对象。一个线程拥有一个对象的锁后,可以调用wait方法,释放锁,然后自己进入休眠,直到另一个线程在对这个对象处理之后,唤醒它。
wait方法是Object的方法。
调用wait后,线程会进入休眠,直到:
看一个示例:
一个线程负责读取文件,一个线程负责进一步处理该文件。存在一种同步关系,后面的线程需要文件对象读取完成后才能继续执行。这里可以用wait方法等待文件对象的完成。
可以看到,第一个线程又试图对文件对象加锁,开启文件读取线程后,让文件对象wait,此时第一个线程会休眠。
当文件读取线程获取到文件对象的锁后,读取文件,最后唤醒文件对象上的休眠线程。此时就会唤醒第一个线程,继续处理清单文件。
用join方法也可以,但是意思是第一个线程会在文件读取线程完全执行完毕后再执行,而这里控制的粒度更细,是对文件对象进行控制。
当多个线程等待同一个对象时,等待和通知更常用,下面看例子2,一个线程需要读取entries的每个元素进行处理(消费者),多个日志线程会读取日志文件并存入entries(生产者):
消费者:
这里加锁后判断entries是否为空,为空就阻塞(用了wait)。
消费者被唤醒后,还得判断一下entries是否为空,状态是否真的可以进行消费,因此用了while循环。
生产者:
当前线程读取不到日志后,中断其他等待的线程,而不用卡在这一直等日志。读到日志后,给entries加锁,再插入,再唤醒消费者。
结束
线程池和Executor
为什么要使用线程池?
- 这有大量的IO操作—可以在计算压缩时,并行读取写入操作
- 数据压缩是CPU密集度很高的操作,不能运行太多线程,所以要固定线程数量
- 主程序的运行速度远远超过压缩线程
压缩线程:
主程序:
submit提交任务,那哪里开始任务呢?调用shutdown方法通知线程池没有更多任务要加入队列了,完成所有任务后将关闭线程池。
如果要忽略正在等待处理的任务,并立刻关闭,需要调用shutdownNow()方法。比如网络程序中,任务都会无限延续,在管理界面中关闭时是希望立刻关闭的。