线程状态+线程安全问题+synchronized的用法

java中的线程状态🥇

在操作系统层面,一个线程就两个状态:就绪和阻塞状态.

但是java中为了在线程阻塞时能够更快速的知晓一个线程阻塞的原因,又将阻塞的状态进行了细化.

  1. NEW:线程对象已经创建好了,但是系统层面的线程还没创建好,或者说线程对象还没调用start()
  2. TERMINATED:系统中的线程已经销毁,但是代码中的线程对象还在,也就是run()跑完了,Thread对象还在
  3. RUNNABLE:线程位于就绪队列,随时都有可能被cpu调度执行
  4. TIMED_WAITING:线程执行过程中,线程对象调用了sleep(),进入阻塞,休眠时间到了,就会回到就绪队列
  5. BLOCKED:有一个线程将一个对象上锁(synchronized)之后,另一个线程也想给这个对象上锁,就会陷入BLOCKED状态,只有第一个线程将锁对象解锁了,后一个线程才有可能给这个对象进行上锁.
  6. WAITING:搭配synchronized进行使用wait(),一旦一个线程调用了wait(),会先将所对象解锁,等到另一个线程进行notify(),之后wait中的线程才会被唤醒,当然也可以在wait()中设置一个最长等待时间,防止出现死等.

线程安全问题案例分析👂

多线程对同一变量进行写操作🏎

  1. 概念:一串代码什么时候叫作有线程安全问题呢?首先线程安全问题的罪恶之源是,多线程并发执行的时候,会有抢占式执行的现象,这里的抢占式执行,执行的是机器指令!那一串代码什么时候叫作有线程安全问题呢?多线程并发时,不管若干个线程怎么去抢占式执行他们的代码,都不会影响最终结果,就叫作线程安全,但是由于抢占式执行,出现了和预期不一样的结果,就叫作有线程安全问题,出bug了!

  2. 典型案例:使用两个线程对同一个数进行自增操作10w次:

    public class Demo1 {
        private static int count=0;
        public static void main(String[] args) {
            Thread t1=new Thread(()->{
                for(int i=0;i<50000;i++){
                    count++;
                }
            });
            t1.start();
    
            Thread t2=new Thread(()->{
                for(int i=0;i<50000;i++){
                    count++;
                }
            });
            t2.start();
    
            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println(count);
        }
    }
    //打印结果:68994
    

    显然预期结果是10w,但算出来就是6w多,这就是出现了线程安全问题.

    分析原因:

    仅针对每个线程的堆count进行自增的操作:首先要明白,进行一次自增的机器指令有三步:从主内存中把count值拿到cpu寄存器中->把寄存器中的count值进行自增1->把寄存器中的count值刷新到主内存中,我们姑且把这三步叫作:load->add->save

    我们假设就是在一个cpu上(画两个cpu好表示)并发执行两组指令(就不会出现同时load这样的情况了):

    如出现上图的情况:

    观察发现:两个线程都是执行了一次count++,但是两次++的结果却不如意,相当于只进行了一次自增,上述就是出现了线程安全问题了.

    并且我们可以预测出上述代码的结果范围:5w-10w之间!,为什么呢?

    上面两张图表示的是出现线程安全问题的情况,表现的结果就是两次加加当一次去用了,如果两个线程一直处于这样的状态(也是最坏的状态了),可不就是计算结果就是5w咯,那如果两个线程一直是一个线程完整的执行完load-add-save之后,另一个线程再去执行这样的操作,那就串行式执行了,可不就是10w咯.


  3. 针对上述案例如何去解决呢?

    案例最后也提到了,只要能够实现串行式执行,就能保证结果的正确性,那java确实有这样的功能供我们使用,即synchronized关键字的使用.

    也就是说:cpu1执行load之前先给锁对象进行加锁,save之后再进行解锁,cpu2此时才能去给那个对象进行上锁,并进行一系列的操作.此时也就是保证了load-add-save的原子性,使得这三个步骤要么就别执行,执行就一口气执行完.

    那你可能会提问,那这样和只用一个main线程去计算自增10w次有什么区别,创建多线程还有什么意义呢?

    意义很大,因为我们创建的线程很多时候不仅仅只是一个操作,光针对自增我们可以通过加锁防止出现线程安全问题,但是各线程的其他操作要是不涉及线程安全问题那就可以并发了呀,那此时不就大大提升了执行效率咯.

  4. 具体如何加锁呢?

    此处先只说一种加锁方式,先把上述案例的问题给解决了再说.

    使用关键字synchronized,此处使用的是给普通方法加synchronized修饰的方法(除此之外,synchronized还可以修饰代码块和静态方法)

    class Counter{
        private int count;
        synchronized public void increase(){
            this.count++;
        }
        public int getCount(){
            return this.count;
        }
    }
    public class Demo2 {
        private static int num=50000;
        public static void main(String[] args) {
            Counter counter=new Counter();//此时对象中的count值默认就是0
            Thread t1=new Thread(()->{
                for (int i = 0; i < num; i++) {
                    counter.increase();
                }
            });
            t1.start();
    
            Thread t2=new Thread(()->{
                for (int i = 0; i < num; i++) {
                    counter.increase();
                }
            });
            t2.start();
    
            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println(counter.getCount());
        }
    }//打印10W
    

