• Java中如何进行加锁??


    笔者在上篇文章介绍了线程安全的问题,接下来本篇文章就是来讲解如何避免线程安全问题~~
    前言:创建两个线程,每个线程都实现对同一个变量count各自自增5W次,我们来看一下代码:

    class Counter{
        private int count=0;
    
        public void add(){
            count++;
        }
    
        public int get(){
            return count;
        }
    }
    public class Main2 {
        public static void main(String[] args) throws InterruptedException{
            Counter counter=new Counter();
    
            //搞两个线程,两个线程分别对这个count自增5W次
            //线程1
            Thread t1=new Thread(()->{
                for (int i = 0; i < 50000; i++) {
                    counter.add();
                }
            });
    
            //线程2
            Thread t2=new Thread(()->{
                for (int i = 0; i < 50000; i++) {
                    counter.add();
                }
            });
    
            //启动线程
            t1.start();
            t2.start();
    
            //等待两个线程执行结束,然后看一下结果
            t1.join();
            t2.join();
    
            System.out.println(counter.get());
            //预期结果是10W,但是,实际结果像是一个随机值,每次执行的结果都不一样
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    上述代码的运行结果是不确定的,是一个随即值,多次刷新重新运行,结果大概率是不一样的~,预期效果个代码的运行结果不一样,这就是Bug——》线程安全问题!
    通过加锁来有效避免线程安全问题:
    Synchronized是Java中的关键字,可以使用这个关键字来实现加锁的效果~

        public void add(){
           // count++;
            synchronized (this){
                //这里的this可以写任意一个Object对象(基本数据类型不可)
                //此处写了this就相当于Counter counter=new Counter();中的counter
                count++;
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    那么,我们来看一下此时代码的运行结果~
    在这里插入图片描述符合我们预期的一个效果~
    锁有两个核心的操作,加锁和解锁;
    此处使用代码块的方式来表示:进入synchronized修饰的代码块的时候,就会触发加锁,出了synchronized代码块就会触发解锁,{ }就相当于WC~~
    在上述代码中:synchronized(this)——》this是指:锁对象(在针对哪个对象?)!

    如果两个线程针对同一个对象加锁,此时就会出现“锁竞争”(一个线程先拿到锁,另一个线程阻塞等待)!
    如果两个线程针对不同的对象加锁,此时不好存在锁竞争,各种获取各自锁即可!
    加锁本质上是把并发的变成了串行的~

    join()和加锁不一样:
    join()是让两个线程完整的进行串行~
    加锁是让两个线程的某小部分串行了,大部分都是并发的!!

    在这里插入图片描述加锁:在保证线程安全的前提下,同时还能够让代码跑的更快一些,更好的利用CPU,无论如何,加锁都可能导致阻塞,代码阻塞对应程序的效率肯定还是会有影响的,此处虽然加锁了,比不加锁要慢点,肯定还是比串行要更快,同时比不加锁算得更准!!
    在这里插入图片描述如果直接给方法使用synchronized修饰,此时就相当于this为加锁对象!!
    如果synchronized修饰静态方法static(),此时就算不给this加锁了,而是给类对象加锁!!
    在这里插入图片描述更常见的还是自己手动指定一个锁对象:

        //自己手动指定锁对象
        private Object locker=new Object();
        public void add(){
            synchronized (locker){
                //这里的locker可以写任意一个Object对象(基本数据类型不可)
                count++;
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    要牢记:如果多个线程尝试对同一对象加锁,此时就会产生锁竞争!!针对不同的锁对象加锁,就不会有锁竞争~

    另一个线程不安全的场景:由于内存可见性,所引起的线程不安全~
    先写一个带有Bug的代码:

    import java.util.Scanner;
    
    public class Main3 {
        public static int flag=0;
    
        public static void main(String[] args) {
            Thread t1=new Thread(()->{
                while (flag==0){
                    //空着,啥都没有
                }
                System.out.println("循环结束,t1结束");
            });
            
            Thread t2=new Thread(()->{
                Scanner scanner=new Scanner(System.in);
                System.out.println("请输入一个整数: ");
                flag=scanner.nextInt();
            });
    
            t1.start();
            t2.start();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    对该段代码的预期效果:t1通过flag=0作为条件,进行循环,初始情况下,将进入循环,t2通过控制台输入一个整数,一旦用户输入非0的值,此时t1的循环就会立即结束,从而t1线程退出!!
    但是,实际的效果:输入非0的值之后,t1线程并没有退出,循环没有结束!通过jconsole可以看到t1线程仍然在执行,处在RUNNABLE状态。
    实际效果 !=预期效果——》这就是Bug
    为啥有这个问题??这就是内存可见性的锅!!
    所谓的内存可见性,就是多线程环境下,,编辑器对于代码优化产生了误判,从而引起了Bug,进一步导致了咱们的Bug,咱们的处理方式:就是让编辑器针对这个场景暂停优化!!使用Volatile关键字,被volatile修饰的变量,此时编辑器就会紧张上述优化,从而能够确保每次都是从内存中重新读取数据~
    即:针对上述代码的更改:

    volatile public static int flag=0;
    
    • 1

    加上volatile关键字之后,此时编辑器就能够保证每次都是重新从内存读取flag变量的值,此时t2修饰flag,t1就可以立即感知到了,因此t1就可以正确退出了~

    volatile不保证原子性(注意)
    volatile适用的场景是一个线程读,一个线程写的情况
    synchronized则是多个线程写

    volatile的这个效果称为:“保证内存可见性”
    synchronized不确定能不能保证内存可见性

    volatile还有一个效果:禁止指令重排序!指令重排序也是编辑器优化的策略(调整了代码执行的顺序,,让程序更高效,前台也是保证整体逻辑不变)

    关于volatile和内存可见性的补充~
    网上有效资料:线程修改一个变量,会把这个变量先从主内存读取到工作内存,然后修改工作内存的值,再写回到主内存中~
    内存可见性:t1频繁读取主内存,效率比较低,就被优化成直接读取自己的工作内存,t1修改了主内存的结果,由于t1没有读取主内存导致修改不能被识别到!!
    工作内存《——》CPU寄存器
    主内存《——》内存

  • 相关阅读:
    计组——cache替换算法及cache写策略
    onps栈使用说明(2)——ping、域名解析等网络工具测试
    基于matlab的排队系统仿真
    IMX6上获取时间的补充(io的宏定义)
    读书笔记:c++对话系列,模板方法模式(Template Method Pattern)
    Android学习笔记 2.3.5 RadioButton和CheckBox && 2.3.6 ToggleButton和Switch的功能和用法
    Linux删除文件与Python代码删除文件命令
    美团yolov6初体验
    【14-Ⅱ】Head First Java 学习笔记
    数据结构之数组旋转系列一
  • 原文地址:https://blog.csdn.net/weixin_64308540/article/details/132777624