0
点赞
收藏
分享

微信扫一扫

Java基础-并发编程-synchronized关键字使用与原理解析

Java工程师知识树 / Java基础


synchronized的使用

JDK针对共享资源数据同步问题有一种方式为使用synchronized关键字,synchronized提供了一种排他锁机制,可以让程序在同一时间段内只有一个线程执行某些操作。

使用synchronized修饰执行内容后:

package com.thread.study;

public class TicketWindow implements Runnable {

    public static int TICKET_NUM = 10;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (TICKET_NUM > 0) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖了票号为" + TICKET_NUM-- + "的票");
                } else {
                    return;
                }
            }
        }
    }

    public static void main(String[] args) {

        TicketWindow ticketWindow = new TicketWindow();

        Thread t1 = new Thread(ticketWindow, "1号售票窗口");
        Thread t2 = new Thread(ticketWindow, "2号售票窗口");
        Thread t3 = new Thread(ticketWindow, "3号售票窗口");

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

    }
}
// 执行结果
1号售票窗口卖了票号为10的票
3号售票窗口卖了票号为9的票
2号售票窗口卖了票号为8的票
2号售票窗口卖了票号为7的票
2号售票窗口卖了票号为6的票
2号售票窗口卖了票号为5的票
2号售票窗口卖了票号为4的票
2号售票窗口卖了票号为3的票
2号售票窗口卖了票号为2的票
2号售票窗口卖了票号为1的票

将synchronized改为修改run()方法:

package com.thread.study;

public class TicketWindow implements Runnable {

    public static int TICKET_NUM = 10;

    @Override
    public synchronized void run() {
        while (true) {
            if (TICKET_NUM > 0) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "卖了票号为" + TICKET_NUM-- + "的票");
            } else {
                return;
            }
        }
    }

    public static void main(String[] args) {

        TicketWindow ticketWindow = new TicketWindow();

        Thread t1 = new Thread(ticketWindow, "1号售票窗口");
        Thread t2 = new Thread(ticketWindow, "2号售票窗口");
        Thread t3 = new Thread(ticketWindow, "3号售票窗口");

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

    }
}
//执行结果 不管执行多少次都是
1号售票窗口卖了票号为10的票
1号售票窗口卖了票号为9的票
1号售票窗口卖了票号为8的票
1号售票窗口卖了票号为7的票
1号售票窗口卖了票号为6的票
1号售票窗口卖了票号为5的票
1号售票窗口卖了票号为4的票
1号售票窗口卖了票号为3的票
1号售票窗口卖了票号为2的票
1号售票窗口卖了票号为1的票

通过上述两个例子对比,总结synchronized的使用:

  1. 由于synchronized关键字存在排他性,也就是说所有的线程必须串行地经过 synchronized保护的共享区域,如果synchronized作用域越大,则代表着其效率越低,甚至还会丧失并发的优势
  2. synchronized关键字应该尽可能地只作用于共享资源(数据)的读写作用域,或者说是synchronized锁的是有增删改操作的对象。eg:
