您的位置:首页 > 编程语言 > Java开发

【深入理解Java并发】Sysnchronized关键字用法、原理及常见面试考点

2020-06-07 04:42 465 查看

一、Sysnchronized简介

Sysnchronized:能够保证在同一时刻最多只有一个线程执行该段代码
创建线程的两种方法:

  1. 继承Thread类
  2. 实现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种情况(面试常考)

  1. 两个线程同时访问一个对象的同步方法
  2. 两个线程访问的是两个对象的同步方法
  3. 两个线程访问的是synchronized的静态方法
  4. 同时访问同步方法非同步方法
  5. 访问同一个对象的不同的普通同步方法
  6. 同时访问静态synchoronized和非静态synchronized方法
  7. 方法抛异常后,会释放锁
  1. 两个线程串行执行
  2. 两个对象的同步方法各自有一把锁,互不干扰,结果是两个线程并行执行
  3. synchronized修饰的静态方法加的是类锁,所有对象共用一把锁,所以两个线程串行执行
  4. 同步方法的实例和非同步方法的实例之间互不影响,所以两个线程并行执行
  5. 访问同一个对象的不同普通同步方法,一把锁只能同时被一个线程获取,所以是串行执行。
  6. 静态synchoronized修饰的方法加的是类锁,非静态synchronized加的是对象锁,实例彼此不受影响,所以并行执行
  7. 使用throw new RuntimeException()异常后,JVM会自动释放锁。(lock方法需要在finally后释放)

4点核心思想:

  1. 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1、5种情况)
  2. 每个实例都对应有自己的一把锁,不同实例之间互不影响;例外:锁对象是*,class以及synchronized修饰的是static方法时,所有对象共用同一把类锁(对应2、3、4、6情况)
  3. 无论是方法正常执行完毕或者方法抛出异常,都会释放锁(对应第7种情况)
  4. 如果存在一种情况 在被synchronized修饰的方法内调用一个非同步方法,
    那么调用这个非同步方法不是线程安全的

三、 Synchronized的性质

  1. 可重入
    可重入指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁。
    (我已经获得这把锁,现在再次请求这把锁而无需释放当前锁就叫做可重入)
    Java中可重入锁有:Synchronized和ReentrantLock
    好处:避免死锁、提升封装性
    粒度:线程而非调用(用3种情况来说明和pthread的区别)——同一线程层面
  • 情况1:证明同一个方法是可重入的
  • 情况2:证明可重入不要求是同一个方法
  • 情况3:证明可重入不要求是同一个类中的
  1. 不可中断
    一旦这个锁被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁。
    如果别人永远不释放锁那么只能永远等下去。(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接口的方法:
  1. lock();//获取锁
  2. unlock();//释放锁
  3. tryLock();//判断锁是否可用。返回值为:boolean;
  4. tryLock(time,TimeUnit);//在规定的时间内,如果未获得锁,则就放弃。

六、常见面试问题

  1. 使用注意点:锁对象不能为空(锁的信息保存在对象头中)、作用域不宜过大(大部分代码串行,影响效率)、避免死锁
  2. 如何选择Lock和synchronized关键字? 建议都不使用,可以使用java.util.concurrent包中的Automic类、countDown等类
  3. 优先使用现成工具,如果没有就优先使用synchronized关键字,好处是写劲量少的代码就能实现功能。如果 需要灵活的加解锁机制,则使用Lock接口
  4. 多线程访问同步方法的各种情况

七、思考题

  1. 多个线程等待同一个synchronized锁的时候,JVM如何选择下一个获取锁的是哪个线程?
  2. synchronized使得同时只有一个线程可以执行,性能较差,有什么办法可以提升性能?
    :优化synchronized的使用范围、使用其他类型lock(读写锁等)
  3. 如何更灵活地控制锁的获取和释放(释放锁的时机被规定死了怎么办)
  4. 什么是锁的升级、降级?什么是JVM里的偏斜锁、轻量级锁、重量级锁?

八、总结

一句话介绍synchronized:
JVM会自动通过使用monitor来加锁和解锁,保证了同时只有一个线程可以执行指定代码,从而保证了线程安全,同时具有可重入和不可中断的性质。

【最后欢迎大家来我的博客skiron.xyz来玩,一起学习进步!!!】

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: