写点什么

浅析 synchronized

用户头像
朱华
关注
发布于: 2020 年 10 月 11 日

synchronized 概述



synchronized 的作用



同步方法支持一种简单的策略来防止线程干扰和内存一致性错误: 如果一个线程对多个线程可见,则对象变量的所有读取或写入都是通过同步方法来完成的



能够保证在同一时刻最多只有一个线程执行该代码,以达到保证并发安全的效果。

synchronized 的地位

  • synchronized 是 Java 关键字,被 Java 语言原生支持

  • 最基本的互斥同步手段

  • 是并发编程中的元老级角色,是并发编程的必学内容



不使用并发手段会有什么后果? 案例:两个线程同时 a++。

public class DIsappearRequest1 implements Runnable {
static DIsappearRequest1 instance = new DIsappearRequest1();
static int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
i++;
}
}
}

原因:count++,它看上去只是一个操作,实际上包括了三个动作:

  1. 读取 count

  2. 将 count 加 1

  3. 将 count 的值写入到内存中

synchronized 的两个用法



  • 对象锁 包括方法锁(默认锁对象为 this 当前实例对象)和同步方法锁(自己指定锁对象)

  • 类锁 指 synchronized 修饰静态的方法或指定锁为 Class 对象



对象锁



  • 代码块形式 手动指定锁对象。

public class SynchronizedObjectCodeBlock2 implements Runnable{
static SynchronizedObjectCodeBlock2 instance = new SynchronizedObjectCodeBlock2();
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");
}
@Override
public void run() {
synchronized (this) {
System.out.println("我是lock1。我叫 " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " lock1 部分,运行结束。");
}
}
}



方法锁形式 synchronized 修饰普通方法,锁对象默认为 this。

public class SynchronizedObjectMethod3 implements Runnable{

static SynchronizedObjectMethod3 instance = new SynchronizedObjectMethod3();

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");
}

@Override
public void run() {
method();
}

public synchronized void method() {
System.out.println("我的对象锁的方法修饰模式。我叫 " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 运行结束。");
}
}



类锁

  • 概念: Java 类可能有很多对象,但是只有一个 Class 对象。 本质:所以所谓的类锁,不过是 Class 对象的锁而已。 用法和效果:类锁只能在同一时刻被一个对象拥有。

  • 形式 1: synchronized 加在 static 方法上

  • 形式 2: synchronized(*.class)



synchronized 加在 static 方法上

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("我是类锁的第一种形式:static 形式。我叫 " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} 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();
t1.join();
t2.join();
System.out.println("finished");
}
}



类锁的形式 2:synchronized(.class)



public class SynchronizedClassClass5 implements Runnable {
static SynchronizedClassClass5 instance1 = new SynchronizedClassClass5();
static SynchronizedClassClass5 instance2 = new SynchronizedClassClass5();
@Override
public void run() {
method();
}
public void method() {
synchronized (SynchronizedClassClass5.class) {
System.out.println("我是类锁的第二种形式:synchronized(*.class) 形式。我叫 " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} 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();
t1.join();
t2.join();
System.out.println("finished");
}
}

多线程访问同步方法的 7 中情况

  1. 两个线程同时访问一个对象的同步方法 受影响

  2. 两个线程访问的是两个对象的同步方法 

  3. 两个线程访问的是 synchronized 的静态方法 受影响

  4. 同时访问同步方法与非同步方法 

  5. 访问同一对象的不同的普通同步方法 受影响

  6. 同时访问静态 synchronized 和非静态 synchronized 方法

  7. 方法抛出异常后,会释放锁



  • 7 种情况总结:3 点思想

  1. 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应到 1、5 情况)

  2. 每个实例都对应自己的一把锁,不同实例之间互不影响;例外:锁对象是 *.class 以及 synchronized 修饰的是 static 方法的时候,所有对象共用同一把锁(对饮 2、3、4、6 情况)

  3. 无论是方法正常执行完毕或者方法抛出异常,都会释放锁(对应 7 情况)



锁的性质

  • 可重入

  • 不可中断



