Java并发学习之synchronized使用小结

时间:2022-04-27
本文章向大家介绍Java并发学习之synchronized使用小结,主要内容包括synchronized工作原理及使用小结、II. 三中使用姿势、III. 小结、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

synchronized工作原理及使用小结

为确保共享变量不会出现并发问题,通常会对修改共享变量的代码块用synchronized加锁,确保同一时刻只有一个线程在修改共享变量,从而避免并发问题

本篇将集中在synchronized关键字的工组原理以及使用方式上

I. 工作原理

以一个case进行分析,源码如下

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

在加锁的代码块, 多了一个 monitorenter , monitorexit

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权

执行monitorexit的线程必须是objectref所对应的monitor的所有者。

  1. 指令执行时,monitor的进入数减1
  2. 如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者
  3. 其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权

谈到 synchronized 就不可避免的要说到锁这个东西,基本上在网上可以搜索到一大批的关于偏向锁,轻量锁,重量锁的讲解文档,对这个东西基本上我也不太理解,多看几篇博文之后,简单的记录一下

先抛一个结论: 轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能

1. 偏向锁

获取过程

  • 判断是否为可偏向状态
  • 是,则判断线程ID是否指向当前线程
    • 是,即表示这个偏向锁就是这个线程持有, 直接执行代码块
    • 否,通过CAS操作竞争锁
      • 竞争成功, 则设置线程ID为当前线程, 并执行代码块;
      • 竞争失败,说明多线程竞争啦,问题严重了,当偏向锁到达安全点时,将偏向锁升级为轻量锁

释放过程

  • 当偏向锁遇到其他线程尝试竞争时,持有偏向锁的线程会释放,并升级为轻量锁
  • 到达安全点, 暂停拥有偏向锁的线程,判断锁对象是否处于被锁的状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

2. 轻量锁

“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。 但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。 在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁

3. 转换

简单来讲,单线程时,使用偏向锁,如果这个时候,又来了一个线程访问这个代码块,那么就要升级为轻量锁,如果这个线程在访问代码块同时,又来了一个线程来访问这个代码块,那么就要升级为重量锁了。下面更多的显示了这些变动时,标记位的随之改变


II. 三中使用姿势

1. 三种方法说明

  1. 修饰实例方法 多个线程访问同一个实例的加锁方法时,会出现锁的竞争
  2. 修饰静态方法 多个线程访问类的加锁方法时,会出现锁的竞争
  3. 修饰代码块 多线程访问到同一个代码块时,会出现竞争的问题

2. 几个疑问

一个case: TestDemo方法定义如下

public class TestDemo {
  public synchronized void a() { 
    // ...
  }
  public synchronized void b() { 
    // ...
  }
  public static synchronized void c() { 
    // ...
  }
  public static synchronized void d() { 
    // ...
  }
  public void e() {
  // ...
  }
  
  public void f() {
    synchronized(this) {
    // ....
    }
  }
  
  public void g() {
    synchronized(this) {
    // ....
    }
  }
}
  1. 线程1访问a方法时,线程2访问a方法会被阻塞;若此时线程2访问b方法会被阻塞么?访问c,d, e方法呢?
  2. 线程1访问c方法时,线程2访问c方法会被阻塞,若此时线程2访问d方法会被阻塞么,访问a,b,e方法呢?
  3. 线程1进入f方法内部的同步代码块,此时线程2若访问f会被阻塞;那么线程2访问g方法会如何?访问a,b,c,d,e方法又是怎样?

对上面的问题,核心的一点就是synchronized是否只作用于修饰的代码块or方法上

3. 测试验证

TestDemo的具体实现如下

public class TestDemo {

