public class SyncTest{ public synchronized void methodA(){ } public void methodB(){ synchronized(this){ } } public void methodC(){ synchronized(xxx.class){ } } public static synchronized methodD(){ } }
前两种锁的是实例对象,即对象锁,后两种锁的是类对象,即类锁(类锁只是个概念,因为本质锁的本质还是对象)
结论: 1.持有的是同一个实例,是能够达到锁住资源,不让别的线程进入的目的的。就算访问的不是相同的代码块,也会等待。两个线程持有的是同一个syncTest对象,一个执行syncTest.methodA(),另一个执行syncTest.methodB(),也会有锁的效果。
2.但是如果大家都是new SyncTest()的话,即使访问同一段代码,是锁不住的!!!!!!!因为大家所持的实例对象不一样!!!!
3.后两种的话,无论是用类直接调用,还是实例调用,无论是否同一个实例,都会互斥。
4.对象锁和类锁,相互不影响。 类锁和对象锁不是同1个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的。
Synchronized与lock的异同? 相同:二者都可以解决线程安全问题 不同:synchronized机制在执行完相应的代码逻辑以后,自动的释放同步监视器 lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())(同时以为着lock的方式更为灵活)
首先两者都保持了并发场景下的原子性和可见性,区别则是synchronized的释放锁机制是交由其自身控制,且互斥性在某些场景下不符合逻辑,无法进行干预,不可人为中断等。 而lock常用的则有ReentrantLock和readwritelock两者,添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
Synchronized 原理
如果对上面的执行结果还有疑问,也先不用急,我们先来了解Synchronized的原理,再回头上面的问题就一目了然了。我们先通过反编译下面的代码来看看Synchronized是如何实现对代码块进行同步的:
package com.paddx.test.concurrent; public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("Method 1 start"); } } }
反编译结果:
关于这两条指令的作用,我们直接参考JVM规范中描述:
monitorenter :
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows: • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor. • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count. • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
这段话的大概意思为:
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
这段话的大概意思为:
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
我们再来看一下同步方法的反编译结果:
源代码:
package com.paddx.test.concurrent; public class SynchronizedMethod { public synchronized void method() { System.out.println("Hello World!"); } }
反编译结果:
从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor 。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
synchronized关键字是不能继承的
继承时子类的覆盖方法必须显示定义成synchronized。
也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法
synchronized 和 Lock 有什么区别?
首先synchronized是java内置关键字,在jvm层面,Lock是个java类; synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁; synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁; 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了; synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可); Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
synchronized 的内存语义:
这个内存语义就可以解决共享变量内存可见性问题。进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。会造成上下文切换的开销,独占锁,降低并发性
打印结果如下:线程C 计算 count = 3线程B 计算 count = 3线程A 计算 count = 2线程D 计算 count = 1线程E 计算 count = 0线程C,B的打印结果都是3,说明C和B同时对count进行了处理,产生了“非线程安全问题”。而我们想要的得到的打印结果却不是重复的,而是依次递减的。
在某些JVM中,i--的操作要分成如下3步:
-
取得原有变量的值。
-
计算i-1。
-
对i进行赋值。
在这三个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。
解决方法就是使用 synchronized 同步关键字 使各个线程排队执行run()方法。修改后的run()方法:
打印结果:线程B 计算 count = 4线程C 计算 count = 3线程A 计算 count = 2线程E 计算 count = 1线程D 计算 count = 0
把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility)。
原子性 意味着个时刻,只有一个线程能够执行一段代码,这段代码通过一个monitor object保护。从而防止多个线程在更新共享状态时相互冲突。 可见性 则更为微妙,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。