当前位置: 首页 > article >正文

多线程之旅:开启多线程安全之门的钥匙

上次,小编分享到了多线程出现了安全问题。

那么接下来小编来分享下是如何解决这个多线程安全问题的。

上次分享到,多线程安全问题的“罪魁祸首”:线程调度的不确定性。

就是说呢,A、B、C三个线程,然后,A线程执行完,下次系统分配资源执行的时候,不知道是B先执行,还是C先执行,具有不确定性。

就像是在一个房间内,有个领导听A、B、C汇报

A在里面说说话,话没说完呢,就被扯出去了,又随机放 一个人进去房间说话,所以最终导致领导听的汇报出现不一致。

所以,我们可以这样子,这个房间内我加一把锁,A进去之后,锁住房间,期间内不能让其他人进去,等A说完后,再把锁给其他人,按照刚刚的方法。

那么java呢,提供了这样的一个东西,可以保证线程安全。

synchronized

synchronized 是 Java 中的一个关键字,用于控制多线程对共享资源的访问,确保同一时间只有一个线程可以执行某个代码块或方法,从而避免线程间的竞争条件和数据不一致。

用法如下:

synchronized (括号中是一个对象){

}

{}内放的是要打包成一整个整体的代码。

此时呢,如若进入代码块,那么就会针对里面的代码进行加锁。

出了代码块就会解锁。

值得注意的是,锁是不可被抢占,一个线程拿到了锁,其他线程要想拿到这个锁,必须等待这个锁被释放。

使用上一篇文章,开头给的例子,它是线程不安全的例子,接下来使用synchronized。

代码实例:

public class Demo18 {
    public static Object locker=new Object();
    public static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            for (int i=0;i<5000;i++){
                synchronized (locker){
                   count++;
                }
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<5000;i++){
                synchronized (locker){
                   count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count:"+count);
    }
}

显然,这个代码中,实例化一个Object对象,然后把这个对象名locker作为参数给到t1线程和t2线程中的synchronized。

可见我们的运行结果是正确的。

这里要提到的是,传入给synchronized的参数不一定要求是Object的,可以是其他。

比如String locker=new String()、public static int [] locker=new int[100];

值得注意的是,要对同一个对象加锁才行,对不同的对象加锁,所起的作用是无效的。

那么这个加锁操作只能这样子了吗?

当然不是,还有其他的操作。

比如

synchronized可以修饰方法。

代码实例

class Counter{
    public static int count=0;
    synchronized public void add(){
        count++;
     }
}
public class Demo14 {
    public static void main(String[] args) throws InterruptedException {
        //synchronized修饰方法
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
            for (int i=0;i<5000;i++){
                synchronized (counter){
                    counter.add();
                }
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<5000;i++){
                synchronized (counter){
                    counter.add();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(Counter.count);

    }
}

那么这个add()方法可以等价于这样写的

  public void add(){
       synchronized (this){
            count++;
        }
    }

里面的参数改为了this,即谁调用该方法,谁就要被锁了。

除了这样的操作,这个synchronized还可以修饰静态方法。

synchronized可以修饰静态方法。

代码实例:

class Counter{
    public static int count=0;
 synchronized public static void add(){
        count++;
    }
public class Demo14 {
    public static void main(String[] args) throws InterruptedException {
        //synchronized修饰方法
        Counter counter=new Counter();
        Thread t1=new Thread(()->{
            for (int i=0;i<5000;i++){
                synchronized (counter){
                    Counter.add();
                }
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<5000;i++){
                synchronized (counter){
                    Counter.add();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(Counter.count);

    }
}

同样的add()方法等价于:

public static void add(){
    synchronized (Demo14.class){
        count++;
    }
}

那么里面的Demo14.class是什么意思呢?

Demo14.class用于获取Demo14的Class的对象。

每个主类都只有一个Class对象。

当然也可以这样写的

synchronized (Demo14.class){
   count++;
}

然而,锁这么好用,但是也不能随便用的,会发生死锁问题的。

死锁问题

那什么 又是死锁呢?

场景一:一个线程一把锁,然后这个线程对此把锁进行连续加锁两次。

来一个代码引入:

 public static Object locker=new Object();
    public static int count=0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            for (int i=0;i<5000;i++){
                synchronized (locker){
                   count++;
                   synchronized (locker){
                       
                   }
                }
            }
        });
  t1.start();
}

显然此时,synchronized使用了两次,还是用了同一个locker对象。

那么此时当代码进入第一个sychronized的代码块内部

那么这时候,里面就拿到锁了。

然后当代码执行到第二个scychronized的时候,此时发现,这个locker对象,还是被第一个sychronized持有,因为它还没出代码块呢,所以还没释放这个锁资源。

所以,第二个synchronized想拿到locker对象,但是第一个sychronized还没释放这个锁,

要释放这个锁,就必须把第二个sychronized加锁给执行完,显然,就僵住了,就是说阻塞了,你不让我,我不让你。

但是,看会执行结果,显然运行成功了。

这是为什么呢?

因为这是因为java的synchronized是一个可重入锁。

那什么又是可重入锁?

可重入锁(Reentrant Lock) 是一种同步机制,允许同一个线程多次获取同一把锁。这种锁是可重入的,意味着如果一个线程已经持有了某个锁,它可以再次获取该锁而不会被阻塞。

意思就是说,加锁的时候判定下,当前这个锁是否是被占用状态,是被哪个线程占用了,如若是当前线程对这个锁进行多次加锁,此时后续的加锁将不会进行真正的加锁操作。

所以,以上的例子情况是synchronized对这情况进行了特殊处理了。

场景二:两个线程,两把锁

即有两个线程,t1和t2,锁1,锁2

t1线程一开始对锁1进行加锁,t2线程一开始对锁2加锁

t1线程不释放锁1的情况下,再对锁2进行加锁,t2线程不释放锁2情况下,再对锁1进行加锁

代码引入:

public class Demo15 {
    public static Object locker1=new Object();
    public static Object locker2=new Object();

    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (locker1){
                System.out.println("t1加锁locker1完成!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2){
                    System.out.println("t1加锁locker2完成!");
                }
            }
        });
        Thread t2=new Thread(()->{
            synchronized (locker2){
                System.out.println("t2加锁locker2完成!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker1){
                    System.out.println("t2加锁locker1完成!");
                }
            }
        });
        t2.start();
        t1.start();
    }
}

运行结果

此时,出现了阻塞,还有两条语句没有进行打印。

可以看出,代码出现了循环依赖咯。

这也是个死锁发生的例子。

当然,上面是两把锁,两个线程。

如若是N个线程,M把锁,此时呢,一旦出现问题,效果也是类似的,就是会阻塞特别严重。

ok,接下来总结下,死锁发生的条件

死锁必要条件

1.锁是互斥的(基本特性)
2.锁是不可抢占的(基本特性)

3.请求和保持(代码结构)

即在拿到A锁情况下,同时不释放锁A,去拿锁B,但此时,锁B也没有释放资源

4.循环等待(代码结构),多个线程获取锁的过程中,出现了循环等待

那么出现了死锁,那就要尝试去解决它。

尝试解决死锁问题

我们可以从条件入手,当然条件1、2显然是不能用了,毕竟是基本特性。

条件3中,我们可以尝试这样解决

1.一次性申请所有资源,即不是在执行过程中,再次逐步申请

2.资源预分配

在任务启动之前,预先分配所有需要的资源。

……

条件4中

有一个简单的办法,即对锁进行排序,按照锁的顺序来,进行一定顺序的加锁,那么此时可以避免死锁问题。

比如,这个例子

代码实例:

public class Demo15 {
    public static Object locker1=new Object();
    public static Object locker2=new Object();

    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (locker1){
                System.out.println("t1加锁locker1完成!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2){
                    System.out.println("t1加锁locker2完成!");
                }
            }
        });
        Thread t2=new Thread(()->{
            synchronized (locker1){
                System.out.println("t2加锁locker2完成!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2){
                    System.out.println("t2加锁locker1完成!");
                }
            }
        });
        t2.start();
        t1.start();
    }
}

