【深入理解Java并发】Sysnchronized关键字用法、原理及常见面试考点
一、Sysnchronized简介
Sysnchronized:能够保证在同一时刻最多只有一个线程执行该段代码
创建线程的两种方法:
- 继承Thread类
- 实现Runnable接口
thread1.join()方法:等待线程1执行完后才接着执行
二、Sysnchronized两种用法(对象锁和类锁)
- 对象锁
包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)
第一种:同步代码块锁(自己指定锁对象)
public class SynchronizedObject implements Runnable { static SynchronizedObject instance = new SynchronizedObject(); @Override public void run() { synchronized (this) { 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()) { } } }
代码结果:Thread1 Thread2串行执行,此时用的锁是this
现在设置两把不同的锁lock1和lock2,分别有两段不同的同步代码块
static SynchronizedObject instance = new SynchronizedObject(); static Object lock1 = new Object(); static Object lock2 = new Object(); @Override public void run() { synchronized (lock1) { 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) { System.out.println("我是lock2。我叫" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "lock2运行结束"); }
运行结果:首先,线程0获取lock1锁并执行代码块;3s后线程0lock1执行结束。然后Thread0 和Thread1并行的获取Lock2、Lock1;Thread0lock2部分和Thread1lock1部分同时结束;Thread1获取lock2锁;3s后Thread1lock2执行结束。
第二种:方法锁形式synchronized修饰普通方法,锁对象默认为this
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()+ "运行结束"); }
- 类锁
指Sysnchronized修饰静态的方法或指定锁为CLASS对象
类锁:Java类可能有很多个对象,但只有1个Class对象
形式1:synchronized加在static方法上
static SynchronizedObject instance1 = new SynchronizedObject(); static SynchronizedObject instance2 = new SynchronizedObject(); @Override public void run() { method(); } public static synchronized void method() { System.out.println("我是类锁的第一种形式,我叫" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+ "运行结束"); }
形式2: synchronized(*.class)代码块
public void method() { synchronized (SynchronizedObject.class) { System.out.println("我是类锁的第二种形式,我叫" + Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "运行结束"); } }
多线程访问同步方法的7种情况(面试常考)
- 两个线程同时访问一个对象的同步方法
- 两个线程访问的是两个对象的同步方法
- 两个线程访问的是synchronized的静态方法
- 同时访问同步方法与非同步方法
- 访问同一个对象的不同的普通同步方法
- 同时访问静态synchoronized和非静态synchronized方法
- 方法抛异常后,会释放锁
- 两个线程串行执行
- 两个对象的同步方法各自有一把锁,互不干扰,结果是两个线程并行执行
- synchronized修饰的静态方法加的是类锁,所有对象共用一把锁,所以两个线程串行执行
- 同步方法的实例和非同步方法的实例之间互不影响,所以两个线程并行执行
- 访问同一个对象的不同普通同步方法,一把锁只能同时被一个线程获取,所以是串行执行。
- 静态synchoronized修饰的方法加的是类锁,非静态synchronized加的是对象锁,实例彼此不受影响,所以并行执行
- 使用throw new RuntimeException()异常后,JVM会自动释放锁。(lock方法需要在finally后释放)
4点核心思想:
- 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1、5种情况)
- 每个实例都对应有自己的一把锁,不同实例之间互不影响;例外:锁对象是*,class以及synchronized修饰的是static方法时,所有对象共用同一把类锁(对应2、3、4、6情况)
- 无论是方法正常执行完毕或者方法抛出异常,都会释放锁(对应第7种情况)
- 如果存在一种情况 在被synchronized修饰的方法内调用一个非同步方法,
那么调用这个非同步方法不是线程安全的
三、 Synchronized的性质
- 可重入
可重入指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁。
(我已经获得这把锁,现在再次请求这把锁而无需释放当前锁就叫做可重入)
Java中可重入锁有:Synchronized和ReentrantLock
好处:避免死锁、提升封装性
粒度:线程而非调用(用3种情况来说明和pthread的区别)——同一线程层面
- 情况1:证明同一个方法是可重入的
- 情况2:证明可重入不要求是同一个方法
- 情况3:证明可重入不要求是同一个类中的
- 不可中断
一旦这个锁被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁。
如果别人永远不释放锁那么只能永远等下去。(Lock类拥有中断或退出的能力)
四、深入原理
- 加锁和释放锁的原理:现象、时机、深入JVM看字节码
现象
每一个类的实例对应着一把锁,而每一个synchronized方法都必须首先获得调用该方法的类的实例的 锁,才能执行。否则线程就会阻塞,而方法一旦执行,就会独占这把锁,直到该方法返回,或者抛出异常,才将锁释放。释放之后,其他被阻塞的线程才能获得这把锁,重新进入可执行的状态。
获取和释放锁的时机:内置锁
获取这个锁的时机就是进入这个锁保护的同步代码块或方法中,退出或抛出异常会释放这个锁。
等价代码
public synchronized void method1() { System.out.println("我是Synchronized形式的锁"); } public void method2() { lock.lock(); try{ System.out.println("我是Lock形式的锁"); }finally { lock.unlock(); } }
深入JVM看字节码
synchronize用的锁是java对象头里的一个字段(每一个对象都有一个对象头,对象头可以存储很多信息,其中有一部分就是用来存储synchronize关键字的锁)
获取锁和释放锁是基于monitor对象来实现同步方法和同步代码块的,Monitor对象主要是两个指令,一个是Monitorenter(插入到同步代码块开始的位置),Monitorexit(退出,插入到方法结束和退出时候)。JVM规范要求一个enter对应一个或多个exit。每一个对象都有一个monitor和他关联,并且monitor被持有后,就会处于锁定状态,当线程执行到Monitorenter指令时,会尝试获取这个对象对应的monitor的所有权,也是尝试获取对象的锁。
原理:Monitorenter和Monitorexit在执行的时候会使对象的锁计数加1或者减1,和操作系统中的PV操作(多线程对临界资源的访问)很像,每一个对象都和一个Monditor相关联。
释放的过程就是将Monditor的计数器减1,减完之后变成0就意味着当前线程不在拥有对Monditor的所有权,就是解锁,如果减完之后不是0,意味着刚才是可重入进来的,所以还继续持有这把锁,最终减到0之后,不仅意味着释放锁了,还意味着刚才被阻塞的线程,会再次尝试获取对该把锁的所有权。
- 可重入原理:加锁次数计数器
JVM负责跟踪对象被加锁的次数;
有个monitor计数器,线程第一次给对象加锁的时候,计数变为1.每当这个相同的线程在此对象上再次获得锁时,计数会递增;
任务结束离开,则会执行monitorexit,计数递减,当计数为0时锁完全被释放; - 可见性原理:Java内存模型
被synchronized修饰后,被锁住的对象所做的任何操作,都要在释放锁之前,从线程内存写回到主内存中(不会存在线程内存和主内存不一致的情况)。同样,在进入代码块得到锁之后,被锁定对象的数据也是直接从主内存中读取出来。
五、缺陷
- 效率低:锁的释放情况少,试图获得锁时不能设定超时,不能中断一个正在试图获得锁的线程。
- 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
- 无法知道是否成功获取到锁
对比Lock接口的方法:
- lock();//获取锁
- unlock();//释放锁
- tryLock();//判断锁是否可用。返回值为:boolean;
- tryLock(time,TimeUnit);//在规定的时间内,如果未获得锁,则就放弃。
六、常见面试问题
- 使用注意点:锁对象不能为空(锁的信息保存在对象头中)、作用域不宜过大(大部分代码串行,影响效率)、避免死锁
- 如何选择Lock和synchronized关键字? 建议都不使用,可以使用java.util.concurrent包中的Automic类、countDown等类
- 优先使用现成工具,如果没有就优先使用synchronized关键字,好处是写劲量少的代码就能实现功能。如果 需要灵活的加解锁机制,则使用Lock接口
- 多线程访问同步方法的各种情况
七、思考题
- 多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的是哪个线程?
- synchronized使得同时只有一个线程可以执行,性能较差,有什么办法可以提升性能?
答:优化synchronized的使用范围、使用其他类型lock(读写锁等) - 如何更灵活地控制锁的获取和释放(释放锁的时机被规定死了怎么办)
- 什么是锁的升级、降级?什么是JVM里的偏斜锁、轻量级锁、重量级锁?
八、总结
一句话介绍synchronized:
JVM会自动通过使用monitor来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质。
【最后欢迎大家来我的博客skiron.xyz来玩,一起学习进步!!!】
- 深入理解Java并发机制(2)--volatile关键字
- 深入理解Java并发机制(2)--volatile关键字
- 深入理解Java并发机制(2)--volatile关键字
- 深入理解Java并发机制(2)--volatile关键字
- 深入理解Java并发之synchronized实现原理
- 基于JVM原理、JMM模型和CPU缓存模型深入理解Java并发编程
- Java并发编程之深入理解线程池原理及实现
- 深入理解Java并发之synchronized实现原理
- 深入理解Java并发2——Java线程实现原理
- 深入理解Java并发之synchronized实现原理
- 深入理解Java并发之synchronized实现原理
- 深入理解Java并发之synchronized实现原理
- 3.Java高级教程_深入理解Java并发之synchronized实现原理
- Java并发专题(三)深入理解volatile关键字
- 深入理解Java并发之synchronized实现原理
- 深入理解Java并发之synchronized实现原理
- 【Java面试整理之JVM】深入理解JVM结构、类加载机制、垃圾回收GC原理、JVM内存分配策略、JVM内存泄露和溢出
- 深入理解Java并发4——synchronized关键字
- 深入理解Java并发之synchronized实现原理
- 深入理解Java并发机制(2)--volatile关键字