Java 线程和操作系统的线程有啥区别?
- 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用),切换成本低,不可以利用多核。
- 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问),创建和切换成本高,可以利用多核。
JDK1.2 之前的Java线程本质就是用户线程,现在的 Java 线程的本质其实就是操作系统的线程
乐观锁和悲观锁
什么是悲观锁?
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
像 Java 中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
悲观锁的缺点?
1、高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。
2、悲观锁还可能会存在死锁问题,影响代码的正常运行。
什么是乐观锁?
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去使用使用版本号机制或 CAS 算法验证对应的资源(也就是数据)是否被其它线程修改了
乐观锁的优势?
高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,在性能上往往会更胜一筹,也不会有死锁的问题,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能。所以,乐观锁通常多用于写比较少的情况,悲观锁适用于写比较多的情况。
如何实现乐观锁?
乐观锁一般会使用版本号机制或 CAS 算法实现。
版本号机制
一般是在数据表中加上一个数据版本号 version
字段,表示数据被修改的次数。当数据被修改时,version
值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version
值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version
值相等时才更新,否则重试更新操作,直到更新成功。
CAS 算法
CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
CAS 涉及到三个操作数:
- V:要更新的变量值(Var)
- E:预期值(Expected)
- N:拟写入的新值(New)
当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。
乐观锁存在哪些问题?
1、ABA问题
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们不能能说明它的值没有被其他线程修改过,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。
JDK 1.5 以后的 AtomicStampedReference
类就是用来解决 ABA 问题的,其中的 compareAndSet()
方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2、 循环时间长开销大
CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
3、只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,我们可以使用锁或者JDK1.5之后可以利用AtomicReference
类把多个共享变量合并成一个共享变量来操作。
ReentrantLock
ReentrantLock 是什么?
ReentrantLock
实现了 Lock
接口,是一个可重入且独占式的锁,和 synchronized
关键字类似。不过,ReentrantLock
更灵活、更强大,增加了轮询、超时、中断、支持公平锁和非公平锁等高级功能。
ReentrantLock
里面有一个内部类 Sync
,Sync
继承 AQS(AbstractQueuedSynchronizer
),添加锁和释放锁的大部分操作实际上都是在 Sync
中实现的。Sync
有公平锁 FairSync
和非公平锁 NonfairSync
两个子类,所以ReentrantLock
的底层就是由 AQS 来实现的
公平锁和非公平锁有什么区别?
- 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
- 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
synchronized 和 ReentrantLock 有什么区别?
1、两者都是可重入锁
2、synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
3、ReentrantLock 比 synchronized 增加了一些高级功能,比如等待可中断、可实现公平锁、可实现选择性通知
可中断锁和不可中断锁有什么区别?
- 可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。
ReentrantLock
就属于是可中断锁。 - 不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。
synchronized
就属于是不可中断锁
ReentrantReadWriteLock
ReentrantReadWriteLock 是什么?
ReentrantReadWriteLock
实现了 ReadWriteLock
,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。
ReentrantReadWriteLock
其实是两把锁,一把是 WriteLock
(写锁),一把是 ReadLock
(读锁) 。读锁是共享锁,写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。
共享锁和独占锁有什么区别?
-
共享锁:一把锁可以被多个线程同时获得。
-
独占锁:一把锁只能被一个线程获得。
线程持有读锁还能获取写锁吗?
- 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
- 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
读锁为什么不能升级为写锁?
写锁可以降级为读锁,但是读锁却不能升级为写锁。这是因为读锁升级为写锁会引起线程的争夺,毕竟写锁属于是独占锁,这样的话,会影响性能。
另外,还可能会有死锁问题发生。举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。