1.1.我们现在知道常用的锁策略
那synchronized基于这些策略有哪些特性呢?
1.2.synchronized 的一些锁优化机制(jdk 1.8)
JVM 将 synchronized 锁分为四种情况.
四种锁状态也会根据实际情况依次进行升级.
锁粗化涉及到锁的粒度
-
锁的粒度: 加锁代码涉及到的范围.(不是整体多个加锁总和的范围,而是单个锁涉及的范围)
-
如果两次加锁之间隔的代码比较多,不会影响整体运行状态的情况下,一般不会优化.
-
如果之间间隔比较小(中间间隔的代码少),间隔过少的时候可能涉及多次释放锁后,又马上申请锁的操作.就很可能触发这个优化(JVM 就会自动把锁粗化)
有这么一种情况: 有些代码,明明不用加锁,结果你给加上锁了.编译器就会发现这个加锁好像没啥必要,就直接把锁给去掉了(锁消除).
当然你可能说,我不会乱加锁的.不过,有的时候加锁操作并不是那么明显,稍不留神就会做出了这种错误的决定.
例如: StringBuffer, Vector…这些类,在标准库中进行了加锁操作,而我们在单个线程中用到这些类的时候,就是单线程进行了加锁解锁.而我们并不会发觉,不过我们也不用担心.因为编译器会发现并处理这一类情况,也就是上述的锁消除.
2.1.Callable接口的使用
Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为 Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.所以 FutureTask 就可以负责这个 等待结果出来 的工作.
-
创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
-
重写 Callable 的 call 方法, 完成一个任务. 直接通过返回值返回计算结果.
-
把 callable 实例使用 FutureTask 包装一下.
-
创建线程, 线程的构造方法传入 FutureTask .此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
-
在主线程中调用 task.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args) {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i 1000; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> task = new FutureTask<>(callable);
Thread t = new Thread(task);
t.start();
try {
System.out.println(task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
2.2.Callable 和 Runnable 比较
- 都是描述一个任务
-
Callable 描述的是带有返回值的任务.
-
Runnable 描述的是不带返回值的任务.
3.1.ReentrantLock:可重入锁
有三个主要的方法
相对于习惯使用 synchronized 的我来说,这里加锁和解锁 分开的做法,是不太友好的,因为很容易遗漏 unlock(),出现锁死.
import java.util.concurrent.locks.ReentrantLock;
public class Test2 {
public static void main(String[] args) {
ReentrantLock locker = new ReentrantLock(true);
locker.lock();
try {
} finally {
locker.unlock();
}
}
}
3.2.Semaphore:信号量
锁就相当于一个二元信号量,可用资源只有一个.计数器非0,即1
Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用.
类的参数为信号量个数
import java.util.concurrent.Semaphore;
public class Test {
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(4);
semaphore.acquire();
System.out.println("申请成功");
semaphore.acquire();
System.out.println("申请成功");
semaphore.acquire();
System.out.println("申请成功");
semaphore.acquire();
System.out.println("申请成功");
semaphore.release();
semaphore.acquire();
System.out.println("申请成功");
}
}
3.3.CountDownLatch 同时等待N个任务执行结束.
类的参数.表示需要等待任务的个数.
-
countDown()方法.任务调用此方法表示任务结束.此时CountDownLatch内部的计数器减1.
-
await()方法.调用此方法的线程阻塞等待所有任务执行完毕. 就是计数器减为0的时候.
import java.util.concurrent.CountDownLatch;
public class Test {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
Thread t = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "到达终点");
countDownLatch.countDown();
});
t.start();
}
countDownLatch.await();
System.out.println("比赛结束");
}
}
3.4.CopyOnWriteArrayList 写时拷贝
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy.复制出一个新的容器后往新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器.
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为只读的时候不会有线程安全问题.
3.5.ConcurrentHashMap 多线程环境下可以使用的哈希表
ConcurrentHashMap 相比于 Hashtable 又做出了一系列的改进和优化
-
读操作没有加锁(但是使用了 volatile 保证从内存读取结果),只对写操作进行加锁.加锁的方式仍然是用 synchronized,但不是对整个对象加锁, 而是 “锁桶” (用每个链表的头结点作为锁对象,让锁加到每个链表的头结点上), 大大降低了锁冲突的概率.
-
充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
-
优化了扩容方式: 化整为零
-
发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
-
扩容期间, 新老数组同时存在.
-
后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小 部分元素.
-
搬完最后一个元素再把老数组删掉.
-
这个期间, 插入只往新数组加.
-
这个期间, 查找需要同时查新数组和老数组.
Original: https://blog.csdn.net/m0_58154870/article/details/127813542
Author: 魚小飛
Title: synchronized的优化机制和一些多线程的常见类
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/652427/
转载文章受原作者版权保护。转载请注明原作者出处!