当前位置:   article > 正文

JavaEE初阶之线程安全(一)

JavaEE初阶之线程安全(一)

 

目录

题外话

正题

1.线程调度是随机的

2.修改共享数据

知识点

线程同步机制

线程异步机制

举例说明

synchronized()

知识点

举例说明

举例代码详解

死锁

举个例子:

代码

小结


题外话

这两天忽冷忽热的感冒了,昨天状态特别不好断更了一天,今天继续加油!

我会把所有知识点放在每个小标题最前面,省的大家东找一个西找一个,大家可以先略微看看知识点,把文章看一遍回过头再看知识点,就有种豁然开朗的感觉

然后我比较喜欢用费曼学习法,大家有兴趣可以查查

这里简单说下费曼学习法,大家应该也或多或少知道这个,大家在跟着视频学习的时候,可以附和视频中的老师,或者试着对话,再或者遇到知识点,可以用给别人讲课的方式,把知识点说出来,这样确实很温故知新

就是在宿舍自言自语,自我对话会被别人当成傻子,怕社死的还是慎重考虑

我觉得做笔记也很关键,我从开始学javase,就开始把所有java知识点都放在了一个word文档里

复习起来也很方便,不需要再去翻找视频什么的费时费力,(如下图)

正题

我们先说说引发线程安全的因素

1.线程调度是随机的

大家都知道,JVM中线程是抢占式调度,所有线程去抢占cup的资源,线程抢的资源越多,执行该线程的速度也会越快

我们要做的是在写代码的时候,使多线程让任意执行顺序下都可以顺利执行

2.修改共享数据

知识点

一.先提出两个概念

我们把线程排队执行(线程一个一个执行),叫做;线程同步机制

而抢占式执行(多个线程一起执行),叫做;线程异步机制

先说说线程同步机制和线程异步机制的优缺点

线程同步机制

优点:可以保证数据安全问题

缺点:因为排队执行,导致效率变低,而且利用synchronized也会产生一些问题(如死锁等等)

线程异步机制

优点:多线程抢占式执行,效率很高

缺点:因为线程各自独立运行,互不影响,修改共享数据会产生数据错乱问题

二.再说说在java中什么数据会产生修改共享数据的问题

1.一般情况下:局部变量不存在线程安全问题。(尤其是基本数据类型不存在线程安全问题,因为它们在栈中,栈不共享数据,如果是引用数据类型,就另说了)

2.实例变量可能存在线程安全问题,实例变量在堆中,堆是多线程共享的。

3.静态变量也可能存在线程安全问题,静态变量在堆中,堆是多线程共享的

举例说明

先让大家更清晰理解修改共享数据会产生什么问题

比如一个银行账户被张三和李四两个人使用,两个人都把这张卡绑定支付,两个人都想取钱出来花,(如下图)

首先,银行会各自读取银行卡余额,然后再各自取钱,按照JVM中线程抢占式调用资源,

我们不知道张三取钱的时候,李四会不会才进行到获取银行卡余额这种情况,如果发生了这种情况,就会出现银行卡余额数据异常(当然,银行肯定非常安全,这种问题早就被解决了)

上述问题就是修改共享数据

所以面对这种问题,我们并不需要抢占式执行,而是需要排队执行,给张三取完钱,再去执行李四取钱的操作.

我们就需要synchronized()

synchronized()

知识点

1.线程同步的本质是:线程排队执行就是同步机制。

2. 语法格式:

synchronized(必须是需要排队的这几个线程共享的对象){

需要同步的代码

}

“必须是需要排队的这几个线程其享的对象”这个必须选对,

这个如果选错了,可能会无故增加同步的线程数量,导致效率降低。

3.可以给一块代码加锁,也可以对实例方法,静态方法加锁

加锁并不是一个很好的选择

加锁优缺点

优点:防止修改共享数据造成数据错乱

缺点:1.加锁的位置需要很慎重考虑

    因为有些线程需要修改共享数据,而有些线程不需要修改共享数据,如果把这种数据全部加锁,就会产生效率问题

        2.加锁占用资源比较大

        3.当我们写的代码越来越多,如果滥用锁的话会产生死锁等等一系列问题,导致效率很低

举例说明

synchronized()就是锁,大家可以理解为上厕所

一套屋子只有一个厕所,有人进去了,其他人只能等他出来才能进去

通过一段代码,让大家更清晰理解上锁会怎么样

这里代码没有上锁,让大家看看修改共享数据会造成什么后果

举例代码详解

以下是实现两个线程各自执行一万次count累加

public class ThreadDemo14 {
     static int count=0;
    public  void add()
    {

        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadDemo14 t=new ThreadDemo14();
        Thread t1=new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                t.add();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 10000; i++) {
               t.add();
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }

}
正常来说我们理想状态打印出的count应该是20000,但是修改共享数据会发生数据异常(如下图)



这次大家看好我加锁后的代码
public class ThreadDemo14 {
     static int count=0;
//直接给add方法加锁
    public synchronized void add()
    {

        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadDemo14 t=new ThreadDemo14();
        Thread t1=new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                t.add();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 10000; i++) {
               t.add();
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(1000);
        System.out.println(count);
    }

}

而这次运行结果和咱们理想结果一样

 

 

我是在add方法上直接加锁了,当然也可以在方法里加锁,synronized(this){代码块}这种形式即可

死锁

我们再说一下死锁的问题,其实死锁就是互相矛盾

举个例子:

小明在厕所1,厕所1只能洗手,一个厕所只能进入一个人

小红在厕所2,厕所2只能蹲坑,一个厕所只能进入一个人

小明洗完手想去蹲坑,而小红蹲完坑想去洗手,但是一个厕所只能进入一个人,这样他们永远不能走出自己所在的厕所,如下图

让大家看看代码,或许就明白了!

代码
public class ThreadDemo15 {
    public static void main(String[] args) {
//创建两个Oberject对象o1,o2
        Object o1=new Object();
        Object o2=new Object();
//建立两个线程t1,t2
        Thread t1=new MyThread01(o1,o2);
        Thread t2=new MyThread01(o1,o2);
//t1命名t1,t2命名t2
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}
//创建类继承Thread
class MyThread01 extends Thread
{
//创建私密变量o1,o2
    private Object o1;
    private Object o2;
//创建构造方法
    public MyThread01(Object o1,Object o2)
    {
        this.o1=o1;
        this.o2=o2;
    }
//重写run方法
    @Override
    public void run() {
//如果线程名字等于t1
        if ("t1".equals(Thread.currentThread().getName()))
        {
//进入o1锁
            synchronized (o1)
            {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
        System.out.println("解开了");
//进入o2锁
                synchronized (o2)
                {
                    
                }
            }
        }
//如果线程名字和t2一样
       else if ("t2".equals(Thread.currentThread().getName()))
        {
//进入o2锁
            synchronized (o2)
            { try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
              System.out.println("解开了");
//进入o1锁
                synchronized (o1)
                {
                    
                }
            }
        }
    }
}

当我们运行的时候就会发现,t1和t2都进入了锁中,但是永远不会结束进程,永远不会报错,就这样一直锁着,如下图

所以说还是需要好好掌握一下synchronized()的使用方式

小结

写完这篇有点晚了,但今天收获还是很大的,喜欢的朋友多多支持一下吧!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/366276?site
推荐阅读
相关标签
  

闽ICP备14008679号