高并发下线程安全的单例模式(最全最经典)

在所有的设计模式中,单例模式是我们在项目开发中最为常见的设计模式之一,而单例模式有很多种实现方式,你是否都了解呢?高并发下如何保证单例模式的线程安全性呢?如何保证序列化后的单例对象在反序列化后任然是单例的呢?这些问题在看了本文之后都会一一的告诉你答案,赶快来阅读吧!

什么是单例模式?

在文章开始之前我们还是有必要介绍一下什么是单例模式。单例模式是为确保一个类只有一个实例,并为整个系统提供一个全局访问点的一种模式方法。

从概念中体现出了单例的一些特点:

(1)、在任何情况下,单例类永远只有一个实例存在

(2)、单例需要有能力为整个系统提供这一唯一实例 


为了便于读者更好的理解这些概念,下面给出这么一段内容叙述:

在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。

正是由于这个特点,单例对象通常作为程序中的存放配置信息的载体,因为它能保证其他对象读到一致的信息。例如在某个服务器程序中,该服务器的配置信息可能存放在数据库或文件中,这些配置数据由某个单例对象统一读取,服务进程中的其他对象如果要获取这些配置信息,只需访问该单例对象即可。这种方式极大地简化了在复杂环境 下,尤其是多线程环境下的配置管理,但是随着应用场景的不同,也可能带来一些同步问题。
    

各式各样的单例实现

温馨提示:本文叙述中涉及到的相关源码可以在这里进行下载源码,读者可免积分下载。

1、饿汉式单例

饿汉式单例是指在方法调用前,实例就已经创建好了。下面是实现代码:

package org.mlinge.s01;

public class MySingleton {
	
	private static MySingleton instance = new MySingleton();
	
	private MySingleton(){}
	
	public static MySingleton getInstance() {
		return instance;
	}
	
}
以上是单例的饿汉式实现,我们来看看饿汉式在多线程下的执行情况,给出一段多线程的执行代码:

package org.mlinge.s01;

public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		System.out.println(MySingleton.getInstance().hashCode());
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[10];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}

以上代码运行结果:

1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954

从运行结果可以看出实例变量额hashCode值一致,这说明对象是同一个,饿汉式单例实现了。

2、懒汉式单例

懒汉式单例是指在方法调用获取实例时才创建实例,因为相对饿汉式显得“不急迫”,所以被叫做“懒汉模式”。下面是实现代码:

package org.mlinge.s02;

public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	
	public static MySingleton getInstance() {
		if(instance == null){//懒汉式
			instance = new MySingleton();
		}
		return instance;
	}
}
这里实现了懒汉式的单例,但是熟悉多线程并发编程的朋友应该可以看出,在多线程并发下这样的实现是无法保证实例实例唯一的,甚至可以说这样的失效是完全错误的,下面我们就来看一下多线程并发下的执行情况,这里为了看到效果,我们对上面的代码做一小点修改:

package org.mlinge.s02;

public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	
	public static MySingleton getInstance() {
		try { 
			if(instance != null){//懒汉式 
				
			}else{
				//创建实例之前可能会有一些准备性的耗时工作 
				Thread.sleep(300);
				instance = new MySingleton();
			}
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}
这里假设在创建实例前有一些准备性的耗时工作要处理,多线程调用:

package org.mlinge.s02;

public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		System.out.println(MySingleton.getInstance().hashCode());
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[10];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}

执行结果如下:

1210420568
1210420568
1935123450
1718900954
1481297610
1863264879
369539795
1210420568
1210420568
602269801

从这里执行结果可以看出,单例的线程安全性并没有得到保证,那要怎么解决呢?

3、线程安全的懒汉式单例

要保证线程安全,我们就得需要使用同步锁机制,下面就来看看我们如何一步步的解决 存在线程安全问题的懒汉式单例(错误的单例)。

(1)、 方法中声明synchronized关键字

出现非线程安全问题,是由于多个线程可以同时进入getInstance()方法,那么只需要对该方法进行synchronized的锁同步即可:

package org.mlinge.s03;

public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	
	public synchronized static MySingleton getInstance() {
		try { 
			if(instance != null){//懒汉式 
				
			}else{
				//创建实例之前可能会有一些准备性的耗时工作 
				Thread.sleep(300);
				instance = new MySingleton();
			}
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}

此时任然使用前面验证多线程下执行情况的MyThread类来进行验证,将其放入到org.mlinge.s03包下运行,执行结果如下:

1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373

从执行结果上来看,问题已经解决了,但是这种实现方式的运行效率会很低。同步方法效率低,那我们考虑使用同步代码块来实现:

(2)、 同步代码块实现

package org.mlinge.s03;

public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	
	//public synchronized static MySingleton getInstance() {
	public static MySingleton getInstance() {
		try { 
			synchronized (MySingleton.class) {
				if(instance != null){//懒汉式 
					
				}else{
					//创建实例之前可能会有一些准备性的耗时工作 
					Thread.sleep(300);
					instance = new MySingleton();
				}
			}
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}
这里的实现能够保证多线程并发下的线程安全性,但是这样的实现将全部的代码都被锁上了,同样的效率很低下。

(3)、 针对某些重要的代码来进行单独的同步(可能非线程安全)

针对某些重要的代码进行单独的同步,而不是全部进行同步,可以极大的提高执行效率,我们来看一下:

package org.mlinge.s04;

public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() {
		try {  
			if(instance != null){//懒汉式 
				
			}else{
				//创建实例之前可能会有一些准备性的耗时工作 
				Thread.sleep(300);
				synchronized (MySingleton.class) {
					instance = new MySingleton();
				}
			} 
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}
此时同样使用前面验证多线程下执行情况的MyThread类来进行验证,将其放入到org.mlinge.s04包下运行,执行结果如下:
1481297610
397630378
1863264879
1210420568
1935123450
369539795
590202901
1718900954
1689058373
602269801
从运行结果来看,这样的方法进行代码块同步,代码的运行效率是能够得到提升,但是却 没能保住线程的安全性。看来还得进一步考虑如何解决此问题。

(4)、 Double Check Locking 双检查锁机制(推荐)

为了达到线程安全,又能提高代码执行效率,我们这里可以采用DCL的双检查锁机制来完成,代码实现如下:

package org.mlinge.s05;

public class MySingleton {
	
	//使用volatile关键字保其可见性
	volatile private static MySingleton instance = null;
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() {
		try {  
			if(instance != null){//懒汉式 
				
			}else{
				//创建实例之前可能会有一些准备性的耗时工作 
				Thread.sleep(300);
				synchronized (MySingleton.class) {
					if(instance == null){//二次检查
						instance = new MySingleton();
					}
				}
			} 
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}
将前面验证多线程下执行情况的MyThread类放入到org.mlinge.s05包下运行,执行结果如下:
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
从运行结果来看,该中方法保证了多线程并发下的线程安全性。

这里在声明变量时使用了volatile关键字来保证其线程间的可见性;在同步代码块中使用二次检查,以保证其不被重复实例化。集合其二者,这种实现方式既保证了其高效性,也保证了其线程安全性。

4、使用静态内置类实现单例模式

DCL解决了多线程并发下的线程安全问题,其实使用其他方式也可以达到同样的效果,代码实现如下:

package org.mlinge.s06;

public class MySingleton {
	
	//内部类
	private static class MySingletonHandler{
		private static MySingleton instance = new MySingleton();
	} 
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() { 
		return MySingletonHandler.instance;
	}
}
以上代码就是使用静态内置类实现了单例模式,这里将前面验证多线程下执行情况的MyThread类放入到org.mlinge.s06包下运行,执行结果如下:
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
从运行结果来看,静态内部类实现的单例在多线程并发下单个实例得到了保证。

5、序列化与反序列化的单例模式实现

静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到序列化对象时,默认的方式运行得到的结果就是多例的。

代码实现如下:

package org.mlinge.s07;

import java.io.Serializable;

public class MySingleton implements Serializable {
	 
	private static final long serialVersionUID = 1L;

	//内部类
	private static class MySingletonHandler{
		private static MySingleton instance = new MySingleton();
	} 
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() { 
		return MySingletonHandler.instance;
	}
}
序列化与反序列化测试代码:

package org.mlinge.s07;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SaveAndReadForSingleton {
	
	public static void main(String[] args) {
		MySingleton singleton = MySingleton.getInstance();
		
		File file = new File("MySingleton.txt");
		
		try {
			FileOutputStream fos = new FileOutputStream(file);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			oos.writeObject(singleton);
			fos.close();
			oos.close();
			System.out.println(singleton.hashCode());
		} catch (FileNotFoundException e) { 
			e.printStackTrace();
		} catch (IOException e) { 
			e.printStackTrace();
		}
		
		try {
			FileInputStream fis = new FileInputStream(file);
			ObjectInputStream ois = new ObjectInputStream(fis);
			MySingleton rSingleton = (MySingleton) ois.readObject();
			fis.close();
			ois.close();
			System.out.println(rSingleton.hashCode());
		} catch (FileNotFoundException e) { 
			e.printStackTrace();
		} catch (IOException e) { 
			e.printStackTrace();
		} catch (ClassNotFoundException e) { 
			e.printStackTrace();
		}
		
	}
}
运行以上代码,得到的结果如下:

865113938
1442407170
从结果中我们发现,序列号对象的hashCode和反序列化后得到的对象的hashCode值不一样,说明反序列化后返回的对象是重新实例化的,单例被破坏了。那怎么来解决这一问题呢?

解决办法就是在反序列化的过程中使用readResolve()方法,单例实现的代码如下:

package org.mlinge.s07;

import java.io.ObjectStreamException;
import java.io.Serializable;

public class MySingleton implements Serializable {
	 
	private static final long serialVersionUID = 1L;

	//内部类
	private static class MySingletonHandler{
		private static MySingleton instance = new MySingleton();
	} 
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() { 
		return MySingletonHandler.instance;
	}
	
	//该方法在反序列化时会被调用,该方法不是接口定义的方法,有点儿约定俗成的感觉
	protected Object readResolve() throws ObjectStreamException {
		System.out.println("调用了readResolve方法!");
		return MySingletonHandler.instance; 
	}
}

再次运行上面的测试代码,得到的结果如下:

865113938
调用了readResolve方法!
865113938
从运行结果可知,添加readResolve方法后反序列化后得到的实例和序列化前的是同一个实例,单个实例得到了保证。

6、使用static代码块实现单例

静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码块的这个特性的实现单例设计模式。

package org.mlinge.s08;

public class MySingleton{
	 
	private static MySingleton instance = null;
	 
	private MySingleton(){}

	static{
		instance = new MySingleton();
	}
	
	public static MySingleton getInstance() { 
		return instance;
	} 
}
测试代码如下:

package org.mlinge.s08;

public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		for (int i = 0; i < 5; i++) {
			System.out.println(MySingleton.getInstance().hashCode());
		}
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[3];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}
运行结果如下:

1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
从运行结果看,单例的线程安全性得到了保证。

7、使用枚举数据类型实现单例模式

枚举enum和静态代码块的特性相似,在使用枚举时,构造方法会被自动调用,利用这一特性也可以实现单例:

package org.mlinge.s09;

public enum EnumFactory{ 
    
    singletonFactory;
    
    private MySingleton instance;
    
    private EnumFactory(){//枚举类的构造方法在类加载是被实例化
        instance = new MySingleton();
    }
        
    public MySingleton getInstance(){
        return instance;
    }
    
}

class MySingleton{//需要获实现单例的类,比如数据库连接Connection
    public MySingleton(){} 
}
测试代码如下:

package org.mlinge.s09;

public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		System.out.println(EnumFactory.singletonFactory.getInstance().hashCode());
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[10];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}
执行后得到的结果:
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
运行结果表明单例得到了保证,但是这样写枚举类被完全暴露了,据说违反了“职责单一原则”,那我们来看看怎么进行改造呢。

8、完善使用enum枚举实现单例模式

不暴露枚举类实现细节的封装代码如下:

package org.mlinge.s10;

public class ClassFactory{ 
	
	private enum MyEnumSingleton{
		singletonFactory;
		
		private MySingleton instance;
		
		private MyEnumSingleton(){//枚举类的构造方法在类加载是被实例化
			instance = new MySingleton();
		}
 
		public MySingleton getInstance(){
			return instance;
		}
	} 
 
	public static MySingleton getInstance(){
		return MyEnumSingleton.singletonFactory.getInstance();
	}
}

class MySingleton{//需要获实现单例的类,比如数据库连接Connection
	public MySingleton(){} 
}

验证单例实现的代码如下:

package org.mlinge.s10;

public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		System.out.println(ClassFactory.getInstance().hashCode());
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[10];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}
验证结果:

1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
验证结果表明,完善后的单例实现更为合理。

以上就是本文要介绍的所有单例模式的实现,相信认真阅读的读者都已经明白文章开头所引入的那几个问题了,祝大家读得开心:-D!


备注:本文的编写思路和实例源码参照《Java多线程编程核心技术》-(高洪岩)一书中第六章的学习案例撰写。


  • 145
    点赞
  • 509
    收藏
    觉得还不错? 一键收藏
  • 65
    评论
### 回答1: 在Linux系统下,可以使用线程实现串口通信例程。 首先,需要引入一些头文件,如`<stdio.h>, <stdlib.h>, <unistd.h>, <fcntl.h>, <termios.h>, <pthread.h>`,以便使用相关函数和数据结构。 接下来,打开串口设备文件,使用`open()`函数,并通过`<fcntl.h>`中的`O_RDWR`参数设置为可读写模式。例如,打开`/dev/ttyS0`串口设备: ```c int fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY); if (fd == -1) { perror("打开串口失败"); exit(EXIT_FAILURE); } ``` 然后,配置串口属性,包括波特率、数据位、停止位等。首先需要获取当前串口属性,使用`tcgetattr()`函数,并通过`<termios.h>`中的数据结构`struct termios`进行配置。例如,设置波特率为115200: ```c struct termios attr; if (tcgetattr(fd, &attr) == -1) { perror("获取串口属性失败"); close(fd); exit(EXIT_FAILURE); } cfsetispeed(&attr, B115200); cfsetospeed(&attr, B115200); if (tcsetattr(fd, TCSANOW, &attr) == -1) { perror("设置串口属性失败"); close(fd); exit(EXIT_FAILURE); } ``` 接下来,创建一个线程,用于接收串口数据。使用`pthread_create()`函数,并编写线程函数。例如,以下为接收串口数据的线程函数: ```c void *receiveThread(void *arg) { char buffer[256]; int len; while (1) { len = read(fd, buffer, sizeof(buffer)); if (len > 0) { // 处理接收到的数据 // ... } } return NULL; } pthread_t tid; pthread_create(&tid, NULL, receiveThread, NULL); ``` 最后,主线程(或其他线程)可以通过`write()`函数向串口发送数据。例如,向串口发送一个字符串: ```c char *str = "Hello, Serial!"; write(fd, str, strlen(str)); ``` 整个程序运行时,主线程可以继续执行其他任务,而串口数据的接收则在单独的线程中进行。 这样,就完成了一个简单的Linux下线程实现串口通信的例程。 ### 回答2: 在Linux下,可以通过使用线程来实现串口通信。下面是一个简单的示例代码: ```c #include <stdio.h> #include <fcntl.h> #include <termios.h> #include <unistd.h> #include <pthread.h> int fd; // 串口文件描述符 pthread_t thread_id; // 线程ID void* read_thread(void* arg) { char buf[255]; while(1) { int len = read(fd, buf, sizeof(buf)); // 从串口读取数据 if (len > 0) { buf[len] = '\0'; // 添加字符串结束符 printf("接收到的数据: %s\n", buf); } } } int main() { // 打开串口 fd = open("/dev/ttyS0", O_RDWR | O_NOCTTY | O_NDELAY); if (fd == -1) { printf("无法打开串口\n"); return -1; } // 配置串口 struct termios options; tcgetattr(fd, &options); cfsetispeed(&options, B9600); cfsetospeed(&options, B9600); options.c_cflag |= (CLOCAL | CREAD); options.c_cflag &= ~PARENB; options.c_cflag &= ~CSTOPB; options.c_cflag &= ~CSIZE; options.c_cflag |= CS8; options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); tcsetattr(fd, TCSANOW, &options); // 创建读取数据的线程 pthread_create(&thread_id, NULL, read_thread, NULL); // 主线程继续执行其他任务 while(1) { // 发送数据到串口 char msg[] = "Hello, Serial Port!"; write(fd, msg, sizeof(msg)); usleep(1000000); // 等待1秒 } // 关闭串口 close(fd); return 0; } ``` 在上述代码中,通过`open`函数打开了串口设备文件`/dev/ttyS0`(请根据实际情况更改),然后使用`termios`结构体配置了串口的波特率、数据位、停止位等属性。接下来,通过`pthread_create`函数创建了一个线程,该线程负责读取串口数据。主线程则负责发送数据到串口。 需要注意的是,该例程只是一个简单的示例,仅用于说明线程实现串口通信的基本思路。实际应用中,还需考虑数据的解析、错误处理、线程同步等问题。 ### 回答3: 在Linux下,可以使用串口通信库来实现线程的串口通信例程。下面是一个简单的例子: 1. 首先,我们需要安装和配置串口通信库。常用的库包括`libserialport`和`termios`。你可以使用包管理工具来安装这些库。 2. 在程序中,我们需要引入相关的头文件和库: ``` #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> #include <serialport.h> ``` 3. 然后,创建一个线程来读取串口数据。可以定义一个函数作为线程入口点,例如`serial_read`: ``` void *serial_read(void *data) { struct sp_port *serial_port = (struct sp_port *)data; char buffer[256]; int n; while (1) { n = sp_blocking_read(serial_port, buffer, sizeof(buffer), 100); if (n > 0) { // 处理接收到的数据 printf("Received: %.*s\n", n, buffer); } else { // 读取数据出错或超时 printf("Serial read error or timeout\n"); } } return NULL; } ``` 4. 接下来,创建一个线程来发送串口数据。可以定义一个函数作为线程入口点,例如`serial_write`: ``` void *serial_write(void *data) { struct sp_port *serial_port = (struct sp_port *)data; char message[] = "Hello, Serial Port!\n"; while (1) { sp_nonblocking_write(serial_port, message, sizeof(message) - 1); usleep(1000000); // 暂停1秒钟 } return NULL; } ``` 5. 在主函数中,打开串口设备并创建两个线程: ``` int main() { struct sp_port *serial_port; pthread_t read_thread, write_thread; // 打开串口设备(例如:/dev/ttyS0) sp_get_port_by_name("ttyS0", &serial_port); sp_open(serial_port, SP_MODE_READ_WRITE); // 创建读取线程 pthread_create(&read_thread, NULL, serial_read, (void *)serial_port); // 创建写入线程 pthread_create(&write_thread, NULL, serial_write, (void *)serial_port); // 等待线程结束 pthread_join(read_thread, NULL); pthread_join(write_thread, NULL); // 关闭串口设备 sp_close(serial_port); sp_free_port(serial_port); return 0; } ``` 这是一个简单的例程,通过两个线程实现了线程的串口通信。读取线程通过不断调用`sp_blocking_read`函数读取串口数据,而写入线程通过不断调用`sp_nonblocking_write`函数发送串口数据。你可以根据需求来修改和扩展这个例程。
评论 65
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值