Skip to main content

说说并发编程 synchronized

并发编程是码农一个绕不开的话题,更是区分高手和普通的一块雷区。面试中,更是屡试不爽。那今天就说说并发编程中synchronized,这是java自带的关键字,是原生保证线程安全的机制。

本文的目录是:

  • 什么是线程安全
  • 怎么解决线程安全
  • synchronized用法和原理介绍
  • 1. 什么是线程安全

    想理解线程安全,就必须知道什么是线程。线程是相对进程而言的。下边是线程和进程的简单介绍,如果需要更深的了解,可以查询操作系统的原理。

    进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
    线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
    一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
    相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
    进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。相对并发编程,线程的并发控制要比进程的并发控制要容易,进程之间的并发安全需要借助第三方媒介,比如文件,操作系统,而线程则可以通过指令,内部变量实现。

    说完线程,那就说说线程安全。一般对共享的资源,当有多个线程同事访问的时候,就会有线程安全,因为每个线程是个独立的执行体,可以理解为一个人,资源是有自己的数据和行为的,当多个线程(人)去执行同一资源的行为,就会导致资源内在数据的变化,谁先执行,谁后执行,就会引发线程安全的问题。比如你的银行账号,当同时几个人取钱,如何保证账户的钱是对的,对银行来说是很重要的问题。

    2.怎么解决线程安全

    线程安全的原因就是对共享资源或者叫临界区的访问,所以保证安全,就是确保同一时刻,只有一个线程可以改变该共享资源的内在行为。一般java里有2中策略: (1) Lock (2) Synchronized

    3.synchronized用法和原理介绍

    java里支持3中方法定义同步块:

  • 同步类方法
  • [java]
    package com.learn.core.ch02;

    public class InsertData {

    public static synchronized void method1() {
    System.out.println("Method 1 start");
    try {
    System.out.println("Method 1 execute");
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Method 1 end");
    }

    public static synchronized void method2() {
    System.out.println("Method 2 start");
    try {
    System.out.println("Method 2 execute");
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
    final InsertData test = new InsertData();
    final InsertData test2 = new InsertData();

    new Thread(new Runnable() {

    @Override
    public void run() {
    test.method1();
    }
    }).start();

    new Thread(new Runnable() {

    @Override
    public void run() {
    test2.method2();
    }
    }).start();
    }
    }

    [/java]

    类方法同步
    Method 1 start
    Method 1 execute
    Method 1 end
    Method 2 start
    Method 2 execute
    Method 2 end
    实例方法同步(去掉上边的static)
    Method 1 start
    Method 1 execute
    Method 2 start
    Method 2 execute
    Method 2 end
    Method 1 end

    字节码
    从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。只不过,实例方法同步和类方法同步,monitor一个是监测对象,一个是监测类

  • 同步实例方法
  • [java]
    class class_name {
    synchronized type method_name() {
    statement block
    }
    }
    [/java]

  • synchronized代码块
  • [java]
    class class_name {
    type method_name() {
    synchronized (object) {
    statement block
    }
    }
    }
    [/java]

    线程执行到这个方法时,对先尝试得到对象(object)的锁,如果拿不到锁,就会等待,直到锁释放,拿到锁。 对象可以是当前对象this,也可以某个变量。比如下面2个例子原理是一样的:
    [java]
    public class InsertData {

    private ArrayList<Integer> arrayList = new ArrayList<Integer>();

    public void insert(Thread thread) {
    synchronized (this) {
    for (int i = 0; i < 100; i++) {
    System.out.println(thread.getName() + "insert data #" + i);
    arrayList.add(i);
    }
    }
    }
    }

    public class InsertData {

    private ArrayList<Integer> arrayList = new ArrayList<Integer>();
    private Object object = new Object();

    public void insert(Thread thread) {
    synchronized (object) {
    for (int i = 0; i < 100; i++) {
    System.out.println(thread.getName() + "insert data #" + i);
    arrayList.add(i);
    }
    }
    }
    }
    [/java]

    这2个例子为啥效果一样呢?大家可以仔细想想。那同步块,是如何保证安全的?其实它是通过指令来完成的,看下边的字节码截图:
    字节码

    关于这两条指令的作用,我们直接参考JVM规范中描述:

    monitorenter :

    Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
    • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
    • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
    • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

    这段话的大概意思为:

    每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
    1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
    2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
    3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

    monitorexit:

    The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
    The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

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

      通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
    更详细的资料,请参考JVM指令介绍:monitorenter/monitorexit

    参考:
    http://www.importnew.com/18523.html