线程间的同步与通讯(1)——同步代码块Synchronized

前言

同步代码块(Synchronized Block) 是java中最基础的实现线程间的同步与通讯的机制之一,本篇咱们将对同步代码块以及监视器锁的概念进行讨论。java

系列文章目录编程

什么是同步代码块(Synchronized Block)

同步代码块简单来讲就是将一段代码用一把给锁起来, 只有得到了这把锁的线程才访问, 而且同一时刻, 只有一个线程能持有这把锁, 这样就保证了同一时刻只有一个线程能执行被锁住的代码.segmentfault

这里有两个关键字须要注意: 一段代码.并发

一段代码

通常来讲, 由 synchronized 锁住的代码都是拿{}括起来的代码块:函数

synchronized(this) {
    //由锁保护的代码
}

但值得注意的是, synchronized 也能够用来修饰一个方法, 则对应的被锁保护的一段代码很天然就是整个方法体.this

public class Foo {
    public synchronized void doSomething() {
        // 由锁保护的代码
    }
}

其实锁这个东西提及来很抽象, 你能够就把它想象成现实中的锁, 因此它只不过是一块令牌, 一把尚方宝剑, 它是木头作的仍是金属作的并不重要, 你能够拿任何东西看成锁, 重要的是它表明的含义: 谁持有它, 谁就有独立访问临界区(即上面所说的一段代码)的权利..net

在java中, 咱们能够拿一个对象看成锁.线程

这里引用<<java并发编程实战>>中的一段话:code

每一个java对象均可以用作一个实现同步的锁, 这些锁被称为内置锁(Intrinsic Lock)或者监视器锁(Monitor Lock). 线程在进入同步代码块以前会自动得到锁, 而且在退出同步代码块时自动释放锁.

得到内置锁的惟一途径就是进入由这个锁保护的同步代码块或方法.对象

因此, synchronized 同步代码块的标准写法应该是:

synchronized(reference-to-lock) {
    //临界区
}

其中, 括号里面的reference-to-lock就是锁的引用, 它只要指向一个Java对象就行, 你能够本身随便new一个不相关的对象, 将它做为锁放进去, 也能够像以前的例子同样, 直接使用this, 表明使用当前对象做为锁.

有的同窗就要问了, 咱们前面说能够用synchronized修饰一个方法, 而且也知道对应的由锁保护的代码块就是整个方法体, 可是, 它的锁是什么呢?
要回答这个问题,首先要区分synchronized 所修饰的方法是不是静态方法:

若是 synchronized所修饰的是静态方法, 则其所用的锁为 Class对象
若是 synchronized所修饰的是非静态方法, 则其所用的锁为 方法调用所在的对象

当使用synchronized 修饰非静态方法时, 如下两种写法是等价的:

//写法1
public synchronized void doSomething() {
    // 由锁保护的代码
}

//写法2
public void doSomething() {
    synchronized(this) {
        // 由锁保护的代码
    }
}

到底拿什么锁住了同步代码块

同步代码块中最难理解的部分就是拿什么做为了锁, 上面咱们已经提到了三个 this, Class对象, 方法调用所在的对象, 而且咱们也说明了能够拿任何java对象做为锁.

this方法调用所在的对象

这两个实际上是一个意思, 咱们须要特别注意的是, 一个Class能够有多个实例(Instance), 每个Instance均可以做为锁, 不一样Instance就是不一样的锁, 同一个Instance就是同一个锁, this方法调用所在的对象 指代的都是调用这个同步代码块的对象.

这么说可能比较抽象, 咱们直接上例子: (如下例子转载自博客Java中Synchronized的用法)

class SyncThread implements Runnable {
    private static int count;

    public SyncThread() {
        count = 0;
    }

