Synchronized关键字
思考:不使用并发手段会有什么后果?
一、一句话总结其作用:
能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。
二、Synchronized 的两个用法
-
对象锁
1.1 代码块形式
栗子:
public class SynchronizedObjectCodeBlock2 implements Runnable{
static SynchronizedObjectCodeBlock2 instance = new SynchronizedObjectCodeBlock2();
Object lock1 = new Object();//自己设置对象,自己设置锁,等同于this
Object lock2 = new Object();
@Override
public void run() {
synchronized (lock1){//synchronized (this)
System.out.println("我是lock1。我叫"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"lock1部分运行结束。");
}
synchronized (lock2){//synchronized (this)
System.out.println("我是lock2。我叫"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"lock2部分运行结束。");
}
}
public static void main(String[] args){
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()){
}
System.out.println("finished!");
}
1.2 方法锁示例:
栗子:
public class SynchronizedObjectMethod3 implements Runnable{
static SynchronizedObjectMethod3 instance = new SynchronizedObjectMethod3();
@Override
public void run() {
method();
}
public synchronized void method(){
System.out.println("我是方法锁形式。我叫"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"运行结束。");
}
public static void main(String[] args){
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()){
}
System.out.println("finished!");
}
}
-
类锁
2.1 概念(重要):Java类可能有很多对象,但只有一个Class对象
本质:所谓类锁,不过是Class 对象的锁而已。
用法和效果:类锁只能在同一个时刻被一个对象拥有
2.2 形式1:synchronized加在static 方法上
**
* 类锁的第一种形式,static形式
*/
public class SynchronizedClassStatic4 implements Runnable{
static SynchronizedClassStatic4 instance1 = new SynchronizedClassStatic4();
static SynchronizedClassStatic4 instance2 = new SynchronizedClassStatic4();
public void run(){
method();
}
public static synchronized void method(){
//不加static 关键字 时,2个线程几乎同时运行,加了static 第一个线程运行结束后,第二个才运行
System.out.println("我是类锁的第一种形式。我叫"+Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"运行结束。");
}
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()){
}
System.out.println("finished!");
}
}
加了static 关键字就是方法:
不加static 关键字:
形式2:
public class SynchronizedClassClass5 implements Runnable{
static SynchronizedClassClass5 instance1 = new SynchronizedClassClass5();
static SynchronizedClassClass5 instance2 = new SynchronizedClassClass5();
public void run(){
try {
method();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void method() throws InterruptedException{
//无论哪个对象,应用的都是同一个SynchronizedClassClass5.class实例
synchronized (SynchronizedClassClass5.class){
System.out.println("我是类锁的第二种形式(*.class)。我叫"+Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName()+"运行结束!");
}
}
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()){
}
System.out.println("finished!");
}
}
三、七种常见情况:
-
两个线程同时访问一个对象的同步方法
-
两个线程访问的是两个对象(指两个实例)的同步方法
他们之间不受干扰,因为采用的锁不是同一个 -
两个线程访问的是synchronized 的静态方法
一个一个执行,锁生效 -
同时访问同步和非同步方法
非同步方法不受到影响 -
访问同一个对象的不同的普通同步方法
串行的,不能同步运行 -
同时访问静态synchronized 和非静态synchronized方法
可以并行执行 -
方法抛异常后,会释放锁
三点核心思想:
1.一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应1,5情况)
2.每个实例都对应有自己的一把锁,不同实例之间互不影响;例外锁对象时*.class以及synchronized修饰的是static方法的时候,所有的对象共用一把类锁(对应2,3,4,6情况)
3.无论是方法正常执行完毕,或者方法抛出异常,都会释放锁(对应7情况)
四、Synchronized的性质:
-
可重入(递归锁)
定义:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁。
好处:避免死锁、提升封装性
粒度(范围):线程而非调用- 情况一:证明同一个方法是可重入
- 情况二:证明可重入不要求是同一个方法
- 情况三:证明可重入不要求是同一个类中的
- 不可中断
一旦这个锁已经被别人获得,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁。如果别人永远不释放这个锁,那么我只能永远地等下去。
而Lock类: a.可以中断别的线程,b.不想等待可以退出
五、原理:
-
加锁和释放锁的原理:现象、获取和释放锁的时机
现象:
获取和释放锁的时机:内置锁。深入JVM看字节码,锁信息都是保存在对象头中的
synchronized字段:是通过一个对象头来实现的。
当线程执行到moniterenter,就会尝试获取该对象的锁,而moniterexit是释放锁的
/**
* 反编译字节码
* 打开终端-》进入到类路径--》编译该类==》输入 javap -verbose "类.class"文件
*/
public class DeCompilation14 {
private Object object = new Object();
public void insert (Thread thread){
synchronized (object){
}
}
}
上面方法反编译后:
解读moniterenter指令:加锁,会在执行的时候让对象的锁计数器加1
每一对象都与moniterenter有关
解读moniterexit指令:会在执行的时候让对象的锁计数器减1
释放锁的所有权
-
可重入原理:加锁次数计数器(可加可减的计数器)
2.1 JVM负责跟踪对象被加锁的次数
2.2 线程第一次给对象加锁的时候,计数变为1.。每当这个相同的线程在此对象上再次获得锁时,计数会递增
2.3 当任务离开时,计数递减,当计数为0时,锁完全被释放 -
可见性原理:Java内存模型
每个线程都有独立的本地内存副本,每当副本改变,都会同步到主内存中。其他线程也会去刷新本地内存的副本,通过这个方法,来实现线程之间同步。
六、缺陷:
- 效率低(Lock效率更高):锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正试图获得锁的线程。
- 不够灵活(读写锁更灵活):加锁和释放锁的时机单一
- 无法知道是否成功获取到锁
七、常见面试问题:
- 使用synchronized对象注意点:锁对象不能为空、作用域不易过大、避免死锁
- 如何选择Lock和synchronized关键字?核心思想:避免出错
尽量是用包装的类,不易出错。如果非要选择的话,优先选择synchronized,可以减少代码编写,非要使用Lock的话,才使用Lock接口 - 多线程访问同步方法的各种具体情况
八、总结:
一句话介绍synchronized:
JVM会自动通过使用moniter来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质。