qidao123.com技术社区-IT企服评测·应用市场
标题:
【Java ee 初阶】多线程(8)
[打印本页]
作者:
我爱普洱茶
时间:
2025-5-8 11:18
标题:
【Java ee 初阶】多线程(8)
Synchronized优化:
一、锁升级
锁升级时一个自适应的过程,自适应的过程如下:
在Java编程中,有一部分的人不肯定能正确地使用锁,因此,Java的设计者为了让大家使用锁的门槛更低,就在synchronized中引入了很多优化计谋。
*首先,根据前面的学习,我们已经开端相识了自旋锁和重量级锁,那么图中的方向锁是什么?
方向锁,不是真正加锁,而只是做个标记,如果整个过程中,没有其他线程来竞争这个锁,方向锁状态就始终保持,直到最终解锁。但是,如果在过程中,遇到其他线程也实验来竞争这个锁,就可以在其他线程拿到锁之前,争先获取这个锁。如许子仍旧可以使其他线程产生阻塞,保证线程的安全。
锁升级的过程:
1.方向锁->轻量级锁:说明该线程上发生了锁竞争
2.轻量级锁->方向锁:竞争更加猛烈
那我们如何去相识竞争的猛烈水平呢?——JVM内部,统计了这个锁上面有多少个线程在等待获取,这统统都取决于JVM的实现。
而我们作为步伐员,关心的是计谋,而不是参数,因为参数都是可以调解的,但是计谋是不变的。
锁升级,对于当前主流的JVM实现来说,是“不可逆”的,一旦升级了,就无法回头降级了
二、锁消除
有些代码,那你写了加锁,但是在JVM执行的时候会发现,这个地方没必要加锁,因此JVM就会自动把锁给去除掉
例如:我们知道,StringBuilder不带有synchronized,StringBuffer带有synchronized,因此大概有人在单线程情况下就使用StringBuffer,此时这个锁只在一个线程中,因此会被JVM给优化掉。
JVM的优化是一方面,咱们作为步伐员,也不能够完全摆烂。
三、锁粗化
首先我们先相识一个概念——锁的粒度。加锁和解锁范围中代码越多,锁的粒度就越粗,反之锁的粒度就越细
如图我们可知这两段代码,上面那一段代码的粒度更粗,下面代码的粒度更细。
获取到一次锁,大概是一件不太容易的事变。因此有的时候,JVM会进行锁粗化,将加锁解锁的次数淘汰,以此提高服从。synchronized关键字,作为Java的关键字,底层实现是在JVM内部完成的,不是通过Java来实现的,而是通过C++来实现的。
总结:synchronized优化重要表现在:
1.锁升级
2.锁消除
3.锁粗化
四、CAS
CAS,全称compare and swap,比力和交换。
这是一串cas的伪代码(不是严酷符合语法等待,知识用来描述一下逻辑)
address:是一个内存地址
expectValue,swapValue:是CPU寄存器
这段代码实现的内容是:拿着内存中的值,和寄存器1的值进行比力,如果二者相等,就把内存和寄存器2的值进行交换。一般来说,只关心内存中值的变化,而不关心寄存器2中发生了什么变化。上述的交换,也可以近似理解成赋值。
注意,上述所有的逻辑,都是通过“一个CPU指令”完成的。一条指令,意味着这是原子的!!没有线程安全的问题。
CPU的特殊指令完成了上述的所有操作—>操作系统封装了这个指令,形成了一个系统的API—>Java中又封装了操作系统的API,由unsafe包进行提供,这里提供的操作,比力底层,大概不安全。
CAS的典范应用
1.实现原子类
可以将原来的三步打包成原子的,通过AtomicInteger,原子的进行++ -- 等操作
例如:
package Thread;
public class demo1 {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for(int i=0;i<5000;i++){
count++;
}
});
Thread t2 = new Thread(()->{
for(int i=0;i<5000;i++){
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
复制代码
输出效果:
出现了运算符重载的问题!
什么是运算符重载:运算符重载就是让已有的运算符针对差异的类,也能够有对应的效果,Java并不支持。运算符重载的利益,就是可以用使代码的一致性更高,比如,定义一个类,“复数”通过运算符重载,就可以使得复数类和其他的数字范例相似,都可以进行加减乘除。定义一个矩阵类也可以通过运算符重载。
Java中认为,上述写法,容易被滥用,因此Java并不支持。
package Thread;
import java.util.concurrent.atomic.AtomicInteger;
public class demo1 {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for(int i=0;i<5000;i++){
//下列这些操作,都是原子的,不加锁也能保持线程安全
count.getAndIncrement();//count++
//count.incrementAndGet();//++count
//count.getAndIncrement();//count++
//count.getAndDecrement();//--count
//count.GetAndDecrement();//count--
//count.getAndAdd(10);//count+=10;
}
});
Thread t2 = new Thread(()->{
for(int i=0;i<5000;i++){
count.getAndIncrement();//count++
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
复制代码
此时的输出:
如许的操作,不但线程安全,而且服从更高,不涉及到阻塞。因此在实际开发中,如果有这种计数的需求,优先考虑原子类,而不是本身去加锁。
划红线的部分,就是JVM封装过的,比上述谈到的CAS还要更加复杂一点,但是他们的本质是一样的。
伪代码实现:
CAS如果一样,就说明在这两者之前没有其他线程修改value内存,此时就可以安全的对value进行修改了。如果CAS发现有其他线程插队,就会进入循环,重新load。此处的oldValue是寄存器,而不是变量。C语言曾经能够定义寄存器变量,但是厥后也废除的,也就只有汇编能够直接操作寄存器了,和之前传统的++相比,在真正进行修改之前,又做了判断。
2.实现自旋锁
CAS实现自旋锁的伪代码
如果owner为null,解锁状态,否则就会保存持有锁的线程的引用。如果锁已经被占用了,这个循环,就会快速地反复地循环。循环体中是没有任何sleep。这是一种消耗cpu,换来尽快地加锁速率。但是,如果锁竞争很猛烈,大量的线程都会如许自旋,就会消耗非常多的cpu,那么cpu就负担不起了,锁释放之后,这些线程还是要竞争,还是意味着大量的线程是无法第一时间拿到锁的。
下面的赋值操作天然就是原子的,判断-赋值(check and set),典范的非原子的操作。
CAS的ABA问题
通过CAS来判断,当前load到寄存器的内容和内存的内容是否一致,如果一致,就认为没有其他线程修改过这个变量,接下来本线程的修改就是安全的。
然而,这内里存在着一个缺陷:大概出现这种情况:另一个线程又把内存的值从A改成B,又从B改回了A,此时CAS是感知不到的,仍旧会认为没有其他线程修改过的。
ABA问题,通常情况下都是没事的,纵然其他线程真的修改了,由于又修改回了原来的值,所以ABA现象不肯定给步伐引起BUG,但是如果遇到特别极端的场景,那还是有大概的,下面是一个非常极端的例子:
上述场景下,由于ABA问题,导致了重复扣款。
那么,针对ABA问题,我们应该如何解决呢?上述问题之所以出现ABA问题,是因为他针对余额的修改大概加,也大概减,如果改变成“只能加,不能减”,那么就可以解决上述问题。因此,我们引入版本号,约定每次修改,都需要对版本号+1,并且每次CAS比力的时候都是比力版本号是否雷同(雷同的话,进行版本号+1 和 余额修改)
我们可以定义一个类,包含版本号和余额,如下:
五、Callable接口
callable接口,类似于Runnable,也是属于JUC(java.util.concurrent)这个包的。call自定义返回值,run返回值是void。
接下来让我们使用Callable接口创建一个线程,并且通过这个线程计算1+2+3+...+100
package Thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class demo2 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
//callable 带有泛型参数 泛型参数就是返回值类型
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int result = 0;
for(int i=0;i<=100;i++){
result+=i;
};
return result;
};
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
//get会阻塞等待线程执行完毕,拿到返回值
System.out.println(futureTask.get());
}
}
复制代码
创建线程的方式:1.继续Thread 2.实现Runnable 3.基于lambda(本质还是Runnable)4.实现Callable 5.基于线程池
六、ReentranLock
ReentrantLock是一把比力传统的锁,在synchronized还不成熟的时候,这个锁就是进行多线程编程加锁的重要方案
package Thread;
import java.util.concurrent.locks.ReentrantLock;
public class demo3 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantLock locker = new ReentrantLock();
Thread t1 = new Thread(()->{
for(int i=0;i<5000;i++){
//加锁
locker.lock();
count++;
//解锁
locker.unlock();
}
});
Thread t2 = new Thread(()->{
for(int i=0;i<5000;i++){
locker.lock();
count++;
locker.unlock();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
复制代码
这种写法,容易把unlock遗漏
换成这种写法,不太美观
区别与联系
1.synchronized是一个关键字,是JVM内部实现的(大概率是基于C++实现)
ReentrantLock是尺度库的一个类,在JVM外实现的(基于Java实现的)
2.synchronized使用的时候不需要手动释放锁
ReentantLock使用的时候需要手动释放,使用的时候更加灵活,但是也容易遗漏。
3.synchronized是非公平锁,ReentrantLock默认是非公平锁,可以通过构造方法传入一个true开启公平锁模式
4.synchronized在申请锁失败的时候会死等,ReentrantLock可以通过trylock的方式等待一段时间就放弃(这是synchronized不具备的特点,trylock加锁失败的时候会直接放弃)
5.ReentrantLock拥有更强盛的叫醒机制,synchronized是通过Object的wait /notify实现 等待-叫醒。每次叫醒的是一个随机等待的线程 ReentrantLock搭配Condition类来实现等待-叫醒,九二一更加准确控制叫醒某个指定的线程
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 qidao123.com技术社区-IT企服评测·应用市场 (https://dis.qidao123.com/)
Powered by Discuz! X3.4