代码改变世界

synchronized 与volatile

2022-04-01 10:28  shadowsky88  阅读(26)  评论(0编辑  收藏  举报

关键字synchronized的主要作用是保证同一时刻,只有一个线程可以执行某一个方法,或是某一个代码块,synchronized可以修饰方法及代码块。随着JDK的版本升级,synchronized关键字在执行效率上得到很大提升。它包含三个特征。

1)可见性:synchronized具有可见性。

2)原子性:使用synchronized实现了同步,同步实现了原子性,保证被同步的代码段在同一时间只有一个线程在执行。

3)禁止代码重排序:synchronized禁止代码重排序。

关键字volatile的主要作用是让其他线程可以看到最新的值,volatile只能修饰变量。它包含三个特征:

1)可见性:B线程能马上看到A线程更改的数据。

2)原子性:在32位系统中,针对未使用volatile声明的long或double数据类型没有实现写原子性,如果想实现,则声明变量时添加volatile,而在64位系统中,原子性取决于具

2.3.3 禁止代码重排序的测试

使用关键字volatile可以禁止代码重排序。

在Java程序运行时,JIT(Just-In-Time Compiler,即时编译器)可以动态地改变程序代码运行的顺序,例如,有如下代码:


A代码-重耗时
B代码-轻耗时
C代码-重耗时
D代码-轻耗时

在多线程的环境中,JIT有可能进行代码重排,重排序后的代码顺序有可能如下:


B代码-轻耗时
D代码-轻耗时
A代码-重耗时
C代码-重耗时

这样做的主要原因是CPU流水线是同时执行这4个指令的,那么轻耗时的代码在很大程度上先执行完,以让出CPU流水线资源给其他指令,所以代码重排序是为了追求更高的程序运行效率。

重排序发生在没有依赖关系时,例如,对于上面的A、B、C、D代码,B、C、D代码不依赖A代码的结果,C、D代码不依赖A、B代码的结果,D代码不依赖A、B、C代码的结果,这种情况下就会发生重排序,如果代码之间有依赖关系,则代码不会重排序。

使用关键字volatile可以禁止代码重排序,例如,有如下代码:


A变量的操作
B变量的操作
volatile Z变量的操作
C变量的操作
D变量的操作

那么会有4种情况发生:

1)A、B可以重排序。

2)C、D可以重排序。

3)A、B不可以重排到Z的后面。

4)C、D不可以重排到Z的前面。

换言之,变量Z是一个“屏障”,Z变量之前或之后的代码不可以跨越Z变量,这就是屏障的作用,关键字synchronized具有同样的特性。

1.实现代码重排序的测试

虽然代码重排序后能提高程序的运行效率,但在有逻辑性的程序中就容易出现一些错误,下面通过示例验证一下。

创建测试用的项目reorderTest。

创建类代码如下:


package test;

public class Test1 {
    private static long x = 0;
    private static long y = 0;
    private static long a = 0;
    private static long b = 0;
    private static long c = 0;
    private static long d = 0;
    private static long e = 0;
    private static long f = 0;

    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (;;) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            e = 0;
            f = 0;
            count++;
            Thread t1 = new Thread(new Runnable() {
                public void run() {
                    a = 1;
                    c = 101;
                    d = 102;
                    x = b;
                }
            });

            Thread t2 = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    e = 201;
                    f = 202;
                    y = a;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            String showString = "count=" + count + " " + x + "," + y + "";
            if (x == 0 && y == 0) {
                System.err.println(showString);
                break;
            } else {
                System.out.println(showString);
            }
        }
    }
}

程序运行后控制台输出最后的部分结果如下:


count=119630 0,1
count=119631 0,1
count=119632 0,1
count=119633 0,1
count=119634 0,1
count=119635 0,0

程序输出x和y都是0,这时就出现了代码重排序,重排后的顺序如下:


x = b;
a = 1;
c = 101;
d = 102;


y = a;
b = 1;
e = 201;
f = 202;

结果是x和y均为0。

2.关键字volatile之前的代码可以重排