List<String> list = new ArrayList<String>();
for (int i = 0; i < 10000; i++) {
    new Thread(() -> {
        synchronized (list) {//synchronized锁的是有增删改操作的对象
            list.add(Thread.currentThread().getName());
        }
    }, String.valueOf(i)).start();
}
try {
    Thread.sleep(100);
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println(list.size());

synchronized 关键字原理

synchronized说明:

synchronized 关键字是解决共享资源数据同步的常用解决方案,有以下三种使用方式:

  • 同步普通方法,锁的是当前对象
  • 同步静态方法,锁的是当前 Class 对象。
  • 同步块,锁的是 {} 中的对象。

synchronized 实现原理:

具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和可能发生异常处插入 monitor.exit 的指令。monitor指令是使用C++实现的。

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

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

synchronized 特性:

  • 互斥性(确保线程互斥的访问同步代码)
  • 可见性(保证共享变量的修改能够及时可见)
  • 有序性(有效解决重排序问题)
互斥性
可见性
有序性

流程图如下:

同步代码块

public class Synchronize{
    public static void main(String[] args) {
        synchronized (Synchronize.class){
            System.out.println("Synchronize");
        }
    }
}
---------使用 javap-c 编译 Synchronize类 可以查看编译之后的具体信息。-----------
{
  public com.thread.study.Synchronize();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class com/thread/study/Synchronize
         2: dup
         3: astore_1
         4: monitorenter    //同步方法调用前加入一个 monitor.enter 指令
         5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #4                  // String Synchronize
        10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit     //退出的地方插入 monitor.exit 的指令
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit     //异常可能出现的地方插入 monitor.exit 的指令 确保可以正常退出
        21: aload_2
        22: athrow
        23: return
      Exception table:
         from    to  target type
             5    15    18   any
            18    21    18   any
      LineNumberTable:
        line 5: 0
        line 6: 5
        line 7: 13
        line 8: 23
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 18
          locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
}
SourceFile: "Synchronize.java"

可以看到在同步块的入口和出口分别有 monitorenter,monitorexit指令。

同步方法

package com.thread.study;

public class TicketThread {

    public static void main(String[] args) {
        TicketRunnable ticketRunnable = new TicketThread.TicketRunnable();
        Thread t1 = new Thread(ticketRunnable,"zhao");
        Thread t2 = new Thread(ticketRunnable,"qian");
        t1.start();
        t2.start();
    }

    static class TicketRunnable implements Runnable{
        private int TICKET_NUM = 10;
        @Override
        public synchronized void run() {  // 同步方法
            if (TICKET_NUM > 0) {
                System.out.println(Thread.currentThread().getName() +TICKET_NUM);
                TICKET_NUM --;
            }
        }

    }
}
----------使用 javap-c 编译 TicketThread 可以查看编译之后的具体信息。主要看内部类TicketRunnable------------

{
  com.thread.study.TicketRunnable();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 16: 0

  public synchronized void run();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED     // ACC_SYNCHRONIZED标志
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #2                  // Field TICKET_NUM:I
         3: ifle          45
         6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: new           #4                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
        16: invokestatic  #6                  // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
        19: invokevirtual #7                  // Method java/lang/Thread.getName:()Ljava/lang/String;
        22: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        25: getstatic     #2                  // Field TICKET_NUM:I
        28: invokevirtual #9                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        31: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        37: getstatic     #2                  // Field TICKET_NUM:I
        40: iconst_1
        41: isub
        42: putstatic     #2                  // Field TICKET_NUM:I
        45: return
      LineNumberTable:
        line 20: 0
        line 21: 6
        line 22: 37
        line 24: 45
      StackMapTable: number_of_entries = 1
        frame_type = 45 /* same */

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        10
         2: putstatic     #2                  // Field TICKET_NUM:I
         5: return
      LineNumberTable:
        line 17: 0
}
SourceFile: "TicketThread.java"

可以看出在synchronized修饰的同步方法的flags中会有ACC_SYNCHRONIZED标识

无论是monitorenter、 monitorexit,或者是ACC_SYNCHRONIZED,其都是基于Monitor机制实现的。

Monitor

monitor直译过来是监视器的意思,专业一点叫管程。Monitor机制一个重要特点是,在同一时间,只有一个线程/进程能进入monitor所定义的临界区,这使得monitor能够实现互斥的效果无法进入monitor的临界区的进程/线程,应该被阻塞,并且在适当的时候被唤醒

java则基于monitor机制实现了它自己的线程同步机制,就是synchronized内置锁。

基本元素
  • 临界区

临界区是被synchronized包裹的代码块,可能是个代码块,也可能是个方法。

  • monitor对象和锁

monitor对象是monitor机制的核心,它本质上是jvm用c语言定义的一个数据类型。对应的数据结构保存了线程同步所需的信息,比如保存了被阻塞的线程的列表,还维护了一个基于mutex的锁,monitor的线程互斥就是通过mutex互斥锁实现的。

  • 条件变量

条件变量和下方wait signal方法的使用有密切关系 。在获取锁进入临界区之后,如果发现条件变量不满足使用wait方法使线程阻塞,条件变量满足后signal唤醒被阻塞线程。 tips:当线程被signal唤醒之后,不是从wait那继续执行的,而是重新while循环一次判断条件是否成立

  • 定义在monitor对象上的wait,signal操作
举报

相关推荐

0 条评论