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

    java final 关键字

    说到final关键字,搞过java的都不陌生。基本上每个码农对final的基本用法都清楚,但是经常看到一些奇怪的用法和理解,那今天就好好梳理下。

    1.final关键字的基本用法
    在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)

    1.1修饰类
    当final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。默认final类中的方法都是final,成员变量可以设为final也可以不是。

    [java]
    package com.learn.core.ch02;

    public final class Country {

    private String name;

    public void display() {

    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public Country(String name) {
    this.name = name;
    }
    }

    public class CountrySub extends Country{
    //The type CountrySub cannot subclass the final class Country
    }

    [/java]

    1.2 修饰方法
    下面这段话摘自《Java编程思想》第四版第143页:

    “使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。“

    因此,如果只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。
    注:类的private方法会隐式地被指定为final方法。

    1.3 修饰变量(成员变量或者局部变量)
    对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。注意如果是引用类型的变量,只是说引用不能再变,但是引用所代表的对象还是可以改变该对象内在的行为。
    成员变量:必须初始化或者在构造器中初始化

    [java]
    package com.learn.core.ch02;

    public final class Country {

    private String name;

    public Country(String name) {
    this.name = name;
    }

    public static void main(String[] args) {
    final Country country = new Country("china");
    country.display();

    // update to us
    country.setName("US");
    country.display();

    // output
    // china
    // US

    // country = new Country("GE");
    // error The final local variable country cannot be assigned. It must be blank and not using a compound
    // assignment
    }

    public void display() {
    System.out.println(name);
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    }

    [/java]

    2. 深究final关键字
    2.1 类的final变量和普通变量有什么区别?
    当用final作用于类的成员变量时,成员变量(注意是类的成员变量,局部变量只需要保证在使用之前被初始化赋值即可)必须在定义时或者构造器中进行初始化赋值,而且final变量一旦被初始化赋值之后,就不能再被赋值了
    2.2 final的引用变量指向的对象内容可变吗?
    只要不改变引用,引用所指向的对象是可以改变内在行为或者外在行为的,就是可以调各种方法。可以参考上边的例子。
    2.3 匿名内部类中使用的外部局部变量为什么只能是final变量?
    关于这个,可以参考另外一篇文章。
    2.4 static 和final一起使用,可以定义不可变的常量
    2.5 所有的局部变量定义成final,除了匿名内部类可以使用的有点,还有其他优势吗?

    [java]
    public void display() {
    System.out.println(name);
    String title = "Hello";
    Date date = new Date();
    System.out.println(title + "," + date);
    }
    [/java]
    001.png
    局部变量

    [java]
    public void display() {
    System.out.println(name);
    final String title = "Hello";
    final Date date = new Date();
    System.out.println(title + "," + date);
    }
    [/java]

    final局部变量

    通过查看字节码,发现final修饰的局部变量在编译阶段就会被替换掉,有点类似C语言的宏定义,如果没有final修饰的局部变量,会在方法运行栈中多用2个指令(aload_)去存储和加载局部变量所指向的值

    12: astore_1
    13: getstatic #31 // Field java/lang/System.out:Ljava/io/PrintStream;
    16: aload_1

    所以final局部变量确实可以减少指令,提升程序的性能,但是我认为如果所有的局部变量都用final修饰,会影响代码的整洁。至于选择性能还是代码的可阅读性,要看自己的习惯了!

    欢迎大家拍砖纠正!

    参考资料
    深入理解Java中的final关键字
    浅析Java中的final关键字

    Linux 命令(3) mount

    在unix系统中,所有可以访问的文件都是一个树形的结构。unix文件系统,根目录为/。某些文件可以来源于不同的设备中。 mount命令主要是用来挂载各种各样的设备到这个树形结构上。unmount命令用来卸载对应的设备。

    命令格式为:
    mount [-fnrsvw] [-t vfstype] [-o options] device dir

    常用的命令如下:
    1.mount -t type device dir
    挂载给定的设备到目录dir。假如之前的dir有内容,挂载之后便不可见,只有卸载之后才能重新可见,所以一般挂载的目录最好是新目录
    2.mount -h | mount -v
    查看一些帮助信息,版本
    3.mount [-l] [-t type]
    列出当前挂载的所有的设备
    [bash]
    [caveup0@iZ23myrure3Z ~]$ mount -l
    /dev/xvda1 on / type ext4 (rw)
    proc on /proc type proc (rw)
    sysfs on /sys type sysfs (rw)
    devpts on /dev/pts type devpts (rw,gid=5,mode=620)
    tmpfs on /dev/shm type tmpfs (rw)
    none on /proc/xen type xenfs (rw)
    none on /proc/sys/fs/binfmt_misc type binfmt_misc (rw)
    [/bash]
    4.mount –bind olddir newdir
    可以将文件系统同时绑定到两个不同的位置。其短格式:mount -B olddir newdir 或者在 fstab 中添加如下的条目:
    /olddir /newdir none bind
    这样,就可以从两个不同的位置访问完全相同的内容了。
    甚至可以将同一个文件或目录绑定到自身(只是创建了一个挂载点而已):
    mount –bind foo foo

    上面的绑定挂载只能绑定一个单独的文件系统,而不包含其下级子目录上的文件系统。
    如果想要递归绑定整个目录树上所有的文件系统,可以使用:
    mount –rbind olddir newdir
    或者其短格式:
    mount -R olddir newdir

    使用 –bind/–rbind 绑定挂载文件系统的时候,并不能改变其原有的挂载选项。
    如果想要改变挂载选项,必须在绑定之后,再使用 remount 选项来修改:

    更详细的命令,大家可以调 man mount。 欢迎大家拍砖!