您的位置:首页 > 编程语言 > Java开发

【098】Java利用对象池配合synchronized同步块实现较高效率的线程同步

2017-11-30 23:50 573 查看

业务场景

web服务器端开发的时候,一般我们的Java代码是多线程执行的,并且是多线程地向数据库里插入数据。在插入数据的时候,我们很可能碰到这样一种情况:一个用户在一定条件下,只可能向数据库里插入一条数据。同时许多相关的业务逻辑也是以只有一条数据为基础而设计实现的。为了保证数据的完整性,我们应该确保只有一条数据。但是在实际应用中,服务器程序很可能向数据库插入了多条数据。

为了给读者解释地更明白,我假设下面这样一种场景:

在线考试系统,有一个用户答案表,记录每个用户填写的每道题目的答案。假设表的结构如下:

表名: t_user_answer

主键: c_id

用户ID: c_user_id

题目ID: c_question_id

用户答案: c_user_answer

显然,保存一个用户做的某一道题目的答案,只需要一条记录就够了。也就是说,用户ID和题目ID的组合,理应是唯一的。即从业务上来说,用户ID和题目ID可以做 t_user_answer 的联合主键。当然用联合主键也是解决此问题的办法之一,但是在这篇文字中,我们主要讨论 synchronized 同步块的解决方法。

synchronized(exp){} 代码中,表达式 exp 必须返回某个对象的引用。当线程进入同步块,会给exp返回的对象加锁,当线程离开同步块,会给对象解锁。当两个线程持有同一个exp返回对象,这两个线程互斥,只有其中一个线程执行完同步块里的代码,另一个线程才能进入。如果对象已经被其它线程加锁,则在解锁之前,该线程阻塞。注意,如果两个线程的exp返回的不是同一个对象,哪怕两个对象相等,那么这两个线程也是并发的,互不影响。实际应用中,根据业务调整同步块锁的粒度,可以降低同步块对性能的影响。

演示代码

演示代码一共有三个文件:ZhangChaoLock.java LockObjectPool.java Main.java 。

其中 ZhangChaoLock.java 是同步块中用于加锁的对象。

LockObjectPool.java 是加锁对象的对象池。

Main.java 包含 main 方法,用于测试。

为了演示方便,演示代码没有访问数据库。而是用创建文件来模拟并发的情况。逻辑是这样的:

程序先检查 E:/test 文件夹里面有没有文件,如果有文件,什么也不做。如果没文件,就新建一个文件,文件名是时间戳。在多线程的情况下,理想状况是 E:/test 只有一个新建文件。下面放出代码。

ZhangChaoLock.java

package zc.testSychronized;

/**
* 锁,用于 synchronized 关键字。作用于同步块:
* synchronized(exp){
*   ...
* }
* 中的 exp。
* @author 张超
*
*/
public class ZhangChaoLock {

/**
* 为了方便调试加的id属性
*/
private int id = 0;

public ZhangChaoLock() {
super();
}

public ZhangChaoLock(int id) {
super();
this.id = id;
}

// 按照阿里巴巴的Java代码规范,重写equals方法和hashCode方法。

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + id;
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ZhangChaoLock other = (ZhangChaoLock) obj;
if (id != other.id)
return false;
return true;
}

/**
* 方便调试,重写toString
*/
@Override
public String toString() {
String superStr = super.toString();
return new StringBuilder().append("QstLock:  ").append(this.id).append("  ---  ").append(superStr).toString();
}

}


LockObjectPool.java

package zc.testSychronized;

/**
* 锁对象的对象池
* @author 张超
*
*/
public final class LockObjectPool {
/**
* 对象池的大小。
*/
private static final int POOL_SIZE;

/**
* 存放对象的数组。
*/
private static final ZhangChaoLock[] arr;

static {
POOL_SIZE = 2000;
arr = new ZhangChaoLock[POOL_SIZE];
for (int i = 0; i < POOL_SIZE; i++) {
ZhangChaoLock lock = new ZhangChaoLock(i);
arr[i] = lock;
}
}

/**
* 取绝对值
* @param a
* @return 整数a的绝对值
*/
private final static int absoluteValue(int a) {

if (a >= 0) {
return a;
} else if (a == Integer.MIN_VALUE) { // 考虑边界值
int b = a + 1;
return -b;
}else {
return -a;
}
}

/**
* 根据字符串str的哈希值,取得对应的锁。
* @param str
* @return
*/
public final static ZhangChaoLock getLock(final String str) {
int hashCode = str.hashCode();
hashCode = absoluteValue(hashCode);
int pos = hashCode % POOL_SIZE;
return arr[pos];
}
}


Main.java

package zc.testSychronized;

import java.io.File;
import java.io.IOException;

public class Main {

/*
test1和test2是测试方法。逻辑如下
1.检查 E:/test 文件夹下有没有文件。如果没有文件就新建一个文件。文件名是时间戳。
2.如果E:/test 文件夹下没有任何文件,就什么操作也不做。
3.运行程序前清空 E:/test。理想状态下,文件夹下应该只有一个文件。

test1 没有并发控制, E:/test 会出现两个文件。
test2 有并发控制, E:/test 会出现一个文件。
test3 两个相同的字符串,因为String 对象不同,所以两个线程是并发的,没有同步。启用对象池主要是为了应对字符串出现test3的情况。

实际应用中,synchronized不会对所有线程加同一把锁。
比如保存学生作业答案,每个线程都有 userId 和 questionId,把userId和questionId 拼成
一个字符串,可以得到一把锁。LockObjectPool利用哈希码尽可能根据 userId questionId 分配不同的锁。
这样,只有持有相同的 userId 和questionId 的多个线程才会互斥。
*/

static void test1(String str) {
File d = new File("E:/test");
File[] files = d.listFiles();

// sleep 是为了方便重现并发问题。
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (files == null || files.length == 0) {
File f = new File("E:/test/" + System.currentTimeMillis() + ".txt");
try {
f.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
} else {
// do nothing
}
}

static void test2(String str) {
ZhangChaoLock lock = LockObjectPool.getLock(str);
System.out.println(lock);
synchronized(lock) {
File d = new File("E:/test");
File[] files = d.listFiles();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (files == null || files.length == 0) {
File f = new File("E:/test/" + System.currentTimeMillis() + ".txt");
try {
f.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
} else {

}
} // end synchronized
}

static void test3(String str) {

synchronized(str) {
File d = new File("E:/test");
File[] files = d.listFiles();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (files == null || files.length == 0) {
File f = new File("E:/test/" + System.currentTimeMillis() + ".txt");
try {
f.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
} else {

}
} // end synchronized
}

public static void main(String[] args) {

Thread t1 = new Thread(new Runnable(){
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//              test1("user1_id_and_question1_id");
test2("user1_id_and_question1_id");
//              test3("user1_id_and_question1_id");
}
});

Thread t2 = new Thread(new Runnable(){
public void run() {
//              test1("user1_id_and_question1_id");
test2("user1_id_and_question1_id");
//              test3(new String("user1_id_and_question1_id"));
}
});

t1.start();
t2.start();

}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 线程 web服务器
相关文章推荐