数据库并发操纵与synchronized的用法

    添加时间:2013-8-5 点击量:

          比来在写调库审核的功能,生成调库文件时须要从数据库获取文件的批次号(VOUCH_NO)。因为在审核的时辰有可能存在多个操纵员同时进行审核的景象,获取批次号后对其批改,然后更新数据库里的批次号,这个操纵须要互斥。我在本身电脑上模仿了一下这种景象,我在本机上的MySQL中建了一个SYS_PARAM表,并且拷贝了SysParamUtil类和公司封装的类库到项目中。以下事务把握内部的代码模仿的是ess中FileLogQueryBo.genFileLog()办法的实现(事务把握代码用于模仿ess中的AOP事务把握)。



    package com.strikew.modules.fileLog;
    


    import com.strikew.bean.domain.SysParam;
    import com.strikew.constant.SysParamNameConst;
    import com.strikew.dao.SysParamDao;
    import com.strikew.util.SysParamUtil;/
    测试并发从数据库获取批次号是否会导致数据不一致
    @author Wangsiy

    /
    class GetVouchNo implements Runnable {
    private static SysParamDao sysParamDao = new SysParamDao();
    public void run() {
    String rs
    = null;
    try {
    System.out.println(Thread.currentThread().getName()
    + transaction begin!);
    HibernateUtil.beginTransaction();
    rs
    = TransactionNestUtil.reference();

    SysParam bean
    = SysParamUtil.getInstance().getSysParamByName(SysParamNameConst.VOUCH_NO);
    synchronized (bean) {
    int vouchNo = Integer.parseInt(bean.getCharVal());
    // 批改批次号
    vouchNo += 1;
    bean.setCharVal(String.valueOf(vouchNo));
    // SysParam
    sysParamDao.makePersistent(bean, true);
    }

    TransactionNestUtil.releaseRef(rs);
    if (!TransactionNestUtil.isReference()) {
    HibernateUtil.commitTransaction();
    }
    System.out.println(Thread.currentThread().getName()
    + transaction commit!);
    }
    catch (Exception e) {
    TransactionNestUtil.releaseRef(rs);
    if(!TransactionNestUtil.isReference()){
    HibernateUtil.rollbackTransaction();
    }
    System.err.println(e.getMessage());
    e.printStackTrace();
    }
    finally {
    if (!TransactionNestUtil.isReference()) {
    HibernateUtil.closeSession();
    }
    }

    }
    }


    public class FileLogQuery {

    public static void main(String[] args) {
    Thread t1
    = new Thread(new GetVouchNo());
    Thread t2
    = new Thread(new GetVouchNo());
    t1.setName(
    Thread 1);
    t2.setName(
    Thread 2);
    t1.start();
    t2.start();
    }

    }


           周五我看到这块代码时,认为有可能产生Lost Update(丧失更新)题目[见《Head First Java P512》]。当时我认为若是有A、B两个操纵员(可以用两个线程庖代),B进入了同步代码块(synchronized),更新了数据库里的批次号,然则在同步块外面的A获得的批次号是旧的。今天我实验后发明我当时的认知是错误的实验中我发明A、B线程中的bean引用的是堆里的同一个对象(因为SysParmUtil.getInstance()办法调用了内部的genSysParam()办法,获取数据表SYS_PARAM里的所有记录,将它们以<paramName, bean>的key-value情势存在了内部的HashTable table里)是以B在同步块中对bean所做的批改在A中会获得反应,也就是说这里并不存在Lost Update的题目,因为Update的项目组已经在同步块里了不过前提是A、B两个线程在同一个Java虚拟机里,若是不在同一个Java虚拟机也就是说法度是分布式的,那A、B就不成能引用到同一个堆里的同一个对象。我揣摩将来电子凭证这个项目必然是安排在一台主机上的,操纵员仅仅经由过程浏览器登岸这个体系进行操纵罢了,所以并发审核的题目必然只是产生在同一个Java虚拟机景象里的。


          再来看看《Head First Java》中描述的导致并发题目产生的原因:



          It all comes down to one potentially deadly scenario: two or
    
          more threads have access to a single object’s data. In other
          words, methods executing on two different stacks are both
          calling, say, getters or setters on a single object on the heap.


    多个线程同时操纵堆里的同一个对象,由此就会激发数据不一致的题目。获取并更新批次号这个题目完全合适上述的定义:多个线程(A、B)操纵(批改批次号)堆里的同一个对象(bean)。解决这种题目的思路就是要将多个操纵绑缚在一路作为一个不成分别的原子操纵。在上述代码中须要绑缚的就是以下的批次号批改和持久化操纵:



             int vouchNo = Integer.parseInt(bean.getCharVal());
    
    // 批改批次号
    vouchNo += 1;
    bean.setCharVal(String.valueOf(vouchNo));
    // SysParam
    sysParamDao.makePersistent(bean, true);


    只有履行了makePersistent()(内部调用Hibernate的Session.save()办法)将bean持久化后,才真正的把对bean的批改转化成对数据库表中行的批改。



          如今我们已经知道了要同步哪些操纵了,接下来很首要的就是该推敲要对哪一个对象加锁。


    synchronized的用法


    synchronized关键字的用法有两种,一种是写在办法头部的前面,别的一种是写在办法的内部。无论写在哪里,它的感化都是加锁,独一的差别是加锁的对象不合罢了。加锁的意思是进入synchronized包含的区域前须要取得被加锁对象的锁,拿不到锁你就进不去。比如上述代码若是把synchronized关键字写在run办法头部的前面,那么就无法达到互斥的目标了。为什么呢?因为synchronized放到办法头部默示加锁的对象是GetVouchNo类的实例,而main函数中每一个线程都有属于本身的GetVouchNo实例,所以这种锁没有任何感化。除非把main函数也改成下面如许:



    public static void main(String[] args) {
    
    Runnable job
    = new GetVouchNo();
    Thread t1
    = new Thread(job);
    Thread t2
    = new Thread(job);
    t1.setName(
    Thread 1);
    t2.setName(
    Thread 2);
    t1.start();
    t2.start();

    }



    此时就可以或许达到互斥的目标了。因为这时每个线程应用的是同一个GetVouchNo实例,当一个线程t1进入了实例job的run办法时须要取得实例job的锁,那么线程t2就只能在外面等着,直到t1履行完毕把锁偿还后,t2才有资格进入job的run办法。可以看到将synchronized放到办法头部时互斥的局限太大了,我们仅仅须要互斥批次号的批改和持久化那几条语句罢了,所以我们可以将synchronized写在办法内部:



    class GetVouchNo implements Runnable {
    
    private static SysParamDao sysParamDao = new SysParamDao();
    public synchronized void run() {
    String rs
    = null;
    try {
    System.out.println(Thread.currentThread().getName()
    + transaction begin!);
    HibernateUtil.beginTransaction();
    rs
    = TransactionNestUtil.reference();

    SysParam bean
    = SysParamUtil.getInstance().getSysParamByName(SysParamNameConst.VOUCH_NO);
    synchronizedthis) {
    int vouchNo = Integer.parseInt(bean.getCharVal());
    // 批改批次号
    vouchNo += 1;
    bean.setCharVal(String.valueOf(vouchNo));
    // SysParam
    sysParamDao.makePersistent(bean, true);
    }

    TransactionNestUtil.releaseRef(rs);
    if (!TransactionNestUtil.isReference()) {
    HibernateUtil.commitTransaction();
    }
    System.out.println(Thread.currentThread().getName()
    + transaction commit!);
    }
    catch (Exception e) {
    TransactionNestUtil.releaseRef(rs);
    if(!TransactionNestUtil.isReference()){
    HibernateUtil.rollbackTransaction();
    }
    System.err.println(e.getMessage());
    e.printStackTrace();
    }
    finally {
    if (!TransactionNestUtil.isReference()) {
    HibernateUtil.closeSession();
    }
    }

    }
    }


    public class FileLogQuery {

    public static void main(String[] args) {
    Runnable job
    = new GetVouchNo();
    Thread t1
    = new Thread(job);
    Thread t2
    = new Thread(job);
    t1.setName(
    Thread 1);
    t2.setName(
    Thread 2);
    t1.start();
    t2.start();
    }
    }



    上方synchronized后面括号里写的就是须要加锁的对象,this默示当前实例(GetVouchNo的实例),与synchronized写在办法头时造成的感化是一样的(重视main函数代码与最上方的那个版本不合)不一样的是此时同步的局限缩小了,仅仅只有synchronized块包裹的语句被同步了!这种应用办法加倍地灵活。与最上方的代码进行对比:



    synchronized (bean) {
    
    int vouchNo = Integer.parseInt(bean.getCharVal());
    // 批改批次号
    vouchNo += 1;
    bean.setCharVal(String.valueOf(vouchNo));
    /
    / SysParam
    sysParamDao.makePersistent(bean, true);
    }



    这里被加锁的对象是bean,意思是进入这个代码块时须要获得对象bean的锁,因为bean只有一个,所以锁只有一个,是以同一时刻只能有一个线程可以或许进入这个区域,这就达到了互斥的目标!别的还有一种斗劲特别的是给一个类加锁



    synchronized (GetVouchNo.class) {
    
    int vouchNo = Integer.parseInt(bean.getCharVal());
    // 批改批次号
    vouchNo += 1;
    bean.setCharVal(String.valueOf(vouchNo));
    // SysParam
    sysParamDao.makePersistent(bean, true);
    }


     如许默示给GetVouchNo这个类对象加锁,别忘了在Java中类也是一种对象,是java.lang.Class的对象。在这种景象下,即使main函数写得像下面如许:



    public static void main(String[] args) {
    
    Thread t1
    = new Thread(new GetVouchNo());
    Thread t2
    = new Thread(new GetVouchNo());
    t1.setName(
    Thread 1);
    t2.setName(
    Thread 2);
    t1.start();
    t2.start();
    }


     也可以或许达到互斥的目标,因为被加锁的是GetVouchNo这个类对象,而类对象只存在一个,是以只有一把锁,同一时刻只能有一个线程获得该锁进入同步块中。


     小结:


    以上文字中同步与互斥并没有很严谨的区分,所要表达的意思就是同一时刻只能有一个线程操纵bean。实际景象中同步和互斥并不长短此即彼的,更多景象下是形影不离的。多个线程要达到对批次号(共享对象,堆中只存在一个)互斥批改的目标,那么体系中就只能存在一把锁,哪个线程获得了锁,哪个线程才有资格对批次号进行批改。


    真正的心灵世界会告诉你根本看不见的东西,这东西需要你付出思想和灵魂的劳动去获取,然后它会照亮你的生命,永远照亮你的生命。——王安忆《小说家的十三堂课》
    分享到: