一、什么是线程安全
当我们在聊线程安全的时候,其实是在说,在多线程的情况下,多个线程对“共享资源”的访问(读写操作)如何同在单线程环境一样准确。因为在多线程环境下,可能会出现数据不一致的问题。
如下代码,在多线程环境下是会出现问题的。
public class UnsafeSequence {
private int value;
/** 返回一个唯一的数值**/
public int getValue() {
return value++;// 该操作包含三个子操作:从内存中读取value到线程的工作内存、在线程的的工作内存将value+1、并将结果写回主内存
}
}
二、线程安全的方法
上述代码,我们知道在多线程环境下是不安全的,那么如果能提供一种机制,保证一次只有一个线程可以修改这个值(等同于单线程环境),当然就可以解决线程安全的问题了。当然这是一种解决并发问题的思路,还有一种思路就是,使用ThreadLocal,每个线程都有一个变量的副本拷贝,所有的操作都是操作的线程内部的资源,当然也就不涉及到共享变量的问题。ThreadLocal的应用场景,缺陷等放在后面讲,本篇,单讲一下通过锁机制保证线程安全性场景。
1. synchronized关键字
提起解决线程安全的方案,很多人能想到最简单的方案,就是使用synchronized关键字,因为简单方便见效快。那我们就来聊聊,为什么使用一个关键字就能让多线程这匹野马变的安分。
synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或写都将通过同步的方式来进行
具体表现如下:
- synchronized关键字提供一种锁机制,能够确保共享变量的互斥访问,从而防止数据不一致问题的出现。
- synchronized关键字包括monitor enter和monitor exit两个JVM指令。它能够保证任何时候,任何线程执行到monitor enter成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit运行成功后,共享变量修改的值必须被刷会主内存。
- synchronized的指令严格遵守Java happens-before规则,一个monitor exit指令之前必定要有一个monitor enter指令。
synchronized关键字可以用于对代码块或者方法进行修饰,而不能对class以及变量进行修饰
同步方法:
public synchronized void sync() {
// todo something
}
private final Object lockObj = new Object;// 同步代码块锁对象一定是非null的
public void sync(){
// 同步方法A
synchronized(lockObj) {
// todo something
}
}
// 非同步方法
public void nonSync() {
// todo something
}
synchronized关键字作用在代码块和方法上是不一样的。当作用在方法上是,等同于synchronized(this),意味着当前类整个类是被一个线程锁定的,其他线程如果想要访问该类的非同步方法,比如nonSync()方法时,必须等其他线程释放了这个锁才行。而使用同步代码块,加锁一个非null对象的实例变量,当其他线程访问非同步方法时,比如nonSync()方法时,是可以进行访问的。
synchronized关键字实现的是重量级锁,也是可重入锁,即,比如一个对象中有两个同步方法,syncA()和syncB(),在syncA()方法中调用了syncB,那么,当前线程也拥有syncB()方法的锁。
每个对象都与一个monitor相关联,一个monitor的lock的锁只能被一个线程在同一时间获得,在一个线程尝试获得与对象关联monitor的所有权时,会发生如下几件事情:
1. 如果monitor的计数器为0,则意味着该monitor的lock还没有获得,某个线程获得之后将立即对该计数器+1
2. 如果一个已经拥有该monitor的所有权的线程重入,则会导致monitor计数器再次+1
3. 如果monitor已经被其他线程所拥有,则其他线程尝试获取该monitor的所有权时,会被陷入阻塞状态直到monitor计数器变为0,才能再次尝试获取对monitor的所有权
synchronized的作用域太过于强大,它的排他性是的其他线程必须排队经过synchronized保护的区域,这就会引起并发处理效率低的问题。要知道,计算机引入多线程的目的,就是为了提高并发处理速度的。所以,在使用synchronized关键字时,尽量不要作用在方法上,除非情况特殊。如果可以,请尝试其他锁,比如后面我会讲到的lock锁等,如果使用synchronized关键字,尽量建议使用同步代码块,缩小synchronized的作用范围,记住,同步代码块的同步对象,一定是非空的。