2

Java关键词synchronized解读 - 拿了桔子跑-范德依彪

 1 year ago
source link: https://www.cnblogs.com/knowledgeispower/p/17013849.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1 引入Synchronized

  1. Synchronized是java虚拟机为线程安全而引入的。
  2. 互斥同步是一种最常见的并发正确性的保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条线程使用。
  3. synchronized是最基本的互斥同步手段,它是一种块结构的同步语法。
  4. synchronized修饰代码块,无论该代码块正常执行完成还是发生异常,都会释放锁

synchronized对线程访问的影响:

  • 被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前,会阻塞其他线程的进入。
  • 被synchronized修饰的同步块对同一条线程是可重入

2 Synchronized的使用

可以作用在方法上或者方法里的代码块:

  1. 修饰方法,包括实例方法和静态方法
  2. 修饰方法里的代码块,这时需要一个引用作为参数
  3. Synchronized作用地方不同,产生的锁类型也不同,分为对象锁和类锁

2.1 对象锁

Synchronized修饰实例方法或者代码块(锁对象不是*.class),此时生产对象锁。多线程访问该类的同一个对象的sychronized块是同步的,访问不同对象不受同步限制。

2.1.1 Synchronized修饰实例方法

public static void main(String[] args){
        TempTest tempTest = new TempTest();
        Thread t1 = new Thread(() -> {
            tempTest.doing(Thread.currentThread().getName());
        });
        Thread t2 = new Thread(() -> {
            tempTest.doing(Thread.currentThread().getName());
        });
        t1.start();
        t2.start();
    }

    //同一时刻只能被一个线程调用
    private synchronized void doing(String threadName){
        for(int i=0;i<3;i++){
            System.out.println("current thread is : "+threadName);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {}
        }
    }

运行结果:

current thread is : Thread-0
current thread is : Thread-0
current thread is : Thread-0
current thread is : Thread-1
current thread is : Thread-1
current thread is : Thread-1

2.1.2 Synchronized修饰代码块

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence = new SynchronizedObjectLock();
    @Override
    public void run() {
        // 同步代码块形式:锁为this,两个线程使用的锁是一样的,线程1必须要等到线程0释放了该锁后,才能执行
        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(instence);
        Thread t2 = new Thread(instence);
        t1.start();
        t2.start();
    }
}

运行结果:

我是线程Thread-0
Thread-0结束
我是线程Thread-1
Thread-1结束

2.2 类锁

synchronize修饰静态方法或指定锁对象为Class,此时产生类锁。多线程访问该类的所有对象的sychronized块是同步的

2.2.1 synchronize修饰静态方法

    public static void main(String[] args){
        TempTest tempTest1 = new TempTest();
        TempTest tempTest2 = new TempTest();
        //虽然创建了两个TempTest实例,但是依然是调用同一个doing方法(因为是个static);因此doing还是会依次执行
        Thread t1 = new Thread(() -> tempTest1.doing(Thread.currentThread().getName()));
        Thread t2 = new Thread(() -> tempTest2.doing(Thread.currentThread().getName()));
        t1.start();
        t2.start();
    }

    //修饰静态方法,则是类锁;
    private static synchronized void doing(String threadName){
        for(int i=0;i<3;i++){
            System.out.println("current thread is : "+threadName);
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {}
        }
    }

运行结果:有序输出 【如果去掉static ,则线程会交替执行doing】

current thread is : Thread-0
current thread is : Thread-0
current thread is : Thread-0
current thread is : Thread-1
current thread is : Thread-1
current thread is : Thread-1

2.2.2 synchronize指定锁对象为Class

public class SynchronizedObjectLock implements Runnable {
    static SynchronizedObjectLock instence1 = new SynchronizedObjectLock();
    static SynchronizedObjectLock instence2 = new SynchronizedObjectLock();

    @Override
    public void run() {
        // 所有线程需要的锁都是同一把
        synchronized(SynchronizedObjectLock.class){
            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(instence1);
        Thread t2 = new Thread(instence2);
        t1.start();
        t2.start();
    }
}
我是线程Thread-0
Thread-0结束
我是线程Thread-1
Thread-1结束

3 Synchronized原理分析

3.1 虚拟机如何辨别和处理synchronized