下面通过示例验证volatile“之前”的代码可以出现代码重排的效果,重排后的顺序如下:


x = b;
a = 1;
c = 101;
d = 102;


y = a;
b = 1;
e = 201;
f = 202;

结果是x和y均为0。

程序代码如下:


package test;

public class Test2 {
    private static long x = 0;
    private static long y = 0;
    private static long a = 0;
    private static long b = 0;
    private static long c = 0;
    volatile private static long d = 0;
    private static long e = 0;
    volatile private static long f = 0;

    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (;;) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            e = 0;
            f = 0;
            count++;
            Thread t1 = new Thread(new Runnable() {
                public void run() {
                    a = 1;
                    c = 101;
                    x = b;
                    d = 102;
                }
            });

            Thread t2 = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    e = 201;
                    y = a;
                    f = 202;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            String showString = "count=" + count + " " + x + "," + y + "";
            if (x == 0 && y == 0) {
                System.err.println(showString);
                break;
            } else {
                System.out.println(showString);
            }
        }
    }
}

程序运行结果如下:


count=33853 0,1
count=33854 0,1
count=33855 0,1
count=33856 0,1
count=33857 0,1
count=33858 0,0

关键字volatile之前的代码发生了重排。

3.关键字volatile之后的代码可以重排

下面通过示例验证volatile之后的代码可以出现代码重排的效果,重排后的顺序如下:


c = 101;
x = b;
a = 1;
d = 102;


e = 201;
y = a;
b = 1;
f = 202;

结果是x和y均为0。

程序代码如下:


package test;

public class Test3 {
    private static long x = 0;
    private static long y = 0;
    private static long a = 0;
    private static long b = 0;
    volatile private static long c = 0;
    private static long d = 0;
    volatile private static long e = 0;
    private static long f = 0;

    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (;;) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            e = 0;
            f = 0;
            count++;
            Thread t1 = new Thread(new Runnable() {
                public void run() {
                    c = 101;
                    a = 1;
                    d = 102;
                    x = b;
                }
            });

            Thread t2 = new Thread(new Runnable() {
                public void run() {
                    e = 201;
                    b = 1;
                    f = 202;
                    y = a;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            String showString = "count=" + count + " " + x + "," + y + "";
            if (x == 0 && y == 0) {
                System.err.println(showString);
                break;
            } else {
                System.out.println(showString);
            }
        }
    }
}

程序运行结果如下:


count=20898 0,1
count=20899 0,1
count=20900 0,1
count=20901 0,1
count=20902 0,0

关键字volatile之后的代码发生了重排。

4.关键字volatile之前的代码不可以重排到volatile之后

下面通过示例验证volatile之前的代码不可以重排到volatile之后,所以x和y同时不为0的情况不会发生。

程序代码如下:


package test;

public class Test4 {
    private static long x = 0;
    private static long y = 0;
    private static long a = 0;
    private static long b = 0;
    volatile private static long c = 0;
    volatile private static long d = 0;

    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (;;) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            count++;
            Thread t1 = new Thread(new Runnable() {
                public void run() {
                    x = b;
                    c = 101;
                    a = 1;
                }
            });

            Thread t2 = new Thread(new Runnable() {
                public void run() {
                    y = a;
                    d = 201;
                    b = 1;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            String showString = "count=" + count + " " + x + "," + y + "";
            if (x != 0 && y != 0) {
                System.err.println(showString);
                break;
            } else {
                System.out.println(showString);
            }
        }
    }
}

程序运行后,x和y同时不为0的情况不会发生,所以关键字volatile之前的代码不可以重排到volatile之后。

5.关键字volatile之后的代码不可以重排到volatile之前

下面通过示例验证volatile之后的代码不可以重排到volatile之前,所以x和y的值永远不可能同时为0。

程序代码如下:


package test;

public class Test5 {
    private static long x = 0;
    private static long y = 0;
    private static long a = 0;
    private static long b = 0;
    volatile private static long c = 0;
    volatile private static long d = 0;

    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (;;) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            count++;
            Thread t1 = new Thread(new Runnable() {
                public void run() {
                    a = 1;
                    c = 101;
                    x = b;
                }
            });

