谈到并发时候,离不开JAVA的几个关键字volatile、synchronized和lock类。下面就分别介绍一下它们的实现原理、用法以及使用场景。
首先先了解一下Java内存模型的抽象示意如图所示
*每个线程都有自己的本地内存空间(java栈中的帧)。线程执行时,先把变量从内存读到线程自己的本地内存空间,然后对变量进行操作。
* 对该变量操作完成后,在某个时间再把变量刷新回主内存。
1)volatile
原理:volatile变量修饰的共享变量的时候,拥有Lock前缀的指令,他在多核处理器下会引发了两件事情。①将当前处理器缓存行的数据写回到系统内存。②这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
具体展开就是 当对volatile标记的变量进行修改时,会将其他缓存中存储的修改前的变量清除,然后重新读取。一般来说应该是先在进行修改的缓存A中修改为新值,然后通知其他缓存清除掉此变量,当其他缓存B中的线程读取此变量时,会向总线发送消息,这时存储新值的缓存A获取到消息,将新值穿给B。最后将新值写入内存。当变量需要更新时都是此步骤,volatile的作用是被其修饰的变量,每次更新时,都会刷新上述步骤。所以,用volatile修饰的变量是可以保证可见性的。
这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:
假如某个时刻变量inc的值为10,
线程1对变量进行自增操作,线程1先读取了变量A=10的原始值,然后线程1被阻塞了;
然后线程2对变量进行自增操作,线程2也去读取变量A的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量A的缓存行无效,所以线程2会直接去主存读取A的值,发现A的值时10,然后进行加1操作,并把11后的值写入工作内存,最后写入主存。
然后线程1接着进行加1操作,由于已经读取了A的值,注意此时在线程1的工作内存中A的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
那么两个线程分别进行了一次自增操作后,A只增加了1。
这里要说到一个点就是 自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。
因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。下面列举几个Java中使用volatile的几个场景。
1.状态标记量
1
2
3
4
5
6
7
8
9
|
volatile
boolean
flag =
false
;
while
(!flag){
doSomething();
}
public
void
setFlag() {
flag =
true
;
}
|
1
2
3
4
5
6
7
8
9
10
|
volatile
boolean
inited =
false
;
//线程1:
context = loadContext();
inited =
true
;
//线程2:
while
(!inited ){
sleep()
}
doSomethingwithconfig(context);
|
2.double check(双重检查) 这里用到volatile的禁止重排序的特性。当声明对象的引用为volatile后,代码中的的重排序,在多线程环境中将会被禁止
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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;
}
}
|
1)synchronized
在讲synchronized之前我们先来看一下CPU两个指令monitorenter 和monitorexit:
① monitorenter
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
②monitorexit
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
原理:通过反编译可以看到Synchronized底层会运行CPU的两个指令monitorenter 和monitorexit,Synchronized的语义底层是通过一个monitor的对象来完成。
Synchronized可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍然可以访问该object中的非加锁代码块。
使用场景:
①synchronized 方法
方法声明时使用,这时,线程获得的是成员锁,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。
②synchronized 同步方法块
对某一代码块使用,synchronized后跟括号,括号里是变量,这样,一次只有一个线程进入该代码块.此时,线程获得的是成员锁。
③静态同步方法
synchronized 修饰 静态方法和非静态方法的区别:
synchronized 修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。不同对象在两个线程中调用同一个同步方法,不会产生互斥。
synchronized 修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。用类直接在两个线程中调用两个不同的同步方法会产生互斥。
3)lock
Lock不是Java语言内置的,它是一个类,通过这个类可以实现同步访问;
java.util.concurrent.locks包下常用的类
public interface Lock {
//获取锁,如果锁被其他线程获取,则进行等待
void lock();
//当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
void lockInterruptibly() throws InterruptedException;
/**tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成
*功,则返回true,如果获取失败(即锁已被其他线程获取),则返回
*false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。*/
boolean tryLock();
//tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock(); //释放锁
Condition newCondition();
}
使用lock 的代码结构如下:
Lock l = ...; l.lock(); try { // 执行代码 } finally { l.unlock(); }
trylock使用方法:
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
ReentrantLock
ReentrantLock,意思是“可重入锁”,是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
总结:
volatile和synchronized区别
①volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
②volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
③volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性.
④volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
⑤当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。
⑥使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。
synchronized和lock区别
①Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
②synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
③Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
④通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
⑤Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。