synchronized的使用

本文是学习 默课网-Java高并发之魂:synchronized深度解析 所做的笔记。

一、Synchronized

1.1 synchronized简介

  1. 官方解释:

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

  1. 通俗易懂:

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

1.2 并发出现的问题

两个线程同时执行i++,最后的结果比我们预期的少。代码如下:

public class DisappearRequest1 implements Runnable{

    private static DisappearRequest1 instance = new DisappearRequest1();

    private static int i = 0;

    @Override
    public void run() {
        for (int j = 0; j < 100000; j++) {
            i ++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);

        t1.start();
        t2.start();
        // join()方法的作用是,当前线程(主线程)进入阻塞状态,等待执行join()方法的线程运行结束后,当前线程(主线程)才可以进入就绪状态。
        t1.join();
        t2.join();

        // 理想的情况是输出200000,但由于多线程并发问题,结果小于200000
        System.out.println(i);

        /** 原因分析:
         * i ++ ,看上去只有一个操作,实际上包含了三个动作
         * 1. 读取i的值
         * 2. 将i加1
         * 3. 将i的值写入内存
         * 由于在多线程情况下,上述三个步骤任何一步都有可能被打断(例如:t1读取到i的值为0,加1,
         * 还没来得及写入内存,此时t2线程进来,读取到i的值依然是0,加1,写入内存,最终i执行了
         * 两次加1,但是i的值不是2,而是1),所以造成最终结果与预期不一致。
         *
         *
         */
    }
}

二、Synchronized的两种用法

2.1 对象锁

包括方法锁(默认锁对象是this当前实例对象)和同步代码块锁(自己指定锁对象)

2.11 方法锁形式

synchronized修饰普通方法,锁对象默认是this

public class SynchronizedObjectMethod3 implements Runnable {

    private static SynchronizedObjectMethod3 instance = new SynchronizedObjectMethod3();

    @Override
    public void run() {
        method();
    }
    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() + "  运行结束");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);

        t1.start();
        t2.start();
        // while空循环的作用是,让t1,t2线程执行结束后再执行主线程
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }
}

运行结果

我是对象锁的方法修饰方式,我叫 Thread-0
Thread-0  运行结束
我是对象锁的方法修饰方式,我叫 Thread-1
Thread-1  运行结束
finished
2.12 代码块锁形式

第一种,使用this作为锁对象

public class SynchronizedObjectBlock2 implements Runnable{

    private static SynchronizedObjectBlock2 instance = new SynchronizedObjectBlock2();

    @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,t2线程执行结束后再执行主线程
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }
}

运行结果:

我是对象锁的代码块形式,我叫 : Thread-0
Thread-0运行结束
我是对象锁的代码块形式,我叫 : Thread-1
Thread-1运行结束
finished

第二种,使用自定义对象作为锁对象(有多个代码块时,就需要自定义锁对象)

public class SynchronizedObjectBlock2 implements Runnable{

    private static SynchronizedObjectBlock2 instance = new SynchronizedObjectBlock2();
    Object lock1 = new Object();
    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部分运行结束");
       }
    }



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

运行结果

我是lock1,我叫 : Thread-1
Thread-1  lock1部分运行结束
我是lock2,我叫 : Thread-1
我是lock1,我叫 : Thread-0
Thread-0  lock1部分运行结束
Thread-1  lock2部分运行结束
我是lock2,我叫 : Thread-0
Thread-0  lock2部分运行结束
finished

2.2 类锁

指的是synchronized修饰静态的方法,或者指定锁为Class对象。

2.21 基础知识点
  1. Java类可能有很多个对象,但是只有一个Class对象(类类型class type),所谓的类锁,指的就是该类的Class对象的锁
  2. 类锁使用效果:类锁只能在同一时刻被一个对象拥有。
2.22 类锁的形式
  1. synchronized加在static方法上
public class SynchronizedClassStatic4 implements Runnable {

    private static SynchronizedClassStatic4 instance1 = new SynchronizedClassStatic4();
    private static SynchronizedClassStatic4 instance2 = new SynchronizedClassStatic4();

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