            Thread t2 = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    d = 201;
                    y = a;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            String showString = "count=" + count + " " + x + "," + y + "";
            if (x == 0 && y == 0) {
                System.err.println(showString);
                break;
            } else {
                System.out.println(showString);
            }
        }
    }
}

程序运行后,x和y的值永远不可能同时为0,所以关键字volatile之后的代码不可以重排到volatile之前。

6.关键字synchronized之前的代码不可以重排到synchronized之后

下面通过示例验证synchronized之前的代码不可以重排到synchronized之后,所以x和y同时不为0的情况不会发生。

程序代码如下:


package test;

public class Test6 {
    private static long x = 0;
    private static long y = 0;
    private static long a = 0;
    private static long b = 0;

    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (;;) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            count++;
            Thread t1 = new Thread(new Runnable() {
                public void run() {
                    x = b;
                    synchronized (this) {
                    }
                    a = 1;
                }
            });

            Thread t2 = new Thread(new Runnable() {
                public void run() {
                    y = a;
                    synchronized (this) {
                    }
                    b = 1;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            String showString = "count=" + count + " " + x + "," + y + "";
            if (x != 0 && y != 0) {
                System.err.println(showString);
                break;
            } else {
                System.out.println(showString);
            }
        }
    }
}

程序运行后,x和y同时不为0的情况不会发生,所以关键字synchronized之前的代码不可以重排到synchronized之后。

7.关键字synchronized之后的代码不可以重排到synchronized之前

下面通过示例验证synchronized之后的代码不可以重排到synchronized之前,所以x和y的值永远不可能同时为0。

程序代码如下:


package test;

public class Test7 {
    private static long x = 0;
    private static long y = 0;
    private static long a = 0;
    private static long b = 0;

    private static long count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (;;) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            count++;
            Thread t1 = new Thread(new Runnable() {
                public void run() {
                    a = 1;
                    synchronized (this) {
                    }
                    x = b;
                }
            });

            Thread t2 = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    synchronized (this) {
                    }
                    y = a;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            String showString = "count=" + count + " " + x + "," + y + "";
            if (x == 0 && y == 0) {
                System.err.println(showString);
                break;
            } else {
                System.out.println(showString);
            }
        }
    }
}

程序运行后,x和y的值永远不可能同时为0,所以关键字synchronized之后的代码不可以重排到synchronized之前。

8.总结

关键字synchronized的主要作用是保证同一时刻,只有一个线程可以执行某一个方法,或是某一个代码块,synchronized可以修饰方法及代码块。随着JDK的版本升级,synchronized关键字在执行效率上得到很大提升。它包含三个特征。

1)可见性:synchronized具有可见性。

2)原子性:使用synchronized实现了同步,同步实现了原子性,保证被同步的代码段在同一时间只有一个线程在执行。

3)禁止代码重排序:synchronized禁止代码重排序。

关键字volatile的主要作用是让其他线程可以看到最新的值,volatile只能修饰变量。它包含三个特征:

1)可见性:B线程能马上看到A线程更改的数据。

2)原子性:在32位系统中,针对未使用volatile声明的long或double数据类型没有实现写原子性,如果想实现,则声明变量时添加volatile,而在64位系统中,原子性取决于具体的实现,在X86架构64位JDK版本中,写double或long是原子的。另外,针对用volatile声明的int i变量进行i++操作时是非原子的。

3)禁止代码重排序。

学习多线程与并发,要着重“外炼互斥,内修可见,内功有序”,这是掌握多线程、学习多线程和并发技术的重要知识点。

关键字volatile和synchronized的使用场景总结如下:

1)当想实现一个变量的值被更改时,让其他线程能取到最新的值时,就要对变量使用volatile。

2)当多个线程对同一个对象中的同一个实例变量进行操作时,为了避免出现非线程安全问题,就要使用synchronized。