Java多线程编程核心技术-2-对象及变量的并发访问

在上一篇文章Java多线程编程核心技术-1-多线程技术基础中,提到了共享变量造成的非线程安全问题。

本文就是为了解决非线程安全的相关问题。

synchronized同步方法

关键字synchronized可用来保障原子性、可见性和有序性。

但是在学习synchronized之前,我们必须明确一点:

synchronized锁住的是对象。即使是synchronized方法。

线程安全问题和解决方案

就像前一文中提到的,只有多线程共享的变量才会面临线程安全问题。

即定义在线程类中并且被多个实例化为多个线程。

而方法体中的局部变量,无论如何都不会被多线程共享,因此无论如何都不会出现线程安全问题。

synchronized同步方法的使用

对于共享的变量,在对其进行操作的方法上,加上synchronized关键字,即将其成为同步方法。也就是说所有线程在访问该方法时,必须排队进行访问,而不能同时访问。

(注意这里的例子并不合理,一般都会实现业务和线程解耦。这里不再拘束于这一点。)

例如:设计一个让每个线程使共享变量i增加的程序,每个线程在增加完后将其置0,然后再递增。

线程类1(不加同步):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package multiThread;

public class MyThread1 extends Thread{

private int i = 0;

@Override
public void run() {
while(i < 5) {
i++;
System.out.println(Thread.currentThread().getName() + "update! " + "i=" + i);
}
i = 0;
System.out.println("thread: "+Thread.currentThread().getName()+"reset i= " + i);
}

}

运行类2:

1
2
3
4
5
6
7
8
9
10
11
12
13
package multiThread;

public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread1 thread = new MyThread1();
Thread thread1 = new Thread(thread);
Thread thread2 = new Thread(thread);
Thread thread3 = new Thread(thread);
thread1.start();
thread2.start();
thread3.start();
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Thread-1update! i=2
Thread-3update! i=3
Thread-3update! i=5
Thread-2update! i=2
thread: Thread-3reset i= 0
Thread-1update! i=4
Thread-2update! i=1
Thread-2update! i=3
Thread-2update! i=4
Thread-2update! i=5
thread: Thread-2reset i= 0
Thread-1update! i=2
Thread-1update! i=1
Thread-1update! i=2
Thread-1update! i=3
Thread-1update! i=4
Thread-1update! i=5
thread: Thread-1reset i= 0

可以看到结果是混乱的,因为并没有同步,此时我们为run方法加上同步标志:

线程类2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package multiThread;

public class MyThread1 extends Thread{

private int i = 0;

@Override
synchronized public void run() {
while(i < 5) {
i++;
System.out.println(Thread.currentThread().getName() + "update! " + "i=" + i);
}
i = 0;
System.out.println("thread: "+Thread.currentThread().getName()+"reset i= " + i);
}

}

此时的结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Thread-1update! i=1
Thread-1update! i=2
Thread-1update! i=3
Thread-1update! i=4
Thread-1update! i=5
thread: Thread-1reset i= 0
Thread-3update! i=1
Thread-3update! i=2
Thread-3update! i=3
Thread-3update! i=4
Thread-3update! i=5
thread: Thread-3reset i= 0
Thread-2update! i=1
Thread-2update! i=2
Thread-2update! i=3
Thread-2update! i=4
Thread-2update! i=5
thread: Thread-2reset i= 0

可以看到结果是按照每个线程自己的顺序来进行的,每个线程都没有中断。

同步synchronized在字节码指令中的原理

synchronized同步方法

在方法中使用synchronized关键字实现同步的原因是使用了flag标记ACC_SYNCHRONIZED,当调用方法时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否设置,如果设置了,执行线程先持有同步锁,然后执行方法,最后在方法完成时释放锁。

synchronized同步代码块

如果使用synchronized代码块,则使用monitorenter和monitorexit指令进行同步处理

多个对象多个锁

就像我们之前说的,synchronized锁住的是对象。那么如果线程对象不同,即变量不共享了,那么锁也就不一样了。

例如:

线程类:线程类2

运行类:

1
2
3
4
5
6
7
8
9
10
11
12
package multiThread;

