目录

讨论下java的锁机制

讨论下java的锁机制

一直以来,java中的各种形形色色的锁,让人望而生却,时常把人搞糊涂了。今天就在这篇博文中谈谈java的锁机制。 为什么要弄出个锁的概念 先来看看java中有多少跟锁相关的概念名词,内置锁,显式锁,互斥锁,可重入锁,不可重入锁,读写锁,乐观锁,悲观锁,偏向锁,自旋锁,共享锁,独占锁,锁优化机制,公平锁,分段锁,信号量锁,等等

我们来列出来: 公平锁 / 非公平锁 可重入锁 / 不可重入锁 独享锁 / 共享锁 互斥锁 / 读写锁 乐观锁 / 悲观锁 分段锁 偏向锁 / 轻量级锁 / 重量级锁 自旋锁

是不是看着有点蒙圈了? 一个锁的概念,搞出这么多,这么复杂,难道IT行业都是喜欢折腾自己的高危职业。 为了理解锁,我们要搞清楚2个基本问题,就能抓住问题核心与实质了。

  1. 为什么需要锁?
  2. 为什么需要这么多锁?

先说为什么需要锁, 所有锁都是针对多线程,多核程序来说的。如果是一个主线程从头Run到尾,那不需要锁,因为所有内存的数据这个主线程都能可预期的访问。而当线程变多了后,增加到2个线程A和B,这2个线程对内存的数据的修改,互相并不知情,A线程修改了内存某个数据内容,B线程并不知道,其仍然按照以前的旧版本数据进行计算,于是程序就会出现不可预知的问题bug。所以我们要引入锁的概念,让别的线程想用内存某个数据时,直到这个数据能在被另一个线程使用修改,从而保证这个内存数据,在全局状态下,能被所有线程获得一致性的数据内容。看到没,锁费了老半天劲,就为了达到一个目的,让所有线程对共享数据都能获得一致的认知。归根结底,还不是线程只能看到自己的一亩三分地,无法获得全局性的认知。

好了,现在再说说为啥需要这么多锁? 因为我们为了最大化压榨cpu的处理性能,才要引入这么多锁。要知道每次加锁、去锁,都是从用户态切换到内核态,才能操作的。为什么?因为操作系统内核负责线程的调度,加减锁也是有内核和编译器来添加的。这种切换操作可不是像赋值语句那样,啪唧一下,就完成的,其中涉及到非常多的内存数据存取,数据总线的占用,中断的调用,总之看似一句简单的加锁语句,其背后对应非常多的额外操作。所以,我们为了提高程序运行性能,便在锁的使用上引入了非常多的灵活性,自然这里面锁的概念就复杂了多变了。

现在再来具体讲讲每种锁的概念 内置锁,synchronized,这个就是把加锁的语句集成到java语言的关键字里了,就像我们用if,else,while,for这样。 显式锁,就是ReentrantLockReentrantReadWriteLocKStampedLock,这个代表锁的Class,具体使用时,我们要像使用其他Class一样,要new出来,然后要自己释放。使用方法有点类似于C语言中的malloc分配空间,程序员要是自己忘记free释放了,久而久之可能造成内存泄漏。

ReentrantReadWriteLocK,这个是读写锁,多个线程里读,大家可以一起读,而写只能排队来,一个写完了,才能下一个来写。

StampedLock,这个是比ReentrantReadWriteLocK这个读写锁更细化了,从而在某些情况下,性能更好。也有一说,称之为乐观读锁和悲观读锁。看到里,是否会产生疑问,一个锁咋还跟人类情绪扯上关系了呢?其实很简单,这里的乐观和悲观指的是线程而言。假设你是线程,你现在要用某项数据,你肯定希望这次我取的数据,一定是最新的,别我刚Read了数据,下一刻就有新的线程跑来说,我要更新数据了,前面读了数据的线程傻了吧,你读的是旧版本的,没用啦。 所以,这里的悲观锁指的是,线程相信别的线程总会在其读的时候,跑来写,那么老子弄个悲观锁,你们谁也别写,等我都读完了,把锁释放了,你们再写。 那么与悲观读锁,相对应的是,乐观读锁,那就线程假设一时半会不会有线程来写数据,大部分线程来都是读数据。于是这个线程在读数据时,会检查那个数据是否被改写了,没被改写,直接读,要是被改写了,那只能再重新读一遍。 这个StampedLock标记锁,还有一个写模式,那就是线程只能排队一个一个来写。 记住所有锁的概念,都是正对读和写操作的,因为只有这2个操作,才涉及到内存一致性的问题。 互斥锁,表示的操作互斥,像synchronied, ReentrantReadWriteLocK,StampedLock这些写锁之间都是互斥的,读和写之间也是互斥的,读和读之间不互斥,这很好理解。

可重入锁,ReentrantLocksynchronized,都是可重入的,意思是同一线程能获取多次,这里避免了死锁问题,不可重入锁会带来线程A持有锁B,线程B持有锁A,两个线程都等着对方先释放,于是都是空转,这就叫死锁。

不可重入锁,一般有Semaphore,ReadWriteLock,FileLock,很容易造成死锁,JDK基本都是可重入锁。

可重入锁简单来说,就是锁有几把,锁有无限把,那就可重入,锁只有一把,那就不可重入。

可重入锁是以当前线程为单位来看的,当前线程在获得锁后,在后面的代码执行过程中,还能获取该锁那就是可重入锁。 如果不能,就是不可重入锁。

ReentrantLock分为公平锁和非公平锁,这个锁代表的意思是一个线程获取到锁,其他线程就只能等待,等到锁释放了,才能获取。 非公平锁就是,谁先来,谁拿走。当然就可能导致有的线程一直空转,但是性能较高。

在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

分段锁 分段锁没有具体的class,他是一种数据结构的设计锁模式,用于高并发的数据结构,例如java中的ConcurrentHashMap就是每个段对应一个ReentrantLock,对段是独占的,但是不同段,可以并发读取,提升性能。

ReentrantLock和ReentrantReadWriteLock都是可重入锁,ReentrantLock是独占锁,不分读和写的操作,而RentrantReadWriteLock是共享锁,读锁可同时获取,写锁独占。

偏向锁 偏向锁是Java中的一种锁优化技术,通过偏向锁机制,可以减少无竞争情况下的锁竞争,提高程序性能。

偏向锁的基本思想是:在无锁竞争的情况下,将锁的标记指向当前线程,这样线程再次进入同步代码块时,就可以直接获取锁,而不需要再次竞争。这种方式可以避免因为锁竞争导致的线程切换和上下文切换,从而提高程序的性能。

在JDK6之前,偏向锁默认是开启的,但是在JDK6之后,因为它会占用一定的内存,而且只有在同步代码块中进行大量的读操作,才会产生明显的优化效果,所以默认关闭了偏向锁。需要通过JVM参数来手动开启偏向锁。