内存可见性问题🍨

首先说明:这是有编译器优化导致的,其次要知道cpu读取变量时:先从主内存将变量的值存至缓存或者寄存器中,cpu计算时再在寄存器中读取这个值.

当某线程频繁的从内存中读取一个不变的变量时,编译器将会把从内存获取变量的值直接优化成从寄存器直接获取.之所以这样优化,是因为,cpu从主内存中读取一个变量比在缓存或者寄存器中读取一个变量的值慢成千上万倍,如果每每在内存中读到的都是同一个值,既然缓存里头已经有这个值了,干嘛还大费周折再去主内存中进行获取呢,直接从缓存中直接读取就可以了,可提升效率.

但是:一旦一个线程被优化成上述的情况,那如果有另一个线程把内存中的值修改了,我被优化的线程还傻乎乎的手里拿着修改之前的值呢,或者内存中的变量值被修改了,被优化的线程此时已经感应不到了.

具体而言:

public class Demo3 {
    private static boolean flag=false;
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            while(!flag){
                System.out.println("我是优化完之后直接读取寄存器中的变量值才打印的哦!");
            }
        });
        t1.start();

        flag=true;
        System.out.println("我已经在主线程中修改了标志位");
    }
}

运行上述代码之后,程序并不会终止,而是一直在那打印t1线程中的打印语句.


如何解决上述问题:

引入关键字volatile:防止内存可见性问题,修饰一个变量,那某线程想获取该变量的值的时候,只能去主内存中获取,其次它还可以防止指令重排序,指令重排问题会在线程安全的单例模式(懒汉)进行介绍.具体:

public class Demo3 {
    private static volatile boolean flag=false;
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            while(!flag){
                System.out.println("我是优化完之后直接读取寄存器中的变量值才打印的哦!");
            }
        });
        t1.start();

        try {
            Thread.sleep(1);//主线程给t1留有充足的时间先跑起来
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag=true;
        System.out.println("我已经在主线程中修改了标志位");
    }
}
//打印若干t1中的打印语句之后,主线程main中修改标志位之后,可以终止t1

注意:上述优化现象只会出现在频繁读的情况,如果不是频繁读,就不会出现那样的优化.


指令重排序问题📦

生活案例:买菜

如果是傻乎乎的按照菜单从上到下的去买菜,从路线图可以看出,不必要的路是真的没少走.

如果执行代码时,编译器认为某些个代码调整一下顺序并不会影响结果,那代码的执行顺序就会被调整,就比如可以把上面买菜的顺序调整成:黄瓜->萝卜->青菜->茄子

单线程这样的指令重排一般不会出现问题,但是多线程并发时,还这样优化,就容易出现问题

针对这样的问题,如果是针对一个变量,我们可以使用volatile修饰,如果是针对代码块,我们可以使用synchronized.


synchronized的用法💃

  1. synchronized起作用的本质
  2. 修饰普通方法
  3. 修饰静态方法
  4. 修饰代码块

synchronized起作用的本质🤙

因为我们知道java中所有类都继承了Object,所以所有类都包含了Object的部分,我们可以称这继承的部分是"对象头",使用synchronized进行对象头中的标志位的修改,就可以做到一个对象的锁一个时刻只能被一个线程所持有,其他线程此时不可抢占.这样的设置,就好像把一个对象给锁住了一样.


修饰普通方法📲

如前述两个线程给同一个count进行自增的案例.不再赘述.此时的所对象就是Counter对象

修饰静态方法⚡️

与普通方法类似.只不过这个方法可以类名直接调用.

修饰代码块🍼

首先修饰代码块需要执行锁对象是谁,所以这里可以分为三类,一个是修饰普通方法的方法体这个代码块的写法,其次是修饰静态方法方法体的写法,最后可以单独写一个Object的对象,来对这个Object对象进行上锁.

  1. class Counter{
        private int count;
        public void increase(){
            synchronized(this){
                count++;
            }
        }
        public int getCount(){
            return this.count;
        }
    }
    
  2. class Counter{
        private static int count;
        public static void increase(){
            synchronized(Counter.class){//注意这里锁的是类对象哦
                count++;
            }
        }
        public int getCount(){
            return this.count;
        }
    }
    
  3. class Counter{
        private static int count;
        private static Object locker=new Object();
        public static void increase(){
            synchronized(locker){
                count++;
            }
        }
        public int getCount(){
            return this.count;
        }
    }
    

    注意:java中这种随手拿一个对象就能上锁的用法,是java中一种很有特色的用法,在别的语言中,都是有专门的锁对象的.

Conclusion💯

  1. java中的线程状态,以及如何区分
  2. 线程安全问题
    1. 罪恶之源是抢占式执行
    2. 多线程对同一个变量进行修改,多线程只读一个变量是没有线程安全问题的
    3. 修改操作是非原子性的
    4. 内存可见性引起的线程安全问题
    5. 指令重排序引起的线程安全问题
  3. synchronized的本质和用法
  • 9
    点赞
  • 6
    收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:像素格子 设计师:CSDN官方博客 返回首页
评论 6

打赏作者

奋斗&&奋进

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值