什么是可重入 指的是同一线程的外层函数获得锁之后,内层函数可以直接获取该锁 好处:避免死锁、提升封装性 粒度:线程而非调用



粒度

  • 情况 1:证明同一个方法是可重入的

  • 情况 2:证明可重入不是要求同一个方法

  • 情况 3:证明可重入不要求是同一个类中的



情况1

public class SynchronizedRecursion10 {
int i = 0;
public static void main(String[] args) {
SynchronizedRecursion10 instance = new SynchronizedRecursion10();
instance.method();
}
public synchronized void method() {
System.out.println("这是 method, i = " + i);
if (i == 0) {
i++;
method();
}
}
}

情况2

public class SynchronizedOtherMethod11 {
public synchronized void method1() {
System.out.println("我是 method 1");
method2();
}
public synchronized void method2() {
System.out.println("我是 method 2");
}
public static void main(String[] args) {
SynchronizedOtherMethod11 instance = new SynchronizedOtherMethod11();
instance.method1();
}
}

情况3

public class SynchronizedSuperClass12 {
public synchronized void doSomething() {
System.out.println("我是父类方法");
}
}
class TestClass extends SynchronizedSuperClass12 {
public synchronized void doSomething() {
System.out.println("我是子类方法");
super.doSomething();
}
public static void main(String[] args) {
TestClass testClass = new TestClass();
testClass.doSomething();
}
}

不可中断

一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,知道别的线程释放这个锁。如果别人永远不释放锁,那么我只能永久的等下去 相比之下,Lock 类,可以拥有中断的能力,如果我觉得等的时间太长了,有权中断现在已经获取到锁的线程的执行;如果我觉得我等待的时间太长了不想再等了,也可以退出。



加锁和释放锁的原理

  • 加锁和释放锁的原理: 现象、时机、深入 JVM 看字节码

  • 可重入原理: 加锁次数计数器

  • 保证可见性的原理: 内存模型



加锁和释放锁的原理

  • 现象

  • 获取和释放锁的时机:内置锁

  • 等价代码

public class SynchronizedToLock13 {
Lock lock = new ReentrantLock();
public synchronized void method1() {
System.out.println("我是 synchronized 形式的锁");
}
public synchronized 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 看字节码

  • 概况

  • 如何反编译:javap -v *.class

  • Monitorenter 和 Monitorexit 指令



可重入原理:加锁次数计数器

  • JVM 负责跟踪对象被加锁的次数

  • 线程第一次给对象加锁的时候,计数变为 1.每当这个相同的线程在此对象上在此获得锁时,计数会增加

  • 每当任务离开时,计数递减,当计数为 0 的时候,锁被完全释放



可见性



缺陷:

  • 效率低 所得释放情况少,试图获得锁时,不能设定超时、不能中断一个正在试图获得锁的线程

  • 不够灵活 加锁和释放的时间单一,每个锁仅有单一的条件(某个对象),可能是不够灵活的。

  • 无法知道是否成功获取到锁

public class LockExample15 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
lock.lock();
lock.unlock();
// lock.tryLock(1000, TimeUnit.SECONDS);
}
}

常见面试题

  1. 使用注意点:锁对象不能为空,作用于不宜过大(synchronized 包含的范围)、避免死锁

  2. 如何选择 Lock 和 synchronized 关键字?如果可以的话,这两个都不要使用,使用工具包中的如果 synchronized 关键字在程序中适用,我们就选它如果需要使用到 Lock 时,使用 Lock

  3. 多线程访问同步方法的各种具体情况



思考题:

  1. 多个线程等待同一个 synchronized 锁的时候,JVM 如何选择下一个获取锁的线程是哪个线程?

  2. synchronized 使得同时只有一个线程可以执行,性能较差,有什么办法可以提升性能?

  3. 想要更灵活的控制锁的获取和释放,怎么办?

  4. 什么是锁的升级、降级?什么是 JVM 里的偏斜锁、轻量级锁、重量级锁?



总结

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



发布于: 2020 年 10 月 11 日阅读数: 42
用户头像

朱华

关注

见自己,见天地,见众生。 2018.08.07 加入

还未添加个人简介

评论

发布
暂无评论
浅析 synchronized