    public synchronized void a(String msg) {
        System.out.println(Thread.currentThread().getName() + ":a() before");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":a() after: " + msg);
    }


    public synchronized void b(String msg) {
        System.out.println(Thread.currentThread().getName() + ":b() before");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":b() after: " + msg);
    }

    public static synchronized void c(String msg) {
        System.out.println(Thread.currentThread().getName() + ":c() before");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":c() after: " + msg);
    }


    public static synchronized void d(String msg) {
        System.out.println(Thread.currentThread().getName() + ":d() before");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":d() after: " + msg);
    }


    public void e(String msg) {
        System.out.println(Thread.currentThread().getName() + ":e() before");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":e() after: " + msg);
    }


    public void f(String msg) {
        System.out.println(Thread.currentThread().getName() + ":f() before");
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":f() after: " + msg);
    }


    public void g(String msg) {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + ":a() before");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":a() after: " + msg);
        }
    }


    public void h(String msg) {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + ":h() before");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":h() after: " + msg);
        }
    }
}

测试一 实例加锁方法的访问测试

/**
 * 非静态同步方法测试
 */
private void nonStaticSynFun() throws InterruptedException {

    TestDemo testDemo = new TestDemo();

    Thread thread1 = new Thread(()->testDemo.a("访问同一加锁方法"), "thread1");
    Thread thread2 = new Thread(()->testDemo.a("访问同一加锁方法"), "thread2");

    System.out.println("---两个线程,访问同一个加锁方法开始---");
    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();
    System.out.println("---两个线程,访问同一个加锁方法结束---n");

  
    // 

    TestDemo testDemo2 = new TestDemo();
    thread1 = new Thread(()->testDemo.a("访问第一个实例同一加锁方法"), "thread1");
    thread2 = new Thread(()->testDemo2.a("访问第二个实例同一加锁方法"), "thread2");

    System.out.println("---两个线程,访问两个实例同一个加锁方法开始---");
    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();
    System.out.println("---两个线程,访问两个同一个加锁方法结束---n");

    //

    thread1 = new Thread(()->testDemo.a("访问两个加锁方法"), "thread1");
    thread2 = new Thread(()->testDemo.b("访问两个加锁方法"), "thread2");
    System.out.println("---两个线程,访问两个加锁方法开始---");
    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();
    System.out.println("---两个线程,访问两个加锁方法结束---n");


    //
    thread1 = new Thread(()->testDemo.a("访问加锁实例方法"), "thread1");
    thread2 = new Thread(()->TestDemo.c("访问加锁静态方法"), "thread2");
    System.out.println("---两个线程,访问实例和静态加锁方法开始---");
    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();
    System.out.println("---两个线程,访问实例和静态加锁方法结束---n");

}

@Test
public void testNoStaticSynFun() throws InterruptedException {
    for(int i = 0; i < 2000; i++) {
        nonStaticSynFun();
    }
}

上面的测试case主要覆盖:

  1. 两个线程访问一个实例的同一个加锁方法(期望阻塞,顺序执行)
  2. 线程1访问实例1的加锁方法,线程2访问实例2的加锁方法(无阻塞,并发执行)
  3. 两个线程访问一个实例的两个加锁方法
  4. 两个线程,一个访问实例的加锁方法,一个访问静态加锁方法

输出结果如下

---两个线程,访问同一个加锁方法开始---
thread1:a() before
thread1:a() after: 访问同一加锁方法
thread2:a() before
thread2:a() after: 访问同一加锁方法
---两个线程,访问同一个加锁方法结束---

---两个线程,访问两个实例同一个加锁方法开始---
thread1:a() before
thread2:a() before
thread2:a() after: 访问第二个实例同一加锁方法
thread1:a() after: 访问第一个实例同一加锁方法
---两个线程,访问两个同一个加锁方法结束---

---两个线程,访问两个加锁方法开始---
thread1:a() before
thread1:a() after: 访问两个加锁方法
thread2:b() before
thread2:b() after: 访问两个加锁方法
---两个线程,访问两个加锁方法结束---

---两个线程,访问实例和静态加锁方法开始---
thread1:a() before
thread2:c() before
thread2:c() after: 访问加锁静态方法
thread1:a() after: 访问加锁实例方法
---两个线程,访问实例和静态加锁方法结束---

