多线程(2):synchronized关键字

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/fibonacci2015/article/details/84326579

  多线程操作相同资源的时候,会出现线程安全的问题,导致结果与预期的不一致。

  如下例子,设计四个线程,其中两个对线程对变量加1操作,两个线程对变量减1操作。理想状态是,线程顺序执行,相同次数的加减操作,最后变量的值不变。

1.线程不安全的操作

public class FourThreadAddMinus {

    private int i = 0;

    public static void main(String[] args) {
        FourThreadAddMinus ft = new FourThreadAddMinus();
        for(int i = 0; i < 2; i++){
            new Thread(ft.new Add(), "add线程-" + i).start();
            new Thread(ft.new Minus(),"minus线程-" + i).start();
        }
    }

    // 内部类,加1操作(不用内部类也行,这里偷懒,把代码都放到一个class文件下)
    class Add implements Runnable{

        @Override
        public void run() {
            for(int i = 0; i < 5; i++) {
                add();
            }
        }
    }

    // 内部类,减1操作
    class Minus implements Runnable{

        @Override
        public void run() {
            for(int i = 0; i < 5; i++){
                minus();
            }
        }
    }

    private void add(){
        i++;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 执行 >>> " + i);

    }

    private void minus(){
        i--;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 执行 >>> " + i);

    }
}

  打印输出结果:

1542793183193 - add线程-0 执行 >>> 1
1542793183193 - minus线程-1 执行 >>> 0
1542793183193 - add线程-0 执行 >>> 1
1542793183193 - minus线程-1 执行 >>> 0
1542793183193 - add线程-0 执行 >>> 1
1542793183193 - minus线程-0 执行 >>> -1
1542793183193 - add线程-0 执行 >>> 0
1542793183193 - minus线程-1 执行 >>> 0
1542793183194 - add线程-0 执行 >>> 0
1542793183194 - minus线程-0 执行 >>> -1
1542793183194 - minus线程-1 执行 >>> -1
1542793183194 - minus线程-0 执行 >>> -2
1542793183194 - minus线程-1 执行 >>> -3
1542793183194 - add线程-1 执行 >>> -3
1542793183195 - add线程-1 执行 >>> -2
1542793183195 - add线程-1 执行 >>> -1
1542793183195 - add线程-1 执行 >>> 0
1542793183194 - minus线程-0 执行 >>> -4
1542793183195 - minus线程-0 执行 >>> 0
1542793183195 - add线程-1 执行 >>> 1

  从执行的结果看,同一时刻,有多个线程同时运行且顺序不定,会出现某个线程拿到的值是另一个线程未保存的,也可以理解成数据库里面的脏读,造成的结果就是共享资源变量i的值不可控,每次执行,结果可能都不一样。这不是我们想要的结果。

  这种情况,我们需要控制线程的并发状态,即同一时刻,只能有一个线程对变量i进行操作,其他线程阻塞。等该线程执行结束,i的值进行变更后,其他线程拿到最新的i值再进行操作。

2.线程安全的操作(多线程抢占同一实例的锁)

  java中提供了一个关键字,synchronized可以控制线程的并发。

  对代码进行如果修改add()和minuss()方法加上,synchronized关键字

public class FourThreadAddMinus {

    private int i = 0;

    public static void main(String[] args) {
        FourThreadAddMinus ft = new FourThreadAddMinus();
        for(int i = 0; i < 2; i++){
            new Thread(ft.new Add(), "add线程-" + i).start();
            new Thread(ft.new Minus(),"minus线程-" + i).start();
        }
    }

    // 内部类,加1操作(不用内部类也行,这里偷懒,把代码都放到一个class文件下)
    class Add implements Runnable{

        @Override
        public void run() {
            for(int i = 0; i < 5; i++) {
                add();
            }
        }
    }

    // 内部类,减1操作
    class Minus implements Runnable{

        @Override
        public void run() {
            for(int i = 0; i < 5; i++){
                minus();
            }
        }
    }

    private synchronized void add(){
        i++;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 执行 >>> " + i);

    }