运行结果

此时呢,加锁顺序都是按照locker1再到locker2的顺序来,显然,运行是没有问题的。

当然,解决死锁问题,还有一个方案

即银行家算法

核心思想:在分配资源之前,系统会检查此次分配是否会导致系统进入不安全状态。如果分配后系统仍然是安全的,才允许分配;否则,拒绝分配。

但是由于日常开发中不是经常使用这个,毕竟不够接地气,所以这里不多介绍了。


http://www.kler.cn/a/514756.html

相关文章:

  • 计算机组成原理(计算机系统3)--实验八:处理器结构拓展实验
  • 计算机毕业设计hadoop+spark股票基金推荐系统 股票基金预测系统 股票基金可视化系统 股票基金数据分析 股票基金大数据 股票基金爬虫
  • QT调用OpenSceneGraph
  • Java 8 实战 书籍知识点散记
  • 精选100+套HTML可视化大屏模板源码素材
  • 前端【7】javascript-dom操作
  • 如何使用CRM数据分析优化销售和客户关系?
  • 【搞机】GMK-G3因特尔n100处理器核显直通win10虚拟机
  • 如何有效使用Python爬虫将网页数据存储到Word文档
  • 机器学习实战第一天:LSTM(长短时记忆网络)
  • Git 如何将旧仓库迁移新仓库中,但不显示旧的提交记录
  • C语言初阶牛客网刷题——JZ17 打印从1到最大的n位数【难度:入门】
  • 【JavaSE】(8) String 类
  • 计算机怎么入门
  • 动态规划(DP)(细致讲解+例题分析)
  • ChatGPT接入苹果全家桶:开启智能新时代
  • HBased的原理
  • HDBaseT和KVM 和POE是怎么融合在一块的
  • 国产编辑器EverEdit - 文件列表
  • 08-Elasticsearch
  • 区块链的数学基础:核心原理与应用解析
  • ImportError: cannot import name ‘datapoints‘ from ‘torchvision‘
  • # [Unity]【游戏开发】 脚本生命周期与常见事件方法
  • 局域网中 Windows 与 Mac 互相远程连接的最佳方案
  • 网络编程-网络原理HTTP初识
  • 【Python】笔试面试题之生成器、闭包、字典