在Java的并发编程中,锁是一个非常重要的概念。

什么是锁(Lock)

在计算机科学中,锁或互斥(Mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。锁旨在强制实施互斥排他、并发控制策咯。

为什么要加锁?目的就是为了防止不同的线程访问同一共享资源造成混乱。

举个简单的例子,就是上洗手间上锁。这里把人比作不同的线程,把洗手间比作共享资源,一个人(当前线程)上洗手间(共享资源)的时候肯定要把门上锁,防止其他人进来和你一起上(手动滑稽)。只有当你上完了,把锁解锁了,别人(另一个线程)才能上。

锁的粒度(Granularity)

锁有一个重要属性,就是锁的粒度。所谓锁的粒度,就是指要被加锁的范围有多大。

还是洗手间的例子,如果你在家里的主卧上洗手间,你只要锁上洗手间的门,而不用锁上主卧的门吧,这时候洗手间就是你的加锁粒度。

合理的加锁粒度可以使共享资源使用最大化,因此优化加锁粒度十分重要。如果上面的例子中你上洗手间就要锁上主卧的门,这时有人要进主卧拿东西,因为有锁的存在,就被挡在门外了,就浪费了本应该可以同步的资源。

要设计合理的加锁粒度,就要了解锁的三个概念:

锁开销(Lock Overhead):锁的开销指的是锁占用的内存空间、初始化和销毁锁对CPU的占用、获取和释放锁的时间。程序使用的锁越多,相应的锁开销就会越大。

锁竞争(Lock Contention):一个进程或线程试图获取另一个进程或线程持有的锁,就会发生锁竞争。锁的粒度越小,发生锁竞争的可能性就越小。

死锁(Dead Lock):至少两个任务中的每一个都等待另一个任务持有的锁的情况

锁粒度是衡量锁保护的数据量大小。通常选择粗粒度的锁(锁的数量少,每个锁保护大量的数据),在单线程访问受保护的数据时锁开销小,但是当多个线程同时访问时性能会很差,因为增大了锁的竞争。相反,如果选择使用细粒度的锁(锁的数量多,每个锁保护少量的数据),就增加了锁的开销,而减少了锁的竞争。

锁粒度在数据库锁中应用得就很好,提供有表锁、页锁、行锁、字段锁、字段的一部分锁等。

锁的种类(Kind)

锁的种类可以分为8种,一共15个,分别是:

1.公平锁/非公平锁

2.可重入锁/不可重入锁

3.独享锁/共享锁

4.互斥锁/读写锁

5.乐观锁/悲观锁

6.分段锁

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

8.自旋锁

这些分类并不全是指的锁的状态,有的指的是锁的特性,有的指的是锁的设计。

公平锁/非公平锁

公平锁指的是多个线程按照申请锁的顺序来获取锁。

非公平锁则是指的多个线程获取锁的顺序并不是按照申请锁的顺序,后申请锁的线程有可能会比先申请锁地线程优先获取锁。有可能会造成优先级反转饥饿现象。

Java中对于ReentrantLock(可重入锁)而言,通过构造函数指定该锁是否是公平锁(默认是非公平锁)。非公平锁的优点在于吞吐量比公平锁大。

而对于Synchronized而言,也是一种非公平锁。由于其不像ReentrantLock那样通过AQS(AbstractQueuedSynchronizer,JDK1.5提供的一个基于FIFO等待队列实现的一个用于实现同步器的基础框架)实现线程调度,所以没有任何办法使其变成公平锁。

可重入锁/不可重入锁

广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或class),这样的锁就叫做可重入锁。ReentrantLock和synchronized都是可重入锁。

synchronized void setA() throws Exception{
   Thread.sleep(1000);
   setB();
}
synchronized void setB() throws Exception{
   Thread.sleep(1000);
}

上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。

不可重入锁与可重入锁相反,不可递归调用,递归调用就发生死锁。

独享锁/共享锁

独享锁和共享锁在你去读C.U.T包下的ReeReentrantLock和ReentrantReadWriteLock你就会发现,他俩一个是独享锁,一个是共享锁。

独享锁每次只能被一个线程所持有。

共享锁可以被多个线程共有,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁却每次只能被独占。

另外读锁的共享可保证并发读是非常高效的,但是读写和写写,写读都是互斥的。

独享锁与共享锁也是通过AQS实现的,通过实现不同的方法,来实现独享或共享。

对于synchronized而言,当然是独享锁。

互斥锁/读写锁

互斥锁在访问共享资源之前进行加锁操作,在访问完成之后进行解锁操作。加锁有,任何其它试图再次加锁的线程会被阻塞,直到当前线程解锁。如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都变成就绪状态,第一个变为就绪状态的线程又执行加锁操作,其它的线程又会进入等待。在这种方式下,只有一个线程能够访问被互斥锁保护的资源。

读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排他)的。读写锁有三个状态:读加锁状态、写加锁状态和不加锁状态。读写锁在Java中的具体实现就是ReadWriteLock。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态锁,这也是它可以实现高并发的原因。当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;如果是处于读状态下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放。为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远多于写操作的情况。

乐观锁/悲观锁

悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人向拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里面就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在做操作之前先上锁。Java中的synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁则总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_conditon机制,其实都是提供的乐观锁。在Java中的java.util.concurrent.atomic包下的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

并发容器类的加锁机制是基于粒度更小的分段锁,分段锁也是提升并发程序性能的重要手段之一。

在并发程序中,串行操作是会降低可伸缩性,并且上下文切换也会降低性能。在锁上发生竞争时将会导致这两种问题,使用独占锁时保护受限资源的时候,基本上是采用串行方式:每次只有一个线程能访问它。所以对于可伸缩性来说最大的威胁就是独占锁。

一般有三种方式降低锁的竞争程度:

1.减少锁的持有时间。

2.降低锁的请求频率。

3.使用带有协调机制的独占锁,这些机制允许更高的并发性。

简单地说就是,容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同地数据段的数据时,线程间就不会存在锁竞争,从而可以有效地提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段记住,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其它段的数据也能被其它线程访问。

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

锁的状态有四个:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。

锁的状态是通过对象监视器在对象头中的字段来表明的。

四种状态会虽则竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。

这四种状态都不是Java语言中的锁,而是JVM为了提高锁的获取与释放效率而做的优化(使用synchronized时)。

偏向锁是指一段同步代码一直被一个线程访问,那么该线程会自动获取该锁,降低获取锁的代价。

轻量级锁是指锁是偏向锁的时候,被另一个线程访问,偏向所就会升级为轻量级锁,其它线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其它申请的线程进入阻塞,性能降低。

自旋锁

CAS算法是乐观锁的一种实现方式,而CAS算法中又涉及到自旋锁。

CAS(Compare and Swap,比较并交换)是一种有名的无锁算法。无锁编程,即在不适用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS在更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B,否则不会执行任何操作。一般情况下是一个自旋操作,即不断地重试。

自旋锁(Spin Lock)是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断地判断锁是否能够被成功获取,直到获取到锁才会退出循环。它是为了实现保护共享资源而提出的一种锁机制。其实,自旋锁和互斥锁表类似,他们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就是说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态;但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别得执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,这也是自旋锁得名的原因。

"你要去做一个不动声色的大人了。

不要情绪化,不要偷偷想念,不要回头看。

去过自己另外的生活。

你要听话,不是所有的鱼都会生活在同一片海里。" 

03-16 20:05