引言
本章节来聊聊线程安全类的实现方式,日常开发中读者可能都用过的,这里作一个简单的汇总。目前有几种常用的线程安全类的实现方式,悉数如下:
- 设计不变的类,如枚举类;
- 利用同步锁来保证线程操作的安全性,参考示例如何实现线程安全的单例类;
- 使用
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 年对单例模式的描述为:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式的静态类图如下(引自百度百科):
自定义单例模式的要点
- 构造函数私有化;
- 实例获取方法是线程安全的;
- 根据创建实例的时间不同,区分懒汉模式和饿汉模式。
单例类,由于全局只有一个实例,所以需要使用同步措施来保证单例对象操作的安全性。数据库连接池或者数据库连接等管理类,通常被设计为单例类。
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 自己备份的,而是通过 ThreadLocal
的 set
方法完成的,它的本质是以当前线程的 Id
为 key
,存储该线程的数据。如果每个线程 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
的是同一个引用时,还是需要留意同步问题。