JAVAEE之线程安全问题及解决方案(二)

在(一)中,简历介绍了何为线程安全,以及线程安全所产生的过程和原因。

本篇主要介绍线程安全问题的解决方案。


一.加锁

什么是加锁

加锁是针对修改操作并非原子的线程安全问题的解决方案,主要的办法是把“非原子”的操作打包成“原子”。

以下通过图例来形象解释

假设一个“滑稽老哥”要上厕所,在一个人最脆弱的时候,往往是不希望被人打扰的,这时,他把厕所上了锁,在滑稽老哥结束前,其他滑稽老铁不能强行闯进来。

当滑稽老哥完事后,就会打开厕所的锁,让后面的滑稽老铁进来上厕所。

这一系类操作就是操作加锁,程序中,加锁使一个线程在执行的过程中,其他的进程不能插队进来。

如何实现加锁

JAVA中,提供了synchronized()关键字,来完成 加锁操作。

synchronized()是关键字,并不是函数,后面的()并非参数,需指定一个锁对象(可以指定任何对象),来进行后面的判断。        

 public static Object lock=new Object();
      synchronized (lock) {
                    count++;
                }

此为synchronized的使用方法,后面{}里填写需加打包成原子的代码。

{}还可以放任意的其他代码,包括调用别的方法,只要有合法的Java代码都可以。

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

注:只有锁{}里的代码是串行的,之外的是并发执行

有了加锁后,线程安全问题得到了解决。

 synchronized如何实现加锁

假设t1线程加锁成功,则继续执行{}里的代码。(相当于上厕所操作)

t2线程后加锁,发现t1线程已经加锁(厕所里有人了),于是进入阻塞(等t1),当t1加锁{}代码执行完毕,t2线程开始执行加锁{}里的代码。

本质上是把随机的并发执行强制变成了串行,从而解决线程安全。

加锁的注意事项

上述代码有效的前提是,两个线程都加锁了,而且是针对同一个对象加锁。

1)假如t1加锁,t2不加锁,count结果会怎样?

public class Demo1 {
    private static int count;
    public static Object lock=new Object();
    public static void main(String[] args) throws InterruptedException {
    Thread t1=new Thread(()->{
        for (int i = 0; i < 50000; i++) {
                synchronized (lock) {
                    count++;
                }
        }
    });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                    count++;

            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count="+count);
    }
}

从运行结果上,可以看到count结果又随机起来了。

这可以形象的理解为,一个滑稽老铁上厕所加锁了,另一个滑稽老哥不走门,走窗户进去了。

2)假如t1与t2加不同对象的锁,结果又如何

public class Demo1 {
    private static int count;
    public static Object lock=new Object();
    public static Object lock2=new Object();
    
    public static void main(String[] args) throws InterruptedException {
    Thread t1=new Thread(()->{
        for (int i = 0; i < 50000; i++) {
                synchronized (lock) {
                    count++;
                }
        }
    });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (lock2) {
                    count++;
                }

            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count="+count);
    }
}

可以发现,结果又出bug了

这可以形象理解为,两个滑稽老铁在不同的厕所上厕所,两个厕所没有联系。

但线程里的count++也变成并行执行了,开始抢占式执行。

锁对象的作用,就是区分多个线程是否针对“同一个对象”加锁

是针对同一个对象加锁,此时就会出现阻塞(锁竞争)

不是针对同一个对象加锁,此时不会出现阻塞,两个线程仍是随机并发执行

锁对象填哪个对象并不重要,重要的是多个线程是否是同一把锁。

注 :JAVA为了减少程序员写出死锁的概率,引入了特殊机制"可重复锁",可以针对一个线程重复加锁。

使用锁的缺点

锁应该在需要的时候使用,不需要的时候不要使用。

使用锁,就有可能发生阻塞,一旦某个线程阻塞,啥时能恢复阻塞,继续执行是不可期的(需要付出性能的代价)。

胡乱的加锁也可能产生“死锁”。

二.死锁

死锁是指在多线程或多进程环境中,两个或多个进程因争夺资源而导致相互等待,从而无法继续执行的状态。

死锁场景

1.一个线程,重复锁
 void fun(){
        synchronized (this){  //第一次加锁
            synchronized (this){  //第二次加锁
                
            }
        }
    }

上述代码进行重复加锁,当加到第一个锁后,对第二个锁加锁需释放第一个锁,但第二个锁在第一个锁内,于是就发生了阻塞。由于无法释放锁,也就无法对第二个锁进行加锁,代码就走不下去了,此时发生了“死锁”。

但是,JAVA为了减少程序员写出死锁的概率,引入了特殊机制"可重复锁",对一个线程重复加锁进行了优化,根据当前锁是否被占用来判断是对哪个锁加锁了(它暖,我哭)。

2.两个线程,两把锁

设有线程1,线程2。

锁A,锁B。

