引入
线程安全问题
在java的多线程的编程中,经常出现线程安全的问题,主要就在于正确性:即多线程对临界资源进行访问时,如果不进行控制,每个线程拿到的值都不能确定是有效的,依赖这个值进行的判断或者计算也就可能会造成错误。
临界资源
指会被多个并发线程/进程进行竞争性访问的共享的、可变的资源【个人的理解】
实现线程安全的思路及手段
引发线程安全的一个例子
1.png线程X在访问后线程Y对临界资源进行了修改。随后线程X用计算值覆盖A时,由于读取的A已经是错误的了,计算结果也就是错的。由此引发了错误。
解决的思路
解决问题的关键就是在线程的角度来说,线程拿到临界资源访问权限后必须保证在自己完成自己的相关操作前,临界资源的值是有效的。也就是说,此线程的操作不允许进行打断和其他操作的插入。需要采用同步机制来协同对临界资源的访问。即在同一时刻,只能有一个线程访问临界资源,也称作 同步互斥访问。从线程的角度来说,是将线程的相关操处理成原子的操作。
实现这个效果有两个方法synchronized
,lock
。本文章是lock
源码的引入文章,主要介绍synchronized
以及synchronized
和lock
实现原理的区别。
synchronized
用法
synchronized
是互斥锁,即 能到达到互斥访问目的的锁。举个简单的例子,如果对临界资源加上互斥锁,当一个线程在访问该临界资源时,其他线程便只能等待。
synchronized
可以加在方法或者代码块上,这样在线程执行此段代码时必须要获得该锁。
代码示例:
class A{
public synchronized void hehe(){
xxxxxxxxx
}
public void haha(){
synchronized(xx){
xxxxxxxxx
}
}
}
大概就是这么用的。下面将进行介绍
注意点
synchronized
针对的是线程
介绍
synchronized
针对的是访问对象的线程,即当一个线程获得这个对象的互斥锁时,其他的线程无法访问此对象。
示例
class B {
public synchronzed void methodA(){
return null;
}
public synchronized void methodB(){
this.methodA();
}
public static void main(String[] args){
new A().methodB();
}
}
此示例是可以正确执行的。执行的有以下特点:
-
当主线程在执行
methodB()
时,其他的线程无法执行methodB()
,methodA()
。即只能同时有一个线程获得此对象的互斥锁 -
主线程获得互斥锁,在执行
this.methodA();
时是可以执行的,因为此线程已经获得了锁,从代码上看,获得锁的线程可以再次获得同一个锁,即synchronized
锁是可重入的 -
在方法上用
synchronized
,默认用的是此对象的锁。如果是用代码块的话,就得指出用的哪个对象的锁。
synchronized
的获得和释放
锁的获得
进入synchronized
标注的方法或者代码块即认为获得锁
锁的释放
情况一、正常执行
执行完代码块或者synchronized
即认为释放了锁,要么是顺序执行执行完了,要么是抛了异常直接从synchronized
代码块出去了。
情况二、wait()
,notify()
,notifyAll()
wait()
前置条件:线程获得了锁
行为:当前线程挂起等待并释放锁
后置条件:此线程保持挂起状态,等待设置的等待时间到达或者被
notify()
,notify
唤醒后重新进入到此锁的竞争中
notify()
- 前置条件:线程获得了锁
- 行为:唤醒一个因为
wait()
方法而挂起的线程并使其参与到锁的竞争中。本线程不必释放锁- 后置条件:本线程正常占用锁并执行。
wait()
挂起的线程被唤醒一个
notifyAll()
和notify()
一致,只是这个是唤醒所有的wait()
方法挂起的线程。
注意
notify()
,notifyAll()
只是唤醒,真的能不能获得锁要看线程的调度或者看竞争能不能竞争的上。
synchronized
锁定的是对象
对方法使用
如上的class B
代码。直接在实例方法前加synchronized
关键词,表示要执行此方法的线程必须先获得此对象的锁。
对代码块使用
如上的class A
代码。自己写一个synchronized
块即可。
代码示例一
借用大佬博客的一段代码:
class Sync {
public synchronized void test() {
System.out.println("test开始..");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test结束..");
}
}
class MyThread extends Thread {
public void run() {
Sync sync = new Sync();
sync.test();
}
}
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
Thread thread = new MyThread();
thread.start();
}
}
}
执行结果是:
test开始..
test开始..
test开始..
test结束..
test结束..
test结束..
不是我们期待的,原因是synchronized
锁定的是对象,三个线程的run()
中各自new
了三个Sync
对象,各用各的锁,所以没有互相阻塞。
将代码修改:
public void test() {
synchronized(this){
System.out.println("test开始..");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test结束..");
}
}
情况不便,还是用自己的锁,三个对象三个锁。
继续修改:
class MyThread extends Thread {
private Sync sync;
public MyThread(Sync sync) {
this.sync = sync;
}
public void run() {
sync.test();
}
}
public class Main {
public static void main(String[] args) {
Sync sync = new Sync();
for (int i = 0; i < 3; i++) {
Thread thread = new MyThread(sync);
thread.start();
}
}
}
一个对象一个锁,三个线程竞争,所以输出如下:
test开始..
test结束..
test开始..
test结束..
test开始..
test结束..
继续修改,静态的锁:
class Sync {
public void test() {
synchronized (Sync.class) {
System.out.println("test开始..");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test结束..");
}
}
}
锁住的是静态的Sync.class
,synchronized(xx.class)
和static synchronized
一样。一个xx.class
就一个锁,所以他们也相当于全局锁。
注意:锁的使用重点是是不是同一个锁以方便同步,具体是谁的锁,叫啥名无所谓,上面那个例子你用synchronized(Object.class)
也行,只是可能会顺手阻塞一大把无关线程。
比较常见的例子是锁一个字符串,如:
synchronized("哈哈哈"){
}
会将字符串常量池锁住,一锁锁一片,会严重影响程序的执行效率。
代码示例二
class A{
}
class C implements Runnable{
private A fieldA;
C(A input){
this.fieldA = input;
}
public void run(){
synchronized(fieldA){
//哈哈哈
}
}
public static void main(String[] args){
A a = new A();
Thread thread1 = new Thread(new C(a));
Thread thread2 = new Thread(new C(a));
Thread thread3 = new Thread(new C(new A()));
thread1.start();
thread2.start();
thread3.start();
}
}
注意:synchronized
锁住的是对象而不是对象的引用。其中thread1
,thread2
用的都是对象a
的锁,会互斥,即使都是new
出来的不同的C
实例的field
。最后一个是新的A
不是同一个锁,不会互斥。
synchronized
不锁定对象的所有东西
介绍
synchronized
只是一个用来标出代码块的锁,即使是同一个对象,如果有的方法你没标synchronized
,也是可以多线程同时访问的。
实例
class C{
public synchronized void method1(){
}
public void method2(){
}
}
两个Thread
分别访问method1()
,method2()
不会引起阻塞。
静态的synchronized
介绍
synchronized
锁住的是对象实例或者类。多个实例,他们的锁之间不会影响,因为每个实例都有自己的锁。类锁之间会影响,因为同一个类只有一个类锁。
实例
上面“针对线程”的那一节有介绍。
底层实现方法
synchronized
是java的关键字,在JVM层面实现了对临界资源的同步互斥访问,在编译时会在代码块的开头加 monitorenter
在代码块结尾加**monitorexit
**。
引用大佬博客的一段话:
monitorenter
指令执行时会让对象的锁计数加1,而monitorexit
指令执行时会让对象的锁计数减1,其实这个与操作系统里面的PV操作很像,操作系统里面的PV操作就是用来控制多个进程对临界资源的访问。对于synchronized
方法,执行中的线程识别该方法的method_info
结构是否有ACC_SYNCHRONIZED
标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。有一点要注意:对于
synchronized
方法 或者synchronized
代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。
lock
引入lock
的原因——lock
的优点
粒度更细
我们上面介绍了synchronized
,他在jvm层面实现了对临界资源的互斥访问,但是很多时候我们为了性能的需要想更好的定制互斥的粒度,比如对磁盘文件的读写操作,不同线程的读操作之间就没有必要做的互斥,写操作和写操作、读操作和写操作互斥即可。
可定制等待锁的时间
在synchronized
的块中可以通过wait()
释放锁并挂起,但是很多时候等待IO时或者调用sleep()
时,程序会睡眠等待,白白占用锁,导致很多线程一直等待。lock
提供了可以限制锁的等待时间的操作,超时则直接不再等待;lock
还增加了 新的解决方案,可以相应中断并释放锁。
可控制获得和释放顺序
synchronized
的锁的获得和释放是通过进入、退出代码块来实现的。这就意味着互斥锁的获得释放遵循栈的顺序。如果我想实现下面锁的获得、释放顺序呢?
- 获得锁A
- 获得锁B
- 释放锁A
- 获得锁C
- 释放锁B
- 释放锁C
虽然这样的操作是不太好的,这种锁的顺序容易引起死锁。但是。。。。。保不准就有这样的需求是吧。
监听锁的状态
synchronized
把代码块框起来之后要不线程是卡在代码块这里,要不就是在执行,无法获得锁的状态。而lock
提供了可以查看是否能获得锁的操作。
注意事项——lock
的缺点
锁不会主动释放
synchronized
锁只要你退出代码块就会退出锁,不管是执行完还是异常抛出去,都能够及时的将锁释放。lock
锁在遇到异常时不会结束锁,必须手动释放。如果没有做释放就容易一直占着锁。【lock
锁不限制时间、不设置可打断的情况下】
用法
lock()
阻塞方法,尝试获得锁,获得不到就等着获得锁。
如果采用lock()
,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用lock
必须在try…catch…
块中进行,并且将释放锁的操作放在finally
块中进行,以保证锁一定被被释放。
Lock lock = ...;
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
tryLock()
非阻塞方法,直接返回,获得锁返回true
,锁已被占用则返回false
。
和lock()
一样,一定一定要记得释放锁
tryLock(long time, TimeUnit unit)
阻塞方法,获得锁,如果锁被占用则等待传入的时间,还获得不到则返回false
。可以响应中断。如果在传入的时间内得到锁则返回true
。
lockInterruptibly()
阻塞方法,等待获得锁。等待锁的过程中可以相应中断。
如果B线程在等待锁,其他的线程调用了ThreadB.interrupt()
会造成B线程在lockInterruptibly()
处抛出InterruptedException
并停止等待。
注意:当一个线程获得锁之后,是不会被interrupt()
中断的。
其他
引用博主的一段话:
最佳实践 (Best Practice):在使用Lock时,无论以哪种方式获取锁,习惯上最好一律将获取锁的代码放到
try…catch…
,因为我们一般将锁的unlock操作放到finally子句中,如果线程没有获取到锁,在执行finally
子句时,就会执行unlock操作,从而抛出IllegalMonitorStateException
,因为该线程并未获得到锁却执行了解锁操作。
参考文献
synchronized:
- http://www.cnblogs.com/LeeScofiled/p/7225562.html
- https://leokongwq.github.io/2017/02/24/java-why-wait-notify-called-in-synchronized-block.html
- https://javarevisited.blogspot.com/2014/07/top-50-java-multithreading-interview-questions-answers.html
- https://javarevisited.blogspot.com/2011/05/wait-notify-and-notifyall-in-java.html
- https://javarevisited.blogspot.com/2012/02/what-is-race-condition-in.html
- https://www.cnblogs.com/adamzuocy/archive/2010/03/08/1680851.html
- https://blog.csdn.net/xiao__gui/article/details/8188833
- https://blog.csdn.net/justloveyou_/article/details/54381099
- http://jasshine.iteye.com/blog/1617813
lock:
网友评论