锁的一生

1.1 锁的种类

公平锁/非公平锁

可重入锁/不可重入

独享锁/共享锁

读写锁

分段锁

偏向锁/轻量级锁/重量级锁

自旋锁

1.2.1 公平锁,非公平锁

  公平锁就是保障了多线程下各线程获取锁的顺序,先到的线程优先获取锁,当一个线程获取到锁后,这时如果其他多个线程同时请求获取锁,会将其他线程按到达顺序排成队列,当持有锁的线程释放锁后,队列中的线程会依次按照队列顺序获取锁。

  而非公平锁则无法提供这个保障。与公平锁的区别时,当一个线程持有锁时,其他线程请求时会加入队列中,当一个持有锁的线程释放锁后,其他多个线程获取锁的顺序没有保证,是按照抢占机制实现的,谁先得到就是谁的。

  举一个在银行办理业务的例子来说明:

  公平锁图示:


  在公平锁中,当其他线程(新来顾客)请求锁(工作人员)时,当锁空闲(即没有顾客排队)时,新线程直接获取锁,当有队列时,无论锁是否空闲,新线程都是直接加入到队列中,等待执行。

  非公平锁图示:



   在非公平锁中,当其他线程(新来顾客)请求锁(工作人员)时,当锁空闲(工作人员休息)时,新线程直接尝试获取锁,当锁被其他线程占有时(工作人员正在为其他顾客办理业务时),加入到队列中。

  公平锁和非公平锁的源码如下;

公平锁:

非公平锁:

  从源码可以看出公平锁需要判断队列是否有值。

  非公平锁和公平锁的实现是通过下面的方法实现的:

Lock lock =newReentrantLock(true);//公平锁Lock lock =newReentrantLock(false);//非公平锁

  公平锁消耗的时间比非公平锁消耗的时间要多,因此非公平锁效率高于公平锁,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。

1.2.2 可重入锁,不可重入锁

可重入锁是对于同一个类的对象,线程在执行一个任务时,会获取一次锁,当执行完会释放锁,如果这个线程还要继续执行这个对象的其他任务,是不需要重新获取锁的,但执行完任务就要释放锁,顾名思义,锁的重入性。不可重入锁则相反,底层原理详情见https://www.cnblogs.com/xyzyj/p/11148497.html 2.2.3节

  举一个二狗子看门的例子,

  现有一只二狗子“旺财”,它的任务就是在自家门口看门,它能听懂各种语言,当陌生人去他家时,只要告诉它是主人的朋友就可以进去,出门时也要告诉它要离开,才会放你出去。


  可以把这户人家看成一个对象,这户人家院子里有三间房子,代表对象的三个方法,这个女访客代表一个线程,“旺财”代表一把锁。

  当访客第一次进入院子里面时,要经过二哈(旺财)的同意(获取锁),进入院子后,访客可以随便去哪间房间了,不用征求二哈的同意(不用再次获取锁),但是,从每一间房子出来,都要告诉二哈逛完了(释放锁)。

1.2.3 独享锁,共享锁

  独享锁(互斥锁):同时只能有一个线程获得锁。比如,ReentrantLock 是互斥锁,ReadWriteLock 中的写锁是互斥锁。 共享锁:可以有多个线程同时获得锁。比如,Semaphore、CountDownLatch 是共享锁,ReadWriteLock 中的读锁是共享锁。举例子下面读写锁中。

1.2.4 读写锁

  读写锁既是互斥所又是共享锁,它实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

  综合有以下规则:

(1)多个线程读,可以同时读,此时有5个线程读。

