• JAVA 多线程(3)


    再讲线程安全:

    一、脏读

    脏读:在于读字,意在在读取实例变量时,实例变量有可能被另外一个线程更改了,导致获取到的数据出现异常。

    在非线程安全的情况下,如果线程A与线程B 共同使用对象实例C中的方法method,如果实例C存在实例变量,同时在method中会操作这个实例变量a,则有可能出现脏读的情况。

    也就是期望值不同,读取的数据不同,因为线程A与B会同时使用实例C的方法。

    例如:

    private String name;
    
        public static void main(String[] args){
            Test2 test2 = new Test2();
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
    //                System.out.println(Thread.currentThread().getName());
                    test2.testUnsafe("a","我是A");
                }
            });
    
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
    //                System.out.println(Thread.currentThread().getName());
                    test2.testUnsafe("b","我是B");
                }
            });
    
            t.start();
            t2.start();
        }
    
        public void testUnsafe(String name,String param){
            this.name = name;
            try {
          Thread.sleep(1000);
          System.out.println(this.name+":"+param);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
    }

     执行结果如下:

    代码中加上了sleep 为了模拟运算所需的时间。

    可以看出来,大概是这样的过程:线程B在run时首先抢到了资源,并运行了实例方法,并把实例变量name修改为b,

    然后继续执行,这时候线程A抢回了资源(因为不是同步的),它把实例变量那么又修改为b,这个是时候开始执行其他逻辑操作(这里用sleep模拟),

    然后2个线程轮番执行最后的操作-打印。

    因为这个时候实例变量name 已经变为a了, 所以线程B 出现脏读,和期望输出的  b:我是B  结果出现差异。

    如果操作的是不同的对象实例(在synchronized 同步里 jvm 会创建多个锁,下面的例子控制2个实例,也就是创建了2个锁),就不会出现这个问题了,还有如果name不是实例变量,只是私有变量的话也不会出现这种情况。

    修改一下看看:

     public static void main(String[] args){
    
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    Test2 test2 = new Test2();
                    test2.testUnsafe("a","我是A");
                }
            });
    
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    Test2 test2 = new Test2();
                    test2.testUnsafe("b","我是B");
                }
            });
    
            t.start();
            t2.start();
        }

    输出结果:

    如果想要保证使用实例变量而又不出现这种问题,怎么办,同步~ 可以使用 synchronized 方法或 synchronized代码块。

    例如:

    public synchronized void testUnsafe(String name,String param){
            this.name = name;
            try {
                Thread.sleep(1000);
                System.out.println(this.name+":"+param);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
        }
    

     输出结果:

     关于synchronized 想看的可以下看之前的随笔(《JAVA 多线程(1)》)。

    这里我想记录一下类与对象和实例的个人理解():

    Class 是类,编写时候我们称为类,编译好的class我们可以称为class对象,或者说类对象,由类对象new出来的是实例或者说叫对象实例也就是instance。

    它们分时期,分关系。类是抽象的概念,对象为具体的事物,人是类,张三是人,张三是人这个类具象,是一个对象实例的存在,它的嘴巴是实例变量,吃饭是实例方法,米饭和菜是吃饭这个对象方法中的方法私有变量,

    打比方张三、李四在一起去吃饭(2个线程),都调用了吃饭这个方法(吃饭这个方法是人类共有的),分布是土豆A与土豆B,如果不做同步,有可能2个人会夹到同一条土豆丝。

    好吧,上面的比方看看就好。

    二、可重入锁

    如果一个线程获取了或者说抢到了cpu资源,拿到了实例对象锁,那么在实例方法中在调用其他同步方法时,依然会获取到同一个锁,因为已经抢到了,

    比方说,一个屋子有3个房间(方法),张三(线程),抢到了房间1的锁,由于李四和王五压根就没进入这个屋子,所有他们俩必须等张三出来才行,这样的话

    张三进过房间1后,还能获得房间2、3的锁。

    这个锁指的是对象锁,或者说一个实例对象只有一把锁会更好理解。因为李四和王五想进房间2,虽然不是房间1,但是只有一把锁,这个锁在张三手上。

    可重入锁个人感觉主要贡献在于可继承,如果B基础了A,那么如果操作B,获取到了实例对象B的锁,那么也可以继续获得A的锁。

    三、异常释放锁

    当线程出现异常时,锁会自动释放。

    四、同步不具有继承

    如果类A 有同步方法 methodA,类B继承了类A并重写了methodA 方法,但是没有加上同步关键字,那么实际上,B类中重写的methodA 并不是同步方法。

    五、同步方法与同步代码块

    之前在1中写过,这里再提及,同步代码块同样时获得的是对象锁,当不同的实例方法各自有各自的同步代码块,当线程A访问methodA 时获得实例对象锁,如果线程B访问的实例对象与线程A相同,那么线程B如想

    访问methodB中的同步代码块时同样需要等线程A使用完methodA,释放对象锁,才能执行。

    六、非当前实例对象锁(任意对象监视器)

    优点:同一个类中的多个方法或者同一个方法中有多个代码块,为了提高性能,使用多个对象锁,来加快运行速度,或者说减少阻塞。

    例如:

    private Object object = new Object();

    public static void main(String[] args){
    Test2 test2 = new Test2();
    Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
    test2.test();
    }
    });

    Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
    test2.test2();
    }
    });

    t.start();
    t2.start();
    }
    public void test2(){
    synchronized (object){
    System.out.println("我是第二个块"+Thread.currentThread().getName());
    }
    }

    public void test(){
    synchronized (this){
    try {
    System.out.println("我是第一个块 开始:"+Thread.currentThread().getName());
    Thread.sleep(100);
    System.out.println("我是第一个块 结束:"+Thread.currentThread().getName());
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

    输出:

    通过控制台打印结果可以看出,在A线程访问test方法执行的同时,B线程同样访问可test2方法。

    所以,他们互不干扰,因为不是一把锁。

    但是问题又出来,就因为这样提高了性能,但是又有可能会导致脏读,如果methodA与methodB 中有对同一个实例变量的写操作,那么及有可能出现脏读。

    如下:

    private Object object = new Object();
        private static List<String> list = new ArrayList<>();
        public static void main(String[] args){
            Test2 test2 = new Test2();
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    test2.test();
                }
            });
    
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    test2.test2();
                }
            });
    
            t.start();
            t2.start();
            try {
                Thread.sleep(6000);
                System.out.println(list.size());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        public void test2(){
            synchronized (object){
                judge("test2");
            }
        }
    
        public void test(){
            synchronized (this){
                judge("test");
    
            }
        }
    
        public void judge(String what){
            try {
                if(list.size() < 1){
                    Thread.sleep(2000);
                    list.add(what);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    

     输出结果:

    出现了,以为是异步的,所以线程A和线程B都可以同时访问到judge方法,又同时进入到了if语句中,导致最终结果输出为2,而不是我们期望的1。

    所以,方法就是,给judge加上同步锁:

    synchronized (list){
                    if(list.size() < 1){
                        Thread.sleep(2000);
                        list.add(what);
                    }
                }
    

    这样拿到list对象锁的操作就变成同步的了~

    明儿个继续~

    成灰之前,抓紧时间做点事!!
  • 相关阅读:
    leetcode刷题总结401-450
    leetcode刷题总结351-400
    马哥博客作业第六周
    马哥教育第一阶段考试
    马哥博客作业第四周
    马哥博客作业第三周
    马哥博客作业第二周
    马哥博客作业第一周
    马哥博客预习作业第三周
    马哥博客预习作业第二周
  • 原文地址:https://www.cnblogs.com/jony-it/p/10770342.html
Copyright © 2020-2023  润新知