多线程——volatile关键字

今天我就来讲一讲volatile关键字,以及相关的一些延伸知识,希望能对大家在JavaEE学习阶段能有所收获和帮助。

一、volatile关键字的作用

当我们在使用多线程的时候,会遇到很多线程安全问题,在我们之前有讨论过,是什么 导致的线程安全问题的出现,其中内存的不可见性和非原子性操作都会导致出现线程安全的问题。其中详细的请参考我的一篇文章,里面详细展示了我对于线程安全问题的理解。点这里立即查看

在JDK1.8的时候,修改了JMM的存储机制,在主内存和线程之间设想了一个本地内存的概念,这里就可能会出现内存可见性的问题,如果有两个线程同时使用一个数据资源,从主内存当中拿去两份数据存放到各自的本地内存当中,那么在线程一修改数据完提交给主内存中,但线程二并没有读取已经修改的主内存数据,是直接使用的本地内存的数据,就会导致内存不可见性,使用volatile关键字,则可以避免出现这样的情况。

public class Demo1 {
    // 创建一个计时类
    static class Counter {
        private int count = 0;
    }


    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            // 输入的数不等于0应该跳出循环
           while(counter.count == 0) {
            
           }
            System.out.println("循环结束!");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数");
            counter.count = scanner.nextInt();
        });

        t2.start();
        t1.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 应该打印我们手动输入的count值
        System.out.println(counter.count);
    }
}

在上面的代码示例中,我们可以发现,我们的代码陷入循环体内,没有打印出我们想要的count值,这就是因为我们的t1线程使用的是自己本地内存中的count值,我们的t2虽然修改count的值,但我们的t1并不知道count的值已经被修改,所以就会陷入死循环!这个时候我们就可以使用volatile关键字来进行内存可见性的操作了。

public class Demo1 {
    static class Counter {
        //private int count = 0;
        // 使用volatile关键字修饰count变量,使得多个线程可以同时访问该变量
        private volatile int count = 0;
    }


    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
           while(counter.count == 0) {

           }
            System.out.println("循环结束!");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数");
            counter.count = scanner.nextInt();
        });

        t2.start();
        t1.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter.count);
    }
}

注意看,我们把成员变量count用volatile进行修饰,这样就可以让t1强制读取主内存中的更新数据,但会牺牲程序本身的效率!因为在我们前面提到JMM的构建思想,就是会在不同线程中存在属于线程自己的本地内存,这个其实是一个抽象的概念,本质上来说还是依赖CPU的寄存器来实现的,它的读写是很快的,但不能保证数据的精确性,就可能会引起我们上述的线程安全问题。我们的volatile的思想就是强制将修饰的数据在内存当中进行读写操作,内存的读写肯定是要比CPU慢很多的,但是大大提高的数据的精确性,不易发生线程安全问题。下面我用一张图帮你更好的理解volatile关键字!

二、volatile关键字与synchronized关键字的区别

我们知道synchronized关键字可以保证原子性的相关操作,但volatile不能像synchronized那样做到这一点,我们还是以上述代码来做一个延伸,去写一个增加的方法,并在两个线程中同时开始,看看结果如何:

public class Demo1 {
    static class Counter {
        //private int count = 0;
        // 使用volatile关键字修饰count变量,使得多个线程可以同时访问该变量
        private volatile int count = 0;
        // 实现count自增功能
        public void increase() {
            count++;
        }
    }


    public static void main(String[] args) {
        Counter counter = new Counter();
       /* Thread t1 = new Thread(() -> {
           while(counter.count == 0) {

           }
            System.out.println("循环结束!");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数");
            counter.count = scanner.nextInt();
        });*/
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                counter.increase();
            }
        });

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                counter.increase();
            }
        });


        t2.start();
        t1.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter.count);
    }
}

我们可以发现我们的count值并没有按照想象那样自增为20000,而是存在线程安全问题,这就是因为volatile关键字不具备原子性,两个线程可能将count值自增为同一个数并存在主内存当中,就会少了一个自增。我们可以使用synchronized关键字来解决这个棘手的问题,还是以上述代码作延伸来演示一下:

public class Demo1 {
    static class Counter {
        //private int count = 0;
        // 使用volatile关键字修饰count变量,使得多个线程可以同时访问该变量
        private volatile int count = 0;
        // 使用synchronized关键字修饰方法,使得只有一个线程可以访问该方法
        synchronized void increaseSafe() {
            count++;
        }

        public void increase() {
            count++;
        }
    }


    public static void main(String[] args) {
        Counter counter = new Counter();
       /* Thread t1 = new Thread(() -> {
           while(counter.count == 0) {

           }
            System.out.println("循环结束!");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数");
            counter.count = scanner.nextInt();
        });*/
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                counter.increaseSafe();
            }
        });

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                counter.increaseSafe();
            }
        });


        t2.start();
        t1.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter.count);
    }
}

我们可以看到,当我们使用synchronized关键字修饰自增方法的时候,可以有效解决此类问题,我们也可以使用synchronized来去解决内存不可见的问题:

public class Demo1 {
    static class Counter {
        private int count = 0;
        // 使用volatile关键字修饰count变量,使得多个线程可以同时访问该变量
        //private volatile int count = 0;
        // 使用synchronized关键字修饰方法,使得只有一个线程可以访问该方法
       /* synchronized void increaseSafe() {
            count++;
        }*/

        // 使用synchronized关键字修饰成员变量,使得只有一个线程可以访问该变量


        public void increase() {
            count++;
        }
    }


    public static void main(String[] args) {
        Counter counter = new Counter();
       /* Thread t1 = new Thread(() -> {
           while(counter.count == 0) {

           }
            System.out.println("循环结束!");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数");
            counter.count = scanner.nextInt();
        });*/
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                // 使用synchronized关键字修饰字段,使得只有一个线程可以访问该字段
                synchronized (counter) {
                    counter.increase();
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                // 使用synchronized关键字修饰字段,使得只有一个线程可以访问该字段
                synchronized (counter) {
                    counter.increase();
                }
            }
        });


        t2.start();
        t1.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter.count);
    }
}

三、总结

在这篇文章中,我列举了很多跟volatile关键字有关的代码片段,但有些场景不能单单只靠volatile关键字来进行解决,我们需要配合synchronized关键字一同使用才能避免出现线程安全问题,但synchronized关键字可以解决 volatile关键字能解决的问题,而且更容易理解,那是不是证明synchronized要比volatile更加有用,更被我们日常开发中所信赖,事实真是如此吗?

我们要知道,使用synchronized会需要更多的锁开销,线程和线程之间可能会产生锁竞争,会导致线程的阻塞,也有可能引发死锁点我了解死锁是什么等一系列复杂的问题,在一些轻量化的场景当中,使用synchronized可能会浪费很多额外且不必要的开销,会影响程序的效率,在一些轻量化的需求场景中,不涉及到复杂的操作和变量管理,我们可以使用volatile关键字来使用,比如在我们使用的第二个代码样例中,只是对count值进行简单的读写操作,就不必要使用synchronized关键字来解决这个问题,使用volatile关键字就绰绰有余了,在实际的开发中,我们也需要根据实际性能和需求来进行相应技术的实现,可以大大节约我们的效率和不必要的线程安全问题!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

comerun

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

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值