public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread1 thread = new MyThread1();
MyThread1 thread1 = new MyThread1();
MyThread1 thread2 = new MyThread1();
thread.start();
thread1.start();
thread2.start();
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Thread-0update! i=1
Thread-2update! i=1
Thread-2update! i=2
Thread-1update! i=1
Thread-2update! i=3
Thread-0update! i=2
Thread-2update! i=4
Thread-1update! i=2
Thread-2update! i=5
Thread-0update! i=3
thread: Thread-2reset i= 0
Thread-1update! i=3
Thread-0update! i=4
Thread-0update! i=5
thread: Thread-0reset i= 0
Thread-1update! i=4
Thread-1update! i=5
thread: Thread-1reset i= 0

可以看到线程是异步运行的,但是由于变量不共享,所以并不会发生混乱,最终结果还是正确的。

即:

不共享的变量具有同锁,即使是相同的类实例化的。

静态同步synchronized方法

关键字synchronized还可以应用在static静态方法上,如果这样写,那是对当前的*.java文件对应的Class类对象进行持锁,Class类的对象是单例的,更具体地说,在静态static方法上使用synchronized关键字声明同步方法时,使用当前静态方法所在类对应Class类的单例对象作为锁。

例如:

服务类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package service;

public class Service {

synchronized public static void printA() {
try {
System.out.println("线程名称为:" + Thread.currentThread().getName()
+ "在" + System.currentTimeMillis() + "进入printA");
Thread.sleep(3000);
System.out.println("线程名称为:" + Thread.currentThread().getName()
+ "在" + System.currentTimeMillis() + "离开printA");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

synchronized public static void printB() {
System.out.println("线程名称为:" + Thread.currentThread().getName()
+ "在" + System.currentTimeMillis() + "进入printB");
System.out.println("线程名称为:" + Thread.currentThread().getName()
+ "在" + System.currentTimeMillis() + "离开printB");
}

}

线程类A:

1
2
3
4
5
6
7
8
9
10
11
package extthread;

import service.Service;

public class ThreadA extends Thread {
@Override
public void run() {
Service.printA();
}

}

线程类B:

1
2
3
4
5
6
7
8
9
10
package extthread;

import service.Service;

public class ThreadB extends Thread {
@Override
public void run() {
Service.printB();
}
}

运行类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package test;

import service.Service;
import extthread.ThreadA;
import extthread.ThreadB;

public class Run {

public static void main(String[] args) {

ThreadA a = new ThreadA();
a.setName("A");
a.start();

ThreadB b = new ThreadB();
b.setName("B");
b.start();

}

}

结果:

1
2
3
4
线程名称为:A在1403596294939进入printA
线程名称为:A在1403596297939离开printA
线程名称为:B在1403596297939进入printB
线程名称为:B在1403596297939离开printB

虽然该程序运行结果和将synchronized关键字加到非static方法上的效果是一样的——同步,但两者还是有本质上的不同,synchronized关键字加到static静态方法上的方式是将Class类对象作为锁,而synchronized关键字加到非static静态方法上的方式是将方法所在类的对象作为锁。

synchronized用法的几条基本原则(重要)

这里有几个显而易见的解论:

  1. 在Java中只有“将对象作为锁”这种说法,并没有“锁方法”这种说法。
  2. 在Java语言中,“锁”就是“对象”,“对象”可以映射成“锁”,哪个线程拿到这把锁,哪个线程就可以执行这个对象中的synchronized同步方法。
  3. A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法。
  4. A线程先持有object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法,则需要等待,也就是同步。
  5. 如果在X对象中使用了synchronized关键字声明非静态方法,则X对象就被当成锁。

脏读

由于线程是可以睡眠的,所以可能线程运行到一半暂停了,此时可能共享数据正操作一半,此时的数据即被称为脏数据。如果此时读取数据,那么必然是不正确的。所以我们也应当对共享变量的读取添加限制,回想上面第4条。对一个对象不同的synchronized方法,也是同步的,会排队进行。

因此解决办法是将读取方法也设为同步,那么如果设置方法再sleep中,线程不会释放锁,则读取办法也会等待设置方法执行完才能运行,从而避免脏读。

例如:

服务类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package entity;

public class PublicVar {

public String username = "A";
public String password = "AA";

synchronized public void setValue(String username, String password) {
try {
this.username = username;
Thread.sleep(5000);
this.password = password;

System.out.println("setValue method thread name="
+ Thread.currentThread().getName() + " username="
+ username + " password=" + password);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void getValue() {
System.out.println("getValue method thread name="
+ Thread.currentThread().getName() + " username=" + username + " password=" + password);
}
}

线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package extthread;

import entity.PublicVar;

public class ThreadA extends Thread {

private PublicVar publicVar;

public ThreadA(PublicVar publicVar) {
super();
this.publicVar = publicVar;
}
@Override
public void run() {
super.run();
publicVar.setValue("B", "BB");
}
}

运行类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package test;

import entity.PublicVar;
import extthread.ThreadA;

public class Test {

public static void main(String[] args) {
try {
PublicVar publicVarRef = new PublicVar();
ThreadA thread = new ThreadA(publicVarRef);
thread.start();

Thread.sleep(200);// 输出结果受此值大小影响

publicVarRef.getValue();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

此时通过getValue获取的数字是一组脏数据,因为getValue不是同步方法,再setValue运行到一半就读取了数据。所以解决办法是将getValue也变为同步方法。

1
2
3
4
5
synchronized public void getValue() {
System.out.println("getValue method thread name="
+ Thread.currentThread().getName() + " username=" + username + "
password=" + password);
}

synchronized锁重入

关键字synchronized拥有重入锁的功能,即在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以得到该对象锁的,这也证明在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。

锁重入支持继承的环境

锁重入支持继承的环境即如果类A继承了类B,且类B中有同步方法fn,A重写了fn方法,那么可以通过super.fn调用父类的方法,那么此时的锁仍然是持有的。即重入了锁。

例如:

父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package myservice;

public class Main {

public int i = 10;

synchronized public void operateIMainMethod() {
try {
i--;
System.out.println("main print i=" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}

子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package myservice;

public class Sub extends Main {

synchronized public void operateISubMethod() {
try {
while (i > 0) {
i--;
System.out.println("sub print i=" + i);
Thread.sleep(100);
super.operateIMainMethod();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
package myservice;

public class Sub extends Main {

synchronized public void operateISubMethod() {
try {
while (i > 0) {
i--;
System.out.println("sub print i=" + i);
Thread.sleep(100);
super.operateIMainMethod();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}

线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
package extthread;

import myservice.Main;
import myservice.Sub;

public class MyThread extends Thread {
@Override
public void run() {
Sub sub = new Sub();
sub.operateISubMethod();
}

}

运行类:

1
2
3
4
5
6
7
8
9
10
package test;

import extthread.MyThread;

public class Run {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
sub print i=9
main print i=8
sub print i=7
main print i=6
sub print i=5
main print i=4
sub print i=3
main print i=2
sub print i=1
main print i=0

可以看到由于调用了父类的方法,所以i也再依次减小。并且锁持续持有,完成了锁重入。

出现异常,锁自动释放

当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

重写方法不使用synchronized

重写方法如果不使用synchronized关键字,即是非同步方法,使用后变成同步方法。

holdsLock()方法

public static native boolean holdsLock(Object obj)方法的作用检测当前线程是否持有指定对象的锁(这里也就印证了前面第一条)。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
package test9;

public class Test1 {
public static void main(String[] args) {
System.out.println("A " + Thread.currentThread().holdsLock(Test1.class));
isLocked();
System.out.println("C " + Thread.currentThread().holdsLock(Test1.class));
}

synchronized public static void isLocked(){
System.out.println("B " + Thread.currentThread().holdsLock(Test1.class));
}
}

结果:

1
2
3
A false
B true
C false

synchronized同步语句块

synchronized方法是将当前对象作为锁,而synchronized代码块是将任意对象作为锁。可以将锁看成一个标识,哪个线程持有这个标识,就可以执行同步方法。

其主要格式是:

1
2
3
synchronized(obj){
// ...
}

这里的obj即为要锁住的对象。也就是说持有该锁后,其他要访问该对象的代码都必须同步。实际上synchronized方法于就等同于:

1
2
3
synchronized(this){
// 锁住当前对象
}

synchronized同步代码块的使用

当两个并发线程访问同一个对象object中的synchronized(obj)同步代码块时,一段时间内只能有一个线程得到执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

例如:

线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package multiThread;

public class MyThread1 extends Thread{

private int i = 1;
private int j = 1;

@Override
public void run() {
synchronized(this) {
i = 100;
j = 100;
System.out.println(String.valueOf(i)+":"+String.valueOf(i)+" current thread: "+this.getName());
i = 1;
j = 1;
System.out.println(String.valueOf(i)+":"+String.valueOf(i)+" current thread: "+this.getName());
}
}
}

运行类:

1
2
3
4
5
6
7
8
9
10
11
12
package multiThread;

public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread1 thread = new MyThread1();
Thread thread1 = new Thread(thread);
Thread thread2 = new Thread(thread);
thread1.start();
thread2.start();
}
}

结果:

1
2
3
4
100:100 current thread: Thread-0
1:1 current thread: Thread-0
100:100 current thread: Thread-0
1:1 current thread: Thread-0

可以看到两个线程是按顺序进入的然后运行的。

锁住任意对象

前面的例子锁住的是this,即当前的对象。但synchronized代码块还可以锁住任意对象,只需要将其传入代码块的参数即可。

例如:

线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package multiThread;

public class MyThread1 extends Thread{

private String str = "string";

@Override
public void run() {
synchronized(str) {
str = "new String";
System.out.println("update str: " + str);
str = "string";
System.out.println("recover str: " + str);
}
}
}

执行类:

1
2
3
4
5
6
7
8
9
10
11
12
package multiThread;

public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread1 thread = new MyThread1();
Thread thread1 = new Thread(thread);
Thread thread2 = new Thread(thread);
thread1.start();
thread2.start();
}
}

结果:

1
2
3
4
update str: new String
recover str: string
update str: new String
recover str: string

可以看到仍然是同步执行的。

synchronized同步块的三个结论

synchronized(非this对象x)格式的写法是将x对象本身作为“对象监视器”,这样就可以分析出3个结论:

  1. 当多个线程同时执行synchronized(x){}同步代码块时呈同步效果。
  2. 当其他线程执行x对象中synchronized同步方法时呈同步效果。
  3. 当其他线程执行x对象方法里面的synchronized(this)代码块时呈现同步效果。

其实这几个结论都由一条准则获得:synchronized锁住的是对象,所以只要是需要对应obj锁的代码,都必须是同步的;而不需要的,就是异步。

这里的结论与前面synchronized方法原理基本一致。

类Class的单例性

每一个*.java文件对应Class类的实例都是一个。

同步syn(class)代码块可以对类的所有对象实例起作用

同样的,可以通过synchronized代码块来改写synchronized方法。

例如:

服务类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package service;

public class Service {

public void printA() {
synchronized (Service.class) {
try {
System.out.println("线程名称为:" + Thread.currentThread().getName()
+ "在" + System.currentTimeMillis() + "进入printA");
Thread.sleep(3000);
System.out.println("线程名称为:" + Thread.currentThread().getName()
+ "在" + System.currentTimeMillis() + "离开printA");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}

public void printB() {
synchronized (Service.class) {
System.out.println("线程名称为:" + Thread.currentThread().getName()
+ "在" + System.currentTimeMillis() + "进入printB");
System.out.println("线程名称为:" + Thread.currentThread().getName()
+ "在" + System.currentTimeMillis() + "离开printB");
}
}
}

这里就是直接锁定了Service.class,就像静态synchronized方法。

String常量池特性与同步相关的问题与解决方案

值得注意的是,常量有一个常量池,通过字面量定义的String对象。可能会被放到常量池中,导致虽然看起来不同,但是实际上是同一个对象,所以其锁确实同一个锁,导致错误。

其解决办法也很简单,使用字符串常量作为锁对象时,不用字面量定义,而用构造方法来定义,如:

1
String lock = new String("lock");

这样就可以保证获取到的字符串对象完全不同。

锁对象不改变依然同步执行

只要对象不变,运行的结果即为同步。其实这也对应了一个对象一把锁的原则。

即对象本身不变,其属性改变,并不会影响对象的锁。

锁对象改变则锁改变

同样的,如果锁对象改变,则锁也相应改变,所以不同的线程就会变成异步执行。

volatile关键字

首先了解并发编程中的的三个重要特性:

  • 可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  • 有序性:即程序执行的顺序按照代码的先后顺序执行。

而volatile基本保证了上述特征,但是在原子性上,其并不完整,具体如下:

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

可见性测试

简单的内存模型

简单来说,线程再执行的时候是在CPU中执行,而为了缓解内存速度太慢的问题。CPU中就设计了缓存,而不同的线程都有不同的缓存,一般是先将内存中的变量复制到CPU缓存中。而不同的线程就有不同的副本,因此就产生了同步问题。

而关键字volatile具有可见性,可见性是指A线程更改变量的值后,B线程马上就能看到更改后的变量的值。

其原理在于用volatile标记的变量每次读写值都是从内存中读取,而不是读写局部缓存。

测试

线程代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package multiThread;

public class MyThread1 extends Thread{

private String str = "string";

@Override
public void run() {
while(str != "") {
System.out.println("looping!");
}
System.out.println("stop!");
}

public void setStr(String str) {
this.str = str;
}
}

运行代码:

1
2
3
4
5
6
7
8
9
10
11
package multiThread;

public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread1 thread = new MyThread1();
thread.start();
Thread.sleep(2000);
thread.setStr("");
}
}

其结果就是无论如何都停不下来。

但是如果将str改为volatile修饰就可以将其停下来。

1
volatile private String str = "string";

则会在大致两秒后停下来。

原子性

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

在64位系统中,原子性取决于具体的实现,在X86架构64位JDK版本中,写double或long是原子的。

另外,volatile关键字最致命的缺点是不支持原子性,也就是多个线程对用volatile修饰的变量i执行i–操作时,i–操作还会被分解成3步,造成非线程安全问题的出现。

解决原子性

使用synchronized

可以使用synchronized来同步赋值方法可以实现原子性。这个方法很容易想到。

使用Atomic原子类进行i++操作实现原子性

除了在i++操作时使用synchronized关键字实现同步外,还可以使用AtomicInteger原子类实现原子性。

原子操作是不能分割的整体,没有其他线程能够中断或检查处于原子操作中的变量。一个原子(atomic)类型就是一个原子操作可用的类型,它可以在没有锁(lock)的情况下做到线程安全(thread-safe)。

例如:

线程类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package extthread;

import java.util.concurrent.atomic.AtomicInteger;

public class AddCountThread extends Thread {
private AtomicInteger count = new AtomicInteger(0);

@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(count.incrementAndGet());
}
}
}

执行类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package test;

import extthread.AddCountThread;

public class Run {

public static void main(String[] args) {
AddCountThread countService = new AddCountThread();

Thread t1 = new Thread(countService);
t1.start();

Thread t2 = new Thread(countService);
t2.start();

Thread t3 = new Thread(countService);
t3.start();

Thread t4 = new Thread(countService);
t4.start();

Thread t5 = new Thread(countService);
t5.start();

}

}

结果:

1
2
3
4
5
6
7
8
9
49992
49993
49994
49995
49996
49997
49998
49999
50000

有序性

使用关键字volatile可以禁止代码重排序。保证代码的有序性。

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

1
2
3
4
A代码-重耗时
B代码-轻耗时
C代码-重耗时
D代码-轻耗时

可能会被重排为

1
2
3
4
B代码-轻耗时
D代码-轻耗时
A代码-重耗时
C代码-重耗时

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

重排序发生在没有依赖关系时。

但如果加入了volatile,则保证被volatile声明的语句前后无法重排。

例如:

1
2
3
4
5
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具有同样的特性。

volatile重排的规则

由上可以得出volatile声明的规则:

  1. 关键字volatile之前的代码可以重排
  2. 关键字volatile之后的代码可以重排
  3. 关键字volatile之前的代码不可以重排到volatile之后
  4. 关键字volatile之后的代码不可以重排到volatile之前
  5. 关键字synchronized之前的代码不可以重排到synchronized之后
  6. 关键字synchronized之后的代码不可以重排到synchronized之前

总结

关键字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。(即volatile尽量只用于解决可见性问题。)

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

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :