多线程极大的提升了效率,但是也带来了隐患,比如两个线程同时操作一个对象,就可能造成数据异常。
为什么会出现线程安全问题
以下是一个线程不安全的程序,运行结果有时是10000,有时比10000小,而且每次都可能不同,这就是线程不安全,因为count++的操作不是原子性的,分为三步,读改写,即先读取数据,在执行修改操作(+1),再将数据回写到内存中,但这样就会出现问题,比如线程一和线程二获取到了数据10,然后线程二进行修改和回写操作,将数据变为11,此时线程一开始执行,但是此时线程一读取到的数据还是10而部署线程二修改后的11,这样就会造成线程一和二各做一次增加操作,但是count从10变为了11,即产生了数据异常。
public class ThreadUnsafeDemo {
private static int count;
private static class Thread1 extends Thread {
public void run() {
for (int i = 0; i < 5000; i++) {
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread1 t1 = new Thread1();
Thread1 t2 = new Thread1();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
那么为什么会出现线程不安全问题,主要有三点
- 原子性:一个或者多个操作在 CPU 执行的过程中被中断(CPU上下文切换)
- 可见性:一个线程对共享变量的修改,另外一个线程不能立刻看到
- 有序性:程序执行的顺序没有按照代码的先后顺序执行
前两点前面已经举例了,现在在解释一下第三点。为什么程序执行的顺序会和代码的执行顺序不一致呢?java平台包括两种编译器:静态编译器(javac)和动态编译器(jit:just in time)。静态编译器是将.java文件编译成.class文件(二进制文件),之后便可以解释执行。动态编译器是将.class文件编译成机器码,之后再由jvm运行。问题一般会出现在动态编译器上,因为动态编译器为了程序的整体性能会对指令进行重排序,虽然重排序可以提升程序的性能,但是重排序之后会导致源代码中指定的内存访问顺序与实际的执行顺序不一样,就会出现线程不安全的问题。
如何保证线程安全
针对原子性问题,JDK中有atomic类,这些类本身通过CAS保证操作的原子性。
针对可见性问题,可以通过synchronized关键字加锁来解决。与此同时,java还提供了一种轻量级的锁,即volatile关键字,要优于synchronized的性能,同样可以保证修改对其他线程的可见性。volatile一般用于对变量的写操作不依赖于当前值的场景中,比如状态标记量等。
针对有序性问题,可以通过synchronized关键字定义同步代码块或者同步方法保障有序性,另外也可以通过Lock接口保障有序性。
synchronized
Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:
- 原子性:确保线程互斥的访问同步代码;
- 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
- 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;
synchronized 内置锁 是一种 对象锁(锁的是对象而非引用变量),作用粒度是对象 ,可以用来实现对 临界资源的同步互斥访问 ,是可重入的。其最大的作用是避免死锁,如:
子类同步方法调用了父类同步方法,如没有可重入的特性,则会发生死锁;
对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象
使用方法
修饰方法
SynchronizedDemo修饰的是代码块,SynchronizedDemo1修饰的是方法,但是两者是等价的,都是锁定了对象,锁定了对象后对对象的操作只能等待上一个锁释放后进行
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("Method 1 start");
}
}
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
synchronizedDemo.method();
}
}
public class SynchronizedDemo1 {
public synchronized void method() {
System.out.println("Method 1 start");
}
public static void main(String[] args) {
SynchronizedDemo1 synchronizedDemo = new SynchronizedDemo1();
synchronizedDemo.method();
}
}
修饰代码块
public class SynchronizedDemo2 {
public static void main(String args[]) {
SyncThread s1 = new SyncThread();
SyncThread s2 = new SyncThread();
Thread t1 = new Thread(s1);
Thread t2 = new Thread(s2);
// SyncThread s = new SyncThread();
// Thread t1 = new Thread(s);
// Thread t2 = new Thread(s);
t1.start();
t2.start();
}
static class SyncThread implements Runnable {
private static int count;
public SyncThread() {
count = 0;
}
public void run() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public int getCount() {
return count;
}
}
}
运行结果
因为实例化了两个SyncThread对象,各锁各的,导致计数异常,要实现控制需要对Class加锁
Thread-0:0
Thread-1:1
Thread-0:2
Thread-1:2
Thread-1:3
Thread-0:3
Thread-0:4
Thread-1:4
Thread-0:5
Thread-1:6
public class SynchronizedDemo2 {
public static void main(String args[]) {
// SyncThread s1 = new SyncThread();
// SyncThread s2 = new SyncThread();
// Thread t1 = new Thread(s1);
// Thread t2 = new Thread(s2);
SyncThread s = new SyncThread();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
t1.start();
t2.start();
}
static class SyncThread implements Runnable {
private static int count;
public SyncThread() {
count = 0;
}
public void run() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public int getCount() {
return count;
}
}
}
运行结果
因为只实例化了一个SyncThread对象,当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象,所以计数正确
Thread-1:0
Thread-1:1
Thread-1:2
Thread-1:3
Thread-1:4
Thread-0:5
Thread-0:6
Thread-0:7
Thread-0:8
Thread-0:9
修饰部分代码块
public class SynchronizedDemo4 {
public static void main(String args[]) {
Counter counter = new Counter();
Thread thread1 = new Thread(counter, "A");
Thread thread2 = new Thread(counter, "B");
thread1.start();
thread2.start();
}
static class Counter implements Runnable {
private int count;
public Counter() {
count = 0;
}
public void countAdd() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//非synchronized代码块,未对count进行读写操作,所以可以不用synchronized
public void printCount() {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + " count:" + count);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.equals("A")) {
countAdd();
} else if (threadName.equals("B")) {
printCount();
}
}
}
}
运行结果
B线程的调用是非synchronized,并不影响A线程对synchronized部分的调用。从上面的结果中可以看出一个线程访问一个对象的synchronized代码块时,别的线程可以访问该对象的非synchronized代码块而不受阻塞
A:0
B count:1
A:1
B count:1
A:2
B count:3
B count:3
A:3
B count:4
A:4
修饰指定对象
public class SynchronizedDemo3 {
public static void main(String args[]) {
Account account = new Account("zhang san", 10000.0f);
AccountOperator accountOperator = new AccountOperator(account);
//开启10个线程,进行存取款
final int THREAD_NUM = 10;
Thread threads[] = new Thread[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i++) {
threads[i] = new Thread(accountOperator, "Thread" + i);
threads[i].start();
}
}
static class Account {
String name;
float amount;
public Account(String name, float amount) {
this.name = name;
this.amount = amount;
}
//存钱
public void deposit(float amt) {
amount += amt;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//取钱
public void withdraw(float amt) {
amount -= amt;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public float getBalance() {
return amount;
}
}
/**
* 账户操作类
*/
static class AccountOperator implements Runnable {
private Account account;
public AccountOperator(Account account) {
this.account = account;
}
public void run() {
//锁定指定对象account
synchronized (account) {
account.deposit(500);
System.out.println("存款完成" + Thread.currentThread().getName() + ":" + account.getBalance());
account.withdraw(500);
System.out.println("取款完成" + Thread.currentThread().getName() + ":" + account.getBalance());
}
}
}
}
运行结果
在AccountOperator 类中的run方法里,我们用synchronized 给account对象加了锁。这时,当一个线程访问account对象时,其他试图访问account对象的线程将会阻塞,直到该线程访问account对象结束。也就是说谁拿到那个锁谁就可以运行它所控制的那段代码。
存款完成Thread0:10500.0
取款完成Thread0:10000.0
存款完成Thread9:10500.0
取款完成Thread9:10000.0
存款完成Thread8:10500.0
取款完成Thread8:10000.0
存款完成Thread5:10500.0
取款完成Thread5:10000.0
存款完成Thread3:10500.0
取款完成Thread3:10000.0
存款完成Thread7:10500.0
取款完成Thread7:10000.0
存款完成Thread6:10500.0
取款完成Thread6:10000.0
存款完成Thread4:10500.0
取款完成Thread4:10000.0
存款完成Thread2:10500.0
取款完成Thread2:10000.0
存款完成Thread1:10500.0
取款完成Thread1:10000.0
修饰静态方法
public class SynchronizedDemo5 {
public static void main(String args[]) {
SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "SyncThread1");
Thread thread2 = new Thread(syncThread2, "SyncThread2");
thread1.start();
thread2.start();
}
static class SyncThread implements Runnable {
private static int count;
public SyncThread() {
count = 0;
}
public synchronized static void method() {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void run() {
method();
}
}
}
运行结果
syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。
SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9
修饰一个类
public class SynchronizedDemo6 {
public static void main(String args[]) {
SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "SyncThread1");
Thread thread2 = new Thread(syncThread2, "SyncThread2");
thread1.start();
thread2.start();
}
static class SyncThread implements Runnable {
private static int count;
public SyncThread() {
count = 0;
}
public static void method() {
synchronized (SyncThread.class) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public synchronized void run() {
method();
}
}
}
运行结果
给class加锁和上例的给静态方法加锁是一样的,所有对象公用一把锁,保证顺序输出
SyncThread2:0
SyncThread2:1
SyncThread2:2
SyncThread2:3
SyncThread2:4
SyncThread1:5
SyncThread1:6
SyncThread1:7
SyncThread1:8
SyncThread1:9
使用总结
- 当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this),即当前实例,但是如果创建了多个实例,各锁各的就无法控制输出;
- 当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例;
- 当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁;
- 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
- 实现同步开销巨大,甚至可能造成死锁,所以尽量避免无谓的同步控制。
实现原理
执行SynchronizedDemo后,查看字节码
Compiled from "SynchronizedDemo.java"
public class com.example.offer.thread.demo3.SynchronizedDemo {
public com.example.offer.thread.demo3.SynchronizedDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void method();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String Method 1 start
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
public static void main(java.lang.String[]);
Code:
0: new #5 // class com/example/offer/thread/demo3/SynchronizedDemo
3: dup
4: invokespecial #6 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #7 // Method method:()V
12: return
}
重点是其中的monitorenter和monitorexit
monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;
通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
volatile
volatile关键字的作用:保证了变量的可见性(visibility)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象,volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取,volatile仅能使用在变量级别
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
使用场景
状态标记
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
双重校验
第一次校验:由于单例模式只需要创建一次实例,如果后面再次调用getInstance方法时,则直接返回之前创建的实例,因此大部分时间不需要执行同步方法里面的代码,大大提高了性能。如果不加第一次校验的话,那跟上面的懒汉模式没什么区别,每次都要去竞争锁。
第二次校验:如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。
需要注意的是,private volatile static Singleton instance = null;需要加volatile关键字,否则会出现错误。问题的原因在于JVM指令重排优化的存在。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错,所以需要volatile禁止指令重排序优化。
instance = new Singleton();中new命令并不是一个原子指令,它分为三步
分配对象内存
调用构造器方法,执行初始化
将对象引用赋值给变量
虚拟机实际运行时,以上指令可能发生重排序。以上代码 2,3 可能发生重排序,但是并不会重排序 1 的顺序。也就是说 1 这个指令都需要先执行,因为 2,3 指令需要依托 1 指令执行结果。
Java 语言规规定了线程执行程序时需要遵守 intra-thread semantics。intra-thread semantics 保证重排序不会改变单线程内的程序执行结果。这个重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能。
虽然重排序并不影响单线程内的执行结果,但是在多线程的环境就带来一些问题。
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
使用方法
修饰变量
public class VolatileDemo {
private static int count;
public static void main(String args[]) {
SyncThread s = new SyncThread();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
t1.start();
t2.start();
}
static class SyncThread implements Runnable {
public SyncThread() {
count = 0;
}
public void run() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public int getCount() {
return count;
}
}
}
运行结果
Thread-0:0
Thread-1:1
Thread-0:2
Thread-1:3
Thread-0:4
Thread-1:5
Thread-0:6
Thread-1:7
Thread-0:8
Thread-1:9
使用总结
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是
立即可见的。
2)禁止进行指令重排序。
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
实现原理
有volatile变量修饰的共享变量进行操作时会增加一行汇编命令,命令有前缀lock,而lock前缀的指令在多核处理器中会触发以下两件事情
- 将当前处理器缓存的数据回写到主内存
- 回写主内存操作会引起其他CPU中缓存了该内存地址的数据无效
synchronized和volatile的区别
1.volatile仅能使用在变量级别;
synchronized则可以使用在变量、方法、和类级别的
2.volatile仅能实现变量的修改可见性,并不能保证原子性;
synchronized则可以保证变量的修改可见性和原子性
3.volatile不会造成线程的阻塞;
synchronized可能会造成线程的阻塞。
4.volatile标记的变量不会被编译器优化;
synchronized标记的变量可以被编译器优化
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。