volatile、synchronized和lock解析
首先了解下 java 的内存模型:
- 每个线程都有自己的本地内存空间(java栈中的帧)。线程执行时,先把变量从内存读到线程自己的本地内存空间,然后对变量进行操作。
- 对该变量操作完成后,在某个时间再把变量刷新回主内存。
那么我们再了解下锁提供的两种特性:互斥(mutual exclusion) 和可见性(visibility):
- 互斥(mutual exclusion):互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据;
- 可见性(visibility):简单来说就是一个线程修改了变量,其他线程可以立即知道。保证可见性的方法:volatile,synchronized,final(一旦初始化完成其他线程就可见)。
volatile
volatile是一个类型修饰符(type specifier)。它是被设计用来修饰被不同线程访问和修改的变量。确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
上面的话有些拗口,简单概括 volatile
,它能够使变量在值发生改变时能尽快地让其他线程知道。
问题来源
首先我们要先意识到有这样的现象,编译器为了加快程序运行的速度,对一些变量的写操作会先在寄存器或者是CPU缓存上进行,最后才写入内存。而在这个过程中,变量的新值对其他线程是不可见的。
一个例子如下,这里定义了 isRunning 成员变量,来控制子线程结束:
public class RunThread extends Thread {
private boolean isRunning = true;
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
@Override
public void run() {
System.out.println("进入到run方法中了");
while (isRunning == true) {
}
System.out.println("线程执行完成了");
}
public static void main(String[] args) {
try {
RunThread thread = new RunThread();
thread.start();
Thread.sleep(1000);
thread.setRunning(false);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在主线程中设置了thread.setRunning(false);
但是子线程并不会结束而是一直在循环,
解决方法
volatile private boolean isRunning = true;
原理
当对 volatil
e 标记的变量进行修改时,会将其他缓存中存储的修改前的变量清除,然后重新读取。一般来说应该是先在进行修改的缓存A
中修改为新值,然后通知其他缓存清除掉此变量,当其他缓存 B
中的线程读取此变量时,会向总线发送消息,这时存储新值的缓存 A
获取到消息,将新值穿给B
。最后将新值写入内存。当变量需要更新时都是此步骤,volatile
的作用是被其修饰的变量,每次更新时,都会刷新上述步骤。
synchronized
Java 语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。
当两个并发线程访问同一个对象
object
中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object
的一个加锁代码块时,另一个线程仍然可以访问该object
中的非加锁代码块。
synchronized 方法
方法声明时使用,放在范围操作符 (public
等)之后,返回类型声明(void
等)之前.这时,线程获得的是成员锁,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized
方法内部的线程)执行完该方法后,别的线程才能进入。
public synchronized void synMethod(){
//方法体
}
如在线程 t1
中有语句obj.synMethod();
那么由于synMethod
被synchronized
修饰,在执行该语句前, 需要先获得调用者obj
的对象锁, 如果其他线程(如t2
)已经锁定了obj
(可能是通过obj.synMethod
,也可能是通过其他被synchronized
修饰的方法obj.otherSynMethod
锁定的obj
), t1
需要等待直到其他线程(t2
)释放obj
, 然后t1
锁定obj
, 执行synMethod
方法. 返回之前之前释放obj
锁。
简单来说就是如果对象中有方法是使用 synchronized
来同步,必须先获得该方法对象的锁,才能调用该方法,否则只能等待锁释放。
synchronized 块
对某一代码块使用 synchronized
后跟括号,括号里是变量,这样,一次只有一个线程进入该代码块。此时线程获得的是成员锁。
synchronized (this)
- 当两个并发线程访问同一个对象
object
中的这个synchronized(this)
同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。 - 当一个线程访问
object
的一个synchronized(this)
同步代码块时,其他线程对object
中所有其它synchronized(this)
同步代码块的访问将被阻塞。 - 然而,当一个线程访问
object
的一个synchronized(this)
同步代码块时,另一个线程仍然可以访问该object 中的除
synchronized(this)` 同步代码块以外的部分。 - 第三个例子同样适用其它同步代码块。也就是说,当一个线程访问
object
的一个synchronized(this)
同步代码块时,它就获得了这个object
的对象锁。结果,其它线程对该object
对象所有同步代码部分的访问都被暂时阻塞。 - 以上规则对其它对象锁同样适用。
第三点举例说明:
public class Thread2 {
public void m4t1() {
synchronized(this) {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
}
public void m4t2() {
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
public static void main(String[] args) {
final Thread2 myt2 = new Thread2();
Thread t1 = new Thread( new Runnable() { public void run() { myt2.m4t1(); } }, "t1" );
Thread t2 = new Thread( new Runnable() { public void run() { myt2.m4t2(); } }, "t2" );
t1.start();
t2.start();
}
}
含有 synchronized
同步块的方法 m4t1
被访问时,线程中 m4t2()
依然可以被访问。
wait() 与notify()/notifyAll()
-
wait():
释放占有的对象锁,线程进入等待池,释放
cpu
,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。而sleep()
不同的是,线程调用此方法后,会休眠一段时间,休眠期间,会暂时释放cpu
,但并不释放对象锁。也就是说,在休眠期间,其他线程依然无法进入此代码内部。休眠结束,线程重新获得cpu
,执行代码。wait()
和sleep()
最大的不同在于wait()
会释放对象锁,而sleep()
不会! -
notify():
该方法会唤醒因为调用对象的wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,并不会立即释放锁,而是继续执行当前代码,直到synchronized中的代码全部执行完毕,才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁,执行代码。需要注意的是,wait()和notify()必须在synchronized代码块中调用。
-
notifyAll():
则是唤醒所有等待的线程。
lock
synchronized 的缺陷
synchronized
是 java
中的一个关键字,也就是说是 Java
语言内置的特性。那么为什么会出现 Lock
呢?
如果一个代码块被 synchronized
修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:
- 获取锁的线程执行完了该代码块,然后线程释放对锁的占有;
- 线程执行发生异常,此时JVM会让线程自动释放锁。
那么如果这个获取锁的线程由于要等待IO
或者其他原因(比如调用sleep
方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,试想一下,这多么影响程序执行效率。
因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过 Lock
就可以办到。
再举个例子:
当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
但是采用 synchronized
关键字来实现同步的话,就会导致一个问题:
-
如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
-
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过
Lock
就可以办到。 -
另外,通过
Lock
可以知道线程有没有成功获取到锁。这个是synchronized
无法办到的。
总结一下,也就是说 Lock
提供了比synchronized
更多的功能。但是要注意以下几点:
Lock
不是Java
语言内置的,synchronized
是Java
语言的关键字,因此是内置特性。Lock
是一个类,通过这个类可以实现同步访问;Lock
和synchronized
有一点非常大的不同,采用synchronized
不需要用户去手动释放锁,当synchronized
方法或者synchronized
代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock
则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
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 lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
trylock使用方法:
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
注意:
当一个线程获取了锁之后,是不会被 interrupt()
方法中断的。因为本身在前面的文章中讲过单独调用interrupt()
方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
而用 synchronized
修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
ReentrantLock
ReentrantLock
,意思是“可重入锁”,是唯一实现了Lock
接口的类,并且ReentrantLock
提供了更多的方法。
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock(); //注意这个地方
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
lock.lock();
try {
System.out.println(thread.getName()+"得到了锁");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"释放了锁");
lock.unlock();
}
}
}
如果锁具备可重入性,则称作为可重入锁。像 synchronized
和 ReentrantLock
都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized
方法时,比如说method1
,而在method1
中会调用另外一个synchronized
方法method2
,此时线程不必重新去申请锁,而是可以直接执行方法method2
。
实例代码:
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述代码中的两个方法 method
1和method2
都用synchronized
修饰了,假如某一时刻,线程A
执行到了method1
,此时线程A
获取了这个对象的锁,而由于method2
也是synchronized
方法,假如synchronized
不具备可重入性,此时线程A
需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A
一直等待永远不会获取到的锁。
而由于 synchronized
和 Lock
都具备可重入性,所以不会发生上述现象。
volatile和synchronized区别
volatile
本质是在告诉jvm
当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized
则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。volatile
仅能使用在变量级别,synchronized
则可以使用在变量、方法。volatile
仅能实现变量的修改可见性,而synchronized
则可以保证变量的修改可见性和原子性。《Java编程思想》上说,定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作)原子性。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
。所以说,在具体使用时要根据适当情况选择。
本文由 zealzhangz 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为:
2020/03/18 22:49