synchronized

来源:互联网 发布:无线网速监控软件 编辑:程序博客网 时间:2024/06/11 02:15

在Java程序运行环境中,JVM需要对两类线程共享的数据进行协调:
1、保存在堆中的对象实例
2、保存在方法区中的Class实例

在Java虚拟机中,每个对象和类在逻辑上都和一个监视器相关联。
(1) 对于对象来说,相关联的监视器保护对象的实例变量。
(2) 对于类来说,相关联的监视器保护类的类变量。
(3) 如果一个对象没有实例变量,或者说一个类没有变量,相关联的监视器就什么也不监视。

synchronized关键字

关于线程的同步,Java中提供了两种锁机制:synchronized和Lock。今天我们看一下synchronized的实现,最后再通过三段代码来体会下synchronized的用法。

无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象。在java中每一个对象都可以作为锁,它主要体现在下面三个方面:
(1) 对于同步方法,锁是当前实例对象。
(2) 对于同步方法块,锁是synchronized括号里配置的对象。
(3) 当作用在静态方法时锁住的便是对象对应的Class实例。

线程状态及状态转换

当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:
- Contention List:所有请求锁的线程将被首先放置到该竞争队列
- Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
- OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
- Owner:获得锁的线程称为Owner
- !Owner:释放锁的线程
- Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set

下图反映了状态转换关系:
这里写图片描述

新请求锁的线程将首先被加入到ConetentionList中。当某个拥有锁的线程(Owner状态)调用unlock之后,如果发现EntryList为空,则从ContentionList中移动线程到EntryList。下面说明下ContentionList和EntryList的实现方式:

  1. ContentionList虚拟队列
    ContentionList并不是一个真正的Queue,而只是一个虚拟队列,原因在于ContentionList是由Node及其next指针逻辑构成,并不存在一个Queue的数据结构。ContentionList是一个后进先出(LIFO)的队列,每次新加入Node时都会在队头进行,通过CAS改变第一个节点的的指针为新增节点,同时设置新增节点的next指向后续节点,而取得操作则发生在队尾。显然,该结构其实是个Lock-Free的队列。因为只有Owner线程才能从队尾取元素,也即线程出列操作无争用,当然也就避免了CAS的ABA问题。
    这里写图片描述

  2. EntryList
    EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发访问,为了降低对ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在Hotspot中把OnDeck的选择行为称之为“竞争切换”。

OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中。考虑到公平性,在EntryList中的位置不发生变化(依然在队头)。如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒,则再次转移到EntryList。

自旋锁

那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操作由操作系统完成(在Linxu下通过pthread_mutex_lock函数)。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。

缓解上述问题的办法便是自旋,其原理是:当发生争用时,若Owner线程能在很短的时间内释放锁,则那些正在争用线程可以稍微等一等(自旋),在Owner线程释放锁后,争用线程可能会立即得到锁,从而避免了系统阻塞。但Owner运行的时间可能会超出了临界值,争用线程自旋一段时间后还是无法获得锁,这时争用线程则会停止自旋进入阻塞状态(后退)。基本思路就是自旋,不成功再阻塞,尽量降低阻塞的可能性,这对那些执行时间很短的代码块来说有非常重要的性能提高。

还有个问题是,线程自旋时做些啥?其实啥都不做,可以执行几次for循环,可以执行几条空的汇编指令,目的是占着CPU不放,等待获取锁的机会。所以说,自旋是把双刃剑,如果旋的时间过长会影响整体性能,时间过短又达不到延迟阻塞的目的。

那synchronized实现何时使用了自旋锁?答案是在线程进入ContentionList时,也即第一步操作前。线程在进入等待队列时首先进行自旋尝试获得锁,如果不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平。还有一个不公平的地方是自旋线程可能会抢占了Ready线程的锁。自旋锁由每个监视对象维护,每个监视对象一个。

偏向锁

按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操作,比如线程进入ContentionList后等待线程数加1调用incrementAndGet方法。而CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用直接避免CAS操作。说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。

CAS为什么会引入本地延迟?这要从SMP(对称多处理器)架构说起,下图大概表明了SMP的结构:
这里写图片描述
所有的CPU会共享一条系统总线(BUS),靠此总线连接主存。每个核都有自己的一级缓存,各核相对于BUS对称分布,因此这种结构称为“对称多处理器”。Core1和Core2可能会同时把主存中某个位置的值Load到自己的L1 Cache中,当Core1在自己的L1 Cache中修改这个位置的值时,会通过总线,使Core2中L1 Cache对应的值“失效”,而Core2一旦发现自己L1 Cache中的值失效(称为Cache命中缺失)则会通过总线从内存中加载该地址最新的值,大家通过总线的来回通信称为“Cache一致性流量”,因为总线被设计为固定的“通信能力”,如果Cache一致性流量过大,总线将成为瓶颈。而当Core1和Core2中的值再次一致时,称为“Cache一致性”。

CAS恰好会导致Cache一致性流量,如果有很多线程都共享同一个对象,当某个Core CAS成功时必然会引起总线风暴,这就是所谓的本地延迟,本质上偏向锁就是为了消除CAS,降低Cache一致性流量。

测试

测试一

public class ThreadTest implements Runnable {    @Override    public synchronized void run() {        for (int i = 0; i < 3; i++) {            System.out.println(Thread.currentThread().getName() + "run......");        }    }    public static void main(String[] args) {        // 一共new了5个ThreadTest对象,每个线程都会持有自己线程对象的对象锁,这必定不能产生同步的效果。        for (int i = 0; i < 5; i++) {            new Thread(new ThreadTest(), "Thread_" + i).start();        }    }}

运行结果:
这里写图片描述

测试二

public class ThreadTest extends Thread {    private String lock;    private String name;    public ThreadTest(String name, String lock) {        this.name = name;        this.lock = lock;    }    @Override    public void run() {        // 这些线程的lock私有变量实际上指向的是堆内存中的同一个区域,在这里synchronized锁住的就是lock这个String对象,所以对象锁是唯一且共享的。线程同步!        synchronized (lock) {            for (int i = 0; i < 3; i++) {                System.out.println(name + " run......");            }        }    }    public static void main(String[] args) {        String lock = new String("test");        for (int i = 0; i < 5; i++) {            new ThreadTest("ThreadTest_" + i, lock).start();        }    }}

运行结果:
这里写图片描述

测试三

public class ThreadTest extends Thread {    // 对象锁就是该静态方法所在类的Class实例。不管我们创建了该类的多少实例,但是它的类实例仍然是一个。所以对象锁是唯一且共享的,线程同步!    public synchronized static void test() {        for (int i = 0; i < 3; i++) {            System.out.println(Thread.currentThread().getName() + " run......");        }    }    @Override    public void run() {        test();    }    public static void main(String[] args) {        for (int i = 0; i < 5; i++) {            new ThreadTest().start();        }    }}

运行结果:
这里写图片描述

参考
1、参考博文http://blog.csdn.net/chen77716/article/details/6618779
2、参考博文http://blog.csdn.net/chenssy/article/details/47280317

0 0