    public  void run() {
        synchronized(this) {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public static void main(String[] args) {
        SyncThread syncThread = new SyncThread();
        //线程1和线程2使用了SyncThread类的同一个对象实例
        //所以, 这两个线程中的synchronized(this), 持有的是同一把锁
        Thread thread1 = new Thread(syncThread, "SyncThread1");
        Thread thread2 = new Thread(syncThread, "SyncThread2");
        thread1.start();
        thread2.start();
    }
}

运行结果:

SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9

这里两个线程SyncThread1SyncThread2 持有同一个对象syncThread的锁, 所以同一时刻, 只有一个线程能访问同步代码块, 线程SyncThread2 只有等 SyncThread1 执行完同步代码块后, SyncThread1线程自动释放了锁, 随后 SyncThread2才能获取同一把锁, 进入同步代码块.

咱们也能够修改一下main函数, 让两个线程持有同一Class对象的不一样实例的锁:

public static void main(String[] args) {
    Thread thread1 = new Thread(new SyncThread(), "SyncThread1");
    Thread thread2 = new Thread(new SyncThread(), "SyncThread2");
    thread1.start();
    thread2.start();
}

上面这段等价于:

public static void main(String[] args) {
    SyncThread syncThread1 = new SyncThread();
    SyncThread syncThread2 = new SyncThread();
    Thread thread1 = new Thread(syncThread1, "SyncThread1");
    Thread thread2 = new Thread(syncThread2, "SyncThread2");
    thread1.start();
    thread2.start();
}

运行结果:

SyncThread1:0
SyncThread2:1
SyncThread1:2
SyncThread2:3
SyncThread1:4
SyncThread2:5
SyncThread1:6
SyncThread2:7
SyncThread1:8
SyncThread2:9

可见, 两个线程此次都能访问同步代码块, 这是由于线程1执行的是syncThread1对象的同步代码块, 线程2执行的是syncThread2的同步代码块, 虽然这两个同步代码块同样, 可是他们在不一样的对象实例里面, 即虽然它们都用this做为锁, 可是this指代的对象在这两个线程中不是同一个对象, 两个线程各自都能得到锁, 所以各自都能执行这一段同步代码块.

这告诉咱们, 当一段代码用同步代码块包起来的时候, 并不绝对意味着这段代码同一时刻只能由一个线程访问, 这种状况只发生在多个线程访问的是同一个Instance, 也就是说, 多个线程请求的是同一把锁.

再回顾咱们上面两个例子, 第一个例子中, 两个线程使用的是同一个对象实例, 他们须要同一把对象锁 syncThread,
第二个例子中, 两个线程分别使用了一个对象实例, 他们分别请求的是本身访问的对象实例的锁syncThread1, syncThread2, 所以都能访问同步代码块.

致使不一样线程能够同时访问同步代码块的最根本缘由就是咱们使用的是当前实例对象锁(this), 由于类的实例能够有多个, 这致使了同步代码块散布在类的多个实例中, 虽然同一个实例中的同步代码块只能由持有锁的单个线程访问(this对象锁保护), 可是咱们能够每一个线程访问本身的对象实例, 而每个对象实例的同步代码块都是一致的, 这就间接致使了多个线程同时访问了"同一个"同步代码块.

上面这种状况在某些条件下是没有问题的, 例如同步代码块中不存在对静态变量(共享的状态量)的修改.

可是, 对于上面的例子, 这样的状况明显违背了咱们加同步代码块的初衷.

要解决上面的状况, 一种可行的办法就是像第一个例子同样, 多个线程使用同一个对象实例, 例如在单例模式下, 自己就只有一个对象实例, 因此多个线程必将请求同一把锁, 从而实现同步访问.

另外一种方法就是咱们下面要讲的: 使用Class锁.

使用Class级别锁

前面咱们提到:

若是 synchronized所修饰的是静态方法, 则其所用的锁为 Class对象

这是由于静态方法是属于类的而不属于对象的, 所以synchronized修饰的静态方法锁定的是这个类的全部对象。咱们来看下面一个例子(如下例子一样转载自博客Java中Synchronized的用法):

class SyncThread implements Runnable {
    private static int count;

    public SyncThread() {
        count = 0;
    }

    // synchronized 关键字加在一个静态方法上
    public synchronized static void staticMethod() {
        for (int i = 0; i < 5; i ++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void run() {
        staticMethod();
    }

    public static void main(String[] args) {
        SyncThread syncThread1 = new SyncThread();
        SyncThread syncThread2 = new SyncThread();
        Thread thread1 = new Thread(syncThread1, "SyncThread1");
        Thread thread2 = new Thread(syncThread2, "SyncThread2");
        thread1.start();
        thread2.start();
    }
}

运行结果:

SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9

可见, 静态方法锁定了类的全部对象, 用咱们以前的话来讲, 若是说"由于类的实例能够有多个, 这致使了同步代码块散布在类的多个实例中", 那么类的静态方法就是阻止同步代码块散布在类的实例中, 由于类的静态方法只属于类自己.

其实, 上面的例子的本质就是拿Class对象做为锁, 咱们前面也提到了, 能够拿任何对象做为锁, 若是咱们直接拿类的Class对象做为锁, 一样能够保证因此线程请求的都是同一把锁, 由于Class对象只有一个.

类锁其实是经过对象锁实现的,即类的 Class 对象锁。每一个类只有一个 Class 对象,因此每一个类只有一个类锁。
class SyncThread implements Runnable {
    private static int count;

    public SyncThread() {
        count = 0;
    }

    public void run() {
        // 这里直接拿Class对象做为锁
        synchronized(SyncThread.class) {
            for (int i = 0; i < 5; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        SyncThread syncThread1 = new SyncThread();
        SyncThread syncThread2 = new SyncThread();
        Thread thread1 = new Thread(syncThread1, "SyncThread1");
        Thread thread2 = new Thread(syncThread2, "SyncThread2");
        thread1.start();
        thread2.start();
    }
}

这样所获得的结果与上面的类的静态方法加锁是一致的。

几点补充

其实到这里, 重要的部分已经讲完了, 下面补充说明几点:

(1) 当一个线程访问对象的一个synchronized(this)同步代码块时,另外一个线程仍然能够访问该对象中的非synchronized(this)同步代码块。

这个结论是显而易见的, 在没有加锁的状况下, 全部的线程均可以自由地访问对象中的代码, 而synchronized关键字只是限制了线程对于已经加锁的同步代码块的访问, 并不会对其余代码作限制.
这里也提示咱们:

同步代码块应该越短小越好

(2) 当一个线程访问object的一个synchronized(this)同步代码块时,其余线程对object中全部其它synchronized(this)同步代码块的访问将被阻塞。

这个结论也是显而易见的, 由于synchronized(this)拿的都是当前对象的锁, 若是一个线程已经进入了一个同步代码块, 说明它已经拿到了锁, 而访问同一个object中的其余同步代码块一样须要当前对象的锁, 因此它们会被阻塞.

(3) synchronized关键字不能继承。

对于父类中用synchronized 修饰的方法,子类在覆盖该方法时,默认状况下不是同步的,必须显式的使用 synchronized 关键字修饰才行, 固然子类也能够直接调用父类的方法, 这样就间接实现了同步.

(4) 在定义接口方法时不能使用synchronized关键字。
(5) 构造方法不能使用synchronized关键字,但可使用synchronized代码块来进行同步。
(6) 离开同步代码块后,所得到的锁会被自动释放。

总结

  • synchronized关键字经过一把锁住一段代码, 使得线程只有在持有锁的时候才能访问这段代码
  • 任何java对象均可以做为这把锁
  • 能够在synchronized后面用()显式的指定锁. 也能够直接做用在方法上

    • 做用于普通方法时, 至关于以this对象做为锁, 此时同步代码块散布于类的全部实例中, 每个实例的同步代码块的锁 为该实例对象自身。
    • 做用于静态方法时, 至关于以Class对象做为锁, 此时对象的全部实例只能争抢同一把锁。
  • 内置锁的一个重要的特性是当离开同步代码块以后, 会自动释放锁,而其余的高级锁(如ReentrantLock)须要显式释放锁。

思考题

前面咱们说明了synchronized 的使用方法,但对一些底层的细节并不了解,如:

  1. 前面说“得到内置锁的惟一途径就是进入由这个锁保护的同步代码块或方法.”, 这句话看上去颇有道理,实际上是废话,同步代码块到底是怎么得到锁的?
  2. 咱们说,JAVA中任何对象均可以做为锁,那么锁信息是怎么被记录和存储的?
  3. 为何代码离开了同步代码块锁就被释放了,谁释放了锁,怎样叫释放了锁?

这些问题,咱们后续的文章再研究。

(完)

查看更多系列文章:系列文章目录

相关文章
相关标签/搜索