zoukankan      html  css  js  c++  java
  • 关于锁机制:数据库锁

    一、什么是锁机制?数据库为什么要会有锁机制

      数据库是一个多用户使用的共享资源。当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。

      加锁是实现数据库并发控制的一个非常重要的技术。当事务在对某个数据对象进行操作前,先向系统发出请求,对其加锁。加锁后事务就对该数据对象有了一定的控制,在该事务释放锁之前,其他的事务不能对此数据对象进行更新操作。

    二、锁的基本类型

      锁包括行级锁和表级锁

      行级锁是一种排他锁,防止其他事务修改此行;在使用以下语句时,Oracle会自动应用行级锁:
    INSERT、UPDATE、DELETE、SELECT … FOR UPDATE [OF columns] [WAIT n | NOWAIT];
    SELECT … FOR UPDATE语句允许用户一次锁定多条记录进行更新
    使用COMMIT或ROLLBACK语句释放锁

      表级锁又分为5类:

    行共享 (ROW SHARE) – 禁止排他锁定表
    行排他(ROW EXCLUSIVE) – 禁止使用排他锁和共享锁
    共享锁(SHARE) – 锁定表,对记录只读不写,多个用户可以同时在同一个表上应用此锁
    共享行排他(SHARE ROW EXCLUSIVE) – 比共享锁更多的限制,禁止使用共享锁及更高的锁
    排他(EXCLUSIVE) – 限制最强的表锁,仅允许其他用户查询该表的行。禁止修改和锁定表

    三、悲观锁和乐观锁又是什么?

      

    所谓悲观锁就是基于数据库机制实现的。比如在在使用select子句的时候加上for update,那么直到改子句的事务结束为止,任何应用都无法修改select出来的记录。

    所谓乐观锁是基于应用的版本机制来实现的。一般会在表里面设计一个版本字段v(我一般会把这个字段设为timestamp)。一般的update场景是这样:

    1 select a, v from tb where id=1;   

    假设得到数据是:[‘xxx’, 11111]

    2 update tb set a=’yyyy’, v=systimestamp where v=11111; //注意, v一般不会在业务操作的时候修改

    这要求每一次update操作都变更版本字段,否则还是要进程间的数据 还是会被相互覆盖。

    乐观锁无法锁定其他应用对数据的操作。 

    锁(locking) 

    业务逻辑的实现过程中,往往需要保证数据访问的排他性。如在金融系统的日终结算 处理中,我们希望针对某个cut-off时间点的数据进行处理,而不希望在结算进行过程中 (可能是几秒种,也可能是几个小时),数据再发生变化。此时,我们就需要通过一些机制来保证这些数据在某个操作过程中不会被外界修改,这样的机制,在这里,也就是所谓 的“锁”,即给我们选定的目标数据上锁,使其无法被其他程序修改
    Hibernate支持两种锁机制:即通常所说的“悲观锁(Pessimistic Locking)” 和“乐观锁(Optimistic Locking)”。

    四、扩展实践

    【Hibernate中的悲观锁和乐观锁的实现】

    一 :悲观锁Pessimistic Locking) 

    悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定 状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能 真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系
    统不会修改数据)。 一个典型的倚赖数据库的悲观锁调用: select * from account where name=”Erica” for update 这条sql 语句锁定了account 表中所有符合检索条件(name=”Erica”)的记录。 本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。 Hibernate的悲观锁,也是基于数据库的锁机制实现。 下面的代码实现了对查询记录的加锁:

    关于数据库锁机制 - 微湖水生 - 微湖水生的博客 String
    hqlStr = " from TUser as user where user.name=’Erica’ " ; 
    关于数据库锁机制 - 微湖水生 - 微湖水生的博客Query
    query = session.createQuery(hqlStr); 
    关于数据库锁机制 - 微湖水生 - 微湖水生的博客query.setLockMode( " user " ,LockMode.UPGRADE); // 加锁 
    关于数据库锁机制 - 微湖水生 - 微湖水生的博客 List
    userList = query.list(); // 执行查询,

    获取数据 query.setLockMode 对查询语句中特定别名所对应的记录进行加锁(我们为 TUser类指定了一个别名“user”),这里也就是对返回的所有user记录进行加锁。 观察运行期Hibernate生成的SQL语句:

    关于数据库锁机制 - 微湖水生 - 微湖水生的博客 select
    tuser0_.id as id, tuser0_.name as name, tuser0_.group_id as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex from t_user tuser0_ where (tuser0_.name = ’Erica’
    ) for update

    这里Hibernate通过使用数据库的for update子句实现了悲观锁机制。 Hibernate的加锁模式有: 
    ? LockMode.NONE : 无锁机制。 
    ? LockMode.WRITE :Hibernate在Insert和Update记录的时候会自动 获取。 
    ? LockMode.READ : Hibernate在读取记录的时候会自动获取。 

    以上这三种锁机制一般由Hibernate内部使用,如Hibernate为了保证Update 过程中对象不会被外界修改,会在save方法实现中自动为目标对象加上WRITE锁。

    ? LockMode.UPGRADE :利用数据库的for update子句加锁。 
    ? LockMode. UPGRADE_NOWAIT :Oracle的特定实现,利用Oracle的for update nowait子句实现加锁。 

    上面这两种锁机制是我们在应用层较为常用的,加锁一般通过以下方法实现: 
    Criteria.setLockMode 
    Query.setLockMode 
    Session.lock 
    注意,只有在查询开始之前(也就是Hiberate 生成SQL 之前)设定加锁,才会 真正通过数据库的锁机制进行加锁处理,否则,数据已经通过不包含for update 子句的Select SQL加载进来,所谓数据库加锁也就无从谈起。


    二 :乐观锁(Optimistic Locking) 
    相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依 靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库 性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。 如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进 行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过 程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作 员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几 百上千个并发,这样的情况将导致怎样的后果。
    乐观锁机制在一定程度上解决了这个问题。乐观锁 大多是基于数据版本 (Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于 数据库表的版本解决方案中,一般是通过为数据库表增加一个“version”字段来 实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据
    版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。 对于上面修改用户帐户信息的例子而言,假设 :

    数据库中帐户信息表中有一个 version字段,当前值为1;而当前帐户余额字段(balance)为$100。 
    1 :操作员A 此时将其读出(version=1),并从其帐户余额中扣除$50 ($100-$50)。 
    2 : 在操作员A操作的过程中,操作员B也读入此用户信息(version=1),并 从其帐户余额中扣除$20($100-$20)。 
    3: 操作员A完成了修改工作,将数据版本号加一(version=2),连同帐户扣 除后余额(balance=$50),提交至数据库更新,此时由于提交数据版本大 于数据库记录当前版本,数据被更新,数据库记录version更新为2。 
    4: 操作员B完成了操作,也将版本号加一(version=2)试图向数据库提交数 据(balance=$80),但此时比对数据库记录版本时发现,操作员B提交的 数据版本号为2,数据库记录当前版本也为2,不满足“提交版本必须大于记 录当前版本才能执行更新“的乐观锁策略,因此,操作员B 的提交被驳回。 这样,就避免了操作员B 用基于version=1 的旧数据修改的结果覆盖操作 员A的操作结果的可能。 

    从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员A 和操作员B操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系 统整体性能表现。 需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局 限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户 余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在 系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如 将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途
    径,而不是将数据库表直接对外公开)。 Hibernate 在其数据访问引擎中内置了乐观锁实现。如果不用考虑外部系统对数 据库的更新操作,利用Hibernate提供的透明化乐观锁实现,将大大提升我们的 生产力。 Hibernate中可以通过class描述符的optimistic-lock属性结合version 描述符指定。 
    现在,我们为之前示例中的TUser加上乐观锁机制。

    1. 首先为TUser的class描述符添加optimistic-lock属性:

    关于数据库锁机制 - 微湖水生 - 微湖水生的博客 < hibernate – mapping > 
    关于数据库锁机制 - 微湖水生 - 微湖水生的博客  < class 
    关于数据库锁机制 - 微湖水生 - 微湖水生的博客 name = " org.hibernate.sample.TUser " 
    关于数据库锁机制 - 微湖水生 - 微湖水生的博客 table = " t_user " 
    关于数据库锁机制 - 微湖水生 - 微湖水生的博客 dynamic – update = " true " 
    关于数据库锁机制 - 微湖水生 - 微湖水生的博客 dynamic – insert = " true " 
    关于数据库锁机制 - 微湖水生 - 微湖水生的博客 optimistic – lock = " version " 
    关于数据库锁机制 - 微湖水生 - 微湖水生的博客  > 
    关于数据库锁机制 - 微湖水生 - 微湖水生的博客 …… 
    关于数据库锁机制 - 微湖水生 - 微湖水生的博客  </ class > 
    关于数据库锁机制 - 微湖水生 - 微湖水生的博客  </ hibernate – mapping >

    optimistic-lock属性有如下可选取值: 
    ? none 无乐观锁 
    ? version 通过版本机制实现乐观锁 
    ? dirty 通过检查发生变动过的属性实现乐观锁 
    ? all 通过检查所有属性实现乐

    附:Java的锁机制

    一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在java里边就是拿到某个同步对象的锁(一个对象只有一把锁); 如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池等待队列中)。 取到锁后,他就开始执行同步代码(被synchronized修饰的代码);线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码了。这样就保证了同步代码在统一时刻只有一个线程在执行。

    众所周知,在Java多线程编程中,一个非常重要的方面就是线程的同步问题。
    关于线程的同步,一般有以下解决方法:

    1. 在需要同步的方法的方法签名中加入synchronized关键字。

    2. 使用synchronized块对需要进行同步的代码段进行同步。

    3. 使用JDK 5中提供的java.util.concurrent.lock包中的Lock对象。

    另外,为了解决多个线程对同一变量进行访问时可能发生的安全性问题,我们不仅可以采用同步机制,更可以通过JDK 1.2中加入的ThreadLocal来保证更好的并发性。

    本篇中,将详细的讨论Java多线程同步机制,并对ThreadLocal做出探讨。

    大致的目录结构如下:

    一、线程的先来后到——问题的提出:为什么要有多线程同步?Java多线程同步的机制是什么?
    二、给我一把锁,我能创造一个规矩——传统的多线程同步编程方法有哪些?他们有何异同?
    三、Lock来了,大家都让开—— Java并发框架中的Lock详解。
    四、你有我有全都有—— ThreadLocal如何解决并发安全性?
    五、总结——Java线程安全的几种方法对比。


    一、线程的先来后到

    我们来举一个Dirty的例子:某餐厅的卫生间很小,几乎只能容纳一个人如厕。为了保证不受干扰,如厕的人进入卫生间,就要锁上房门。我们可以把卫生间想 象成是共享的资源,而众多需要如厕的人可以被视作多个线程。假如卫生间当前有人占用,那么其他人必须等待,直到这个人如厕完毕,打开房门走出来为止。这就 好比多个线程共享一个资源的时候,是一定要分出先来后到的。

    有人说:那如果我没有这道门会怎样呢?让两个线程相互竞争,谁抢先了,谁就可以先干活,这样多好阿?但是我们知道:如果厕所没有门的话,如厕的人一起涌向 厕所,那么必然会发生争执,正常的如厕步骤就会被打乱,很有可能会发生意想不到的结果,例如某些人可能只好被迫在不正确的地方施肥……

    正是因为有这道门,任何一个单独进入如厕的人都可以顺利的完成他们的如厕过程,而不会被干扰,甚至发生以外的结果。这就是说,如厕的时候要讲究先来后到。


    那么在Java 多线程程序当中,当多个线程竞争同一个资源的时候,如何能够保证他们不会产生“打架”的情况呢?有人说是使用同步机制。没错,像上面这个例子,就是典型的 同步案例,一旦第一位开始如厕,则第二位必须等待第一位结束,才能开始他的如厕过程。一个线程,一旦进入某一过程,必须等待正常的返回,并退出这一过程, 下一个线程才能开始这个过程。这里,最关键的就是卫生间的门。其实,卫生间的门担任的是资源锁的角色,只要如厕的人锁上门,就相当于获得了这个锁,而当他 打开锁出来以后,就相当于释放了这个锁。

    也就是说,多线程的线程同步机制实际上是靠锁的概念来控制的。那么在Java程序当中,锁是如何体现的呢?


    让我们从JVM的角度来看看锁这个概念:

    在Java程序运行时环境中,JVM需要对两类线程共享的数据进行协调:
    1)保存在堆中的实例变量
    2)保存在方法区中的类变量

    这两类数据是被所有线程共享的。
    (程序不需要协调保存在Java 栈当中的数据。因为这些数据是属于拥有该栈的线程所私有的。)

    在java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的。
    对于对象来说,相关联的监视器保护对象的实例变量。

    对于类来说,监视器保护类的类变量。

    (如果一个对象没有实例变量,或者一个类没有变量,相关联的监视器就什么也不监视。) 
    为了实现监视器的排他性监视能力,java虚拟机为每一个对象和类都关联一个锁。代表任何时候只允许一个线程拥有的特权。线程访问实例变量或者类变量不需锁。

    但是如果线程获取了锁,那么在它释放这个锁之前,就没有其他线程可以获取同样数据的锁了。(锁住一个对象就是获取对象相关联的监视器)

    类锁实际上用对象锁来实现。当虚拟机装载一个class文件的时候,它就会创建一个java.lang.Class类的实例。当锁住一个对象的时候,实际上锁住的是那个类的Class对象。

    一个线程可以多次对同一个对象上锁。对于每一个对象,java虚拟机维护一个加锁计数器,线程每获得一次该对象,计数器就加1,每释放一次,计数器就减 1,当计数器值为0时,锁就被完全释放了。

    java编程人员不需要自己动手加锁,对象锁是java虚拟机内部使用的。

    在java程序中,只需要使用synchronized块或者synchronized方法就可以标志一个监视区域。当每次进入一个监视区域时,java 虚拟机都会自动锁上对象或者类。

    看到这里,我想你们一定都疲劳了吧?o(∩_∩)o...哈哈。让我们休息一下,但是在这之前,请你们一定要记着:
    当一个有限的资源被多个线程共享的时候,为了保证对共享资源的互斥访问,我们一定要给他们排出一个先来后到。而要做到这一点,对象锁在这里起着非常重要的作用。

    在上一篇中,我们讲到了多线程是如何处理共享资源的,以及保证他们对资源进行互斥访问所依赖的重要机制:对象锁。



    本篇中,我们来看一看传统的同步实现方式以及这背后的原理。



    很多人都知道,在Java多线程编程中,有一个重要的关键字,synchronized。但是很多人看到这个东西会感到困惑:“都说同步机制是通过对象锁来实现的,但是这么一个关键字,我也看不出来Java程序锁住了哪个对象阿?“


    没错,我一开始也是对这个问题感到困惑和不解。不过还好,我们有下面的这个例程:

    public class ThreadTest extends Thread {   
        private int threadNo;   
        public ThreadTest(int threadNo) {   
            this.threadNo = threadNo;   
        }   
        public static void main(String[] args) throws Exception {   
            for (int i = 1; i < 10; i++) {   
               new ThreadTest(i).start();   
                Thread.sleep(1);   
            }   
         }   
        
        @Override  
         public synchronized void run() {   
            for (int i = 1; i < 10000; i++) {   
                System.out.println("No." + threadNo + ":" + i);   
            }   
         }   
     }   

          这个程序其实就是让10个线程在控制台上数数,从1数到9999。理想情况下,我们希望看到一个线程数完,然后才是另一个线程开始数数。但是这个程序的执行过程告诉我们,这些线程还是乱糟糟的在那里抢着报数,丝毫没有任何规矩可言。
         但是细心的读者注意到:run方法还是加了一个synchronized关键字的,按道理说,这些线程应该可以一个接一个的执行这个run方法才对阿。
         但是通过上一篇中,我们提到的,对于一个成员方法加synchronized关键字,这实际上是以这个成员方法所在的对象本身作为对象锁。在本例中,就是 以ThreadTest类的一个具体对象,也就是该线程自身作为对象锁的。一共十个线程,每个线程持有自己 线程对象的那个对象锁。这必然不能产生同步的效果。换句话说,如果要对这些线程进行同步,那么这些线程所持有的对象锁应当是共享且唯一的! 

    我们来看下面的例程:

    public class ThreadTest2 extends Thread {   
     private int threadNo; private String lock;   
     public ThreadTest2(int threadNo, String lock) {   
      this.threadNo = threadNo;   
         this.lock = lock;   }   
    public static void main(String[] args) throws Exception {   
       String lock = new String("lock");   
         for (int i = 1; i < 10; i++) {     
      new ThreadTest2(i, lock).start();   
          Thread.sleep(1);   
         }   
      }     
    public void run() {     
     synchronized (lock) {   
          for (int i = 1; i < 10000; i++) {   
           System.out.println("No." + threadNo + ":" + i);   
        }      
     }     
     }   
     }  

            我们注意到,该程序通过在main方法启动10个线程之前,创建了一个String类型的对象。并通过ThreadTest2的构造函数,将这个对象赋值 给每一个ThreadTest2线程对象中的私有变量lock。根据Java方法的传值特点,我们知道,这些线程的lock变量实际上指向的是堆内存中的 同一个区域,即存放main函数中的lock变量的区域。
            程序将原来run方法前的synchronized关键字去掉,换用了run方法中的一个synchronized块来实现。这个同步块的对象锁,就是 main方法中创建的那个String对象。换句话说,他们指向的是同一个String类型的对象,对象锁是共享且唯一的!

    于是,我们看到了预期的效果:10个线程不再是争先恐后的报数了,而是一个接一个的报数。

    再来看下面的例程:

     public class ThreadTest3 extends Thread {   
    
          private int threadNo;   
         private String lock;   
        
         public ThreadTest3(int threadNo) {   
              this.threadNo = threadNo;   
         }   
         
         public static void main(String[] args) throws Exception {   
            
             for (int i = 1; i < 20; i++) {   
                 new ThreadTest3(i).start();   
                 Thread.sleep(1);   
             }   
         }       
         public static synchronized void abc(int threadNo) {   
             for (int i = 1; i < 10000; i++) {   
                   
                    System.out.println("No." + threadNo + ":" + i);           
             }   
       }   
        
         public void run() {   
             abc(threadNo);   
         }   
     }  
      • 细心的读者发现了:这段代码没有使用main方法中创建的String对象作为这10个线程的线程锁。而是通过在run方法中调用本线程中一个静态的同步 方法abc而实现了线程的同步。我想看到这里,你们应该很困惑:这里synchronized静态方法是用什么来做对象锁的呢?



        我们知道,对于同步静态方法,对象锁就是该静态放发所在的类的Class实例,由于在JVM中,所有被加载的类都有唯一的类对象,具体到本例,就是唯一的 ThreadTest3.class对象。不管我们创建了该类的多少实例,但是它的类实例仍然是一个!



        这样我们就知道了:

        1、对于同步的方法或者代码块来说,必须获得对象锁才能够进入同步方法或者代码块进行操作;


        2、如果采用method级别的同步,则对象锁即为method所在的对象,如果是静态方法,对象锁即指method所在的
        Class对象(唯一);


        3、对于代码块,对象锁即指synchronized(abc)中的abc;


        4、因为第一种情况,对象锁即为每一个线程对象,因此有多个,所以同步失效,第二种共用同一个对象锁lock,因此同步生效,第三个因为是
        static因此对象锁为ThreadTest3的class 对象,因此同步生效。

        如上述正确,则同步有两种方式,同步块和同步方法(为什么没有wait和notify?这个我会在补充章节中做出阐述)

        如果是同步代码块,则对象锁需要编程人员自己指定,一般有些代码为synchronized(this)只有在单态模式才生效;
        (本类的实例有且只有一个)

        如果是同步方法,则分静态和非静态两种 。

        静态方法则一定会同步,非静态方法需在单例模式才生效,推荐用静态方法(不用担心是否单例)。

        所以说,在Java多线程编程中,最常见的synchronized关键字实际上是依靠对象锁的机制来实现线程同步的。
        我们似乎可以听到synchronized在向我们说:“给我一把 锁,我能创造一个规矩”。

  • 相关阅读:
    digits
    Graph
    noip2013货车运输
    noip2013华容道
    codevs科技庄园
    POJ3321 Apple tree
    第13届景驰-埃森哲杯广东工业大学ACM程序设计大赛--F-等式
    第13届景驰-埃森哲杯广东工业大学ACM程序设计大赛--E-回旋星空
    第13届景驰-埃森哲杯广东工业大学ACM程序设计大赛--D-psd面试
    第13届景驰-埃森哲杯广东工业大学ACM程序设计大赛--A-跳台阶
  • 原文地址:https://www.cnblogs.com/tengqiuyu/p/7704980.html
Copyright © 2011-2022 走看看