实践篇:如何编写线程安全的类

引言

本章节来聊聊线程安全类的实现方式,日常开发中读者可能都用过的,这里作一个简单的汇总。目前有几种常用的线程安全类的实现方式,悉数如下:

  1. 设计不变的类,如枚举类;
  2. 利用同步锁来保证线程操作的安全性,参考示例如何实现线程安全的单例类;
  3. 使用 ThreadLocal ,每个线程维护自己的复本数据,避免共享数据;

如何设计不变类

并发环境中,一个类如果是不变的,那么它一定就是线程安全的。这是设计模式中不变模式的一种:一个对象在创建之后,它的状态就不会再发生变化,它就是不变类。

Java 中的 String,各种基本类型的封装类型,都是不变类。在设计任何一个类的时候,应当慎重考虑其状态是否需要变化,如果其状态没有变化的必要,那么就应当将它设计成不变类。

设计不变的类,应该限制成员变量的操作,只提供 get 方法,且所有引用属性的访问都使用拷贝,避免直接与外界发生联系。一个简单的不变类编写如下:

typescript 复制代码
import java.util.Date;

/**
 * 不可变对象定义,不提供set方法,避免外界对该对象属性进行修改
 * 对象中用到的引用类型,使用拷贝而非直接引用
 * 引用对象的get方法,也是提供拷贝
 */
public class ImmutableModel {
	private String name;
	private int age;
	private Date birthday;
	
	public ImmutableModel(String name,int age,Date birthday){
		this.name = name;
		this.age = age;
		//引用类型,使用拷贝,避免原对象的状态变化的影响
		if(birthday!=null){
			this.birthday = new Date(birthday.getTime());
		}
	}

	public String getName() {
		return name;
	}

	public int getAge() {
		return age;
	}

	//返回引用类型,也使用拷贝,避免外界直接拿到引用数据后修改造成的影响
	public Date getBirthday() {
		if(birthday!=null){
			return new Date(birthday.getTime());
		}
		return null;
	}
}

这个不变类一旦创建,它的状态就是恒定的,所以也是线程安全的。

单例模式的数据库连接管理类

单例,顾名思义,就是整个系统中,某个类的实例只有一个,这是一种设计模式,它可以保证系统中的重要资源类只能创建一个实例对象。艾迪生维斯理 1994 年对单例模式的描述为:

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式的静态类图如下(引自百度百科):

自定义单例模式的要点

  1. 构造函数私有化;
  2. 实例获取方法是线程安全的;
  3. 根据创建实例的时间不同,区分懒汉模式和饿汉模式。

单例类,由于全局只有一个实例,所以需要使用同步措施来保证单例对象操作的安全性。数据库连接池或者数据库连接等管理类,通常被设计为单例类。

SQLite 数据库的元数据存在在一个 .db 文件中,它在开启一个事务时,会将整个数据库文件锁定,因此并发环境下使用 SQLite 数据库需要警惕数据库被锁定的情况发生,将 SQLite 数据库操作类设计成单例、且线程安全的类,可以避免锁库异常。

这是 SqliteJdbcManager 单例实现方式:

csharp 复制代码
    import java.sql.*;
	import java.util.List;

	import org.slf4j.Logger;
	import org.slf4j.LoggerFactory;

	/**
	 *  sqlite数据库管理实体类,单例模式设计,避免锁库异常
	 */
	public class SqliteJdbcManager {

		/**
		 *  数据库 Connection对象
		 */
		private Connection connection;
		
		/**
		 *  数据库 Statement对象
		 */
		private Statement statement;

		/**
		 * 定义单例实例,提供静态方法
		 */
		private static SqliteJdbcManager instance = null;

		/**
		 * 单例必须提供一个私有的构造函数
		 */
		private SqliteJdbcManager(){

		}

		/**
		 * 提供单例方法:懒汉模式
		 * @return
		 */
		public static SqliteJdbcManager getInstance(){
			if(instance == null ){
				synchronized (SqliteJdbcManager.class) {
					if (instance == null) {
						instance = new SqliteJdbcManager();
						return instance;
					}
				}
			}

			return instance;
		}

		/**
		 *  日志打印对象 
		 */
		private static Logger logger = LoggerFactory.getLogger(SqliteJdbcManager.class);
		
		
		/**
		 *  sqlite数据文件 
		 */
		private static String dataFile = SystemConstants.dataFilePath + "/monitor.db";
		
		/**
		 *  取得数据库连接 
		 */
		public synchronized Connection getConnection(){
			try {
				Class.forName("org.sqlite.JDBC");
				connection = DriverManager.getConnection("jdbc:sqlite:" + dataFile);
				connection.setAutoCommit(false);
			} catch (Exception e) {
				logger.error("取数据库连接异常", e);	
			}		
			
			return connection;
		}

		/**
		 *  执行数据库操作: 添加  更新  删除
		 */
		public synchronized int executeUpdate(String sql, List<Object> params){
			if(connection == null){
				connection = getConnection();
			}

			int result = 0;
			PreparedStatement statement = null;
			try {
				statement = connection.prepareStatement(sql);
				if(params!=null && params.size()>0){
					for(int i =1 ;i< params.size()+1;i++){
						statement.setObject(i,params.get(i-1));
					}
				}
				result = statement.executeUpdate();
				connection.commit();
			} catch (SQLException e) {
				logger.error("数据操作异常", e);
			}finally {
				if(statement != null ){
					try {
						statement.close();
					} catch (SQLException e) {
						logger.error("数据操作异常", e);
					}
				}
			}
			return result;
		}
		
		/**
		 *  关闭资源
		 */
		public synchronized void close(){
			if(statement != null){
				try {
					statement.close();
					statement = null;
				} catch (SQLException e) {
					logger.error("资源关闭异常", e);	
				}
			}
			
			if(connection != null){
				try {
					connection.close();
					connection = null;
				} catch (SQLException e) {
					logger.error("资源关闭异常", e);	
				}
			}
		}
	}