    private synchronized void minus(){
        i--;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 执行 >>> " + i);

    }
}

  打印输出结果:  

1542793702921 - add线程-0 执行 >>> 1
1542793702922 - add线程-0 执行 >>> 2
1542793702922 - add线程-0 执行 >>> 3
1542793702922 - add线程-0 执行 >>> 4
1542793702922 - add线程-0 执行 >>> 5
1542793702923 - minus线程-0 执行 >>> 4
1542793702923 - minus线程-0 执行 >>> 3
1542793702924 - minus线程-0 执行 >>> 2
1542793702924 - minus线程-0 执行 >>> 1
1542793702924 - add线程-1 执行 >>> 2
1542793702924 - add线程-1 执行 >>> 3
1542793702924 - add线程-1 执行 >>> 4
1542793702924 - add线程-1 执行 >>> 5
1542793702924 - add线程-1 执行 >>> 6
1542793702924 - minus线程-0 执行 >>> 5
1542793702925 - minus线程-1 执行 >>> 4
1542793702925 - minus线程-1 执行 >>> 3
1542793702925 - minus线程-1 执行 >>> 2
1542793702925 - minus线程-1 执行 >>> 1
1542793702926 - minus线程-1 执行 >>> 0

  从执行的结果看,由于add()和minus()方法被synchronized修饰,线程执行该方法,需要获取当前实例的锁而当前实例只有一个FourThreadAddMinus ft = new FourThreadAddMinus()所以,假设add线程-0获取锁以后,minus线程-0,minus线程-1,add线程-1,则处于阻塞状态,。待add线程-0执行完,锁释放后,其他线程抢占资源,获取锁。此时线程one by one执行,最后i的值变为初识状态0。

3.线程不安全的操作(多线程抢占多实例的锁)

  代码再进一步修改,new两个FourThreadAddMinus实例。

public class FourThreadAddMinus {

    private int i = 0;

    public static void main(String[] args) {
        FourThreadAddMinus ft1 = new FourThreadAddMinus();
        FourThreadAddMinus ft2 = new FourThreadAddMinus();
        for(int i = 0; i < 2; i++){
            new Thread(ft1.new Add(), "add线程-" + i).start();
            new Thread(ft2.new Minus(),"minus线程-" + i).start();
        }
    }

    // 内部类,加1操作(不用内部类也行,这里偷懒,把代码都放到一个class文件下)
    class Add implements Runnable{