  • 虚拟机可以从常量池中的方法表结构中的ACC_ SYNCHRONIZED访问标志区分一个方法是否是同步方法。
  • 当调用方法时,调用指令将会检查方法的ACC_ SYNCHRONIZED访问标志是否设置,如果设置了,执行线程将先持有同步锁,然后执行方法,最后在方法完成时释放同步锁。
  • 在方法执行期间,执行线程持有了同步锁,其他任何线程都无法再获得同一个锁。
  • 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的锁将在异常抛到同步方法之外时自动释放。

3.2 虚拟机对synchronized的编译处理

以下代码:

public class Foo {
    void onlyMe(Foo f) {
        synchronized(f) {
            doSomething();
        }
    }
    private void doSomething(){ }
}

编译后,这段代码生成的字节码序列如下:

2327408-20221229235543108-1461129067.jpg
  1. synchronized关键字经过Javac编译之后,会在同步块的前后生成monitorentermonitorexit两个字节码指令。
  2. 指令含义:monitorenter:获取对象的锁monitorexit:释放对象的锁
  3. 执行monitorenter指令时,首先尝试获取对象的锁。如果对象没被锁定,或者当前线程已经持有了对象的锁,就把锁的计数器的值增加1
  4. 执行monitorexit指令时,将锁计数器的值减1,一旦计数器的值为零,锁随即就被释放
  5. 如果获取对象锁失败,那当前线程阻塞等待,直到锁被释放。
  6. 为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理程序,它的目的就是用来执行monitorexit指令。

3.3 虚拟机执行加锁和释放锁的过程

那么重点来了到这里,有几个问题需明确:

  1. 什么叫对象的锁
  2. 如何确定锁被线程持有
  3. 执行monitorenter后,对象发生什么变化
  4. 锁计数值保存在哪里,如何获取到?

1. 什么叫对象的锁?
对象的内存结构参考:2 Java内存层面的对象认识

  1. 锁,一种可以被读写的资源,对象的锁是对象的一部分。
  2. 对象的结构中有部分称为对象头
  3. 对象头中有2bit空间,用于存储锁标志,通过该标志位来标识对象是否被锁定。

2. 如果确定锁被线程持有?

  1. 代码即将进入同步块的时,如果锁标志位为“01”(对象未被锁定),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,存储锁对象Mark Word的拷贝。(线程开辟空间并存储对象头)
  2. 虚拟机将使用CAS操作尝试把对象的Mark Word更新成指向锁记录的指针(对象头的mw存储指向线程“锁记录”中的指针)
  3. 如果CAS操作成功,即代表该线程拥有了这个对象的锁,并且将对象的锁标志位转变为“00”
  4. 如果CAS操作失败,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行,否则就说明这个锁对象已经被其他线程抢占。
  5. 解锁过程:CAS操作把线程中保存的MW拷贝替换回对象头中。假如能够成功替换,那整个同步过程就顺利完成了;如果替换失败,则说明有其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程。

3 执行monitorenter后,对象发生什么变化?

  1. 对象的锁标志位转变为“00”
  2. 拥有对象锁的线程开辟了新空间,保存了对象的Mark Word信息
  3. 对象的Mark Word保存了线程的锁记录空间的地址拷贝

4 锁计数值保存在哪里
我还没搞懂。

monitorenter指令执行的过程

2327408-20221229173521529-902114709.jpg

4 Synchronized与Lock

synchronized的缺陷

  1. 在多线程竞争锁时,当一个线程获取锁时,它会阻塞所有正在竞争的线程,这样对性能带来了极大的影响。
  2. 挂起线程和恢复线程的操作都需要转入内核态中完成,上下文切换需要消耗很大性能。
  3. 效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时
  4. 不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活

5 使用Synchronized有哪些要注意的

  • 锁对象不能为空,因为锁的信息都保存在对象头里
  • 作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错
  • 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果有必要,使用synchronized关键,因为代码量少,避免出错
  • synchronized实际上是非公平的,新来的线程有可能立即获得执行,而在等待区中等候已久的线程可能再次等待,这样有利于提高性能,但是也可能会导致饥饿现象。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK