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

来源:互联网 发布:张晓松口琴淘宝店 编辑:程序博客网 时间:2024/06/06 17:13

业务场景

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();    }}