Java并发编程-synchronized介绍

1,680 阅读8分钟

1.什么是synchronized

官方描述:

A keyword in the Java programming language that, when applied to a method or code block, guarantees that at most one thread at a time executes that code.

Java编程语言中的关键字,当应用于方法或代码块时,该关键字可确保一次最多执行一个线程。

通俗来讲作为Java中的关键字,synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,也就是说线程与线程访问的资源是互斥的,同时它还可以保证共享变量的内存可见性,Java中每一个对象都可以作为锁,这是synchronized实现同步的基础。

2.synchronized用法

万物皆对象,Java中每个对象都可以作为锁,这是synchronized实现同步的基础。总的来讲synchronized修饰的对象可以分为对象锁和类锁。

2.1 对象锁

包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象),针对不同的对象是存在不同的锁。

1.修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;

public void  method(){
    synchronized (this){
    // TODO something
    }
}

2.修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

public synchronized void  method(){
    // TODO something
}

2.2 类锁

java类可能有很多个对象,但只有1个Class对象,所以所谓的类锁,不过是Class对象的锁而已,类锁只能在同一时刻被一个对象拥有

3.修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

public static synchronized void  method(){
    // TODO something
}

4.修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象

public  void  method(){
    synchronized (*.class){
	    // TODO something
    }
}

3. 性质

3.1 可重入(又称递归锁)

  • 定义:指同一线程的外层函数获得锁之后,内层函数可以直接再次获得该锁(无需重新竞争)
  • 好处:避免死锁、提升封装性
  • 粒度:线程为单位,而非调用
    • 同一个方法是可重入的;
    • 可重入不要求是同一个方法;
    • 可重入不要求是同一个类中的
  • 原理:加锁次数计数器
    • JVM 负责跟踪对象被加锁的次数
    • 线程第一次给对象加锁的时候,计数器变为1。每当这个相同线程在此相同对象上再次获得锁,计数器会递增
    • 当任务离开的时候,计数器递减,当计数器为0时,锁被完全释放

3.2 不可中断

  • 定义:一旦锁被他人获得,如果想使用,则需选择等待或者阻塞,直到他人释放锁。

4.实现原理

JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。 具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令。

其本质就是对一个对象监视器(Monitor)进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。

而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit 之后才能尝试继续获取锁。

代码验证

public class SynchronizedObject {
    public static void main(String[] args) {
    
    }
    public void method() {
        synchronized(this){
            System.out.println("Hello world");
            }
        }
}
  • 查看文件编译后信息

>javac SynchronizedObject.java

>javap -verbose SynchronizedObject.class

public void method();
descriptor: ()V
flags: ACC_PUBLIC
Code:
  stack=2, locals=3, args_size=1
     0: aload_0
     1: dup
     2: astore_1
     3: monitorenter
     4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
     7: ldc           #3                  // String Hello world
     9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    12: aload_1
    13: monitorexit
    14: goto          22
    17: astore_2
    18: aload_1
    19: monitorexit
    20: aload_2
    21: athrow
    22: return
  Exception table:
     from    to  target type
         4    14    17   any
        17    20    17   any
  LineNumberTable:
    line 16: 0
    line 17: 4
    line 18: 12
    line 19: 22
  StackMapTable: number_of_entries = 2
    frame_type = 255 /* full_frame */
      offset_delta = 17
      locals = [ class com/pingan/wang/sync/SynchronizedObject, class java/lang/Object ]
      stack = [ class java/lang/Throwable ]
    frame_type = 250 /* chop */
      offset_delta = 4
}

5.实战分析

5.1 举例

多线程中count++ 是线程不安全的,因为 count++ 包含三个操作:

  • 1.读取count

  • 2.将count+1

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

      public class SynchronizedObject implements Runnable {
          static SynchronizedObject instance = new SynchronizedObject();
          static int count = 0;
          public static void main(String[] args) throws InterruptedException{
              Thread thread = new Thread(instance);
              Thread thread1 = new Thread(instance);
              thread.start();
              thread1.start();
              thread.join();
              thread1.join();
              System.out.println(count);
      }
    
      @Override
      public void run() {
          for (int i = 0; i < 10000; i++) {
              count ++;
          }
      }
      }
    
  • 期望值:20000
  • 执行结果:12580(每次直接结果可能会不一样,但是始终小于等于20000)

5.2 多个线程访问 同一对象的 同步代码块

public class SynchronizedObject implements Runnable {
    public static void main(String[] args) {
        SynchronizedObject instance = new SynchronizedObject();
        Thread thread1 = new Thread(instance, "thread1");
        Thread thread2 = new Thread(instance, "thread2");
        thread1.start();
        thread2.start();
    }

    @Override
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 3; i++) {
                try {
                   System.out.println("我是" + Thread.currentThread().getName() + ": count :" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
           }
        }
    }
}
  • 执行结果:

      我是thread1: count :0
      我是thread1: count :1
      我是thread1: count :2
      我是thread1: count :3
      我是thread1: count :4
      我是thread2: count :0
      我是thread2: count :1
      我是thread2: count :2
      我是thread2: count :3
      我是thread2: count :4
    
  • 结论:两个并发线程(thread1和thread2)访问同一个对象(instance)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。

5.3 多个线程访问 不同对象的 同步代码块

public class SynchronizedObject implements Runnable {
public static void main(String[] args) {
    SynchronizedObject instance1 = new SynchronizedObject();
    SynchronizedObject instance2 = new SynchronizedObject();
    Thread thread1 = new Thread(instance1, "thread1");
    Thread thread2 = new Thread(instance2, "thread2");
    thread1.start();
    thread2.start();
}

@Override
public void run() {
    synchronized (this) {
        int count = 0;
        for (int i = 0; i < 5; i++) {
            try {
                System.out.println("我是" + Thread.currentThread().getName() + ": count :" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
}
  • 执行结果:

      我是thread1: count :0
      我是thread2: count :0
      我是thread2: count :1
      我是thread1: count :1
      我是thread2: count :2
      我是thread1: count :2
      我是thread1: count :3
      我是thread2: count :3
      我是thread1: count :4
      我是thread2: count :4  
    
  • 结论:两个SynchronizedObject的对象instance1和instance2,线程thread1执行的是instance1对象中的synchronized代码(run),而线程thread2执行的是instance2对象中的synchronized代码(run);我们知道synchronized锁定的是对象,这时会有两把锁分别锁定instance1对象和instance2对象,而这两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行

5.4 多线程访问 同步方法

大家可以把5.3节的代码修改一下自行验证结果。

5.5 多个线程访问 同一对象的 同步静态方法

public class SynchronizedStaticObject implements Runnable {

static SynchronizedStaticObject instance1 = new SynchronizedStaticObject();
static SynchronizedStaticObject instance2 = new SynchronizedStaticObject();

public static synchronized void method() {
    System.out.println("我是" + Thread.currentThread().getName() + ",同步静态方法");
    try {
        Thread.sleep(1000);
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println( Thread.currentThread().getName() + "执行结束" );
}

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

public static void main(String[] args) {
    Thread thread1 = new Thread(instance1,"thread1");
    Thread thread2 = new Thread(instance1,"thread2");
    thread1.start();
    thread2.start();
    while (thread1.isAlive() || thread2.isAlive()) {

    }
}
}
  • 执行结果:

      我是thread1,同步静态方法
      thread1执行结束
      我是thread2,同步静态方法
      thread2执行结束
    
  • 结论:静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象,该类所有的对象同一把锁,所有会阻塞。

5.6 多个线程访问 不同对象的 同步静态方法

public class SynchronizedStaticObject implements Runnable {

static SynchronizedStaticObject instance1 = new SynchronizedStaticObject();
static SynchronizedStaticObject instance2 = new SynchronizedStaticObject();

public static synchronized void method() {
    System.out.println("我是" + Thread.currentThread().getName() + ",同步静态方法");
    try {
        Thread.sleep(1000);
    } catch (Exception e) {
        e.printStackTrace();
    }
    System.out.println( Thread.currentThread().getName() + "执行结束" );
}

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

public static void main(String[] args) {
    Thread thread1 = new Thread(instance1,"thread1");
    Thread thread2 = new Thread(instance2,"thread2");
    thread1.start();
    thread2.start();
    while (thread1.isAlive() || thread2.isAlive()) {

    }
}
}
  • 执行结果:

      我是thread1,同步静态方法
      thread1执行结束
      我是thread2,同步静态方法
      thread2执行结束
    
  • 结论:和多个线程访问同一对象的同步静态方法结果一致。

5.7 多个线程访问 对象的 同步类代码块

大家可以把5.6 节的代码修改一下自行验证结果。

6.缺陷

6.1. 效率低

  • 锁的释放情况少;
  • 试图获得锁的时间不能设置上限(无限等待);
  • 不能中断一个正在试图获得锁的线程;

6.2 不够灵活

  • 加锁和释放的时机单一
  • 某个锁仅有单一的条件(某个对象)

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

针对这些缺陷大家可以根据实际的情况使用其他的锁工具,如ReentrantLock类等;

7. 总结

7.1 思考以下情况是否同步的

  • 1.两个线程同时访问一个对象的同步方法?
    • 结论:是同步的
  • 2.两个线程访问的是两个对象的同步方法
    • 结论:不是同步的,两个对象是不同的锁
  • 3.两个线程访问的是synchronized的静态方法
    • 结论:是同步的,属于类锁
  • 4.同时访问同步方法与非同步方法
    • 结论:不是同步的,非同步方法不受锁影响
  • 5.访问同一个对象的不同的普通同步方法
    • 结论:是同步的
  • 6.同时访问静态synchronized和非静态synchronized方法
    • 结论:不是同步的
  • 7.方法抛异常后,会释放锁
    • 结论:会自动释放锁

7.2 使用 synchronized 要注意的点

  • 锁对象不能为空
  • 作用域不宜过大
  • 避免死锁

关于synchronized,就暂且聊到这。如有错误,欢迎指正,谢谢~~。