Java高级技术第五章——高并发编程之从synchronized关键字到事务并发的若干问题

前言

前言点击此处查看:
http://blog.csdn.net/wang7807564/article/details/79113195

synchronized关键字

通过该关键字的使用,保证可见性和原子性。
synchronized锁定的是一个对象而不是一个变量,由于保证每个经过synchronized修饰后的代码区域只能由一个线程来占有,这种锁也成为互斥锁。这个具体锁定的对象,可以是实例化后的某一个具体的对象,也可以是this,或者说class类型的对象。

对于:

synchronized(this){代码区}

这种写法,可以将synchronized作为关键字,放在函数函数定义中,例如:

public synchronized void m(){代码区}

对于,synchronized作为关键字来修饰一个static的方法的时候,例如:

public synchronized static void m(){代码区}

等同于:

synchronized(包名.类名.class)

在同一个对象中,同步和非同步方法是可以同时运行的。所谓的同步方法是指用synchronized关键字修饰的方法。

synchronized的缺陷

synchronized是java中的一个关键字,也就是说是Java语言内置的特性。
如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1. 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  2. 线程执行发生异常,此时JVM会让线程自动释放锁。

事务并发产生的问题

脏读问题(dirty read)

脏读是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,与此同时,另外一个事务也访问这个数据,然后使用了这个数据。
因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据。
可以认为,该问题是由于对业务写方法加锁,对业务读方法不加锁,写数据的时候没有保证原子性,导致读取出来的数据可能会不一致。
可以在修改时加排他锁,直到事务提交后才释放,读取时加共享锁,即可解决该问题。

不可重复读(unrepeatable read)

不可重复读是指在同一事务中,两次读取同一数据,得到内容不同。
造成该同一事务两次读取数据不同的主要原因是没有保证该事务两次读取的原子性,导致两次读取的中间被另外一个事务改写,造成第二次读取到的数据是另一个事务改写的结果。
可以将两次读取合并为一个操作,在这两次读取前后加入写锁来保证数据不被修改。

幻读(phantom problem)

幻读与不可重复读类似,只不过幻读所要获得的结果不是查询到的某个数据的结果,而是遍历之后得到的记录数。
即在同一事务中,用同样的操作读取两次,得到的记录数不相同。
模式图如下:

        事务1:查询表中所有记录
                          -------------->事务2:插入一条记录
                          -------------->事务2:调用commit进行提交
        事务1:再次查询表中所有记录

事务隔离

以上的情况很多情况都是出现在数据库系统当中的,数据库系统面对并发读写所造成的问题,为此,数据库系统常有事务隔离机制,其解决方法为:
事务隔离五种级别:

  1. TRANSACTION_NONE 不使用事务。
  2. TRANSACTION_READ_UNCOMMITTED 允许脏读。
  3. TRANSACTION_READ_COMMITTED 防止脏读,最常用的隔离级别,并且是大多数数据库的默认隔离级别
  4. TRANSACTION_REPEATABLE_READ 可以防止脏读和不可重复读,
  5. TRANSACTION_SERIALIZABLE 可以防止脏读,不可重复读取和幻读,(事务串行化)会降低数据库的效率

用表格来表示:

类别 脏读 不可重复读 幻读
Read uncommitted
Read committed ×
Repeatable read × ×
Serializable × × ×

注:× 代表不会发生,√代表可能会发生

重入锁:

重入锁也就是可重入锁,也叫递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁,也就是说synchronized获得的锁是可重入的。同样地,子类也可以调用父类的同步方法。后面接触到的lock接口实现的锁也是可以重入的。
也就是说,在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁。
但是,如果一个对象有两个方法,这两个方法都用synchronized修饰,那么,在两个线程中分别执行同一个对象的这两个方法时,就需要等待。例如:

LockObj r= new LockObj();
new Thread(()->r.m1()).start();
new Thread(()->r.m2()).start();

这两个方法m1与m2都用synchronized修饰了,则使用同一个对象synchronized(this),需要有先后顺序来获得锁。

synchronized的异常处理:

程序在执行过程中,如果出现异常,默认情况锁会被释放,所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况,如果不想释放锁的话,要注意使用catch来处理这个异常。

synchronized优化:

同步代码块中的语句越少越好,能用synchronized(this){代码块},尽量不要用synchronized作为关键字来修饰整个方法区,采用细粒度的锁,可以使线程争用时间变短,从而提高效率。例如:

synchronized(this) {
        count ++;
}

就比粗暴地将synchronized关键字定义在方法前面这种粗粒度的编码方式要强得多。

锁定对象:

锁定某对象o,如果o的属性发生改变,不影响锁的使用。但是如果o变成另外一个对象,也就是说变量o的引用改变了,则锁定的对象发生改变。应该避免将锁定对象的引用变成另外的对象。
锁是锁定的堆内存中的对象,而不是锁定栈内存中某个对象的引用。
在JAVA中,字符串常量也可以作为锁定的对象,但是不要以字符串常量作为锁定对象,因为你的程序和你用到的类库很可能不经意间使用了同一把锁,以造成非常诡异的死锁阻塞。

猜你喜欢

转载自blog.csdn.net/wang7807564/article/details/80008362