验证结果:

  1. 同一个实例中加锁的方法,只要有一个线程已经获取到了锁,其他线程再去访问时,都会被阻塞(即此时的锁,是一个实例共享同一把锁;不同的实例,锁不同)
  2. 一个线程获取到一个实例中的加锁方法的锁时,另一个线程依然可以访问静态加锁方法(即实例的锁与静态方法的锁是不同的,两者不影响)

测试case二: 静态加锁方法测试

private void staticSynFun() throws InterruptedException {
    Thread thread1 = new Thread(() -> TestDemo.c("访问加锁静态方法"), "thread1");
    Thread thread2 = new Thread(() -> TestDemo.c("访问加锁静态方法"), "thread2");

    System.out.println("---两个线程,访问静态加锁方法开始---");
    thread1.start();
    thread2.start();

    thread1.join();
    thread2.join();
    System.out.println("---两个线程,访问静态加锁方法结束---n");


    //

    TestDemo testDemo1 = new TestDemo(), testDemo2 = new TestDemo();
    thread1 = new Thread(() -> testDemo1.c("访问加锁静态方法"), "thread1");
    thread2 = new Thread(() -> testDemo2.d("访问加锁静态方法"), "thread2");
    Thread thread3 = new Thread(() -> testDemo1.a("访问加锁实例方法"), "thread3");

    System.out.println("---两个线程,访问不同实例的静态加锁方法开始---");
    thread1.start();
    thread2.start();
    thread3.start();

    thread1.join();
    thread2.join();
    thread3.join();
    System.out.println("---两个线程,访问不同实例的静态加锁方法结束---n");
}

@Test
public void testStaticSynFunc() throws InterruptedException {
    for (int i = 0; i < 2000; i++) {
        staticSynFun();
    }
}

上面的测试主要覆盖

  • 两个线程访问相同的静态加锁方法(期待阻塞)
  • 三个线程,两个访问不同实例的静态加锁方法,一个访问实例加锁方法

输出结果如下

---两个线程,访问静态加锁方法开始---
thread1:c() before
thread1:c() after: 访问加锁静态方法
thread2:c() before
thread2:c() after: 访问加锁静态方法
---两个线程,访问静态加锁方法结束---

---两个线程,访问不同实例的静态加锁方法开始---
thread1:c() before
thread3:a() before
thread1:c() after: 访问加锁静态方法
thread2:d() before
thread3:a() after: 访问加锁实例方法
thread2:d() after: 访问加锁静态方法
---两个线程,访问不同实例的静态加锁方法结束---

验证结果:

  • 不同的线程访问静态同步方法时,会阻塞(即静态同步方法,共享一把锁;所有的实例访问静态同步方法依然是共享这把锁)
  • 静态同步方法的锁和实例同步方法的锁不同,两者没有关系,不会相互影响

测试case三: 同步代码块

基本上和上面的相同,同步代码块分为静态同步代码块(共享类锁);非静态同步代码块(共享实例锁)

III. 小结

  1. synchronized 三中使用姿势,修饰静态方法,实例方法,(静态/非静态)代码块
  2. 静态同步方法,静态同步代码块共享同一把锁(简易称为类锁),所有这些同步代码的访问,都会去竞争类锁,从而出现阻塞
  3. 一个实例中的同步方法,非静态同步代码块共享一把锁(简易成为实例锁),所有访问同一个实例中的这些同步代码时,都会竞争实例锁,从而出现阻塞
  4. 不同的实例拥有不同的实例锁,彼此相互没有影响
  5. 实例锁和类锁没有影响,不会造成彼此阻塞
  6. synchronized底层主要是通过偏向锁,轻量级锁和重量级锁组合来实现线程同步的功能
  7. 几个锁的简要说明为:单线程时,使用偏向锁,如果这个时候,又来了一个线程访问同步代码块,那么就要升级为轻量锁,如果这个线程在访问代码块同时,又来了一个线程来访问这个代码块,那么就要升级为重量锁了