package com.test;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;publicclass Main {

    Lock lock =new ReentrantLock();

    privateinti = 0;

    publicstaticvoid main(String[] args) {

        finalMain main =new Main();

        for(inti = 0; i < 5; i++) {

            if(i > 5) {

                newThread(new Runnable() {

                    publicvoid run() {

                        main.write(1, Thread.currentThread());

                    }

                }).start();

            } else {

                newThread(new Runnable() {

                    publicvoid run() {

                        main.read(Thread.currentThread());

                    }

                }).start();

            }

        }

    }

    publicvoidwrite(int i, Thread thread) {

        lock.lock();

        try {

            System.out.println(thread.getName() + "write获取了锁");

            this.i = i;

        } catch (Exception e) {

        } finally {

            lock.unlock();

            System.out.println(thread.getName() + "write释放了锁");

        }

    }

    publicvoid read(Thread thread) {

        lock.lock();

        try {

            System.out.println(thread.getName() + "read获取了锁,i=" + i);

        } catch (Exception e) {

        } finally {

            lock.unlock();

            System.out.println(thread.getName() + "read释放了锁");

        }

    }

}

  运行结果:

Thread-0read获取了锁,i=0Thread-2read获取了锁,i=0Thread-2read释放了锁

Thread-4read获取了锁,i=0Thread-0read释放了锁

Thread-3read获取了锁,i=0Thread-3read释放了锁

Thread-4read释放了锁

Thread-1read获取了锁,i=0Thread-1read释放了锁

(2)多个线程写,不能同时写,只能一个线程写完,其他线程才能开始。此时有5个线程写。

  只需要改一行代码:

if(i < 5)

  运行结果:

Thread-0write获取了锁

Thread-0write释放了锁

Thread-1write获取了锁

Thread-1write释放了锁

Thread-4write获取了锁

Thread-4write释放了锁

Thread-3write获取了锁

Thread-3write释放了锁

Thread-2write获取了锁

Thread-2write释放了锁

(3)读写同时进行,读的同时不能写,写的同时不能读,只能读完再写,或写完再读,此时有3个线程写,两个线程读。

  改一行代码:

if(i < 3)

  运行结果:

Thread-0write获取了锁

Thread-0write释放了锁

Thread-2write获取了锁

Thread-2write释放了锁

Thread-1write获取了锁

Thread-1write释放了锁

Thread-3read获取了锁,i=1Thread-3read释放了锁

Thread-4read获取了锁,i=1Thread-4read释放了锁


1.2.5  分段锁

  在 Java 5 之后,JDK 引入了 java.util.concurrent 并发包 ,其中最常用的就是 ConcurrentHashMap 了, 它的原理是引用了内部的 Segment ( ReentrantLock )  分段锁,保证在操作不同段 map 的时候, 可以并发执行, 操作同段 map 的时候,进行锁的竞争和等待。从而达到线程安全, 且效率大于 synchronized。

但是在 Java 8 之后, JDK 却弃用了这个策略,重新使用了 synchronized,关于ConcurrentHashMap的详细讲解,请看https://www.cnblogs.com/xyzyj/p/11283559.html。

1.2.6 偏向锁/轻量级锁/重量级锁

偏向锁,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。

如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

详情见https://www.cnblogs.com/xyzyj/p/11148497.html2.2.5 节。

  轻量级锁,线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。

  重量级锁,重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。

1.2.7 自旋锁

  自旋锁是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

  在JDK1.6中,Java虚拟机提供-XX:+UseSpinning参数来开启自旋锁,默认开启,使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。

  在JDK1.7开始,自旋锁的参数被取消,虚拟机不再支持由用户配置自旋锁,自旋锁总是会执行,自旋锁次数也由虚拟机自动调整。

回到顶部

2.数据库中的锁

  引入:当多个用户并发地存取数据库时就会产生多个失误同时存取同一数据的情况,若对并发操作不加控制就可能会存取和存储不正确的数据,破坏事务的一致性和数据库的一致性。所以数据库管理系统必须提供并发控制机制,并发控制机制是衡量一个数据库管理系统性能的重要标志之一。通过使用MVCC(Multi-Version Concurrency Control)算法自动提供并发控制。MVCC维持一个数据的多个版本使读写操作没有冲突。也就是说数据元素X上的每一个写操作产生X的一个新版本,GBase 8m为X的每一个读操作选择一个版本。由于消除了数据库中数据元素读和写操作的冲突,GBase 8m得到优化,具有更好的性能。特别是对于数据库读和写两种方法,他们不用等待其他同时进行的相同数据写和读的完成。在并发事务中,数据库写只等待正在对同一行数据进行更新的写,这是现有的行级锁的弱点。同时MVCC回收不需要的和长时间不用的内存,防止内存空间的浪费。MVCC优化了数据库并发系统,使系统在有大量并发用户时得到最高的性能,并且可以不用关闭服务器就直接进行热备份。

