线程安全问题(synchronized解决,各种类型全)

发布于:2023-01-26 ⋅ 阅读:(690) ⋅ 点赞:(0)

提示:本文基于上一篇文章继续阐述线程安全问题
全文内容为:
1.线程安全问题产生的原因
2.同步锁的使用和原理
全文手打,都是精华,有案例有内容,代码可复制运行思考,创作不易,请耐心看
本人能力有限,如有遗漏或错误,敬请指正,互相交流,谢谢

其他文章

1.Java多线程基本概念和常用API(面试高频)
2.创建多线程的方式(1)
3.Java并发线程池使用和原理(通俗易懂版)
4.基于SpringBoot+Async注解整合多线程

一、线程安全问题是什么

用Java来说,当多个线程操作同一个变量的时候,就会产生线程安全问题。

举个经典栗子:窗口卖票问题,现在假设有三个窗口ABC在卖票,票数总共100张,那么卖票的规则是只要票数>0并且人员进到了窗口里面就可以买票
现在假设100张票不同的窗口总共已经卖出去了99张,现在剩下最后一张,那么如果有三个人刚好都分别进到了ABC三个窗口里面,如果A窗口卖掉了最后一张,但是此时票数为0的数据由于数据延时还没传到卖票系统中,
所以在BC窗口符合上述卖票的条件==①票数>0 ②人员进了窗口(进入了run方法)

所以最后B和C窗口都卖出了最后一张票,这就导致数据混乱

最终得出:什么叫线程安全问题?

通俗点理解就是:线程一在执行任务的时候,还没有执行完,线程二也进来执行线程一还没执行完的任务,这就乱套了,这种情况就是线程不安全。

上述代码示例:

public class MyRunnable implements Runnable {
    //总票数
    private int ticket;
    @Override
    public void run() {
        while(ticket>0){
            System.out.println(Thread.currentThread().getName()+"卖出最后"+ticket+"票");
            //这里模拟网络延迟,数据没有上传到车站系统中
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //延迟过后才更新数据到后台
            ticket--;
        }
    }
}
public static void main(String[] args) {
        //station:创建车站,假设票数只卖到最后一张
        MyRunnable station=new MyRunnable(1);
        //创建三个窗口,因为传入的runnable对象是同一个,所以ticket是共享的
        Thread t1=new Thread(station,"窗口A");
        Thread t2=new Thread(station,"窗口B");
        Thread t3=new Thread(station,"窗口C");
        t1.start();
        t2.start();
        t3.start();

    }

在这里插入图片描述

二、产生线程安全问题的条件

  1. 多线程并发 (上述多个窗口)
  2. 有共享数据 (最后一张票)
  3. 共享数据有修改的行为(卖出去后,票数要减1)

三、解决线程安全问题:线程同步机制

线程同步机制就是“线程排队执行”,线程不能并发。
代价:牺牲一部分效率,但是数据安全第一的方法

轮流进去,锁住,就算被堵住了,也要等执行完才能下一个
类似于只有一个厕所,三个人上厕所,轮流上,等上一个人出来才能进去

3.1 synchronized同步代码块(代码上)

先不管这个是什么,先记住:只要使用这个关键字,锁必须是同一把(不懂下面有解释)

给同步代码块包裹在内的代码加一把锁,同一时间只有占有这把锁的线程能执行代码
其他线程只能等该线程执行完后,再抢占获取锁,抢占后就能执行代码。这样保证了“同步机制”

按照上面上厕所的例子来说,给这个厕所门上锁,那么外面的人没有钥匙怎么进去呢?

代码举例:改造上述的代码

public class MyRunnable implements Runnable {
	//锁的对象
    private Object object;
    //总票数
    private int ticket;

    @Override
    public void run() {
    	//加锁
        synchronized (object) {
        	//添加一个标志位,当卖出票就设置为true
            boolean flag=false;
            while (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出最后" + ticket + "票");
                //这里模拟网络延迟,数据没有上传到车站系统中
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //延迟过后才更新数据到后台
                ticket--;
                //卖出票设置为true
                flag=true;
            }
            //当没有卖出票走这个判断
            if(!flag){
                System.out.println("票卖完了,"+Thread.currentThread().getName() + "无法卖出票");
            }
        }
    }
public static void main(String[] args) {
        //station:创建车站,假设票数只卖到最后一张。new Object()创建一个对象传入进去,所以三个线程共用object这个对象,也就是共用同一把锁
        MyRunnable station=new MyRunnable(new Object(),1);
        //创建三个窗口,因为传入的runnable对象是同一个,所以ticket是共享的
        Thread t1=new Thread(station,"窗口A");
        Thread t2=new Thread(station,"窗口B");
        Thread t3=new Thread(station,"窗口C");
        t1.start();
        t2.start();
        t3.start();
    }

可以看出保证线程安全了
在这里插入图片描述

3.2 同一把锁的理解

//obj就是锁,要保证不同线程里面的obj,都是同一个,也就是A线程.obj==B线程.obj  
synchronized(obj){ 

}

我们为了保证线程安全,就是要让每个线程不在同一时间执行一样的代码,轮流执行

那么一个门上如果有两个钥匙能开门,那同一时间是否会有两个线程能进去?

synchronized同步锁具有互斥性,这相当于线程由并行执行变成串行执行(轮流),保证了线程的安全性,但损失了性能。

3.3 synchronized同步代码块(方法上)

放在方法上,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B或C或D正在用这个方法(或者该类的其他同步方法,这句话不懂向下看就明白了)
有的话要等正在使用synchronized方法的线程B或C或D运行完这个方法后,再运行此线程A
没有的话,锁定调用者,然后直接运行


synchronized关键字放在实例方法上,锁的是“this”(谁调用方法,锁的对象就是谁)
好比上面这个代码,由于线程的run()方法是由station这个对象执行的(忘记就看上面)
所以锁的对象就是station,由于三个Thread都是由station创建来的,所以是同一把锁

public class MyRunnable implements Runnable {
    private Object object;
    //总票数
    private int ticket;

