5.1 探讨单例模式 209
单例模式非常常见,我们不可避免会在多线程环境中使用它们。并且,系统中使用单例的地方可能非常频繁,因此我们非常迫切地需要一种高效的单例实现。
第一种实现方式(饿汉式)
public class Singleton
private Singleton(){
System.out.println("Singleton is create");
private static Singleton instance = newSingleton();
public static Singleton getInstance() {
return instance;
}
}
缺点:Singleton实例在什么时候创建是不受控制的。即使系统没有要求创建单例,newSingleton()方法也会被调用。
第二种实现方式(延迟加载策略/懒汉式)
public class LazySingleton {
private LazySingleton() {
System.out.println("LazySingleton is create");
}
private static LazySingleton instance = null;
public static synchronized LazySingleton getInstance() {
if(instance == null)
instance = new LazySingleton();
return instance;
}
}
只在真正需要时创建对象。但坏处也很明显,并发环境下加锁,竞争激烈的场合对性能可能产生一定的影响。但总体上,这是一个非常易于实现和理解的方法。
第三种实现方式:双重检查模式(作者不推荐)
第四种实现方式(结合了前两种方式的优点)
public class StaticSingleton (
private StaticSingleton() {
System. out. println ("StaticSingleton is create") ;
}
private static class SingletonHolder {
private static StaticSingleton instance = new StaticSingleton();
}
public static StaticSingleton getInstance() {
return SingletonHolder.instance;
}
}
5.2 不变模式 213
核心:一个对象一旦被创建,则它的内部状态将永远不会发生改变。
不变模式的实现:
- 去除所有的setter方法以及所有修改自身属性的方法。
- 属性设为私有,并且final标记,确保其不可修改。
- 确保子类可以重载修改它的行为。
- 有一个可以创建完整对象的构造函数。
使用不变模式的例子:元数据的包装类和String类
5.3 生产者-消费者模式 215
用BlockigQueue队列作为共享内存缓存区不是一个高性能的实现,它完全使用锁和阻塞等待来实现线程间的同步。转而使用ConcurrentLinkedQueue ,其是一个高性能的队列,而ConcurrentLinkedQueue队列使用了大量无锁的CAS操作。我们可以使用CAS来实现生产者-消费者模式从而获得性能提升。但使用CAS进行编程是非常困难的,目前有一个现成的Disruptor框架,它已经帮助我们实现了这一个功能。
5.4 高性能的生产者-消费者模式:无锁的实现 220
5.4.1 无锁的缓存框架:Disruptor 221
5.4.2 用Disruptor框架实现生产者-消费者模式的案例 222
5.4.3 提高消费者的响应时间:选择合适的策略 225
5.4.4 CPU Cache的优化:解决伪共享问题 226
5.5 Future模式 230
Future模式的核心思想是异步调用,可以让被调用者先立即返回一个”契约“(虚拟数据),然后让其在后台慢慢处理这个请求。调用者可以先处理其他任务。等结果准备好了,再返回给调用者。
5.5.1 Future模式的主要角色 232
5.5.2 Future模式的简单实现 233
5.5.3 JDK中的Future模式 236
下面展示JDK内置的Future模式的使用方法
public class RealData implements Callable<String> {
private String para;
public RealData (String para) {
this para=para;
}
@Override
public String call() throws Exception { //返回的结果比较慢
StringBuffer sb=new StringBuffer() ;
for (int i= 0;i< 10; i++) [
sb.append (para) ;
try {
Thread.sleep (100) ;
} catch (InterruptedException e) {
}
}
return sb. toString();
}
}
public class FutureMain {
public static void main (String[] args) throws InterruptedException, ExecutionException {
//构造FutureTask
FutureTask<String> future = new FutureTask<String> (new RealData("a"));
//future是返回的契约
ExecutorService executor = Executors.newFixedThreadPool (1) ;
//执行FutureTask,相当于上例中的client. request ("a")发送请求
//在这里开启线程进行RealData的call ()方法执行
executor.submit (future);
System. out.println ("请求完毕");
try {
//这里依然可以做额外的数据操作,使用sleep代替其他业务逻辑的处理
Thread. sleep (2000) ; //等待的时候,做一些其他的事情
} catch (InterruptedException e) {
}
//相当于5.5.2节中的data. getResult ()方法,取得call ()方法的返回值
//如果此时call ()方法没有执行完成,则依然会等待
System.out.println("数据= " + future.get()); //最后从契约中拿数据
}
}
5.5.4 Guava对Future模式的支持 238
5.6 并行流水线 240
执行过程中,有数据相关性的运算都是无法完美并行化的,可以将有依赖的操作分配在不同的线程进行计算,从而提升效率。(实际上就是生活中的流水线思想)
5.7 并行搜索 244
并行搜索的策略是将原始数据集合按照期望的线程数进行分割。如果我们计划使用两个线程进行搜索,那么就可以把一个数组或集合分割成两个。每个线程各自独立搜索,当其中有一个线程找到数据后,立即返回结果即可。
5.8 并行排序 246
5.8.1 分离数据相关性:奇偶交换排序 246
奇偶交换排序:分离了数据的相关性
算法步骤
- 选取所有奇数列的元素与其右侧相邻的元素进行比较,将较小的元素排序在前面;
- 选取所有偶数列的元素与其右侧相邻的元素进行比较,将较小的元素排序在前面;
- 重复前面两步,直到所有序列有序为止。
https://blog.csdn.net/lemon_tree12138/article/details/50605563
5.8.2 改进的插入排序:希尔排序 250
希尔排序将整个数组根据间隔h分割为若千个子数组,它们相互穿插在一起,每一次排序时,分别对每一个子数组进行排序。
5.9 并行算法:矩阵乘法 254
如果需要进行并行计算,一种简单的策略是将矩阵A进行水平分割,得到子矩阵A1和A2,矩阵B进行垂直分割,得到子矩阵B1和B2。此时,我们只要分别计算这些子矩阵的乘积,再将结果进行拼接就能得到原始矩阵A和B的乘积。
当然,这个过程是可以反复进行的。为了计算矩阵A1XB1,我们还可以进一步将矩阵A1和矩阵B1分解,直到我们认为子矩阵的大小已经在可接受范围内。
我们使用Fork/Join 框架来实现并行矩阵相乘的想法,使用jMatrices开源软件,作为矩阵计算的工具。
5.10 准备好了再通知我:网络NIO 258
Java NIO是NewIO的简称,它是–种可以替代Java IO的一套新的IO机制。它提供了一套不同于Java标准IO的操作机制。严格来说,NIO与并发并无直接的关系,但是使用NIO技术可以大大提高线程的使用效率。
5.10.1 基于Socket的服务端多线程模式 259
服务器会为每一个客户端连接启用一个线程,这个新的线程将全心全意为这个客户端服务。同时,为了接受客户端连接,服务器还会额外使用一个派发线程。
这种模式的缺点是:倾向于让CPU进行IO等待
例如:如果服务端缓慢地向客户端输入,则服务端的请求处理时间就会变长。因为服务器要先读入客户端的输入,而客户端缓慢的处理速度(当然也可能是一个拥挤的网络环境)使得服务器花费了不少等待时间。
我们可以试想一下,服务器要处理大量的请求连接,如果每个请求都像这样拖慢了服务器的处理速度,那么服务端能够处理的并发数量就会大幅度减少。
在这个案例中,服务器处理请求之所以慢,并不是因为在服务端有多少繁重的任务,而是因为服务线程在等待IO而已。让高速运转的CPU去等待极其低效的网络IO是非常不合算的行为。
5.10.2 使用NIO进行网络编程 264
己注册到一个Selector 中,因此这个Channel就能为Selector所管理。而一一个Selector可以
管理多个SelectableChannel。当SelectableChannel的数据准备好时,Selector 就会接到通知,
得到那些已经准备好的数据,而SocketChannel就是SelectableChannel的- -种。因此,它们
构成了如图5.20所示的结构。
使用Java的NIO就可以将上节的网络IO等待时间从业务处理线程中抽取出来。
NIO中的又一个关键组件Channel。Channel有点类似于流,一个Channel可以和文件或者网络Socket对应。如果Channel对应着一个Soceket,那么往这个Channel中写数据,就等于向Socket中写入数据。
数据需要包装成Buffer的形式才能和Channel交互。 Buffer理解成一个内存区域或者byte数组。
Selector是选择器。SelectableChannel是Channel的一个实现,表示可被选择的通道。任何一个SelectableChannel都可以将自己注册到一个Selector中,即由Selector 所管理。
一个Selector可以由一个线程进行管理,一个Selector可以管理多个SelectableChannel。当SelectableChannel的数据准备好时,Selector 就会接到通知,得到那些已经准备好的数据
结构如图:
5.10.3 使用NIO来实现客户端 272
5.11 读完了再通知我:AIO 274
AIO是异步IO的缩写,即Asynchronized IO。
NIO是同步的,但对于AIO来说,则更进了一步,它不是在IO准备好时再通知线程,而是在IO操作
已经完成后,再给线程发出通知。因此,AIO是完全不会阻塞的。此时,我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。