按照是否占有对象划分
乐观锁
乐观锁会乐观地认为在多线程的情况下不会发生线程安全问题,因此不会主动加锁,仅仅在修改数据的时候进行判断,如果有问题就重做。可以使用CAS和版本号来实现乐观锁。一般用于读多写少的场景。
悲观锁
悲观锁与乐观锁相反,他会认为一定会发生冲突,所以一定要加上锁才能保证安全。具体的实现有synchronize和ReentrantLock。一般用于写多读少的场景。
自旋和适应性自旋
阻塞和唤醒一个java线程需要操作系统切换cpu的资源来实现,这个切换的过程就需要消耗处理器时间,但有些临界区内的代码实现非常简单,使得切换所花费的时间比执行所花费的时间要。多在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。所以就有了自旋锁
自旋锁
某线程尝试获得资源失败就一直在循环尝试获得锁而不是进入阻塞状态,使用方法来解决上面出现的问题。
虽然自旋锁避免了线程切换时的开销但循环查询的开销还是有的。所以如果一个临界区的执行很短的话使用自旋锁的效果就很好,而很长的话反而会浪费资源了。所以为了节省资源,自旋锁的循环次数是有必要设置的,自旋锁的查询次数默认是10次。如果没有获得锁成功就进入阻塞状态。
自旋锁的实现是靠cas来实现的
自适应性自旋锁
自适应自旋锁的停止阈值不再是固定的而是根据情况来动态调整的。
如果一个锁自旋成功过并且目前正在有线程正在执行,那么就认为有希望获得该锁,自旋次数会增多。
如果一个锁很少自旋成功过,那么就认为获取锁的希望不大,自旋次数会减少。
synchronize锁
synchroniez是jdk提供的互斥锁,可以修饰类、方法和代码块,自动进行加锁解锁操作。
有锁升级过程。
具体看另一篇。
公平锁与非公平锁
当一个线程获取不到锁的时候就会进入阻塞状态,线程进入阻塞队列当中。
公平锁
按照线程的申请顺序来获得锁,没获得的线程进入等待队列,也就是只有当前等待队列中的第一个线程才能获得锁。这种方法可以按顺序执行,保证每个线程都能获得锁,不会发生线程饿死的情况。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁
线程申请锁的时候能申请则直接申请,不用管之前的申请顺序。只有获取不到的时候才会进入等待队列。也就是说非公平锁不会按申请顺序进行调度。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
性能方面非公平锁会比公平锁更快,因为在非公平锁中,后来的线程可能会直接获得锁,从而减少了线程状态切换的次数(这种状态切换会从用户态到内核态切换,很费时间)。
synchronize是非公平锁,ReentrantLock默认是非公平锁。
可重入与不可重入
可重入
可重入锁指的是一个线程在执行过程中某操作获得了锁,在其他操作要再次获得锁的时候可以获得该锁不会因为上一个操作没有释放该锁发生阻塞。常见的例子比如递归。
ReentrantLock和synchronized都是重入锁。
独占锁与共享锁
独占锁
独占锁只能被一个线程所获得,一旦被持有后其他线程无法再添加其他独占锁或共享锁。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。
共享锁
共享锁可以被多个线程获得,比如说线程1对数据a加了共享锁后线程2也可以给数据a添加共享锁,共享锁和共享锁之间兼容,共享锁和互斥锁之间不兼容。就是线程2不能添加独占锁。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。