day11并发编程三大特性

1.有序性
当代码前后顺序发生变化互不影响时,虚拟机会对代码进行重排,但是这个操作可能会影响其他线程的运行,例子如下:

public class Test {
    //测试并发有序性
    static int x=0,y=0;  //这边不能用volatile修饰,否则代码不会发生重排,则不会出现1,1答案
    public static void main(String[] args) throws InterruptedException {
        Map<String,Integer> map = new HashMap<>();
        Set<String> set = new HashSet<>();
        while(true){
            x=0;
            y=0;
            map.clear();
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    int a=y;
                    x=1;
                    //x=1;     //可能将上面两行代码重排成这个顺序
                    //int a=y;
                    map.put("a",a);
                }
            });
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    int b=x;
                    y=1;
                    //y=1;      //可能将上面两行代码重排成这个顺序
                    //int b=x;
                    map.put("b",b);
                }
            });
            t1.start(); 
            t2.start();
            t1.join();		
            t2.join();   //等待t1,t2线程的结束,避免a,b还没赋值到map中就将其添加到set中
            set.add("a="+map.get("a")+",b="+map.get("b")); //将map中的值添加到set中
            System.out.println(set); //打印set
        }
    }
}

打印结果:
[a=0,b=0, a=1,b=0, a=0,b=1, a=1,b=1]
有上述4种输出, 结果 1,1 是代码发生重排后的结果。

2.可见性。
当两个线程t1,t2调用一个变量a时会发生的问题,首先t1从主存中读取了a的值,并将a的值发生了改变,在t1将a的值送回主存中的时候,t2又从主存中读取了a的值,但是t2读取到的值是改变前的值,并不是t1返回给主存的值,此时就发生了问题。例子如下:

public class Test4_1 {
    public boolean a = true;
    public void m() {
        int i = 0;
        System.out.println("方法执行开始");
        while (this.a) {										//这会发生死循环
        }
        System.out.println("方法执行完毕");
    }

    public static void main(String[] args) {
        Test4_1 test = new Test4_1();
        //创建线程 执行m()方法
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.m();
            }
        }).start();
        //这里需要让线程睡眠一会,因为执行的m方法一开始还会不断向主存中读取a的值。
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //将a的值改为false;
        test.a = false;
        System.out.println("a的值为" + test.a);
    }
}

解决方法:可在需要使用的变量前加上关键词volatile。

3.原子性
完成一个事物,事物中可能会有多个操作。要么都执行,或者都不执行。
比如转账:小明转给小红100块钱,假设这项事务 1.从小明钱包中扣除100元,2.将小红钱包中的值加上100。在此过程中 如果第二步操作出现了异常,小红的钱没加上,而小明的钱已经扣除,这样显然是不行的,所以两步操作要么都执行,要么都不知道。
例子代码如下:

public class Test4_2 {
    static int a = 0;

    public static void main(String[] args) throws InterruptedException {
        //创建五个线程,并启动,每个线程使a的值增加100000000;
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i1 = 0; i1 < 100000000; i1++) { **//标记(1)**
                        a++;
                    }
                    //打印执行完毕的线程名
                    System.out.println(Thread.currentThread().getName() + "执行完毕");
                }
            }).start();
        }
        //使线程睡眠三秒钟,给上面五个线程足够的运行时间
        Thread.sleep(3000);
        //打印a的值,可以发现上面五个线程运行完后a的值并不等于预想值。
        System.out.println(a);
    }

}

在上述的操作中,打印a的值会小于等于500000000,原因就是没有保证原子性,也就是可能会有多个线程运行到上述标记(1)中,他们获取到的a的值是相同,但是他们都执行了a++然后将a的值返回到主存这个操作,也就是说返回的值比原来多了1,但是所有线程的运行次数大于1,所以最后的值会比预计的小或者等于。
解决办法可以使用AtomicInteger这个类,运用了cas原理

发布了8 篇原创文章 · 获赞 3 · 访问量 108

猜你喜欢

转载自blog.csdn.net/weixin_43814245/article/details/105350395