多线程同步方法数据库悲观锁(for update)
悲观锁,正如其名,具有强烈的独占和排他特性。上来就锁住,把事情考虑的比较悲观,它是采用数据库机制实现的,数据库被锁之后其它用户将无法查看,直到提交或者回滚,锁释放之后才可查看。所以悲观锁具有其霸道性。
简单说其悲观锁的功能就是,锁住读取的记录,防止其它事物读取和更新这些记录,而其他事物则会一直堵塞,知道这个事物结束。
我们可以在dos窗口中来简单测试测试:
1)语句:sqlplus c##drp1/drp1(数据库名和密码)
进入之后写入sql语句 select * from t_table_id;(启动两个窗口),会发现两个结果会一模一样:
2)在一个窗口中写入语句:select * from t_table_id where table_name='t_client' for update;则出现下边的结果,你会发现第一个将字段结果查询出来了,但是在第二个窗口中却毫无反应,这就是“for update",已经加入了悲观锁。
3)使用commit语句提交事物:如下图,会发现第二个可以查看了,为什么呢?因为commint已经完成了提交事物,释放了其权限。
所以我们可以采用for update对其多线程保持同步,就比如我们对数据库进行相加的操作,执行一次,数据增加一,demo如下:
方法一:对数据库操作进行锁住:
public static int generate(String tableName){
// 使用数据库的悲观锁for update
String sqlString="select value from t_table_id where table_name=? for update";
Connection conn=null;
PreparedStatement pstmt=null;
ResultSet rSet=null;
int value=0;
try{
conn=DbUtil.getConnection();
// 手动开启事物,不让其自动提交
DbUtil.beginTransaction(conn);
pstmt=conn.prepareStatement(sqlString);
pstmt.setString(1, tableName);
// 事物提交
rSet=pstmt.executeQuery();
if(!rSet.next()){
throw new RuntimeException();
}
value= rSet.getInt("value");
value++; //自加
modifyValueField(conn,tableName,value);
// 事物手动提交
DbUtil.commitTransaction(conn);
}catch(Exception e){
e.printStackTrace();
// 简单异常抛出
// 如果事物提交失败,则回滚事物
DbUtil.rollbackTransaction(conn);
throw new RuntimeException();
}finally{
DbUtil.close(rSet);
DbUtil.close(pstmt);
// 释放的时候,回复其初始状态,重置connection状态
DbUtil.resetConnection(conn);
DbUtil.close(conn);
}
return value;
}
因为一般情况下,事物是自动提交的,因为悲观锁我们采用主动提交事物,由悲观锁的状态来控制,所以我们采用放来来控制一下:
// 手动开启事物方法
public static void beginTransaction(Connection conn) {
try {
if (conn != null) {
if (conn.getAutoCommit()) {
conn.setAutoCommit(false); //手动提交
}
}
}catch(SQLException e) {}
}
// 事物提交方法
public static void commitTransaction(Connection conn){
try {
if (conn != null) {
if (!conn.getAutoCommit()) {
conn.commit();
}
}
}catch(SQLException e) {}
}
// 如果手动提交事物,遇到问题,回滚方法
public static void rollbackTransaction(Connection conn){
try {
if (conn != null) {
if (!conn.getAutoCommit()) {
conn.rollback();
}
}
}catch(SQLException e) {}
}
// 状态设置,因为之前手动是默认为false的
public static void resetConnection(Connection conn){
try {
if (conn != null) {
if (conn.getAutoCommit()) {
conn.setAutoCommit(false);
}else {
conn.setAutoCommit(true);
}
}
}catch(SQLException e) {}
}
方法二:数值加1的操作,根据数据表明更新数据字段的值
public static void modifyValueField(Connection conn, String tableName, int value) throws SQLException{
String sql = "update t_table_id set value=? where table_name=?";
PreparedStatement pstmt = null;
try {
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, value);
pstmt.setString(2, tableName);
pstmt.executeUpdate();
}finally {
DbUtil.close(pstmt);
}
}
最后我们写一个工作台来测试一下:
public static void main(String[] args){
int retValue=IdGenerator.generate("t_client");
System.out.println(retValue);
}
未运行之前数据库值:
运行之后的测试效果:
运行之后数据库值:
所以通过实例可以发现悲观锁是可以胜任其工作任务的,但是胜任归胜任,还得考虑效率的问题,就比如我一个小时将整个工作任务全部完成,但是仅仅一个事物就占用好长时间,并且还一直占用,不给其他事物的发展空间,这种状况则也没有办法完成任务,纵然你满足的多线程同步,但是却依旧没有完成任务。所以另一种情况就是自己工作的同时尽量不打扰其他事物的运行,并且能够满足多线程同步,这样高效率的情况才是现在大部分的公司所需要的效果。但是对于"for update"而言,只能放到查询语句中,因为只有查询对于数据库锁住才有意义。