ThreadLocal 变量和普通变量的区别

Java 提供了 ThreadLocal 这个类型,具有该类型的成员变量,每个线程都可以保留一份它的复本数据,通过set方法设置;在线程内部用 get 方法获取自己备份的数据。

这个备份并不是 JVM 自己备份的,而是通过 ThreadLocalset 方法完成的,它的本质是以当前线程的 Idkey ,存储该线程的数据。如果每个线程 set 的值都没有关联,那么这个成员的值肯定是线程安全的;但是如果两个线程在 set 时引用了同一个数据,那么就仍然会存在同步问题。

ThreadLocal 的本质是,每个线程只能获取到自己 set 的数据,它和普通成员的区别是:在多线程环境,每个线程都会存一个ThreadLocal 的值到自己的上下文环境,每个线程 get 到的都是自己 set 的复本数据,该类型的变量的数据在整个应用中有 N 份复本;而普通成员则被所有线程共享,是一份数据。

编写一个基于 ThreadLocal 的类,它同时含有两个成员变量,一个为普通类型,一个为 ThreadLocal 类型:

typescript 复制代码
import java.util.Date;

public class MyThreadLocal {
	private ThreadLocal date = new ThreadLocal();
	private Date d = null;
	
	public void process(){
		if(date.get()==null){
			date.set(new Date());
			System.out.println("thread local fileld:"+date.get());
		}
	}
	//操作普通成员,需要同步处理
	public void p(){
		synchronized(MyThreadLocal.class){
			if(d==null){
				d = new Date();
				System.out.println("ordinary field:"+d);
			}
		}
	}
}

测试类:定义一个 MyThreadLocal 对象实例,由 5 个线程同时访问它的方法。

ini 复制代码
import java.util.Date;

public class Test {
	public static void main(String[] args) {
		final MyThreadLocal t = new MyThreadLocal();
		for(int i = 0;i<5;i++){
			Thread thread = new Thread(){
				public void run(){
					t.process();
					t.p();
				}
			};
			thread.start();
		}
		Date d1 = new Date();
		Date d2 = new Date();
		System.out.println(d1==d2);
		System.out.println(d1.hashCode()==d2.hashCode());
	}
}

测试结果:

yaml 复制代码
false
true
thread local fileld:Fri Apr 10 14:47:30 CST 2019
ordinary field:Fri Apr 10 14:47:31 CST 2019
thread local fileld:Fri Apr 10 14:47:30 CST 2019
thread local fileld:Fri Apr 10 14:47:30 CST 2019
thread local fileld:Fri Apr 10 14:47:30 CST 2019
thread local fileld:Fri Apr 10 14:47:30 CST 2019

测试结果分析:共享成员变量 d 只被一个线程初始化了一次,所以 p方法只执行了一次;而 ThreadLocal 类型的成员变量,每个访问该变量的线程都会自己创建一个副本数据,process 方法被执行了五次。

此外,还发现两次 new Date() 得到的对象的 hashCode 很容易相等,但的确是两个不同对象。

笔者本以为 ThreadLocal 的副本是对 set(Object) 传入的对象进行深拷贝后,存在对应线程的 value 中,测试发现自己对误解了。它的 set 仅仅仅是往全局 Map 中存了一个值,这是 set 操作的源码:

scss 复制代码
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

多个线程的 ThreadLocal 维护的是同一个数据时,这个值同样存在同步问题。即当多个线程 set 的是同一个引用时,还是需要留意同步问题。

相关推荐
阿伟*rui2 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
XiaoLeisj4 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck4 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei4 小时前
java的类加载机制的学习
java·学习
码农小旋风5 小时前
详解K8S--声明式API
后端
Peter_chq5 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml45 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~6 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616886 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7896 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot