Java锁是Java中一个非常重要的一个知识,是作为一个中高级Java程序员的一个必备的知识技能,如果对其不了解或了解不够,估计大多数会被同行鄙视。
今天就总结下Java锁相关的知识。包含锁的思想、分类等。最后整理一下自己的思考,希望能对有需要的同学有所帮助。
为什么要说这个呢?上层的设计和下层的实现同样重要,这也是很多程序员容易忽略的地方。
肯定是处理数据(资源)同步问题。如果操作一个数据(资源)不是一个原子操作的话,那么在多线程情况下就有可能出现数据异常的问题,所以锁住主要的作用是处理多线程情况下的数据同步问题。(单线程和数据操作是原子操作的话,锁就无意义了)
锁的分类比较多,很容易把人搞昏,分类之间的边界也不是很清晰,下面简单的总结下。
下面的分享都有一个前提,线程A正在操作资源(已获取到锁),线程B,C没有获取到锁
悲观锁和乐观锁
1、悲观锁
线程A获取到锁正在操作资源,线程B/C想要获取锁将会阻塞。
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
多写的场景下用悲观锁就比较合适。
2、乐观锁
线程A获正在操作资源,线程B/C想要操作资源将会(通过CAS)自旋修改资源。
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
多读的场景下用悲观锁就比较合适。即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
互斥锁和共享税
这个分类尽量结合ReenTrantLock来说明,它的底层原理是基于AQS实现的。
读写锁也是基于AQS实现,表示可以允许多个线程一起读,旨在提高效率。读读共享,读写互斥,写读互斥,写写互斥。
- 互斥锁:
互斥锁顾名思义就是一个资源在临界区内只能有一个线程访问,通过这种方式来保证数据同步,相比之下效率比共享锁有锁下降,所有这个主要用操作比较频繁的场景。 - 共享锁:
共享锁也是基于AQS实现。共享锁表示的在临界区里可以允许多个线程同时访问,提高了访问效率(吞吐量),但这个所有适合于读较多的场景。
可重入锁和不可重入锁
- 可重入锁
可以反复进入临界区 - 不可重入锁
不可以反复进入临界区
即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
CAS算法涉及到三个操作数:
1、需要读写的内存值 V
2、进行比较的值 A
3、拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
AQS实现了基本的锁的获取和释放,并处理了大部分可预见的异常(多个线程同时获取,同时释放,线程自旋,线程终端,共享锁等)。