        @Override
        public void run() {
            for(int i = 0; i < 5; i++) {
                add();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 内部类,减1操作
    class Minus implements Runnable{

        @Override
        public void run() {
            for(int i = 0; i < 5; i++){
                minus();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private synchronized void add(){
        i++;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 执行 >>> " + i);

    }

    private synchronized void minus(){
        i--;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 执行 >>> " + i);

    }
}

  打印输出结果:

1542795420885 - add线程-0 执行 >>> 1
1542795420886 - minus线程-0 执行 >>> -1
1542795420886 - add线程-1 执行 >>> 2
1542795420886 - minus线程-1 执行 >>> -2
1542795421386 - minus线程-1 执行 >>> -3
1542795421386 - add线程-1 执行 >>> 3
1542795421386 - minus线程-0 执行 >>> -4
1542795421386 - add线程-0 执行 >>> 4
1542795421887 - add线程-1 执行 >>> 5
1542795421887 - add线程-0 执行 >>> 6
1542795421887 - minus线程-0 执行 >>> -5
1542795421887 - minus线程-1 执行 >>> -6
1542795422388 - minus线程-1 执行 >>> -7
1542795422388 - add线程-0 执行 >>> 7
1542795422388 - minus线程-0 执行 >>> -8
1542795422388 - add线程-1 执行 >>> 8
1542795422888 - add线程-0 执行 >>> 9
1542795422888 - minus线程-1 执行 >>> -9
1542795422888 - add线程-1 执行 >>> 10
1542795422888 - minus线程-0 执行 >>> -10

  从结果看,add两个线程顺序执行,将i从0加到10;minus两个线程顺序执行,将i从0减到-10。但这里,add线程操作的i和minus操作的i不是同一个,因为是两个实例。锁也只是保证了两个add线程(或两个minus的线程)的顺序。其实我是想写,四个线程,抢两个实例锁,操作同一个资源。不删了,就当复习一下多线程(1)的内容。代码再改一下就好了,将 private int i = 0,改成 private static int i = 0。加一个static关键字,变成所有实例共享的变量。完整代码就不放了,跟上面的一样,就是多个static关键字。

  打印输出结果:

1542796917053 - add线程-0 执行 >>> 1
1542796917054 - minus线程-0 执行 >>> 0
1542796917054 - add线程-1 执行 >>> 1
1542796917054 - minus线程-1 执行 >>> 0
1542796917554 - add线程-0 执行 >>> 0
1542796917554 - minus线程-1 执行 >>> 0
1542796917554 - add线程-1 执行 >>> 1
1542796917554 - minus线程-0 执行 >>> 0
1542796918054 - minus线程-0 执行 >>> 1
1542796918054 - add线程-0 执行 >>> 1
1542796918054 - minus线程-1 执行 >>> 0
1542796918054 - add线程-1 执行 >>> 1
1542796918555 - add线程-1 执行 >>> 1
1542796918555 - minus线程-1 执行 >>> 1
1542796918555 - minus线程-0 执行 >>> 1
1542796918555 - add线程-0 执行 >>> 2
1542796919056 - add线程-1 执行 >>> 2
1542796919056 - minus线程-1 执行 >>> 2
1542796919056 - add线程-0 执行 >>> 3
1542796919056 - minus线程-0 执行 >>> 2

  从结果看,由于add线程和minus线程,抢占的不是同一个实例的锁,所以add和minus之前互不影响,同一时刻,可能add和minus都在执行,而且由于操作同一个资源i,就会出现脏读的情况,最后结果也不可控。

4.synchronized关键字的使用:

  如果一个方法被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1. 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
  2. 线程执行发生异常,此时JVM会让线程自动释放锁。

  synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能),这点确实也是很重要的。

  synchronized关键字最主要有以下3种应用方式

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁(上面讲的都是这种用法)
  2.  修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  3.  修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

  修饰静态方法,也好说,把代码改一下。 

    private synchronized static void add(){
        i++;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 执行 >>> " + i);

    }

    private synchronized static void minus(){
        i--;
        System.out.println(System.currentTimeMillis() + " - " + Thread.currentThread().getName() + " 执行 >>> " + i);

    }

  打印输出结果: 

1542797628413 - add线程-0 执行 >>> 1
1542797628413 - minus线程-0 执行 >>> 0
1542797628413 - minus线程-1 执行 >>> -1
1542797628413 - add线程-1 执行 >>> 0
1542797628914 - minus线程-1 执行 >>> -1
1542797628914 - minus线程-0 执行 >>> -2
1542797628914 - add线程-0 执行 >>> -1
1542797628914 - add线程-1 执行 >>> 0
1542797629414 - add线程-0 执行 >>> 1
1542797629414 - add线程-1 执行 >>> 2
1542797629414 - minus线程-1 执行 >>> 1
1542797629414 - minus线程-0 执行 >>> 0
1542797629915 - add线程-0 执行 >>> 1
1542797629915 - add线程-1 执行 >>> 2
1542797629915 - minus线程-1 执行 >>> 1
1542797629915 - minus线程-0 执行 >>> 0
1542797630415 - add线程-1 执行 >>> 1
1542797630415 - add线程-0 执行 >>> 2
1542797630415 - minus线程-0 执行 >>> 1
1542797630415 - minus线程-1 执行 >>> 0

   从输出结果看,线程之间还是 one by one执行,因为add()和minus()方法都是静态方法,为当前类对象所有,不属于某一个实例。不管创建多少实例,类的对象都是那一个。多线程抢占的是类对象的锁,只有一把锁,所以线程之间互斥,谁抢到锁,谁执行。

猜你喜欢

转载自blog.csdn.net/fibonacci2015/article/details/84326579