java并发学习--第三章 线程安全问题

  线程的安全问题一直是我们在开发过程中重要关注的地方,出现线程安全问题的必须满足两个条件:存在着两个或者两个以上的线程;多个线程共享着共同的一个资源, 而且操作资源的代码有多句。接下来我们来根据JDK自带的方法来解决线程带来的问题。

一、同步代码块synchronized

  我们来看一个实例,创建两个线程,每个线程就对计算器i进行减1操作,当i等于0时停止线程

public class Main implements Runnable {

    int i = 10;

    @Override
    public void run() {
        while (i > 0) {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("这是线程" + Thread.currentThread().getName() + "当前的值是:" + i--);

        }
    }

    public static void main(String[] args) {
        Main main = new Main();
        Thread t1 = new Thread(main);
        Thread t2 = new Thread(main);
        t1.start();
        t2.start();

    }

}

  运行的结果是:

  可以看到,i的值出现了一次相同的情况,这样就出现了线程安全的问题、

  我们来分析这个问题:在两个线程情况下,给i赋值的操作前,我们暂停了1秒,就是说如果此时A线程开始运行,A已经拿到了i的值为2,但是A要暂停1S,但是如果此时B线程抢占资源成功进来了,B也获取了i的值i的值也为2,然后并在暂停后输出了i=2,A在B运行完后也开始输出i=2。

  为了解决这个问题,JDK给我们提供了一个关键字synchronized,synchronized是内置锁,放在普通方法上,内置锁锁的是当前类的实例;

  被synchronized修饰的方法就会锁住方法中请求的资源,只有当它释放后,其他线程才能进来

public class Main implements Runnable {

    int i = 10;

    @Override
    public synchronized void  run() {
        while (i > 0) {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("这是线程" + Thread.currentThread().getName() + "当前的值是:" + i--);

        }
    }

    public static void main(String[] args) {
        Main main = new Main();
        Thread t1 = new Thread(main);
        Thread t2 = new Thread(main);
        t1.start();
        t2.start();

    }

}

 运行的结果绝对不会出现重复的内容:

 

   synchronized除了修饰方法外,还能够直接修饰代码块,直接修饰代码块的好处是,我们只需要在非原则性操作的地方加锁,这样能够提供程序的性能:

public class Main implements Runnable {

    int i = 10;

    @Override
    public void  run() {
        while (i > 0) {

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (Main.class){
            System.out.println("这是线程" + Thread.currentThread().getName() + "当前的值是:" + i--);
            }

        }
    }

    public static void main(String[] args) {
        Main main = new Main();
        Thread t1 = new Thread(main);
        Thread t2 = new Thread(main);
        t1.start();
        t2.start();

    }

}

 

 二、volatile关键字

  我们来看这样一个多线程场景,有A、B两个线程,这两个线程有公共变量isok,如果A修改了这个变量,如何才能够告诉B这个变量进行了修改,我们来看代码:

  

public class Main {

    //A、B两个线程都会使用这个变量
    private static boolean isok = false;

    public static void main(String[] args) {

        //这个是线程A,当A执行完一段代码后,会修改isok的值
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("A线程当前执行了" + i + "次");
                }
                isok = true;
            }
        }).start();

        //这个是线程B
        new Thread(new Runnable() {
            @Override
            public void run() {
                //只有当A线程修改了isok的值,B线程才会停止循环
                while (!isok) {

                }
                System.out.println("B线程开始执行");
            }
        }).start();
    }
}

 

  我们来看看执行的结果:

  

 

  我们可以看到A线程虽然修改了isok的值,但是B线程不知道,并且此时程序还在运行中,说明B的循环没有停止,简单的说B拿到了isok的值后就锁定了,即使其他的线程进行了修改,B拿到的还是最初始的值。

  面对这样的问题,jdk为我们提供了volatile关键字来解决:volatile是轻量级锁,被volatile修饰的变量在线程之间是可见的;volatile具有可见性、有序性,不具备原子性。

  可见的定义:一个线程修改了这个变量的值,在另外一个线程中能够读到这个修改后的值。

  volatile的主要作用有两个:

  1、保证了不同线程对该变量操作的内存可见性

  2、禁止了指令重排序

  我们来看看改造后的代码,只需要用volatile修饰isok就行

  

public class Main {

    //A、B两个线程都会使用这个变量
    private volatile  static boolean isok = false;

    public static void main(String[] args) {

        //这个是线程A,当A执行完一段代码后,会修改isok的值
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("A线程当前执行了" + i + "次");
                }
                isok = true;
            }
        }).start();

        //这个是线程B
        new Thread(new Runnable() {
            @Override
            public void run() {
                //只有当A线程修改了isok的值,B线程才会停止循环
                while (!isok) {

                }
                System.out.println("B线程开始执行");
            }
        }).start();


    }


}

 执行后的结果:

 这样我们就可以清楚的看到A线程修改了isok的值后,在B线程中也能看到

posted @ 2019-09-17 20:09  想去天空的猫  阅读(218)  评论(0编辑  收藏  举报