    @Override //可以注意到同步代码块放在方法上了
    public synchronized void run() {
        boolean flag = false;
        while (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出最后" + ticket + "票");
            //这里模拟网络延迟,数据没有上传到车站系统中
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //延迟过后才更新数据到后台
            ticket--;
            flag = true;
        }
        if (!flag) {
            System.out.println("票卖完了," + Thread.currentThread().getName() + "无法卖出票");
        }
    }
}

适用情况:当整个方法需要同步的时候,并且共享对象是this

ps:补充说明黄色部分

public class A {
    //放在方法上,锁的是this
    public synchronized void method1(){
        
    }
	//放在方法上,锁的是this,和上面是同一把锁,所以如果有线程调用了method2()
	//其他线程也访问不了method1(),因为此时“this”这个锁被占有了
    public synchronized void method2(){

    }
}

3.4 synchronized同步代码块(静态方法上)

放在静态方法中(共享对象是“类”)
适用情况:类锁只有一把,创建100个对象但是类锁还是只有一个
通常用于保护静态变量安全


public class A {  
	//锁的对象是:A.class,所以不管创建多少个A的实例,A.class只有一个
    public synchronized static void AMethod(){}  
} 

public class B{  
    public void BMethod(){  
    	//类锁的第二种写法,直接指明是某个类
        synchronized (B.class){  
 
        }  
    }  
}

3.5 synchronized的原理

加锁实际上就是在锁对象的对象头中写入当前线程id
每个线程要想调用这个同步方法,都会先去锁对象的对象头看看当前线程id是不是自己的。

举例:假设锁对象的对象头是 key,开始key=null。 那么现在线程A获取了锁先进去方法,此时key=A,那么线程B想调用方法的时候,会查看key,发现key=A,不等于B,所以不会执行方法

注意:synchronized是可重入锁。也就是说当线程访问对象的同步方法时,在调用其他同步方法时无需再去获取其访问权。因为我们实际上锁的是对象,对象头里面记录的都是当前线程的ID。

四、死锁问题:都不放弃锁

线程A执行方法1获取了锁obj1,那么其他线程是无法访问方法1的,在方法1最后一行代码是获取方法2的锁,只有执行完这行代码,方法一才能退出

好巧不巧,在线程A获取锁obj1的同时,线程B执行了方法2获取了锁obj2,但是方法2的最后一行代码是获取方法1的锁,只有执行完这行代码,方法二才能退出

所以A和B互相占了对方退出方法时必须要占有的锁,僵持过后就成了死锁

public class MyThread extends Thread{
    Object ob1;
    Object ob2;
    public MyThread(Object ob1,Object ob2){
        this.ob1=ob1;
        this.ob2=ob2;
    }
    public void run(){
    	//拿了obj1这个锁
        synchronized (ob1){
            try {
            	//睡眠这个时间,线程m2执行run()方法,获取了obj2这个锁
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //由于线程m2占有obj2锁,所以线程m1拿不到,线程m2拿不到obj1锁也出不去,最后两个线程都出不去
            synchronized (ob2){
                System.out.println("---");
            }
        }
    }

}
public class MyThread1 extends Thread{
    Object ob1;
    Object ob2;
    public MyThread1(Object ob1,Object ob2){
        this.ob1=ob1;
        this.ob2=ob2;
    }
    public void run(){
        synchronized (ob2){
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (ob1){
                System.out.println("---");
            }
        }
    }

}
public class Test{
    public static void main(String[] args) throws InterruptedException {
        Object ob1=new Object();
        Object ob2=new Object();
        MyThread m1=new MyThread(ob1,ob2);
        MyThread1 m2=new MyThread1(ob1,ob2);
        m1.start();
        m2.start();

    }
}

五、总结

1. java中的同步方法会增加你程序的性能的消耗,所以只有在正真需要的时候才使用同步。使用同步代码块是需要保护资源才使用

2.静态方法加锁,和xx.class 锁效果一样,都是类锁

3.“this锁”需要强调的是,关于同一个类的方法上的锁,来自于调用该方法的对象,如果调用该方法的对象是相同的,那么锁必然相同,否则就不相同。比如 new A().x() 和 new A().x(),对象不同,锁不同

感谢看到最后,码字不易,纯手打,有帮助点个⭐是更新的动力!

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到