1)首先,线程1针对A加锁,线程2对B加锁。

2)线程1在不释放锁A的情况下,再对B加锁。同时,线程2不释放锁B对A加锁。

具体代码如下

public class Demo2 {
    private static Object lockerA=new Object();
    private static Object lockerB=new Object();

    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (lockerA){
                System.out.println("t1加锁A完成");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            synchronized (lockerB){
                System.out.println("线程t1对B加锁");
            }
            }
        });
        Thread t2=new Thread(()->{
        synchronized (lockerB){
            System.out.println("t2加锁B完成");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lockerA){
                System.out.println("线程t2对A加锁");
            }
        }
        });
        t1.start();
        t2.start();
    }
}

我们发现,程序进入了死锁状态

打开jconsole,可以发现两个线程都进入了BLOCKED状态

3.多个线程,多把锁(哲学家用餐)

围绕一张桌子有5个哲学家,卓上放着食物和5根筷子。

每个哲学家吃饭,需要拿起两边筷子

假如1哲学家拿起左右两边筷子吃饭,此时2也开始吃饭,发现右手边筷子在哲学家1手中,此时就发生了阻塞等待

当哲学家1放下筷子,哲学2才能拿起两边筷子吃饭。

每位哲学依次这样执行,此模型便能通过。

但是,当每位哲学家同时拿起左手边筷子时

此时再想拿起右手边筷子,发现右手筷子没了。

由于哲学家比较固执,当他吃不到饭也不会放下左手边筷子,导致所有的哲学家都无法吃饭,于是发生了死锁。

  死锁产生的必要条件(重点)

1.锁是互斥的【基本特性】
2.锁是不可被抢占的【基本特性】

假如线程1拿到了锁A,如果不线程1不主动释放锁,线程2就不能把锁抢过来

3.请求与保持。【代码逻辑】

线程1拿到锁A之后,不释放A的前提下去拿锁B。

4.循环等待,也叫环路等待【代码逻辑】。

多个线程获取锁的过程中,出现循环等待。

上诉死锁条件,缺一不可。

死锁场景的解决方案

1.针对一个线程重复锁

在JAVA中以对其进行了优化,引入了可重复锁机制,通过引用技计数,维护当前已经加了几次锁,以及表述出何时真正释放锁

2.针对两个线程,两把锁。

从死锁必要条件“请求与保持”入手,在释放锁后再加锁

优化代码如下

public class Demo1 {
    private static Object lock1=new Object();
    private static Object lock2=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
           synchronized (lock1){
               System.out.println("线程t1对lock1加锁成功");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }

           synchronized (lock2){
               System.out.println("线程t1对lock2加锁成功");
           }
        });
        Thread t2=new Thread(()->{
           synchronized (lock2){
               System.out.println("线程t2对lock2加锁成功");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
           synchronized (lock1){
               System.out.println("线程t2对lock1加锁成功");
           }
        });
        t1.start();
        t2.start();
    }
}

代码并未死锁,成功运行。

3.针对哲学家用餐问题

给锁进行编号,约定所有线程在加锁时,都按照一定的顺序进行加锁(比如先针对编号小的加锁,再针对编号大的加锁)

所有哲学家拿起低号的筷子,从哲学家2开始,拿起1号筷子,哲学家3拿起2号筷子.....当轮到哲学家1时,应该拿起1号筷子,但1号筷子已经被哲学家2拿起,那哲学家就陷入阻塞,不拿筷子。

此时,5号哲学家面前就有了两根筷子,就能够用餐,当5号哲学用餐结束后,就释放筷子,4号哲学家就能再拿起4号筷子用餐.....如此下去,每个哲学家都能够用餐

只要遵循以上约定进行加锁,无论接下来这个模型运行顺序如何,都不会死锁了。

代码实现如下

public class Demo2 {
    private static Object lock1=new Object();
    private static Object lock2=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (lock1){
                System.out.println("线程t1对lock1加锁成功");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            synchronized (lock2){
                System.out.println("线程t1对lock2加锁成功");
            }
        });
        Thread t2=new Thread(()->{
            synchronized (lock1){
                System.out.println("线程t2对lock2加锁成功");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (lock2){
                System.out.println("线程t2对lock1加锁成功");
            }
        });
        Thread t3=new Thread(()->{
            synchronized (lock1){
                System.out.println("线程t3对lock1加锁成功");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            synchronized (lock2){
                System.out.println("线程t3对lock2加锁成功");
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

按照一定顺序进行加锁,所有线程都执行完毕,未发生死锁。

4.银行家算法

银行家算法是一个用于避免死锁的资源分配算法,特别适用于多进程系统中的资源管理。该算法由 Edsger Dijkstra 提出,主要用于确保系统在分配资源时处于安全状态。(过于复杂,不过多讲)


以上就是全部内容了,如有不对,欢迎指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值