synchronize是平时用的比较多的多线程问题的解决方案,一般说存在多线程问题,加个锁,就用synchronize吧,简单,方便。用lock你还要自己去lock,unlock,没经验的人使用起来没那么溜。今天,就来看看synchronize背后的一些详细知识点,尽量做到广而深。这样的学习才有意思。
对于synchronize的学习,我是准备先实践,再分析,再总结。
情况1:同一个对象在两个线程中分别访问该对象的两个同步方法
结果:会产生互斥。
解释:因为锁针对的是对象,当对象调用一个synchronized方法时,其他同步方法需要等待其执行结束并释放锁后才能执行。
情况2:不同对象在两个线程中调用同一个同步方法
结果:不会产生互斥。
解释:因为是两个对象,锁针对的是对象,并不是方法,所以可以并发执行,不会互斥。形象的来说就是因为我们每个线程在调用方法的时候都是new 一个对象,那么就会出现两个空间,两把钥匙,
2.Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。
情况1:用类直接在两个线程中调用两个不同的同步方法
结果:会产生互斥。
解释:因为对静态对象加锁实际上对类(.class)加锁,类对象只有一个,可以理解为任何时候都只有一个空间,里面有N个房间,一把锁,因此房间(同步方法)之间一定是互斥的。
注:上述情况和用单例模式声明一个对象来调用非静态方法的情况是一样的,因为永远就只有这一个对象。所以访问同步方法之间一定是互斥的。
情况2:用一个类的静态对象在两个线程中调用静态方法或非静态方法
结果:会产生互斥。
解释:因为是一个对象调用,同上。
情况3:一个对象在两个线程中分别调用一个静态同步方法和一个非静态同步方法
结果:不会产生互斥。
解释:因为虽然是一个对象调用,但是两个方法的锁类型不同,调用的静态方法实际上是类对象在调用,即这两个方法产生的并不是同一个对象锁,因此不会互斥,会并发执行。
1.实践一,synchronize使用在一般方法内。
/**
* @author: wayne
* @desc:
* @date: 2018/1/15 13:52
* @version: 1.0
*/
public class User {
public synchronized void test1(){
try {
System.out.println("test1 start");
Thread.sleep(3000);
}catch (Exception e){
}
System.out.println("test1 end");
}
public synchronized void test2(){
try {
System.out.println("test2 start");
Thread.sleep(3000);
}catch (Exception e){
}
System.out.println("test2 end");
}
public static void main(String[] args) {
final User user = new User();
new Thread(new Runnable() {
@Override
public void run() {
user.test1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
user.test2();
}
}).start();
}
}
test2 start
test2 end
test1 start
test1 end
对于同一个对象user,可以看见对于普通方法的时,每一次只能执行一个方法,类似于串行执行的效果。
2.实践二,synchronize使用在类方法内。
public class Test {
public synchronized static void test1(){
System.out.println("test1 in");
try{
System.out.println("test1 start");
Thread.sleep(3000);
}catch (Exception e){
}
System.out.println("test1 end");
}
public synchronized static void test2(){
System.out.println("test2 in");
try{
System.out.println("test2 start");
Thread.sleep(3000);
}catch (Exception e){
}
System.out.println("test2 end");
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
Test.test1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Test.test2();
}
}).start();
}
}
test1 in
test1 start
test1 end
test2 in
test2 start
test2 end
可以看见对于类方法,依然是起到了锁的作用,但是要注意,这边使用的仍然是Test这个Class,可以知道此时的锁加在了Class上了。
3.实践三,synchronize混合使用与类方法和普通方法。
public class Test {
public synchronized static void test1(){
System.out.println("test1 in");
try{
System.out.println("test1 start");
Thread.sleep(3000);
}catch (Exception e){
}
System.out.println("test1 end");
}
public synchronized void test2(){
System.out.println("test2 in");
try{
System.out.println("test2 start");
Thread.sleep(3000);
}catch (Exception e){
}
System.out.println("test2 end");
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
Test.test1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
new Test().test2();
}
}).start();
}
}
test1 in
test1 start
test2 in
test2 start
test1 end
test2 end
这边可以看出运行结果的不同了,发现两个方法并没有被锁住,而是同时执行了。这是为什么呢???这边先简单解释下吧。
这是因为static test1方法是类方法,也就是它属于的是Class,而不是属于这个Class产生的具体的对象.
而test2方法是由Class产生出的具体对象才能调用的,因此它属于的是Class产生的对象,而不是Class本身。
这是两个概念,所以简单来说就是这两种情况锁的条件不一样,因此产生了锁不住的效果。。(具体原因需要分析源码或者底层原理才能给出更规范的解释)。
4.实践四,synchronize修饰普通方法时重入的效果。
public class Test {
public synchronized void test1(){
System.out.println("test1 in");
try{
System.out.println("test1 start");
test2();
}catch (Exception e){
}
System.out.println("test1 end");
}
public synchronized void test2(){
System.out.println("test2 in");
try{
System.out.println("test2 start");
}catch (Exception e){
}
System.out.println("test2 end");
}
public static void main(String[] args) throws Exception{
final Test t1 = new Test();
new Thread(new Runnable() {
@Override
public void run() {
t1.test1();
}
}).start();
}
}
test1 in
test1 start
test2 in
test2 start
test2 end
test1 end
此处可以发现synchronize是一个可重入锁,相信对于可重入的概念,c.u.t包下的ReentrantLock不陌生的同学是明白的。此处,如果synchronize不是可重入的话,那么再调用test2()的方法时应该会出现死锁。但是没有,说明是支持的,同样的,详细的原因需要分析底层原理,此处只是再通过例子来分析它的特性。
5.实践五,synchronize修饰普通方法时不同对象的执行的效果。
public class Test {
public synchronized void test1(){
System.out.println("test1 in");
try{
System.out.println("test1 start");
Thread.sleep(3000);
test2();
}catch (Exception e){
}
System.out.println("test1 end");
}
public synchronized void test2(){
System.out.println("test2 in");
try{
System.out.println("test2 start");
Thread.sleep(3000);
}catch (Exception e){
}
System.out.println("test2 end");
}
public static void main(String[] args) throws Exception{
final Test t1 = new Test();
final Test t2 = new Test();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程:"+Thread.currentThread().getName());
t1.test1();
}
},"t1").start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程:"+Thread.currentThread().getName());
t2.test1();
}
},"t2").start();
}
}
线程:t1
test1 in
test1 start
线程:t2
test1 in
test1 start
test2 in
test2 start
test2 in
test2 start
test2 end
test1 end
test2 end
test1 end
对于不同的对象,没有锁住,对于拿到t1对象的线程,它能够执行t1对象的同步方法,但是对于t2来说,t1的锁对他毫无用处,它依然能执行自己的同步方法。测试1就是对于同一个对象user,它在不同的线程中需要顺序执行,但是测试5却证明,不同的对象之间互相不影响。
探究真理之路:
我称之为探究真理之路,是因为上面的结论都是我们做测试时发现并总结的,然而对于一个程序员来说,光总结可不行,我们要探究为什么会出现这种现象,是什么根本的东西在。
我谈谈我的第一个反应是去看什么吧,之前关于这个原理性的东西我都会准备一些材料,一个是反汇编,一个是JVM虚拟机规范,一个是JVM相关的知识。差不多万变不离其中。
探究之路一反汇编:
public class Test {
public void test1(){
synchronized (this) {
System.out.println("this is add synchronize");
}
}
public static void main(String[] args) {
System.out.println("main out");
}
}
反汇编效果一目了然,存在了两个很特殊的字符。monitorenter和monitorexit,这两个名词需要去JVM虚拟机规范里去找解释。我读的是中文版的,没有下载英文版
monitorenter:
进入一个对象的monitor(此处后续再看什么是对象的monitor)。任何对象都有一个monotor与之关联,当且且当一个monitor被持有后,它就会处于锁定状态,线程执行到monitorenter指令后,将会尝试获取对象所对应的monitor的所有权。过程如下:
1.如果monitor的计数器为0,则线程进入成功,将计数器值设置为1,就意味着当前线程是monitor的所有者。
2.如果线程已经占有了当前monitor,则直接进入,将计数器加1.
3.如果monitor已经被其他线程占有,那么当前线程被阻塞,直到monitor的计数器变成0,重新尝试获取monitor的所有权。
monitorexit:
退出一个对象的monitor。执行monitorexit指令的线程必须是对象对应的monitor的所有者,执行时,线程把monitor的计数器值减1,如果减1后计数器值为0,那线程退出monitor,不再是这个monitor的拥有者,其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
小结一下:其实synchronize的底层语义就是使用monitor对象来实现的。
探究之路二monitor对象是什么:
上面通过反汇编,发现了底层的语义是使用的monitor对象,那么monitor对象是个什么呢?
monitor是一种同步工具,类似于信号量的作用。
java会为每个object对象分配一个monitor对象,当某个对象的同步方法被多线程占用时,对象的monitor将负责处理并发独占要求。因此java的monitor机制的本质在于:任何时候,dui对一个指定object的同步方法只能由一个线程调用。
探究之路三monitor对象的组成是什么样的:
参考文章:monitor对象