本文是笔者 Java 学习笔记之一,旨在总结我的学习 Java 过程当中的心得体会,现将该笔记发表,但愿可以帮助在这方面有疑惑的同行解决一点疑惑,个人目的也就达到了。欢迎分享和转载,转载请注明出处,谢谢合做。因为笔者水平有限,文中不免有所错误,但愿读者朋友不吝赐教,发现错漏我也会及时更新修改,欢迎斧正。(可在文末评论区说明或索要联系方式进一步沟通。)java
在 Java 的多线程环境中,若是有多个线程都要对某些资源进行访问和修改,那么为了防止线程不肯定的执行顺序给资源带来不一致的状态,须要对线程进行加锁,也就是说在同一时刻只能有一个线程对资源进行操做,加锁使多个线程对同一个资源的并发操做被串行化。数据库中的数据也是一种资源,而且数据库中的数据对数据一致性也必须获得保障,这能够经过加锁来达到目的,可是串行化访问的方法虽然可以保证资源的安全,可是在并发量很是高的数据库中会致使极高的用户响应时间,对用户来讲是不可接受的。mysql
对数据库的加锁方式能够分为两种,悲观锁(Pessimistic Lock)和乐观锁(Optimistic Lock)。sql
顾名思义,悲观锁对它在访问数据库的时候老是持有一种悲观的想法,认为在它访问或修改数据库的同时老是会有其它程序也会来对数据库进行访问修改。所以为了保证数据的一致性,悲观锁会在它对数据库进行操做的时候一直锁住它要访问的数据库表(或者锁住其要访问的一条记录),此时其它要对数据库一样位置进行操做的程序将会排队等待得到锁,直到前者操做完毕才释放锁。数据库
悲观锁的使用会致使如上所说的串行化访问的问题,即在一个链接拥有对一个表(或记录)的悲观锁时,其它链接都不能够对该表(或记录)进行操做,所以串行化致使的长响应时间对悲观锁来讲一样存在。apache
一样,乐观锁对它在访问数据库的时候老是持有一种乐观的想法,认为在它访问或修改数据库的时候不会有其它链接会对数据库的同一个位置进行修改,所以不会致使数据不一致的问题。所以乐观锁实际上并无对数据进行加锁处理。安全
可是乐观锁也不能盲目乐观,毕竟 “认为在它访问或修改数据库的时候不会有其它链接会对数据库的同一个位置进行修改” 仅仅是一厢情愿的想法,所以乐观锁也必需要对 “在它访问或修改数据库的时候有其它链接会对数据库的同一个位置进行修改” 这一不乐观的的状况作出弥补。一种常见的方法就是使用 版本号(Version) 来进行标记记录。markdown
乐观锁要求数据库在保存记录的时候也要有一个保留该记录的版本的字段,在对记录进行修改的时候,先把数据记录从数据库中读出来,包括版本号(假设此时版本号为 n
),而后对数据进行修改后存回数据库前对版本号加 1 (即 n+1
),而后再存回数据库,所以整个修改过程可能的 sql
语句以下:session
select * from user where userId=2; -- 取出要修改的记录(含版本号) // 程序取出记录中的版本号,假设为 4 update user set username='newUsername', -- ... 还有其它修改的字段 set version=5 -- 把版本号加 1 (即4+1) where userId=2 and version=4; -- version 必须是 4,也就是说在程序中读 -- 出该记录时到此时写回去期间没有其它链接对该记录进行修改
会有如下两种状况:多线程
Hibernate 中悲观锁的实现是依靠底层数据库(至少在我测试 MySql 时是这样的)来实现的。为了验证这个想法,咱们使用如下的例子:并发
CREATE TABLE `hm_tst_user` ( `userid` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(128) DEFAULT NULL, PRIMARY KEY (`userid`) );
JavaBean
User.javapublic class User implements Serializable { private Long userid; private String username; public User (final String username) { this.username = username; } public User () {} protected void setUserid (final Long userid) { this.userid = userid; } // ... 其它 getter/setter }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping auto-import="true"> <class name="User" table="hm_tst_user"> <id name="userid"> <generator class="identity"/> </id> <property name="username"/> </class> </hibernate-mapping>
public class TestVersion extends TestCase { private SessionFactory sessionFactory; @Override protected void setUp () throws Exception { final StandardServiceRegistry registry = new StandardServiceRegistryBuilder () .configure () .build (); try { sessionFactory = new MetadataSources (registry).buildMetadata ().buildSessionFactory (); } catch (Exception e) { StandardServiceRegistryBuilder.destroy (registry); } } @Override protected void tearDown () throws Exception { if (sessionFactory != null) { sessionFactory.close (); } } @SuppressWarnings ("unchecked") public void test () throws InterruptedException { Session session = sessionFactory.openSession (); session.beginTransaction (); // 如下语句使用了了悲观锁 LockMode.PESSIMISTIC_WRITE final User user = session.get (User.class, 1L, LockMode.PESSIMISTIC_WRITE); // 在读出该记录并对其进行悲观锁锁定后, // 咱们跑去 mysql workbench 执行 sql 语句将该记录的 username 修改为其它(如 world) System.out.println ("Go to update the version field"); // 为了有时间在程序间切换和输入 sql 语句,暂停 10 秒钟 Thread.sleep (10000); // 修改 username 为 hello 加当前时间戳 user.setUsername ("hello"+ System.currentTimeMillis ()); session.getTransaction ().commit (); session.close (); } }
测试程序中,在将记录以悲观锁的模式读出后,暂停了 10 秒钟(模拟这是一个长事务),在此期间咱们从命令行登陆 mysql ,将 userId 为 1 的记录的 username 修改为 world;然而在命令行中输入语句 update hm_tst_user set username='world' where userId=1;
回车后,会发现有很长时间的停顿,当程序和命令行两边都执行完毕后,咱们发现数据库中最终的结果是命令行中的结果 username=world
。命令行中长时间的停顿是因为咱们的测试程序以悲观锁的模式读出,而且在休眠的 10 秒内都锁着这条记录,致使咱们从命令行上的语句须要等待测试程序的锁释放后才能进行,也就是说命令行上的语句执行排队在测试程序后面,这能够经过对测试程序中 Thread.sleep()
增长时间相应的命令行等待时间也会增加这一点来判断获得。
Hibernate 中对乐观锁的实现也是基于版本号来实现的,所以咱们相应的在数据库表,JavaBean
和配置文件中增长一个字段便可,以下。
CREATE TABLE `hm_tst_user` ( `userid` bigint(20) NOT NULL AUTO_INCREMENT, `version` int(11) DEFAULT NULL, -- 增长一个版本号 `username` varchar(128) DEFAULT NULL, PRIMARY KEY (`userid`) );
JavaBean
public class User implements Serializable { private Long userid; private Integer version; private String username; // ... 和以上同样 // ... 其它 getter/setter }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd"> <hibernate-mapping auto-import="true"> <class name="User" table="hm_tst_user"> <id name="userid"> <generator class="identity"/> </id> <version name="version"/> <!-- 使用 version 标签 --> <property name="username"/> </class> </hibernate-mapping>
public class TestVersion extends TestCase { // 和以上相同的 tearDown 和 setUp 和字段 sessionFactory @SuppressWarnings ("unchecked") public void test () throws InterruptedException { Session session = sessionFactory.openSession (); session.beginTransaction (); // 如下语句使用了了乐观锁 LockMode.OPTIMISTIC final User user = session.get (User.class, 1L, LockMode.OPTIMISTIC); // 在读出该记录并对其进行悲观锁锁定后, // 咱们跑去 mysql workbench 执行 sql 语句将该记录的 username 修改为其它(如 world) // 注意在命令行操做要同时把版本号加1 System.out.println ("Go to update the version field"); // 为了有时间在程序间切换和输入 sql 语句,暂停 10 秒钟 Thread.sleep (10000); // 修改 username 为 hello 加当前时间戳 user.setUsername ("hello"+ System.currentTimeMillis ()); session.getTransaction ().commit (); session.close (); } }
测试程序中,咱们以乐观锁的方法读出记录,并对记录进行修改,而后写回。当程序执行到 System.out.println ("Go to update the version field");
时,此时会有如下几种状况:
world
,而且将版本号 version 加 1,因为是乐观锁,咱们的命令行会立刻执行而且将结果反映到数据库中,当测试程序从休眠中唤醒时,再去执行更新操做,会发现数据库中版本号比本身大,说明被其它链接修改了,测试程序没法修改为功,在 hibernate 中会抛出一个异常 org.hibernate.StaleStateException
,仍是很贴切的异常,不新鲜的状态异常
;world
,可是没有将版本号 version 加 ,当测试程序从休眠中被唤醒时,检查到版本号没有变,它会错误地假定在此期间没有其它链接对该记录进行修改,所以对数据库的修改将会进行下去。从以上第二点能够看出,乐观锁的实现是局限于应用程序内的,也就是说若是其它应用程序不遵循 版本号加 1
的约定,那么乐观锁就可能失效。
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <property name="connection.driver_class">com.mysql.jdbc.Driver</property> <property name="connection.url">jdbc:mysql://localhost:3306/test?useSSL=false</property> <property name="connection.username">k</property> <property name="connection.password">k</property> <property name="connection.pool_size">1</property> <property name="dialect">org.hibernate.dialect.MySQLDialect</property> <property name="cache.provider_class">org.hibernate.cache.internal.NoCacheProvider</property> <property name="show_sql">true</property> <mapping resource="User.hbm.xml"/> </session-factory> </hibernate-configuration>
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>ml.kezhenxu.train</groupId> <artifactId>hibernate</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>pom</packaging> <dependencies> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.1.0.Final</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.5</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> </dependency> </dependencies> <build> <testResources> <testResource> <filtering>false</filtering> <directory>src/test/java</directory> <includes> <include>**/*.xml</include> </includes> </testResource> <testResource> <directory>src/test/resources</directory> </testResource> </testResources> </build> </project>