Java 语言中 Synchronized 关键字的最致命误区

版权声明:本文为博主学习笔记, 注明来源情况下随意转载 https://blog.csdn.net/lengxiao1993/article/details/78069439

前言

synchronized 关键字是 java 中一旦涉及并发, 永远绕不过的一个知识点。 如果让你用自己的话, 解释一下 synchronized 关键字, 如果你说出 “被 synchronized 关键字所修饰的代码或方法块不会在同一时间被多个线程执行” 这种话, 那么恭喜你, 掉入了 synchronized 关键字最为常见的误区之一。

sychronized 的误区

  • 错误认识: “被 sychronized 关键字所修饰的代码或方法块不会在同一时间被多个线程执行”

通过一段代码样例来说明上面这个说法为什么是错的。

class SyncThread implements Runnable {
    private static int count;

    public SyncThread() {
        count = 0;
    }

    public void run() {
        `synchronized` (this) {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
            }
        }
    }
    public static void main(String[] args) {
        SyncThread syncThread = new SyncThread();
        Thread thread1 = new Thread(new SyncThread(), "SyncThread1");
        Thread thread2 = new Thread(new SyncThread(), "SyncThread2");
        thread1.start();
        thread2.start();
    }
}

如果说, 被 synchronized 修饰的代码块同一时间只有一个线程能执行的话, 那么程序的输出一定是按顺序从 0 一直到 199. 但是如果你把这个程序 copy 到 IDE 中运行几遍或者调大累加的循环次数, 就一定话发现结果有错乱的情况。

那么 synchronized 关键字如何使用才能保证同步性呢。 对上面的代码稍加修改即可。

class SyncThread implements Runnable {
    private static int count;

    public SyncThread() {
        count = 0;
    }

    public void run() {
        `synchronized` (this) {
            for (int i = 0; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
            }
        }
    }

    public static void main(String[] args) {
        SyncThread syncThread = new SyncThread();
        Thread thread1 = new Thread(syncThread, "SyncThread1");
        Thread thread2 = new Thread(syncThread, "SyncThread2");
        thread1.start();
        thread2.start();
    }
}

上面这段代码, 无论你运行多少次,或者调大累加的循环次数, 它的结果输出一定是有序的从 0 到 199 。

如果你不理解为什么输出是 0 到 199 而不是 0 到 100 , 那么你缺少了多线程最关键的知识点。

  • 同一个进程中的多个线程拥有各自独立的 寄存器 (Register) 指令计数器(Program Counter, 一种特殊的寄存器)、栈内存空间( Stack ) 。 除了以上提到的, 进程中其他的资源都是线程间共享的

  • 具体来说, 一个进程的多个线程会共享一个地址空间(Address space), (heap), 静态数据(static data) 和 代码段(code segments) 和 文件句柄( file descriptors)。 还有其他的一些进程状态也会在线程间共享, 例如 进程id, 文件锁 file lock 等, 但是这些都属于细节实现的问题, 在编写应用的时候, 我们通常并不关心。

    • 一个地址空间 就是指一块物理内存地址逻辑内存地址的映射。所以当我们说一个进程中的所有线程共享相同的地址空间时, 意思是, 当不同的线程访问一个全局变量 foo 时, 他们拿到的是同样的内存地址, 访问的是同一个物理内存区域。
  • 本地变量存储在 “栈内存空间” 空间中, 所以每一个线程都会有一个本地变量的 copy 。所以上述的代码中的变量 i 在两个线程中各自都有一份, 所以两个线程在保证同步的情况下,会对共享的静态变量 count 一共进行 200 次 + 操作 。

这里写图片描述

Sychronized 关键字的正确理解

通过上述的例子, 可以看出 synchronized 关键字, 在同一时间是有可能被多个线程同时执行的。 synchronized 关键字的真正作用是:

  • 多个线程试图调用同一个 Objectsynchronized 方法时, 同一时间只能有一个线程执行 sychronized 所修饰的方法或代码块。

这里可能马上会有同学提出异议, 说 synchronized 修饰静态方法时,这是类级别的同步, 而不是对象级别的同步。 所以这里又要引入新的知识点: Java 中的对象锁

内部锁(Intrinsic Lock)

synchronized 关键字所提供的同步机制是通过 Java 语言中的 内部锁 (intrinsic lock)或 监控器锁 (monitor lock) 实现的。 (java API 文档中通常将其简称为 “监控器” monitor)。内部锁在同步机制的实现过程中起到了两个作用:

  1. 保证了对于一个对象的状态的访问是线程互斥的
  2. 建立了多线程行为间 先行发生(happens-before) 的关系, 这对于保障多线程读写操作的 可见性(visibility) 至关重要。
    • 第 2 点有些晦涩, 因为这又是一个很长的知识点, 这里暂且略过。

每一个对象都有一个与之关联的内部锁。 通常, 当一个线程需要对一个对象的域变量进行排他性(互斥性)且保证一致性的访问时, 该线程必须在访问前, 先获得该对象的内部锁。 在访问结束以后, 释放该对象的内部锁。 在获得对象的内部锁到释放这个锁的期间, 我们称该线程拥有(Own) 这个内部锁。 在此期间, 其他线程如果试图获取该锁, 就会被阻塞。

当一个线程释放了一个内部锁后, 与之后发生的同一个锁的获取行为,就建立了一个 先行发生(happens-before)的关系。

Synchronized 如何使用对象锁

当一个线程调用某个对象的 synchronized 方法时, 它会自动获取这个方法所属对象的内部锁, 在方法调用完毕或被异常终止后(方法 return 后),该对象的锁就会被释放。

也就是说

Class SynchronizedCounter
{
    public  synchronized void  add()
        {
            // do some add stuff
        }
}

Class SynchronizedCounter
{
    public void add()
        {
            synchronized(this)
            {
                // do some add stuff
            }
        }
}

的实现效果是相同的, 任意一个线程调用某个 synchronizedCounter 实例的 add() 方法时, 都需要获取该对象的内部锁。

当 synchronized 关键字修饰静态方法时,

public class SomeClass {
    public synchronized  static void methodA()
    {
        // do something
    }
}

它其实等价于如下写法

public class SomeClass {
    public  void methodA()
    {
        synchronized (SomeClass.class)
        {
            // do something
        }
    }
}

在上面的这例子中, 一个线程如果调用 methodA , 就会去获取 SomeClass.class 这个实例对象的内部锁(intrinsic lock)。 没错, SomeClass.class 的本质是一个对象实例, 它是 Class 类的一个实例!

所以当多个线程试图调用 synchronized 所修饰的静态方法时, 本质上还是在争抢一个对象的内部锁。

synchronized 相关误区总结

  • sychronized 关键字所实现的同步机制都是基于 Object 的, 多个线程只有在调用同一个对象的 synchronized 方法时, 才会互斥。

  • 在 Java 语言中, 并不存在所谓的 “类锁”。 synchonized 无论修饰静态方法或一般的成员方法, 都是通过对象的内部锁(Intrinsic Lock) 实现的。

猜你喜欢

转载自blog.csdn.net/lengxiao1993/article/details/78069439