    // static方法
    public static synchronized void method() {
      System.out.println("我是类锁的第一种形式 :static形式,我叫 " + 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(instance1);
        Thread t2 = new Thread(instance2);

        t1.start();
        t2.start();

        // while空循环的作用是,让t1,t2线程执行结束后再执行主线程
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }
}

运行结果:

我是类锁的第一种形式 :static形式,我叫 Thread-0
Thread-0  运行结束
我是类锁的第一种形式 :static形式,我叫 Thread-1
Thread-1  运行结束
finished
  1. synchronized(*.class)代码块
public class SynchronizedClassClass5 implements Runnable {

    private static SynchronizedClassClass5 instance1 = new SynchronizedClassClass5();
    private 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(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "  运行结束");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);

        t1.start();
        t2.start();

        // while空循环的作用是,让t1,t2线程执行结束后再执行主线程
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }
}

运行结果

我是类锁的第二种形式:synchronized(*.class)代码块形式,我叫 Thread-0
Thread-0  运行结束
我是类锁的第二种形式:synchronized(*.class)代码块形式,我叫 Thread-1
Thread-1  运行结束
finished

现在可以对1.2中的代码进行优化

  1. 使用对象锁的synchronized修饰普通方法形式
 @Override
    public synchronized void run() {
         
        for (int j = 0; j < 100000; j++) {
            i ++;
        }
        
    }
  1. 使用对象锁的代码块形式
 @Override
    public void run() {
        synchronized (this) {
            for (int j = 0; j < 100000; j++) {
                i ++;
            }
        }
    }
  1. 使用类锁的synchronized(*.class)代码块形式
 @Override
    public void run() {
        synchronized (DisappearRequest1.class) {
            for (int j = 0; j < 100000; j++) {
                i ++;
            }
        }
    }
  1. 使用类锁的synchronized修饰zestatic方法形式
  @Override
    public void run() {
        count();
    }

    public synchronized static void count(){
        for (int j = 0; j < 100000; j++) {
            i ++;
        }
    }

以上优化后的代码最终输出结果都是:200000

三、sychonized集几种常见面试题

3.1 多线程访问(synchronized)同步方法的七种情况

  1. 两个线程同时访问一个对象的同步方法(串行)
  2. 两个线程访问的是两个对象的同步方法(并行:锁对象不一致)
  3. 两个线程访问的是sychronized的静态方法(串行:类锁,锁对象一致)
  4. 同时访问同步和非同步的方法(并行:synchronized只会对被修饰的方法起作用)
  5. 访问一个类的不同的普通(非static)同步方法(串行:此种情况是对象锁,因此两个同步方法拿到的锁对象是一致的)
  6. 同时访问静态的sychronized和非静态的synchronized方法(并行:前者是对象锁this,后者是类锁*.class)
  7. sychronized方法抛出异常后,会释放锁对象吗?(JVM会释放锁)
  8. 当一个同步方法中调用了另一个非同步方法的时候,该同步方法还是线程安全的吗?(不安全)

3.2 对以上七种情况的总结

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

四、Synchronized的性质

4.1 可重入性质

  1. 定义:可重入指的的是同一线程的外层函数获得锁之后,内层函数可以直接在此获取该锁。(可重入锁也叫递归锁)
  2. 好处:避免死锁,提升封装性
  3. 粒度(范围):线程而非调用(只要是在同一个线程内,都可以满足可重入性质,即无需释放锁、重新获取锁)

粒度总结:

  1. 同一个方法是可重入的,即递归调用同一个方法,是满足可重入性质的
  2. 可重入不要求是同一个方法,即一个同步方法调用其他同步方法,依然满足可重入性质
  3. 可重入不要求是同一个类,即一个类的同步方法调用其他类的同步方法也满足可重入性质
4.2 不可中断性质
  1. 定义:一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,知道别的线程释放这个锁,如果别人永远不释放,那我只能永远等待下去。

参考

默课网-Java高并发之魂:synchronized深度解析