一、单例模式
什么是单例模式?
它的核心在于,单例模式可以保证一个类仅创建一个实例,并提供一个访问它的全局访问点。
该模式有三个基本要点:一是这个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
结合这三点,我们来实现一个简单的单例:
// 饿汉模式
public final class Singleton {
private static Singleton instance=new Singleton();// 自行创建实例
private Singleton(){}// 构造函数
public static Singleton getInstance(){// 通过该函数向整个系统提供实例
return instance;
}
}
饿汉模式
我们可以发现,以上第一种实现单例的代码中,使用了 static 修饰了成员变量 instance,所以该变量会在类初始化的过程中被收集进类构造器即 <clinit> 方法中。在多线程场景下,JVM 会保证只有一个线程能执行该类的 <clinit> 方法,其它线程将会被阻塞等待。
等到唯一的一次 <clinit> 方法执行完成,其它线程将不会再执行 <clinit> 方法,转而执行自己的代码。也就是说,static 修饰了成员变量 instance,在多线程的情况下能保证只实例化一次。
这种方式实现的单例模式,在类加载阶段就已经在堆内存中开辟了一块内存,用于存放实例化对象,所以也称为饿汉模式。
饿汉模式实现的单例的优点是,可以保证多线程情况下实例的唯一性,而且 getInstance 直接返回唯一实例,性能非常高。
然而,在类成员变量比较多,或变量比较大的情况下,这种模式可能会在没有使用类对象的情况下,一直占用堆内存。试想下,如果一个第三方开源框架中的类都是基于饿汉模式实现的单例,这将会初始化所有单例类,无疑是灾难性的。
懒汉模式
懒汉模式就是为了避免直接加载类对象时提前创建对象的一种单例设计模式。该模式使用懒加载方式,只有当系统使用到类对象时,才会将实例加载到堆内存中。通过以下代码,我们可以简单地了解下懒加载的实现方式:
// 懒汉模式
public final class Singleton {
private static Singleton instance= null;// 不实例化
private Singleton(){}// 构造函数
public static Singleton getInstance(){// 通过该函数向整个系统提供实例
if(null == instance){// 当 instance 为 null 时,则实例化对象,否则直接返回对象
instance = new Singleton();// 实例化对象
}
return instance;// 返回已存在的对象
}
}
以上代码在单线程下运行是没有问题的,但要运行在多线程下,就会出现实例化多个类对象的情况。这是怎么回事呢?
当线程 A 进入到 if 判断条件后,开始实例化对象,此时 instance 依然为 null;又有线程 B 进入到 if 判断条件中,之后也会通过条件判断,进入到方法里面创建一个实例对象。
所以我们需要对该方法进行加锁,保证多线程情况下仅创建一个实例。这里我们使用 Synchronized 同步锁来修饰 getInstance 方法:
// 懒汉模式 + synchronized 同步锁
public final class Singleton {
private static Singleton instance= null;// 不实例化
private Singleton(){}// 构造函数
public static synchronized Singleton getInstance(){// 加同步锁,通过该函数向整个系统提供实例
if(null == instance){// 当 instance 为 null 时,则实例化对象,否则直接返回对象
instance = new Singleton();// 实例化对象
}
return instance;// 返回已存在的对象
}
}
但我们前面讲过,同步锁会增加锁竞争,带来系统性能开销,从而导致系统性能下降,因此这种方式也会降低单例模式的性能。
还有,每次请求获取类对象时,都会通过 getInstance() 方法获取,除了第一次为 null,其它每次请求基本都是不为 null 的。在没有加同步锁之前,是因为 if 判断条件为 null 时,才导致创建了多个实例。基于以上两点,我们可以考虑将同步锁放在 if 条件里面,这样就可以减少同步锁资源竞争。
// 懒汉模式 + synchronized 同步锁
public final class Singleton {
private static Singleton instance= null;// 不实例化
private Singleton(){}// 构造函数
public static Singleton getInstance(){// 加同步锁,通过该函数向整个系统提供实例
if(null == instance){// 当 instance 为 null 时,则实例化对象,否则直接返回对象
synchronized (Singleton.class){
instance = new Singleton();// 实例化对象
}
}
return instance;// 返回已存在的对象
}
}
看到这里,你是不是觉得这样就可以了呢?答案是依然会创建多个实例。这是因为当多个线程进入到 if 判断条件里,虽然有同步锁,但是进入到判断条件里面的线程依然会依次获取到锁创建对象,然后再释放同步锁。所以我们还需要在同步锁里面再加一个判断条件:
// 懒汉模式 + synchronized 同步锁 + double-check
public final class Singleton {
private static Singleton instance= null;// 不实例化
private Singleton(){}// 构造函数
public static Singleton getInstance(){// 加同步锁,通过该函数向整个系统提供实例
if(null == instance){// 第一次判断,当 instance 为 null 时,则实例化对象,否则直接返回对象
synchronized (Singleton.class){// 同步锁
if(null == instance){// 第二次判断
instance = new Singleton();// 实例化对象
}
}
}
return instance;// 返回已存在的对象
}
}
以上这种方式,通常被称为 Double-Check,它可以大大提高支持多线程的懒汉模式的运行性能。那这样做是不是就能保证万无一失了呢?还会有什么问题吗?
其实这里又跟 Happens-Before 规则和重排序扯上关系了,这里我们先来简单了解下 Happens-Before 规则和重排序。
编译器为了尽可能地减少寄存器的读取、存储次数,会充分复用寄存器的存储值,比如以下代码,如果没有进行重排序优化,正常的执行顺序是步骤 1\2\3,而在编译期间进行了重排序优化之后,执行的步骤有可能就变成了步骤 1/3/2,这样就能减少一次寄存器的存取次数。
int a = 1;// 步骤 1:加载 a 变量的内存地址到寄存器中,加载 1 到寄存器中,CPU 通过 mov 指令把 1 写入到寄存器指定的内存中
int b = 2;// 步骤 2 加载 b 变量的内存地址到寄存器中,加载 2 到寄存器中,CPU 通过 mov 指令把 2 写入到寄存器指定的内存中
a = a + 1;// 步骤 3 重新加载 a 变量的内存地址到寄存器中,加载 1 到寄存器中,CPU 通过 mov 指令把 1 写入到寄存器指定的内存中
在 JMM 中,重排序是十分重要的一环,特别是在并发编程中。如果 JVM 可以对它们进行任意排序以提高程序性能,也可能会给并发编程带来一系列的问题。例如,我上面讲到的 Double-Check 的单例问题,假设类中有其它的属性也需要实例化,这个时候,除了要实例化单例类本身,还需要对其它属性也进行实例化:
// 懒汉模式 + synchronized 同步锁 + double-check
public final class Singleton {
private static Singleton instance= null;// 不实例化
public List<String> list = null;//list 属性
private Singleton(){
list = new ArrayList<String>();
}// 构造函数
public static Singleton getInstance(){// 加同步锁,通过该函数向整个系统提供实例
if(null == instance){// 第一次判断,当 instance 为 null 时,则实例化对象,否则直接返回对象
synchronized (Singleton.class){// 同步锁
if(null == instance){// 第二次判断
instance = new Singleton();// 实例化对象
}
}
}
return instance;// 返回已存在的对象
}
}
在执行 instance = new Singleton(); 代码时,正常情况下,实例过程这样的:
- 给 Singleton 分配内存;
- 调用 Singleton 的构造函数来初始化成员变量;
- 将 Singleton 对象指向分配的内存空间(执行完这步 singleton 就为非 null 了)。 如果虚拟机发生了重排序优化,这个时候步骤 3 可能发生在步骤 2 之前。如果初始化线程刚好完成步骤 3,而步骤 2 没有进行时,则刚好有另一个线程到了第一次判断,这个时候判断为非 null,并返回对象使用,这个时候实际没有完成其它属性的构造,因此使用这个属性就很可能会导致异常。在这里,Synchronized 只能保证可见性、原子性,无法保证执行的顺序。
这个时候,就体现出 Happens-Before 规则的重要性了。通过字面意思,你可能会误以为是前一个操作发生在后一个操作之前。然而真正的意思是,前一个操作的结果可以被后续的操作获取。这条规则规范了编译器对程序的重排序优化。
我们知道 volatile 关键字可以保证线程间变量的可见性,简单地说就是当线程 A 对变量 X 进行修改后,在线程 A 后面执行的其它线程就能看到变量 X 的变动。除此之外,volatile 在 JDK1.5 之后还有一个作用就是阻止局部重排序的发生,也就是说,volatile 变量的操作指令都不会被重排序。所以使用 volatile 修饰 instance 之后,Double-Check 懒汉单例模式就万无一失了。
// 懒汉模式 + synchronized 同步锁 + double-check
public final class Singleton {
private volatile static Singleton instance= null;// 不实例化
public List<String> list = null;//list 属性
private Singleton(){
list = new ArrayList<String>();
}// 构造函数
public static Singleton getInstance(){// 加同步锁,通过该函数向整个系统提供实例
if(null == instance){// 第一次判断,当 instance 为 null 时,则实例化对象,否则直接返回对象
synchronized (Singleton.class){// 同步锁
if(null == instance){// 第二次判断
instance = new Singleton();// 实例化对象
}
}
}
return instance;// 返回已存在的对象
}
}
通过内部类实现
以上这种同步锁 +Double-Check 的实现方式相对来说,复杂且加了同步锁,那有没有稍微简单一点儿的可以实现线程安全的懒加载方式呢?
我们知道,在饿汉模式中,我们使用了 static 修饰了成员变量 instance,所以该变量会在类初始化的过程中被收集进类构造器即 <clinit> 方法中。在多线程场景下,JVM 会保证只有一个线程能执行该类的 <clinit> 方法,其它线程将会被阻塞等待。这种方式可以保证内存的可见性、顺序性以及原子性。
如果我们在 Singleton 类中创建一个内部类来实现成员变量的初始化,则可以避免多线程下重复创建对象的情况发生。这种方式,只有在第一次调用 getInstance() 方法时,才会加载 InnerSingleton 类,而只有在加载 InnerSingleton 类之后,才会实例化创建对象。具体实现如下:
// 懒汉模式 内部类实现
public final class Singleton {
public List<String> list = null;// list 属性
private Singleton() {// 构造函数
list = new ArrayList<String>();
}
// 内部类实现
public static class InnerSingleton {
private static Singleton instance=new Singleton();// 自行创建实例
}
public static Singleton getInstance() {
return InnerSingleton.instance;// 返回内部类中的静态变量
}
}
二、原型模式与享元模式
原型模式是通过给出一个原型对象来指明所创建的对象的类型,然后使用自身实现的克隆接口来复制这个原型对象,该模式就是用这种方式来创建出更多同类型的对象。
使用这种方式创建新的对象的话,就无需再通过 new 实例化来创建对象了。这是因为 Object 类的 clone 方法是一个本地方法,它可以直接操作内存中的二进制流,所以性能相对 new 实例化来说,更佳。
实现原型模式
我们现在通过一个简单的例子来实现一个原型模式:
// 实现 Cloneable 接口的原型抽象类 Prototype
class Prototype implements Cloneable {
// 重写 clone 方法
public Prototype clone(){
Prototype prototype = null;
try{
prototype = (Prototype)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return prototype;
}
}
// 实现原型类
class ConcretePrototype extends Prototype{
public void show(){
System.out.println(" 原型模式实现类 ");
}
}
public class Client {
public static void main(String[] args){
ConcretePrototype cp = new ConcretePrototype();
for(int i=0; i< 10; i++){
ConcretePrototype clonecp = (ConcretePrototype)cp.clone();
clonecp.show();
}
}
}
要实现一个原型类,需要具备三个条件:
实现 Cloneable 接口:Cloneable 接口与序列化接口的作用类似,它只是告诉虚拟机可以安全地在实现了这个接口的类上使用 clone 方法。在 JVM 中,只有实现了 Cloneable 接口的类才可以被拷贝,否则会抛出 CloneNotSupportedException 异常。 重写 Object 类中的 clone 方法:在 Java 中,所有类的父类都是 Object 类,而 Object 类中有一个 clone 方法,作用是返回对象的一个拷贝。 在重写的 clone 方法中调用 super.clone():默认情况下,类不具备复制对象的能力,需要调用 super.clone() 来实现。 从上面我们可以看出,原型模式的主要特征就是使用 clone 方法复制一个对象。通常,有些人会误以为 Object a=new Object();Object b=a; 这种形式就是一种对象复制的过程,然而这种复制只是对象引用的复制,也就是 a 和 b 对象指向了同一个内存地址,如果 b 修改了,a 的值也就跟着被修改了。
我们可以通过一个简单的例子来看看普通的对象复制问题:
class Student {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name= name;
}
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student();
stu1.setName("test1");
Student stu2 = stu1;
stu1.setName("test2");
System.out.println(" 学生 1:" + stu1.getName());
System.out.println(" 学生 2:" + stu2.getName());
}
}
如果是复制对象,此时打印的日志应该为:
学生 1:test1
学生 2:test2
然而,实际上是:
学生 2:test2
学生 2:test2
通过 clone 方法复制的对象才是真正的对象复制,clone 方法赋值的对象完全是一个独立的对象。刚刚讲过了,Object 类的 clone 方法是一个本地方法,它直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显。我们可以用 clone 方法再实现一遍以上例子。
// 学生类实现 Cloneable 接口
class Student implements Cloneable{
private String name; // 姓名
public String getName() {
return name;
}
public void setName(String name) {
this.name= name;
}
// 重写 clone 方法
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student(); // 创建学生 1
stu1.setName("test1");
Student stu2 = stu1.clone(); // 通过克隆创建学生 2
stu2.setName("test2");
System.out.println(" 学生 1:" + stu1.getName());
System.out.println(" 学生 2:" + stu2.getName());
}
}
运行结果:
学生 1:test1
学生 2:test2
深拷贝和浅拷贝
在调用 super.clone() 方法之后,首先会检查当前对象所属的类是否支持 clone,也就是看该类是否实现了 Cloneable 接口。
如果支持,则创建当前对象所属类的一个新对象,并对该对象进行初始化,使得新对象的成员变量的值与当前对象的成员变量的值一模一样,但对于其它对象的引用以及 List 等类型的成员属性,则只能复制这些对象的引用了。所以简单调用 super.clone() 这种克隆对象方式,就是一种浅拷贝。
所以,当我们在使用 clone() 方法实现对象的克隆时,就需要注意浅拷贝带来的问题。我们再通过一个例子来看看浅拷贝。
// 定义学生类
class Student implements Cloneable{
private String name; // 学生姓名
private Teacher teacher; // 定义老师类
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Teacher getTeacher() {
return teacher;
}
public void setName(Teacher teacher) {
this.teacher = teacher;
}
// 重写克隆方法
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
}
// 定义老师类
class Teacher implements Cloneable{
private String name; // 老师姓名
public String getName() {
return name;
}
public void setName(String name) {
this.name= name;
}
// 重写克隆方法,堆老师类进行克隆
public Teacher clone() {
Teacher teacher= null;
try {
teacher= (Teacher) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
}
public class Test {
public static void main(String args[]) {
Teacher teacher = new Teacher (); // 定义老师 1
teacher.setName(" 刘老师 ");
Student stu1 = new Student(); // 定义学生 1
stu1.setName("test1");
stu1.setTeacher(teacher);
Student stu2 = stu1.clone(); // 定义学生 2
stu2.setName("test2");
stu2.getTeacher().setName(" 王老师 ");// 修改老师
System.out.println(" 学生 " + stu1.getName + " 的老师是:" + stu1.getTeacher().getName);
System.out.println(" 学生 " + stu1.getName + " 的老师是:" + stu2.getTeacher().getName);
}
}
运行结果:
学生 test1 的老师是:王老师
学生 test2 的老师是:王老师
观察以上运行结果,我们可以发现:在我们给学生 2 修改老师的时候,学生 1 的老师也跟着被修改了。这就是浅拷贝带来的问题。
我们可以通过深拷贝来解决这种问题,其实深拷贝就是基于浅拷贝来递归实现具体的每个对象,代码如下:
public Student clone() {
Student student = null;
try {
student = (Student) super.clone();
Teacher teacher = this.teacher.clone();// 克隆 teacher 对象
student.setTeacher(teacher);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return student;
}
适用场景
前面我详讲了原型模式的实现原理,那到底什么时候我们要用它呢?
在一些重复创建对象的场景下,我们就可以使用原型模式来提高对象的创建性能。例如,我在开头提到的,循环体内创建对象时,我们就可以考虑用 clone 的方式来实现。
例如:
for(int i=0; i<list.size(); i++){
Student stu = new Student();
...
}
我们可以优化为:
Student stu = new Student();
for(int i=0; i<list.size(); i++){
Student stu1 = (Student)stu.clone();
...
}
除此之外,原型模式在开源框架中的应用也非常广泛。例如 Spring 中,@Service 默认都是单例的。用了私有全局变量,若不想影响下次注入或每次上下文获取 bean,就需要用到原型模式,我们可以通过以下注解来实现,@Scope(“prototype”)。
享元模式
享元模式是运用共享技术有效地最大限度地复用细粒度对象的一种模式。该模式中,以对象的信息状态划分,可以分为内部数据和外部数据。内部数据是对象可以共享出来的信息,这些信息不会随着系统的运行而改变;外部数据则是在不同运行时被标记了不同的值。
享元模式一般可以分为三个角色,分别为 Flyweight(抽象享元类)、ConcreteFlyweight(具体享元类)和 FlyweightFactory(享元工厂类)。抽象享元类通常是一个接口或抽象类,向外界提供享元对象的内部数据或外部数据;具体享元类是指具体实现内部数据共享的类;享元工厂类则是主要用于创建和管理享元对象的工厂类。
实现享元模式
我们还是通过一个简单的例子来实现一个享元模式:
// 抽象享元类
interface Flyweight {
// 对外状态对象
void operation(String name);
// 对内对象
String getType();
}
// 具体享元类
class ConcreteFlyweight implements Flyweight {
private String type;
public ConcreteFlyweight(String type) {
this.type = type;
}
@Override
public void operation(String name) {
System.out.printf("[类型 (内在状态)] - [%s] - [名字 (外在状态)] - [%s]\n", type, name);
}
@Override
public String getType() {
return type;
}
}
// 享元工厂类
class FlyweightFactory {
private static final Map<String, Flyweight> FLYWEIGHT_MAP = new HashMap<>();// 享元池,用来存储享元对象
public static Flyweight getFlyweight(String type) {
if (FLYWEIGHT_MAP.containsKey(type)) {// 如果在享元池中存在对象,则直接获取
return FLYWEIGHT_MAP.get(type);
} else {// 在响应池不存在,则新创建对象,并放入到享元池
ConcreteFlyweight flyweight = new ConcreteFlyweight(type);
FLYWEIGHT_MAP.put(type, flyweight);
return flyweight;
}
}
}
public class Client {
public static void main(String[] args) {
Flyweight fw0 = FlyweightFactory.getFlyweight("a");
Flyweight fw1 = FlyweightFactory.getFlyweight("b");
Flyweight fw2 = FlyweightFactory.getFlyweight("a");
Flyweight fw3 = FlyweightFactory.getFlyweight("b");
fw1.operation("abc");
System.out.printf("[结果 (对象对比)] - [%s]\n", fw0 == fw2);
System.out.printf("[结果 (内在状态)] - [%s]\n", fw1.getType());
}
}
输出结果:
[类型 (内在状态)] - [b] - [名字 (外在状态)] - [abc]
[结果 (对象对比)] - [true]
[结果 (内在状态)] - [b]
观察以上代码运行结果,我们可以发现:如果对象已经存在于享元池中,则不会再创建该对象了,而是共用享元池中内部数据一致的对象。这样就减少了对象的创建,同时也节省了同样内部数据的对象所占用的内存空间。
适用场景
享元模式在实际开发中的应用也非常广泛。例如 Java 的 String 字符串,在一些字符串常量中,会共享常量池中字符串对象,从而减少重复创建相同值对象,占用内存空间。代码如下:
String s1 = "hello";
String s2 = "hello";
System.out.println(s1==s2);//true
还有,在日常开发中的应用。例如,线程池就是享元模式的一种实现;将商品存储在应用服务的缓存中,那么每当用户获取商品信息时,则不需要每次都从 redis 缓存或者数据库中获取商品信息,并在内存中重复创建商品信息了。
三、 如何使用设计模式优化并发编程?
线程上下文设计模式
线程上下文是指贯穿线程整个生命周期的对象中的一些全局信息。例如,我们比较熟悉的 Spring 中的 ApplicationContext 就是一个关于上下文的类,它在整个系统的生命周期中保存了配置信息、用户信息以及注册的 bean 等上下文信息。
这样的解释可能有点抽象,我们不妨通过一个具体的案例,来看看到底在什么的场景下才需要上下文呢?
在执行一个比较长的请求任务时,这个请求可能会经历很多层的方法调用,假设我们需要将最开始的方法的中间结果传递到末尾的方法中进行计算,一个简单的实现方式就是在每个函数中新增这个中间结果的参数,依次传递下去。代码如下:
public class ContextTest {
// 上下文类
public class Context {
private String name;
private long id
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
// 设置上下文名字
public class QueryNameAction {
public void execute(Context context) {
try {
Thread.sleep(1000L);
String name = Thread.currentThread().getName();
context.setName(name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 设置上下文 ID
public class QueryIdAction {
public void execute(Context context) {
try {
Thread.sleep(1000L);
long id = Thread.currentThread().getId();
context.setId(id);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 执行方法
public class ExecutionTask implements Runnable {
private QueryNameAction queryNameAction = new QueryNameAction();
private QueryIdAction queryIdAction = new QueryIdAction();
@Override
public void run() {
final Context context = new Context();
queryNameAction.execute(context);
System.out.println("The name query successful");
queryIdAction.execute(context);
System.out.println("The id query successful");
System.out.println("The Name is " + context.getName() + " and id " + context.getId());
}
}
public static void main(String[] args) {
IntStream.range(1, 5).forEach(i -> new Thread(new ContextTest().new ExecutionTask()).start());
}
}
执行结果:
The name query successful
The name query successful
The name query successful
The name query successful
The id query successful
The id query successful
The id query successful
The id query successful
The Name is Thread-1 and id 11
The Name is Thread-2 and id 12
The Name is Thread-3 and id 13
The Name is Thread-0 and id 10
然而这种方式太笨拙了,每次调用方法时,都需要传入 Context 作为参数,而且影响一些中间公共方法的封装。
那能不能设置一个全局变量呢?如果是在多线程情况下,需要考虑线程安全,这样的话就又涉及到了锁竞争。
除了以上这些方法,其实我们还可以使用 ThreadLocal 实现上下文。ThreadLocal 是线程本地变量,可以实现多线程的数据隔离。ThreadLocal 为每一个使用该变量的线程都提供一份独立的副本,线程间的数据是隔离的,每一个线程只能访问各自内部的副本变量。
ThreadLocal 中有三个常用的方法:set、get、initialValue,我们可以通过以下一个简单的例子来看看 ThreadLocal 的使用:
private void testThreadLocal() {
Thread t = new Thread() {
ThreadLocal<String> mStringThreadLocal = new ThreadLocal<String>();
@Override
public void run() {
super.run();
mStringThreadLocal.set("test");
mStringThreadLocal.get();
}
};
t.start();
}
接下来,我们使用 ThreadLocal 来重新实现最开始的上下文设计。你会发现,我们在两个方法中并没有通过变量来传递上下文,只是通过 ThreadLocal 获取了当前线程的上下文信息:
public class ContextTest {
// 上下文类
public static class Context {
private String name;
private long id;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
// 复制上下文到 ThreadLocal 中
public final static class ActionContext {
private static final ThreadLocal<Context> threadLocal = new ThreadLocal<Context>() {
@Override
protected Context initialValue() {
return new Context();
}
};
public static ActionContext getActionContext() {
return ContextHolder.actionContext;
}
public Context getContext() {
return threadLocal.get();
}
// 获取 ActionContext 单例
public static class ContextHolder {
private final static ActionContext actionContext = new ActionContext();
}
}
// 设置上下文名字
public class QueryNameAction {
public void execute() {
try {
Thread.sleep(1000L);
String name = Thread.currentThread().getName();
ActionContext.getActionContext().getContext().setName(name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 设置上下文 ID
public class QueryIdAction {
public void execute() {
try {
Thread.sleep(1000L);
long id = Thread.currentThread().getId();
ActionContext.getActionContext().getContext().setId(id);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 执行方法
public class ExecutionTask implements Runnable {
private QueryNameAction queryNameAction = new QueryNameAction();
private QueryIdAction queryIdAction = new QueryIdAction();
@Override
public void run() {
queryNameAction.execute();// 设置线程名
System.out.println("The name query successful");
queryIdAction.execute();// 设置线程 ID
System.out.println("The id query successful");
System.out.println("The Name is " + ActionContext.getActionContext().getContext().getName() + " and id " + ActionContext.getActionContext().getContext().getId())
}
}
public static void main(String[] args) {
IntStream.range(1, 5).forEach(i -> new Thread(new ContextTest().new ExecutionTask()).start());
}
}
运行结果:
The name query successful
The name query successful
The name query successful
The name query successful
The id query successful
The id query successful
The id query successful
The id query successful
The Name is Thread-2 and id 12
The Name is Thread-0 and id 10
The Name is Thread-1 and id 11
The Name is Thread-3 and id 13
Thread-Per-Message 设计模式
Thread-Per-Message 设计模式翻译过来的意思就是每个消息一个线程的意思。例如,我们在处理 Socket 通信的时候,通常是一个线程处理事件监听以及 I/O 读写,如果 I/O 读写操作非常耗时,这个时候便会影响到事件监听处理事件。
这个时候 Thread-Per-Message 模式就可以很好地解决这个问题,一个线程监听 I/O 事件,每当监听到一个 I/O 事件,则交给另一个处理线程执行 I/O 操作。下面,我们还是通过一个例子来学习下该设计模式的实现。
//IO 处理
public class ServerHandler implements Runnable{
private Socket socket;
public ServerHandler(Socket socket) {
this.socket = socket;
}
public void run() {
BufferedReader in = null;
PrintWriter out = null;
String msg = null;
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(),true);
while ((msg = in.readLine()) != null && msg.length()!=0) {// 当连接成功后在此等待接收消息(挂起,进入阻塞状态)
System.out.println("server received : " + msg);
out.print("received~\n");
out.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//Socket 启动服务
public class Server {
private static int DEFAULT_PORT = 12345;
private static ServerSocket server;
public static void start() throws IOException {
start(DEFAULT_PORT);
}
public static void start(int port) throws IOException {
if (server != null) {
return;
}
try {
// 启动服务
server = new ServerSocket(port);
// 通过无线循环监听客户端连接
while (true) {
Socket socket = server.accept();
// 当有新的客户端接入时,会执行下面的代码
long start = System.currentTimeMillis();
new Thread(new ServerHandler(socket)).start();
long end = System.currentTimeMillis();
System.out.println("Spend time is " + (end - start));
}
} finally {
if (server != null) {
System.out.println(" 服务器已关闭。");
server.close();
}
}
}
public static void main(String[] args) throws InterruptedException{
// 运行服务端
new Thread(new Runnable() {
public void run() {
try {
Server.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
以上,我们是完成了一个使用 Thread-Per-Message 设计模式实现的 Socket 服务端的代码。但这里是有一个问题的,你发现了吗?
使用这种设计模式,如果遇到大的高并发,就会出现严重的性能问题。如果针对每个 I/O 请求都创建一个线程来处理,在有大量请求同时进来时,就会创建大量线程,而此时 JVM 有可能会因为无法处理这么多线程,而出现内存溢出的问题。
退一步讲,即使是不会有大量线程的场景,每次请求过来也都需要创建和销毁线程,这对系统来说,也是一笔不小的性能开销。
面对这种情况,我们可以使用线程池来代替线程的创建和销毁,这样就可以避免创建大量线程而带来的性能问题,是一种很好的调优方法。
Worker-Thread 设计模式
这里的 Worker 是工人的意思,代表在 Worker Thread 设计模式中,会有一些工人(线程)不断轮流处理过来的工作,当没有工作时,工人则会处于等待状态,直到有新的工作进来。除了工人角色,Worker Thread 设计模式中还包括了流水线和产品。
这种设计模式相比 Thread-Per-Message 设计模式,可以减少频繁创建、销毁线程所带来的性能开销,还有无限制地创建线程所带来的内存溢出风险。
我们可以假设一个场景来看下该模式的实现,通过 Worker Thread 设计模式来完成一个物流分拣的作业。
假设一个物流仓库的物流分拣流水线上有 8 个机器人,它们不断从流水线上获取包裹并对其进行包装,送其上车。当仓库中的商品被打包好后,会投放到物流分拣流水线上,而不是直接交给机器人,机器人会再从流水线中随机分拣包裹。代码如下:
// 包裹类
public class Package {
private String name;
private String address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public void execute() {
System.out.println(Thread.currentThread().getName()+" executed "+this);
}
}
// 流水线
public class PackageChannel {
private final static int MAX_PACKAGE_NUM = 100;
private final Package[] packageQueue;
private final Worker[] workerPool;
private int head;
private int tail;
private int count;
public PackageChannel(int workers) {
this.packageQueue = new Package[MAX_PACKAGE_NUM];
this.head = 0;
this.tail = 0;
this.count = 0;
this.workerPool = new Worker[workers];
this.init();
}
private void init() {
for (int i = 0; i < workerPool.length; i++) {
workerPool[i] = new Worker("Worker-" + i, this);
}
}
/**
* push switch to start all of worker to work
*/
public void startWorker() {
Arrays.asList(workerPool).forEach(Worker::start);
}
public synchronized void put(Package packagereq) {
while (count >= packageQueue.length) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.packageQueue[tail] = packagereq;
this.tail = (tail + 1) % packageQueue.length;
this.count++;
this.notifyAll();
}
public synchronized Package take() {
while (count <= 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Package request = this.packageQueue[head];
this.head = (this.head + 1) % this.packageQueue.length;
this.count--;
this.notifyAll();
return request;
}
}
// 机器人
public class Worker extends Thread{
private static final Random random = new Random(System.currentTimeMillis());
private final PackageChannel channel;
public Worker(String name, PackageChannel channel) {
super(name);
this.channel = channel;
}
@Override
public void run() {
while (true) {
channel.take().execute();
try {
Thread.sleep(random.nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test {
public static void main(String[] args) {
// 新建 8 个工人
final PackageChannel channel = new PackageChannel(8);
// 开始工作
channel.startWorker();
// 为流水线添加包裹
for(int i=0; i<100; i++) {
Package packagereq = new Package();
packagereq.setAddress("test");
packagereq.setName("test");
channel.put(packagereq);
}
}
}
我们可以看到,这里有 8 个工人在不断地分拣仓库中已经包装好的商品。