Synchronized关键字作用,官方解释
通俗来说:能够保证在同一时刻最多只有一个现场执行该段代码,以达到保证并发安全的效果。
Synchronized关键字两种用法
对象锁
包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)
类锁
指synchronized修饰静态的方法或指定锁为Class对象
对象锁代码实践
- 代码块形式:手动指定锁对象
public class DisappearRequest2 implements Runnable{
static DisappearRequest2 instance = new DisappearRequest2();
static int i=0;
Object lock = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
@Override
public void run() {
synchronized (lock){
for (int j = 0; j < 100000; j++) {
i++;
}
}
}
}
- 方法锁形式:synchronized修饰普通方法,锁对象默认为this
/**
* @author :LY
* @date :Created in 2021/3/15 21:06
* @modified By:
*/
public class DisappearRequest1 implements Runnable{
static DisappearRequest1 instance = new DisappearRequest1();
static int i=0;
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
@Override
public synchronized 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());
}
}
类锁概念:Java类可能有很多个对象,但只有1个Class对象
- 只有一个Class对象:Java类可能会有很多个对象,但是只有1个Class对象
- 本质:所谓的类锁,不过是Class对象的锁而已。
- 用法和效果:类锁只能在同一时刻被一个对象拥有
形式1:synchronized加在static方法上
形式2:synchronized(*.class)代码块
形式1代码实践
/**
* @author :LY
* @date :Created in 2021/3/17 15:16
* @modified By:
*/
public class SynchronizedClassStatic4 implements Runnable {
static SynchronizedClassStatic4 instance1 = new SynchronizedClassStatic4();
static SynchronizedClassStatic4 instance2 = new SynchronizedClassStatic4();
@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());
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
形式2的代码实践
/**
* @author :LY
* @date :Created in 2021/3/17 15:22
* @modified By:
*/
public class SynchronizedClassClass5 implements Runnable {
static SynchronizedClassClass5 instance1 = new SynchronizedClassClass5();
static SynchronizedClassClass5 instance2 = new SynchronizedClassClass5();
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
method();
}
private void method(){
synchronized (SynchronizedClassClass5.class){
System.out.println("我是类锁的第二种形式,我叫"+Thread.currentThread().getName());
try{
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
}
多线程访问同步方法的7种情况(面试常考)
- 两个线程同时访问一个对象的同步方法
串行执行
- 两个线程访问的是两个对象的同步方法
并行执行
- 两个对象访问的是synchronized的静态方法
串行执行
- 同时访问同步方法与非同步方法
synchronized只作用于你指定的方法
并行执行
- 访问一个对象的不同的普通同步方法
锁当前实例对象
串行执行
- 同时访问静态synchronized和非静态synchroized方法
静态synchronized锁的是.class对象
非静态synchroized锁的是当前实例对象this
并行执行
- 方法抛异常后,会释放锁
由jvm释放锁
总结:三点核心思想
- 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应1,5种情况)
- 每个实例都对应有自己的一把锁,不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象共用同一把类锁(对应2,3,4,6种情况)
- 无论是方法正常执行完毕或者方法抛出异常,都会释放锁(对应第7种情况)
synchronized性质
可重入
- 什么是可重入:指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁。(线程拿到锁之后,再次请求可以直接拿到锁)
-
好处:避免死锁、提升封装性
-
粒度:线程而非调用(用三种情况来说明和pthread区别) 线程范围,与调用无关
- 情况1:证明同一个方法是可重入的
- 情况2:证明可重入不要求同一个方法
- 情况3:证明可重入不要求是同一个类中的
-
不可中断
- 性质:一旦锁被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁。如果别人永远不释放锁,那么我只能永远的等待下去。
- 相比lock类,lock拥有可以中断的能力,第一点,如果我觉得等待时间太长,有权中断现在已经获取到锁的线程的执行;第二点,如果我觉得时间太长不想等待了,也可以直接退出
synchronized原理
- 加锁和释放锁的原理
现象:每一个类的实例对应一把锁,每一个synchronized方法都必须首先获得调用该方法类的实例的锁,才能执行,否则线程阻塞,方法一旦执行就独占此锁,直到该方法返回或者抛出异常才将锁释放,所有java对象都含有一个互斥锁。
获取和释放锁的时机:内置锁,获得内置锁的唯一途径就是线程进入同步代码块的时候,在出现异常或者方法执行完毕就会释放锁
等价代码:
public class SynchronizedToLock13 {
Lock lock = new ReentrantLock();
public synchronized void method1(){
System.out.println("我是synchronized形式的锁");
}
public void method2(){
lock.lock();
try{
System.out.println("我是lock形式的锁");
}finally {
lock.unlock();
}
}
public static void main(String[] args){
SynchronizedToLock13 synchronizedToLock13 = new SynchronizedToLock13();
synchronizedToLock13.method1();
synchronizedToLock13.method2();
}
}
深入JVM:
- 概况:在java对象头中有一个字段表明对象是否被锁住,进入锁或释放锁基于monitor对象实现。
- 反编译查看字节码
首先写一个简单的同步代码块代码
public class Decompilation14 {
private Object object = new Object();
public void insert(Thread thread){
synchronized (object){
}
}
}
通过命令行
javac Decompilation14.java
javap -verbose Decompilation14.class
获得文件字节码内容
找到monitorenter代码段
我们可以看到,加锁时使用monitorenter指令,释放锁使用monitorexit
-
monitorexit和monitorenter指令 :
-
在执行的时候让锁对象计数加一或者减一
-
每一个对象都与一个monitor相关联,而一个monitor对象锁只能被一个线程在同一时间获得
-
一个线程在尝试获得对象关联的monitor所有权时只会发生三种情况:
- 如果monitor计数器为0,意味着目前还没有被获得,此时线程会立刻获得,并把计数器加一。
- 如果说monitor已经拿到了锁的所有权并且又重入了,那么计数器会累加
- 如果monitor已经被其他线程所持有了,新的线程就会进入阻塞状态,直到计数器变为0.
-
-
可重入原理:加锁次数计数器
- JVM负责跟踪对象被加锁的次数
- 线程第一次给对象加锁的时候,计数变为1.每当这个相同的线程在此对象上再次获得锁时,计数会递增
- 每当任务离开时,计数递减,当计数为0的时候,锁被完全释放
-
保证可见性的原理:
- Java内存模型
- synchronized如何做到可见性的实现:在代码块被synchronized修饰,那么它在执行完毕后被锁住的对象所作的任何修改都要在释放锁之前,从线程内存写回到主内存中,同样,线程在进入代码块得到锁之后,被锁定的对象的数据,也是直接从主内存中读取出来的。这样就保证了主内存的数据一定是最新的。保证了可见性
synchronized缺陷
- 效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
- 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
- 无法知道是否成功获取到锁
常见面试问题
1.synchronized使用注意点:锁对象不能为空、作用域不宜过大(指的是synchronized包裹范围)、避免死锁
2.如何选择Lock和synchronized关键字?
- 优先使用synchronized
- 优先使用线程安全工具类
3.多线程访问同步方法的各种具体情况
上面的七种情况
思考:
-
多线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的是哪个线程?
-
synchronized使得同时只有一个线程可以执行,性能较差,有什么办法可以提升性能?
-
- 优化使用范围,尽可能缩小同步代码块大小
- 使用其他类型的 lock(锁),synchronized 使用的锁经过 jdk 版本的升级,性能已经大幅提升了,但相对于更加轻量级的锁(如读写锁)还是偏重一点,所以可以选择更合适的锁。
synchronized总结
- JVM回自动通过使用monitor来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质。