2.1 锁的种类

表级锁定

行级锁定

页级锁定

共享锁/排他锁

修改锁

结构锁

意向锁

批量修改锁 

间隙锁

乐观锁/悲观锁

2.2 锁详细介绍

2.2.1 行级锁定

  偏向InnoDB存储引擎,开销大,加锁慢,会出现死锁,锁定粒度小,发送锁冲突的概率最低,并发度也最高。

  当选中某一行时,如果是通过主键或者索引选中的,这个时候是行级锁;如果是通过其它条件选中的,这个时候行级锁会升级成表锁,其它事务无法对当前表进行更新或插入操作

2.2.2 表级锁定

  表锁更适用于以查询为主,只有少量按索引条件更新数据的应用;行锁更适用于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用。

2.2.3 页级锁定

  行锁锁指定行,表锁锁整张表,页锁是折中实现,即一次锁定相邻的一组记录。 

  oracle没有页锁,和其他数据库的并发机制不一样oracle基于多版本机制、意向锁,提供高并发能力。

  开销和加锁时间介于表锁和行锁之间:会出现死锁,

  锁定粒度介于表锁和行锁之间,并发度一般。

2.2.4 共享锁/排他锁

  同1.2.3。

  排它锁:又称写锁x锁)。若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁,这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。

  共享锁:又称读锁(S锁)。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其它事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁,这就保证了其他事务可以读A,但在T释放A上的s锁之前不能对A做任何修改。

2.2.5 修改锁

  修 改锁在修改操作的初始化阶段用来锁定可能要被修改的资源,这样可以避免使用共享锁造成的死锁现象。因为使用共享锁时,修改数据的操作分为两步,首先获得一 个共享锁,读取数据,然后将共享锁升级为独占锁,然后再执行修改操作。这样如果同时有两个或多个事务同时对一个事务申请了共享锁,在修改数据的时候,这些 事务都要将共享锁升级为独占锁。这时,这些事务都不会释放共享锁而是一直等待对方释放,这样就造成了死锁。如果一个数据在修改前直接申请修改锁,在数据修 改的时候再升级为独占锁,就可以避免死锁。修改锁与共享锁是兼容的,也就是说一个资源用共享锁锁定后,允许再用修改锁锁定。 

2.2.6 结构锁

  结构锁分为结构修改锁(Sch-M)和结构稳定锁(Sch-S)。执行表定义语言操作时,SQL Server采用Sch-M锁,编译查询时,SQL Server采用Sch-S锁。

2.2.7 意向锁

  当一个事务在需要获取资源的锁定时,如果该资源已经被排他锁占用,则数据库会自动给该事务申请一个该表的意向锁。如果自己需要一个共享锁定,就申请一个意向共享锁。如果需要的是某行(或者某些行)的排他锁定,则申请一个意向排他锁。

2.2.8 间隙锁

  当我们用范围条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做”间隙(GAP)”。InnoDB也会对这个”间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。

2.2.9 乐观锁,悲观锁

  乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。一般通过数据版本和时间戳来实现。

悲观锁是当一个线程每次去拿数据的时候都认为其他线程会修改数据,所以每次在拿数据的时候都会上锁,这样其他线程想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。关于synchronized的详细讲解,请看https://www.cnblogs.com/xyzyj/p/11148497.html关于ReentrantLock的详细讲解,请看https://www.cnblogs.com/xyzyj/p/11236398.html。

你可能感兴趣的:(锁的一生)