重量级锁
synchronized
synchronized 可以把任意一个非 NULL 的对象当作锁。
1、当锁住的是实例方法时,锁住的这个类实例(this)。
2、当锁住的是静态方法时,锁住的是Class实例。
3、当所用与一个对象时,锁住的是所有以该对象为锁的代码块。
synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
Java1.6, synchronized 进行了很多的优化, 有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;
自旋锁
自旋锁的原理很简单,就是如果每次线程在获取锁之后,能够在很短的时间内将锁释放给其他线程,其他线程就无需在内核态和用户态之间进行切换,只需要等待(自旋)一会持有锁的线程即可,这样就避免了线程切换的消耗。
线程自旋是需要消耗CPU的,因为在等待时就是让CPU做无用功,如果一直获取不到锁,那线程也不能无止尽地等待下去,因此需要设置一个最大自旋等待时间,如果超出这个时间,线程则会进入阻塞状态。
自适应自旋锁
自适应自旋锁是在自旋锁的基础上进行了优化,在自旋锁中,我们需要自己指定等待的时间,但是在自适应自旋锁中,不需要我们指定循环的次数,它会自身进行判断,对于一些经常拿到的锁,会多循环几次,对于基本没有拿过的锁,则循环的次数会少一些。
轻量级锁
上面讲到的synchronized、自旋锁、自适应自旋锁,都有一个特点,就是在进入方法前进行加锁操作,在方法结束后释放锁。我们都知道,加锁是需要依靠操作系统的,用户态和内核态的切换需要花费大量时间,在某些情况下,某个方法是没有其他线程来进行竞争的,如果这个时候还对该方法进行加锁和释放锁操作,势必会浪费大量时间,因此出现了轻量级锁
轻量级锁认为,当你在方法里面执行的时候,其实是很少刚好有人也来执行这个方法的,所以,当我们进入一个方法的时候根本就不用加锁,我们只需要做一个标记就可以了,也就是说,我们可以用一个变量来记录此时该方法是否有人在执行。也就是说,如果这个方法没人在执行,当我们进入这个方法的时候,采用CAS机制,把这个方法的状态标记为已经有人在执行,退出这个方法时,在把这个状态改为了没有人在执行了。
显然和重量级锁相比,轻量级锁使用CAS操作来改变状态所需要的开销更小
轻量级锁适合用在那种,很少出现多个线程竞争一个锁的情况,也就是说,适合那种多个线程总是错开时间来获取锁的情况。
如果真的遇到了竞争,我们就会认为轻量级锁已经不适合了,我们就会把轻量级锁升级为重量级锁了。
偏向锁
偏向锁是在轻量级锁的基础上,进行了更进一步地优化。Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。即很多看似有很多线程同时访问的方法,实际上只有一个线程在访问。在这种情况下,如果使用轻量级锁的话,每次进入该方法和退出该方法时,都需要使用CAS操作进行状态的修改,偏向锁优化的地方在于,在一个线程第一次进入某个同步方法时,先判断下该方法的标志位,如果标志位为空,则使用CAS操作,在该方法上标记自己的ThreadID,在退出方法的时候,不做任何修改操作,在下一次再次进入该同步方法时,判断下该方法的标志是否是自己的ThreadID,如果是的话,直接进入方法体执行即可。
偏向锁是在只有一个线程执行同步代码块时进一步地提高性能
但是如果真出现了多个线程,即其他线程发现标志位不是自己的ThreadID,这意味着偏向锁已经不适用了,这个时候偏向锁就会升级为轻量级锁
因此,偏向锁只适用于始终只有一个线程在执行一个方法的情况
什么是锁升级呢?
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。
“轻量级” 是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前, 先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
悲观锁
悲观锁就是悲观思想,认为写多,遇到并发写的可能搞,每次读写数据时都会对数据进行加锁,上面提到的synchronized就是悲观锁
乐观锁
乐观锁是一种乐观思想,认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,因此不会上锁。但是在遇到更新的时候会检查有没有人去更新这个数据,采取在写时先读出版本号,加锁后进行修改(修改时检查版本号是否跟先前读取时的版本号是否一致,如果一致则进行修改,反之修改失败),上面的CAS操作也是一种更新方法,CAS方法包括了三个操作数,需要读写的内存位置、进行比较的预期原值、拟写入的值,如果该位置的值和预期原值相同,则进行修改,否则更新失败。
非公平锁
JVM 按随机、就近原则分配锁的机制则称为不公平锁, ReentrantLock 在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。 非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列 。
公平锁
公平锁指的是锁的分配是公平的,通常先对锁提出获取请求的线程会先被分配到锁,ReentrantLock 在构造函数中提供了是否公平锁的初始化方式来定义公平锁。
可重入锁(递归锁)
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。JAVA中的ReentrantLock 和 synchronized 都是可重入锁。
ReentrantLock
ReentantLock 继承接口 Lock 并实现了接口中定义的方法, 是一种可重入锁, 除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
- void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
- boolean tryLock(): 如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于,tryLock()只是”试图”获取锁, 如果锁不可用, 不会导致当前线程被禁用,当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.
- void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 会抛出IllegalMonitorStateException异常。
- Condition newCondition(): 条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将缩放锁。
- getHoldCount() : 查询当前线程保持此锁的次数,也就是此线程执行lock方法的次数。
- getQueueLength():返回正等待获取此锁的线程数,比如启动10个线程,1个线程获得锁,此时返回的是9
- getWaitQueueLength: (Condition condition)返回等待与此锁相关的给定条件的线程数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10
- hasWaiters(Condition condition): 查询是否有线程等待与此锁有关的给定条件(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
- hasQueuedThread(Thread thread): 查询给定线程是否等待获取此锁
- hasQueuedThreads(): 是否有线程等待此锁
- isFair(): 该锁是否公平锁
- isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和 true
- isLock(): 此锁是否有任意线程占用
- lockInterruptibly() : 如果当前线程未被中断,获取锁
- tryLock() : 尝试获得锁,仅在调用时锁未被线程占用,获得锁
- tryLock(long timeout TimeUnit unit): 如果锁在给定等待时间内没有被另一个线程保持,则获取该锁。
ReentrantLock和synchronized
1、ReentrantLock的lock()和unlock()方法和synchronized不同的是,ReentrantLock加锁后需要自己手动释放锁,为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
2、ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用ReentrantLock。
ReentrantLock的中断得使用condition对象,newCondition()获取条件对象,只有持有该锁的线程能够使用该方法。
condition的方法和Object类锁方法的区别
- Condition 类的 awiat 方法和 Object 类的 wait 方法等效
- Condition 类的 signal 方法和 Object 类的 notify 方法等效
- Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
- ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的
condition的signal只能够唤醒由同一个condition对象await的线程,这就是实现唤醒指定线程的方法
Java多线程之ReentrantLock与Condition
Semaphore 信号量
Semaphore 是一种基于计数的信号量。相当于OS所学的Semaphore信号了,它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。 Semaphore 可以用来构建一些对象池,资源池之类的, 比如数据库连接池
Semaphore 与 ReentrantLock
Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,通过 acquire()与release()方法来获得和释放临界资源。经实测, Semaphone.acquire()方法默认为可响应中断锁,与 ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt()方法中断。
此外, Semaphore 也实现了可轮询的锁请求与定时锁的功能,除了方法名 tryAcquire 与 tryLock不同,其使用方法与 ReentrantLock 几乎一致。 Semaphore 也提供了公平与非公平锁的机制,也可在构造函数中进行设定。
Semaphore 的锁释放操作也由手动进行,因此与 ReentrantLock 一样,为避免线程因抛出异常而无法正常释放锁的情况发生,释放锁的操作也必须在 finally 代码块中完成。
AtomicInteger
首先说明,此处 AtomicInteger ,一个提供原子操作的Integer的类,常见的还有AtomicBoolean、 AtomicInteger、 AtomicLong、 AtomicReference 等,它们的实现原理相同,区别在与运算对象类型的不同。还可以通过 AtomicReference<V>将一个对象的所有操作转化成原子操作。
我们知道, 在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。通常我们会使用 synchronized 将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些同步类,使得使用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger的性能是 ReentantLock 的好几倍。
ReadWriteLock 读写锁
为了提高性能, Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。 读写锁分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。
读锁
如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁
写锁
如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁
Java 中 读 写 锁 有 个 接 口 java.util.concurrent.locks.ReadWriteLock , 也有具体的实现ReentrantReadWriteLock。
共享锁和独占锁
独占锁
独占锁模式下,每次只能有一个线程能持有锁, ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了并发性,因为读操作并不会影响数据的一致性。
共享锁
共享锁则允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
- AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等待线程的锁获取模式。
- java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,或者被一个 写操作访问,但两者不能同时进行
分段锁
分段锁也并非一种实际的锁,而是一种思想,ConcurrentHashMap 是学习分段锁的最好实践
锁优化
减少锁持有时间
只用在有线程安全要求的程序上加锁
减小锁粒度
将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是ConcurrentHashMap。
锁分离
最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如 LinkedBlockingQueue 从头部取出,从尾部放数据
锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。但是,凡事都有一个度, 如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。
锁消除
锁消除是在编译器级别的事情。 在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作,多数是因为程序员编码不规范引起。