前面主要讲了基于简单的基本类型的原子操作,对于一些复杂的操作就需要用锁来实现互斥,在JUC中定义了一套锁的实现框架,但是在学习前,感觉有必要先回顾了java中自带的锁synchronized。
介绍
synchronized的作用如下
-
确保线程互斥的访问同步代码(互斥性)。
-
保证共享变量的修改能够及时可见(可见性)。
-
有效解决重排序问题(有序性)。
在这里主要讲解下Synchronized在作用1的实现原理以及使用的方式,对于Synchronized主要有三个使用场景:
-
修饰普通方法
-
修饰静态方法
-
修饰代码块
举个栗子:
public class Counter { private static int count; private Object lock = new Object(); public synchronized void thisIncr1() { count ++; } public static synchronized void classIncr1() { count++; } public void lockIncr() { synchronized(lock) { count++; } } public void thisIncr2(){ synchronized(this) { count ++; } } public synchronized int getCount() { return count; } public void classIncr2() { synchronized(Counter.class) { count++; } }}复制代码
如上thisIncr1中修饰的普通方法,classIncr1中修饰的静态方法,thisIncr2和classIncr2修饰的是代码块,上面的同步逻辑有什么区别呢?
synchronized保护的是对象而非代码,只要是访问同一个对象的synchronized方法,即使是不同的代码,也会被同步顺序访问,在上面的代码中thisIncr1和thisIncr2都是同步的this对象。classIncr1和classIncr2同步的Counter.class对象。lockIncr同步的是lock对象。从上面可以确定thisIncr*、classIncr*、lockIncr相互之间是没有互斥同步关系的,只有在他们本身类型之间才会同步执行, 具体可以自行写测试用例测试下。
synchronized的实现原理
举个栗子:
public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("hello word"); } }}复制代码
通过javap反编译后为:
通过反编译后的代码,可以看到在synchronized开始和结束的时候分别执行了monitorenter和monitorexit,在java中每个对象有一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
-
如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
-
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。
-
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
从上面分析可以看出,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
synchronized的属性
-
可重入性
对同一个执行线程,它在获得了锁之后,在调用其他需要同样锁的代码时,可以直接调用,比如说,在一个synchronized实例方法内,可以直接调用其他synchronized实例方法。可重入是一个非常自然的属性,应该是很容易理解的,之所以强调,是因为并不是所有锁都是可重入的。 -
内存可见性
线程在释放锁时,所有写入都会写回内存,而获得锁后,都会从内存中读最新数据。如果仅仅是为了数据的可见性可以用volitile来代替,成本会小很多。 -
死锁
使用synchronized或者其他锁,要注意死锁,所谓死锁就是类似这种现象,比如, 有a, b两个线程,a持有锁A,在等待锁B,而b持有锁B,在等待锁A,a,b陷入了互相等待,最后谁都执行不下去,解决的办法就是首先,应该尽量避免在持有一个锁的同时去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同的顺序去申请锁,顺便说一下,如果程序出现了死锁可以通过pstack命令来查看进程的状态来发现死锁。