【后端开发】JavaEE初阶—线程安全问题与加锁原理(超详解)
前言:
🌈上期博客:【后端开发】JavaEE初阶—Theard类及常见方法—线程的操作(超详解)-CSDN博客
🌈感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客
🌈小编会在后端开发的学习中不断更新~~~
目录
📚️1.引言
📚️2.线程状态
📚️3.线程安全
3.1什么是线程安全
3.2线程安全问题原理
3.3线程安全问题原因
📚️4.实现加锁
4.1加锁的原因
4.2加锁的目的
4.3加锁的实现
5.加锁的注意事项
6.加锁的其他写法
📚️5.总结
📚️1.引言
Hello!!!小伙伴们,小编上期讲解了关于Tread类的相关知识解析,以及对于线程的相关操作,相信大家对于这类知识有了新的理解,本期将讲解关于线程的重点问题,即关于线程安全和加锁的理解;开始发车了gogogo~~~🥳🥳🥳;
且听小编讲解,包你学会!!!
📚️2.线程状态
关于线程状态,我们之前讲解到,线程大致有两种状态
就绪状态:表示这个线程随时可以实现调度去CPU上执行,并且包括已经在CPU上执行的线程;
阻塞状态:即这个线程不方便去CPU上进行执行,即不方便调度,并且在java中对于阻塞有几种状态;
几种状态:
NEW:表示Thread对象创建后,但是还没有调用start方法在系统内创建线程
RUNNABLE:表示线程进入就绪状态,线程准备被CPU调度,或者已经在CPU上执行
TERMINATED:表示系统内的线程已经被执行了,线程已经销毁,但是Thread类对象还存在TIMED_WAITING:指时间阻塞,到达一定的时间后解除阻塞,主要是在Sleep的休眠等待阻塞WAITING:即不带有时间的阻塞(死等),只有达到一定的条件后,才会解除阻塞,主要是join,wait;
BLOCKED:即产生的锁竞争,引起的阻塞;后面现编会讲到
以上小编WAITING和BLOCKED小编就后面介绍
对于上述的概念比较难懂,那么接下来,小编用代码为大家进行实现,讲解吧~~~
代码实现:
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
System.out.println("线程执行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
System.out.println("线程状态"+ thread.getState());//NEW
thread.start();
System.out.println("线程状态"+ thread.getState());//RUNNABLE
Thread.sleep(2000);//确保线程执行完
System.out.println("线程状态"+thread.getState());//TERMINATED
}
讲解:线程start方法之前,线程还没有被创建,所以这里就是NEW,在线程创建后,此时线程就为就绪状态,此时就是RUNNABLE,执行完后,就是TERMINATED;
当然那还有TIMED_WAITING状态,代码如下:
thread.start();
Thread.sleep(2000);//确保线程进入休眠
System.out.println("线程状态"+thread.getState());//TIMED_WAITING
讲解:这里进行主线程的休眠是为了确保线程进入休眠等待状态,此时状态就TIMED_WAITING
图解实例:
注意:红色框里为线程的一般执行过程,若在加入了一些相关指令,没那么对应的线程状态也要进行改变;
📚️3.线程安全
实现多线程编程是为了实现并发线程,但是实现并发编程并不只有多线程才能实现~~~
多线程编程是比较原始,但是比较朴素的一种实现并发编程的方法
3.1什么是线程安全
当一个任务在单线程的执行下,或者在多个线程的执行下,没有出现BUG的情况下,那么就是线程安全的;
如果一个任务在单线程的执行下是没有BUG的,但是在多个线程的执行下又出现了BUG,那么此时就是线程不安全的;
例如如下代码:
public static int num=0;
//线程安全问题
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
num++;
}
});
Thread t2=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
num++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("num="+num);
}
注解:可以看到,我们这里实现的两个线程来进行num的递增,当两个线程启动后,我们的预料输出就是10万,但是输出如下:
此时可以看到这里和我们的预期输出是不一样的,那么这是为什么呢???
3.2线程安全问题原理
在上述问题中,我们发现此时在实现变量自增时,此时的就发生了线程的安全问题~~~
那么此时我们就要从CPU指令原理来理解了:
注意:这个执行语句num++,实际是由cpu上的三个指令构成的;
📍load:表示从内存中读取数据到cpu寄存器中;
📍add:表示把寄存器中的值加一;
📍save:表示将寄存器的数据写回到内存当中;
又因为线程的随机调度的问题,此时就有一下几种情况:
第一种情况;
那么此时我们就可以看到,这里的执行结果就符合我们数值递增的规律,那么此时又因为线程的随机调度,和抢占式执行,那么大多数就是按照以下情况执行:
第二种情况:
通过上述的图片解析,我们有以下了解:
1.在多线程实现时,存在随机调度的问题,那么此时cpu的三个指令会随机调度到cpu上去执行,此时就存在线程安全问题;
2.在随机调度的上述两中情况是不一定的,还存在无数种;
3.线程安全的前提是第一个线程成功save数据回到内存中后,线程二再load读取数据后,才能保证线程安全,但是这是几率很小的,大多数都是第二种类似的情况;
由于小编画图有限,这里的第二种情况还有很多,大家可以自己试试边画边理解哦;
3.3线程安全问题原因
在上述的总结实现后,小编总结了一下三点问题;
📍(根本原因)由于线程的随机调度,和抢占式执行的,导致了线程之间执行的顺序是不确定的,是有无数种情况的;
📍(代码结构)即多个线程同时执行一个任务,那么此时就会存在线程安全问题
📍(直接原因)上述的num++操作本来就不是“原子的”,即最小执行单元,要么不执行,要么执行完
📍还有内存可见性以及指令重排序的问题,这里代码没有涉及,小编就不再讲解了(小编也不知道😁😁😁)
📚️4.实现加锁
4.1加锁的原因
在上述讲解中,我们了解到线程的安全问题就是由于线程随机调度,导致的执行顺序不确定;
那么对于上述的几种原因,我们能够控制的就是(代码结构)(直接原因)但是为了实现这种代码结构,就会切断提高执行效率的要求,所以我们只能从直接原因入手;
4.2加锁的目的
加锁的目的就是为了实现多个指令,打包成一个原子的操作;
加锁后线程任然要进行随机的调度,但是此时即时在执行的线程被调度走了,但是其他线程仍然不会插队进行执行;
如图:
注意:这里的lock和unlock实现加锁后,添加的两条指令,在java中使用synchronized即可实现;
此时就解决了线程的随机调度和抢占式执行产生的线程安全的问题了~~~
4.3加锁的实现
当我们要实现加锁的操作时,那么就要添加锁对象,加锁解锁的操作都是依靠这个锁对象来执行的
代码实现如下:
public static int num=0;
public static void main(String[] args) throws InterruptedException {
Object ram=new Object();
Thread t1=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
加锁
synchronized (ram){
num++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
synchronized (ram) {
num++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("num="+num);
}
此时可以发现,小编设置了一个Objec类的对象,这里只要是对象就都可以,运用synchronized实现对于要进行加锁代码的操作;
注意:如果一个线程针对一个锁对象进行加锁后,其他线程也针对这个锁对象进行加锁,就会发生阻塞(BLOCKED)直到前一个线程执行完后即释放锁
5.加锁的注意事项
1.当线程加锁,另一个线程不加锁的时候
Thread t1=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
加锁
synchronized (ram){
num++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
num++;
}
});
这里就是一个加锁,另一个不加锁的情况,此时仍然存在线程安全问题;
注意:当写成上述情况时,此时两个线程就不会存在锁竞争了,那么对应的也就没有了线程阻塞问题,此时仍然会因为线程调度,导致线程安全问题;
2,当加锁的对象不同
Thread t1=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
synchronized (ram){
num++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
synchronized (tam) {
num++;
}
}
});
那么此时和上述情况一样,不会存在锁竞争,不会产生阻塞;
3.在加锁时要保证加锁对象一样
除了上述的写法之外,我们还能够用方法来实现:
public static void main(String[] args) throws InterruptedException {
Test t=new Test();
Thread t1=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
t.add();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i <50000 ; i++) {
t.add();
}
});
//线程启动,与等待
}
}
class Test{
public int num=0;
public void add(){
synchronized (this){
num++;
}
}
小编这里省去了线程的启动和等待哦~~~
这里就运用了方法实现线程的加锁,这里的this代表的是创建这个类的对象实例,那么此时两个线程调用方法的对象一样,那么此时也是线程安全的;
注意:综上所述,在解决线程安全问题时,主要是实现线程阻塞,加锁;那么加锁的对象一定要是一致的,否则会导致无法产生线程锁竞争,从而产生线程安全问题
6.加锁的其他写法
在实现加锁的操作时,对于以上的加锁方式,我们可以实现改进;
这里synchronized(this)代码中,我们就可以将synchronized写到方法上,代码如下:
class Test {
public int num = 0;
synchronized public void add() {
num++;
}
}
这种写法和上述的写法作用效果是一样的;
对于当括号里面是类对象时:
class Test {
public int num = 0;
public void add() {
synchronized (Test.class){
num++;
}
}
}
注意:在一个Java进程中只有一个类对象,所以第一个进程拿到的类对象和第二个进程拿到的类对象是同一个对象,此时就满足阻塞的要求;
当synchronized修饰静态方法时:
class Test {
public static int num = 0;
public static void add() {
synchronized (Test.class){
num++;
}
}
}
这个写法可以被代替成:
synchronized public static void add() {
num++;
}
注意:如果synchronized是加在static静态方法上就相当于给类对象加锁;
📚️5.总结
💬💬本期小编总结了关于多线程编程的重要问题,线程状态,以及线程安全问题讲解,并且如何解决线程安全问题,进行了原理讲解,并提出加锁的概念和实现;还附上了代码加以讲解~~~
🌅🌅🌅~~~~最后希望与诸君共勉,共同进步!!!
💪💪💪以上就是本期内容了, 感兴趣的话,就关注小编吧。
😊😊 期待你的关注~~~