前言
在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!”
博客主页:KC老衲爱尼姑的博客主页
博主的github,平常所写代码皆在于此
共勉:talk is cheap, show me the code
作者是爪哇岛的新手,水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!
文章目录
- Synchronized原理
- 偏向锁
- 自旋锁
- 重量级锁
- 其他的优化操作
- 锁消除
- 锁粗化
- Callable接口
- Callable的用法
- **JUC(java.util.concurrent)** **的常见类**
- **ReentrantLock**
- **信号量** **Semaphore**
- CountDownLatch
- 线程安全的集合类
- 多线程环境使用ArrayList
- **多线程环境使用哈希表**
Synchronized原理
Synchronized即是轻量级锁又是重量级锁,它会根据实际情况自适应加锁。
偏向锁
(1)第一次加锁的时候线程,会进入偏向锁 的状态,偏向锁并不是真的加锁,只是给对象头做了一个偏向锁的标记,记录该锁属于哪个线程,如果后续没有其他的线程加锁,就可以不进行加锁操作。如果后续有其他的线程来竞争该锁,那么刚才的锁对象已经记录了当前时锁属于那个线程,很容易知道当前的线程是不是之前记录的线程,那么就取消偏向锁的状态,进入一般的轻量级锁状态,偏向锁是本质是延迟加锁,能不加锁就不加锁,尽量来避免不必要的加锁开销。但是该做的标志还的做,否则无法区分什么时候需要真正加锁。
自旋锁
(2)当其他的线程进入竞争的时候,偏向锁状态消除会进行轻量级锁,也就是自旋锁。
此处的轻量级锁是通过的CAS实现的,具体操作如下
- 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
- 如果更新成功, 则认为加锁成功
- 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).
自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.。因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了. 也就是所谓的 “自适应”。
重量级锁
(3)重量级锁
如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁
此处的重量级锁就是指用到内核提供的 mutex .,具体操作如下
- 执行加锁操作, 先进入内核态.
- 在内核态判定当前锁是否已经被占用
- 如果该锁没有占用, 则加锁成功, 并切换回用户态.
- 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
- 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁.
其他的优化操作
锁消除
JVM自动判定,发现这个地方的代码,不必加锁,如果你写了Synchronized,就会自动的把锁去掉。比如,只有一个线程,或者多个线程不涉及修改同一个变量,如果代码中写Synchronized,此时Synchronized加锁操作,就会被JVM给干掉。Synchronized加锁是先偏向锁的,只是改 了个标记位,按理说这个操作开销也不大?即是如此,能消除的时候,也不是连这一点开销都不想承担。锁消除也是一种编译器优化的行为,编译器的判定,不一定非常准,因此,如果代码的锁百分之100能消除,就给你消除了。如果这个代码的锁,判断的准,就还是不消除了,锁消除只是在编译器/JVM有十足的把握的时候才进行。
示例代码
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加锁解锁操作是没有必要的, 白白浪费了一些资源开销。
锁粗化
锁的粒度,Synchronized对应的代码块中包含多少代码,包含的代码少,粒度细,包含的代码多,粒度粗,锁粗化,就是把细粒度的加锁->粗粒度的加锁。粗的前提是保证代码的逻辑不变,细化的时候代码是正确的,粗化之后还是正确的。
举个栗子理解锁粗化,张三给下交代任务,方式一:张三给下属打电话,交代任务1,挂断电话,再打电话,交代任务2,挂断电话,再打电话,交代任务三,方式二:张三大电话,一次性交代了三个任务,再挂断电话。这就是一个锁细化–>锁粗化的过程。
Callable接口
由于Runnable不提供返回值,而时候需要得到返回值,此时就可以使用Callable。
Callable的用法
Callable 是一个 interface ,描述了一个带返回值的任务,相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算结果.
代码示例
创建线程计算 1 + 2 + 3 + … + 1000, 不使用 Callable 版本
public class Demo {static class Result{public int sum =0;private Object lock = new Object();}public static void main(String[] args) throws InterruptedException {Result result = new Result();Thread t = new Thread(() -> {int sum = 0;for (int i = 0; i <=1000;i++) {sum+=i;}synchronized (result.lock) {result.sum=sum;result.lock.notify();}});t.start();synchronized (result.lock) {while (result.sum==0) {result.lock.wait();}System.out.println(result.sum);}}
}
运行结果:
上述代码需要借助一个辅助类,还需要使用到一系列的加锁和wait/notify,相对而言代码是比较复杂的。
代码示例:创建线程计算 1 + 2 + 3 + … + 1000, 使用 Callable 版本
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class CallableDemo {public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<Integer> callable = new Callable<Integer>(){@Overridepublic Integer call() throws Exception {int sum = 0;for (int i=1;i<=1000;i++) {sum+=i;}return sum;}};FutureTask<Integer> futureTask = new FutureTask<Integer>(callable);Thread t = new Thread(futureTask);t.start();int result = futureTask.get();System.out.println(result);}
}
运行结果:
Callable 通常需要搭配 FutureTask 来使用,FutureTask 用来保存 Callable 的返回结果. 因为allable 往往是在另一个线程中执行的, 啥时候执行完并不确定,FutureTask 就可以负责这个等待结果出来的工作。
JUC(java.util.concurrent) 的常见类
ReentrantLock
ReentrantLock是可重入锁和synchronized类似都是实现互斥效果,保证线程安全。
ReentrantLock 的基础使用
- lock(): 加锁, 如果获取不到锁就死等.
- trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
- unlock(): 解锁
ReentrantLock lock = new ReentrantLock();
-----------------------------------------
lock.lock();
try { // working
} finally { lock.unlock()
}
示例代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockDemo3 {private static Lock lock = new ReentrantLock();private static Condition waitCigaretteQueue = lock.newCondition();private static Condition waitbreakfastQueue = lock.newCondition();private static volatile boolean hasCigrette = false;private static volatile boolean hasBreakFast = false;public static void main(String[] args) throws InterruptedException {new Thread(() -> {lock.lock();try {while (!hasCigrette) {try {waitCigaretteQueue.await();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("等到了它的烟");}}finally {lock.unlock();}}).start();new Thread(() -> {lock.lock();try {while (!hasBreakFast) {try {waitbreakfastQueue.await();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("等到了它的早餐");}}finally {lock.unlock();}}).start();Thread.sleep(1000);sendBreakFast();Thread.sleep(1000);sendCigarette();}private static void sendCigarette() {lock.lock();try {System.out.println("送烟来了");hasCigrette = true;waitCigaretteQueue.signal();}finally {lock.unlock();}}private static void sendBreakFast() {lock.lock();try {System.out.println("送早餐来了");hasBreakFast = true;waitbreakfastQueue.signal();}finally {lock.unlock();}}
}
运行结果:
ReentrantLock 和 synchronized 的区别:
-
synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现).
-
synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
-
synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
-
synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.
-
更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
如何选择使用哪个锁?
- 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
- 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
- 如果需要使用公平锁, 使用 ReentrantLock.
信号量 Semaphore
信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器。举个栗子,可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源。当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作),当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作),如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.。
Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用.
代码示例
import java.util.concurrent.Semaphore;public class SemaphoreDemo {//可用资源设置为1private static Semaphore semaphore = new Semaphore(1);public static void main(String[] args) {Runnable runnable = () -> {try {System.out.println("申请资源");semaphore.acquire();System.out.println("我获取到资源");Thread.sleep(1000);System.out.println("我释放资源了");semaphore.release();} catch (InterruptedException e) {throw new RuntimeException(e);}};for (int i = 0; i <2;i++) {Thread t = new Thread(runnable);t.start();}}
}
运行结果:
CountDownLatch
同时等待 N 个任务执行结束。举个栗子,号称地表最强的下载器IDM,下载文件的时候,会将一个文件分配给多个线程下载,只有当所有的线程下载好了,才是整个文件下载好。
代码示例
假设有十名运动员参加跑步比赛,当所有的运功员通过终点的时候,比赛才结束。
import java.util.concurrent.CountDownLatch;public class CountDownLatchDemo {public static void main(String[] args) throws InterruptedException {//构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成CountDownLatch latch = new CountDownLatch(10);Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName()+"已经到了");latch.countDown();}};for (int i = 0; i <10;i++) {new Thread(runnable).start();}latch.await();System.out.println("比赛结束");}
}
运行结果:
线程安全的集合类
多线程环境使用ArrayList
(1)自己使用同步机制 (synchronized 或者 ReentrantLock)
(2)Collections.synchronizedList(new ArrayList);
synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List,synchronizedList 的关键操作上都带有 synchronized。
(3)使用 CopyOnWriteArrayList
CopyOnWrite容器即写时复制的容器。所谓的写时拷贝,就是当我们往一个容器中添加元素的时候,不直接往当前容器添加,而是先将当前的容器进行copy复制出一个新的容器,然后新的容器里添加元素。添加完元素之后,再将 原来容器的引用指向新容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
多线程环境使用哈希表
HashMap 本身不是线程安全的,在多线程环境下使用哈希表可以使用:Hashtable和ConcurrentHashMap
(1)Hashtable
Hashtable只是简单的在一些关键的方法如get/put上加了synchronized。
这相当于直接针对 Hashtable 对象本身加锁.
- 如果多线程访问同一个 Hashtable 就会直接造成锁冲突.
- size 属性也是通过 synchronized 来控制同步, 也是比较慢的.
- 一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低.
一个Hashtable只有一把锁,两个线程访问的Hashtable中的任意数据都会出现锁竞争。
(2) ConcurrentHashMap
相比于 Hashtable 做出了一系列的改进和优化. 以 Java1.8 为例
- 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然是用 synchronized, 但是不是锁整个对象, 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率.
- 充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
- 优化了扩容方式: 化整为零 , 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.,扩容期间, 新老数组同时存在.,后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素.,搬完最后一个元素再把老数组删掉. 这个期间, 插入只往新数组加,这个期间, 查找需要同时查新数组和老数组。如果是要插入元素,直接在新的数组上添加,如果是删除元素,直接删 了。
各位看官如果觉得文章写得不错,点赞评论关注走一波!谢谢啦!。