java并发编程之一

来源:互联网 发布:手机版淘宝盗图技巧 编辑:程序博客网 时间:2024/06/04 23:25

自旋锁与“自旋”:何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,

它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保

持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥

锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁

已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词

就是因此而得名。

死锁。试图递归地获得自旋锁必然会引起死锁:递归程序的持有实例在第二个实例循环,以试图获得相同

自旋锁时,不会释放此自旋锁。在递归程序中使用自旋锁应遵守下列策略:递归程序决不能在持有自旋锁

时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。此外如果一个进程已经将资源锁定,那么,

即使其它申请这个资源的进程不停地疯狂“自旋”,也无法获得资源,从而进入死循环。

过多占用cpu资源。如果不加限制,由于申请者一直在循环等待,因此自旋锁在锁定的时候,如果不成功,不
会睡眠,会持续的尝试,单cpu的时候自旋锁会让其它process动不了. 因此,一般自旋锁实现会有一个参
数限定最多持续尝试次数. 超出后, 自旋锁放弃当前time slice. 等下一次机会。
所以也可以得出“自旋”一词,指的是如果获取某个有限资源不成功时,不马上进入睡眠或者阻塞而
放弃当前时间片,而是去尝试再次获取n(n>=1)次.

CAS操作:Compare and Swap,比较并操作,CPU指令,在大多数处理器架构,包括IA32、Space中采

用的都是CAS指令,CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告

诉V的值实际为多少”,CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中

一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,

并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值

V相同时,将内存值V修改为B,否则什么都不做。

悲观锁和乐观锁

独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会

造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有

效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失

败就重试,直到成功为止。

线程中断

中断设置线程对应的中断标志位,cpu运行期间会定时去查看各线程的中断标志位,如果某线程对应的中断标

志位被置位,且该线程此时处于阻塞状态,则会将该线程从阻塞状态变为就绪状态。如果被中断时,不是阻塞状

态,则不会发生什么,但是标志位还是会被置位的。

调用Thread#interrupt()会去将中断标志位置位。

使用Thread#isInterrupted( )查询标志位是否被置位了。

使用Thread#interrupted()查询中断标志位,并将其复位,即清除标志位

如果一个线程处于了阻塞状态(如线程调用了thread.sleep、thread.join、thread.wait、1.5中的condition.await、

以及可中断的通道上的 I/O 操作方法后可进入阻塞状态),则在线程在检查中断标示时如果发现中断标示为

true,则会在这些阻塞方法(sleep、join、wait、1.5中的condition.await及可中断的通道上的 I/O 操作方法)调用

处抛出InterruptedException异常,并且在抛出异常后立即将线程的中断标示位清除,即重新设置为false。抛

出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。

其实并不是所有阻塞都会因中断而抛出异常,其实也可以不抛出异常,即在阻塞时不响应中断,那这是怎么做

到的呢。可以看AbstractQueuedSynchronizer#acquire(int arg),这个方法不响应中断,可以看看其内部实现

public final void acquire(int var1) {        if(!this.tryAcquire(var1) && this.acquireQueued(this.addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), var1)) {            selfInterrupt();        }    }final boolean acquireQueued(AbstractQueuedSynchronizer.Node var1, int var2) {        boolean var3 = true;        try {            boolean var4 = false;            while(true) {                AbstractQueuedSynchronizer.Node var5 = var1.predecessor();                if(var5 == this.head && this.tryAcquire(var2)) {//退出循环的条件                    //...                    return var6;                }                if(shouldParkAfterFailedAcquire(var5, var1) && this.parkAndCheckInterrupt()) {//阻塞                    var4 = true;                }            }        } finally {           //...        }    }
主动阻塞是在parkAndCheckInterrupt( )中执行的

private final boolean parkAndCheckInterrupt() {        LockSupport.park(this);//此方法用于阻塞当前线程,调用unpark(Thread)或中断该线程会从该方法返回        return Thread.interrupted();//清除中断    }
由上面方法可以知道,阻塞线程,不响应中断(其实是被唤醒后继续阻塞,只是外部看起来像对中断没反应

一样),是因为被中断从park( )返回后,没有抛出异常,而是返回中断值并对中断标志位复位。对于parkAnd-

CheckInterrupt( )方法返回的中断标志位值,parkAnd-CheckInterrupt( )的调用者(acquireQueued(...))可以

作出响应的,但是并没有。

所以可以知道,调用使线程阻塞的基础工具方法(首先明确的是sleep,join,wait这些都不属于单纯的阻塞

方法)后,线程都会因中断而被变为就绪状态,从而有机会获得cpu执行权,从而从阻塞方法中返回。而sleep

等方法,在从阻塞状态因中断返回后,抛出InterruptedException,只是为了引起注意,让sleep的调用者去

处理这个中断。

而synchronized关键字的实现,是不响应中断的,就是从中断返回继续进入阻塞状态,直到获得锁为止。

synchronized在获锁的过程中是不能被中断的,意思是说如果产生了死锁,则不可能被中断。与synchr-

onized功能相似的reentrantLock.lock()方法也是一样,它也不可中断的,即如果发生死锁,那么reentra-

ntLock.lock()方法无法终止,如果调用时被阻塞,则它一直阻塞到它获取到锁为止。但是如果调用带超时

的tryLock方法reentrantLock.tryLock(long timeout, TimeUnit unit),那么如果线程在等待时被中断,将抛出

一个InterruptedException异常,这是一个非常有用的特性,因为它允许程序打破死锁。你也可以调用reentr-

antLock.lockInterruptibly()方法,它就相当于一个超时设为无限的tryLock方法。


synchronized关键字

同步块:synchronized(lock){  }中,锁是lock对象的monitor。在编译的时候,会解析成在同步代码块开始处

调用monitor-enter指令,而在结束处和异常处加入monitorexit指令,JVM要保证每个monitorenter必须有

对应的monitorexit与之配对。线程执行到monitorenter时,将会尝试获取对象所对应的monitor的所有权,

即获得对象的锁。
同步方法:对于同步方法,锁是当前实例对象;而对于静态同步方法,锁是当前类Class对象(JVM全局单例)

monitor对应有一个同步队列和一个等待。在执行monitorenter获取锁时失败,则会进入同步队列,同步队列

中的节点对应的线程在一个持有锁的线程释放锁后,队列头有机会(队列头线程获得cpu执行权时)获得锁。

而在线程调用锁对应的对象的wait()方法时,进入等待队列,队列中的线程在其他线程释放锁时,不会去也

没有机会获取到锁,需要被唤醒(其他线程调用notify()或notifyAll()方法),唤醒后进入同步队列,有机会获

得锁。调用wait()方法后,在获得锁后或者被interrupt才返回。在这两个队列时,线程分别是BLOCKED和

WAITING状态。调用wait()后,线程从RUNNING状态变为WAITING状态。

等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量

作出的修改。

注意: wait()和notify()需要在获得该方法对应的对象的锁,才可以调用,否则抛异常。

等待/通知经典范式:

等待方遵循如下原则:

1.获取对象的锁

2.如果条件不满足,那么调用对象的wai()方法,被通知并获得锁后仍检查条件

3.条件满足则执行对应的逻辑

伪代码如下:

synchronized(object){

while(condition not enough){

    object.wait();

    }

execute process logic

}

通知方遵循如下原则:

1.获得对象的锁

2.改变条件

3.通知所有等待在对象上的线程

伪代码如下:

synchronized(object){

change condition

object.notifyAll();

}


JAVA中的显式锁(synchronized关键字对应的锁为隐式锁)

Lock接口

synchronized关键字有隐式获取并释放锁的便捷性,而Lock接口实现的锁功能是需要显式地获取和释放

锁,拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字不具备

的同步特性。synchronized的优点在于简化了同步的管理,可是拓展性不如Lock好。针对一个场景,先获得

锁A,然后再获得锁B,当锁B获得后,释放锁A同时获取锁C,当锁C获得后,再释放B同时获取锁D,以此类推。

这种场景下,synchronized关键字就不那么容易实现了,而使用Lock却容易许多。

Lock lock  = new ReentrantLock();

lock.lock();

try{

}finally{

   lock.unlock();

}

要在finally中释放锁,目的是保证在获取锁之后,最终能够被释放。

不要将获取锁的过程写在try中,因为如果在获取锁(自定义锁实现)时发生了异常,异常抛出的同时,也会

导致锁无故释放,当锁没有获取到,而使用了释放,会出现一些严重不可预知的后果。

Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的。

队列同步器AbstractQueuedSynchronizer

队列同步器是实现锁的基础框架,它使用了一个int成员变量表示同步状态,就是对一个变量进行CAS操作,

保证同步,查询同步状态,若状态值显示可以获得锁则修改同步状态,若不可获得锁,则通过内置的FIFO队

列来完成资源获取线程的排队工作。

获取锁,释放锁,其实就是改变同步状态,就那个int成员的值。可以自定义int成员在不同值下的意义。

操作这个值的使用到了AbstractQueuedSynchronized的三个方法,分别是

private volatile int state;//就是那个代表同步状态的值

getState()//获取state的值

setState(int)//设置state的值

compareAndSetState(int expect, int update)//是否等于expect,如果是,就将值置为update,并返回

                    //true,否则返回false。

利用AbstractQueuedSynchronized可以实现共享式获取同步状态和独占式获取同步状态。共享式指的

是这个锁对应n(n>1)个资源,允许n个线程同时访问这个锁。其实实现起来就是,在有n个线程同时访问

这个锁时,再有其他线程想要访问时,就返回一个值告诉他让其排队。独占锁的实现,就是state拥有两个

合法值,一个表示锁被持有了,一个表示可以获取锁。

Lock接口是面对锁用户的,但是AbstractQueuedSynchronized是实现Lock接口的基础框架,一般实现

Lock时,会在该Lock子类中实现一个AbstractQueuedSynchronized。而同步器(AbstractQueued-

Synchronized)的设计是基于模板方法模式的,就是重写那些在模板方法中调用的方法。

AbstractQueuedSynchronized中可重写,并在模板方法中调用的方法:

* boolean tryAcquire(int arg)//获取同步状态,成功返回true,失败返回false

* boolean tryRelease(int arg)//独占式释放同步状态

* int tryAcquireShared(int arg)//共享式获取同步状态,返回大于0的值,表示获取成功,反之失败

* boolean tryReleaseShared(int arg)//共享式释放同步状态

* boolean isHeldExclusively()//当前同步器是否在独占模式i啊被线程占用,一般该方法表示是否被当前

             线程所独占,用于实现重入

同步器(AbstractQueuedSynchronized)提供的模板方法

void acquire(int arg)//独占式获取同步状态,成功则返回,否则进入同步队列,该方法会调用tryAcquire()

void acquireInterruptibly(int arg)//与acquire()相同,该方法响应中断,就是在同步队列中,如果当前线程被

              //中断,则该方法会抛出InterruptedException并返回,对应的Lock#

              //lockInterruptibly()方法

boolean tryAcquireNanos(int arg, long nanos)//在acquireInterruptibly(int arg)基础上增加了超时限制

void acquireShared(int arg)//共享式获取同步状态,

void acquireSharedInterruptibly(int arg)//该方法响应中断

void tryAcquireSharedNanos(int arg,long nanos)//增加了超时限制

boolean release(int arg)//独占式释放同步状态,将同步队列中的第一个节点包含的线程唤醒

boolean releaseShared(int arg)//共享式的释放同步状态

Collection<Thread> getQueuedThreads()//获取等待在同步队列的线程集合

同步队列介绍:

同步器依赖的同步队列(一个FIFO双向队列)来完成同步状态管理,同步器包含了两个节点类型引用,一个

指向头节点,而另一个指向尾节点。将节点加入队列的过程可能有多个线程同时进行,需要保证线程安全,

因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect, Node update),

它需要传递当前线程认为的尾节点和当前节点,只有设置成功后,当前节点才正式与尾节点建立关联,代

码如下:

 private Node addWaiter(Node mode) {        Node node = new Node(mode);        for (;;) {//进入一个死循环,自旋,直到设置成功为止。            Node oldTail = tail;            if (oldTail != null) {                U.putObject(node, Node.PREV, oldTail);                if (compareAndSetTail(oldTail, node)) {                    oldTail.next = node;                    return node;                }            } else {                initializeSyncQueue();            }        }    }
首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将

会在获取同步状态成功时将自己设置为首节点,因为设置头节点是由刚获得锁的线程来完成的,所以不需

要额外的CAS来保证线程安全。

独占式同步状态获取与释放

* 同步状态获取

AbstractQueuedSynchronized的不响应中断,独占式获取锁方法

public final void acquire(int var1) {        if(!this.tryAcquire(var1) && this.acquireQueued(this.addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), var1)) {            selfInterrupt();        }    }
此模板方法调用了可重写方法tryAcquire(),自定义队列同步器时,可根据需要自定义决定获取同步状

态是否成功的逻辑。tryAcquire()是非阻塞式的获取,因为获取失败只是返回一个false,而没有马上选

择阻塞,是否阻塞由调用tryAcquire()方法的模板方法决定。
AbstractQueuedSynchronized#acquireQueued(...)

final boolean acquireQueued(AbstractQueuedSynchronizer.Node var1, int var2) {        boolean var3 = true;        try {            boolean var4 = false;            while(true) {//死循环直到获得锁为止,不会因为中断而退出循环                AbstractQueuedSynchronizer.Node var5 = var1.predecessor();                if(var5 == this.head && this.tryAcquire(var2)) {//只有前驱节点是队列头才去尝试获取锁                    this.setHead(var1);                    var5.next = null;                    var3 = false;                    boolean var6 = var4;                    return var6;                }                if(shouldParkAfterFailedAcquire(var5, var1) && this.parkAndCheckInterrupt()) {                    var4 = true;                }            }        } finally {            if(var3) {                this.cancelAcquire(var1);            }        }    }
阻塞操作是在parkAndCheckInterrupt()中进行的

private final boolean parkAndCheckInterrupt() {        LockSupport.park(this);//该方法让线程阻塞,调用unpark或中断可返回,并不抛异常        return Thread.interrupted();//返回是否被中断的,并清除中断标志位    }
结合parkAndCheckInterrupt()和acquireQueued(...)对parkAndCheckInterrupt()返回结果的处理,可以得

出这样的结论,acquireQueued(...)中的循环不因中断而推出,所以调用acquireQueued(...)方法去获取锁

是不能响应中断,即不能让acquireQueued(...)的调用者感知到该线程曾经被中断过,中断后不留任何痕

迹,连标志位都擦除了。
* 同步状态释放

public final boolean release(int var1) {        if(this.tryRelease(var1)) {            AbstractQueuedSynchronizer.Node var2 = this.head;            if(var2 != null && var2.waitStatus != 0) {                this.unparkSuccessor(var2);            }            return true;        } else {            return false;        }    }

private void unparkSuccessor(AbstractQueuedSynchronizer.Node var1) {        int var2 = var1.waitStatus;        if(var2 < 0) {            compareAndSetWaitStatus(var1, var2, 0);        }        AbstractQueuedSynchronizer.Node var3 = var1.next;        if(var3 == null || var3.waitStatus > 0) {            var3 = null;            for(AbstractQueuedSynchronizer.Node var4 = this.tail; var4 != null && var4 != var1; var4 = var4.prev) {                if(var4.waitStatus <= 0) {                    var3 = var4;                }            }        }        if(var3 != null) {            LockSupport.unpark(var3.thread);        }    }


该方法执行会唤醒节点的后继节点线程,unsparkSuccessor(Node node)方法使用LockSupport.unpark()

来唤醒处于等待状态的线程。
共享式同步状态获取与释放

* 同步状态获取

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同一时刻能否有多个线程同时获取

到同步状态。

对应到AbstractQueuedSynchronized中的是acquireShared(int arg)和releaseShared(int arg)

AbstractQueuedSynchronized#acquireShared(int arg)和doAcquireShared(int var1)

public final void acquireShared(int var1) {        if(this.tryAcquireShared(var1) < 0) {            this.doAcquireShared(var1);        }    }private void doAcquireShared(int var1) {        AbstractQueuedSynchronizer.Node var2 = this.addWaiter(AbstractQueuedSynchronizer.Node.SHARED);        boolean var3 = true;        try {            boolean var4 = false;            while(true) {                AbstractQueuedSynchronizer.Node var5 = var2.predecessor();                if(var5 == this.head) {                    int var6 = this.tryAcquireShared(var1);                    if(var6 >= 0) {                        this.setHeadAndPropagate(var2, var6);                        var5.next = null;                        if(var4) {                            selfInterrupt();                        }                        var3 = false;                        return;                    }                }                if(shouldParkAfterFailedAcquire(var5, var2) && this.parkAndCheckInterrupt()) {                    var4 = true;                }            }        } finally {            if(var3) {                this.cancelAcquire(var2);            }        }    }
看doAcquireShared(int var1)可以看出,acquireShared()和acquire()唯一不同的是一个是调用

tryAcquireShared()一个是tryAcquire()。

* 同步状态释放

public final boolean releaseShared(int var1) {        if(this.tryReleaseShared(var1)) {            this.doReleaseShared();            return true;        } else {            return false;        }    }private void doReleaseShared() {        while(true) {//循环,自旋直到释放成功为止            AbstractQueuedSynchronizer.Node var1 = this.head;            if(var1 != null && var1 != this.tail) {                int var2 = var1.waitStatus;                if(var2 == -1) {                    if(!compareAndSetWaitStatus(var1, -1, 0)) {//作CAS操作                        continue;                    }                    this.unparkSuccessor(var1);                } else if(var2 == 0 && !compareAndSetWaitStatus(var1, 0, -3)) {                    continue;                }            }            if(var1 == this.head) {                return;            }        }    }
tryrelease()和tryReleaseShared()都是对同步状态值操作的,而操作完后,还要唤醒后继节点对应的

线程。共享式和独占式的同步状态释放过程区别较大,其中独占式的释放是不需要CAS操作的,包括对

state成员的操作和唤醒同步队列中的线程,原因前面说过,就是释放操作是获取到锁的,这样就可以保证

的同步。而共享式操作state和唤醒同步队列中的线程都需要作CAS操作的。

AbstractQueuedSynchronized中的可重写方法的实现举例:

1.独占式同步器

private static class Sync extends AbstractQueuedSynchronizer{    //是否处于占用状态    protected boolean isHeldExclusively(){    return getState() == 1;    }        //当状态为0的时候获取锁    public boolean tryAcquire(int acquires){    if(compareAndSetState(0, 1)){    setExclusiveOwnerThread(Thread.currentThread());    return true;    }    }        //释放锁,将状态置为0    protected boolean tryRelease(int release){    if(getState() == 0)    throw IllgalMonitorStateException();    setExclusiveOwnerThread(null);    setState(0);    return true;    }                //返回一个condition    Condition newCondition(){    return new ConditionObject();    }    }
2.共享式同步器

private static final class Sync extends AbstractQueuedSynchronizer{    Sync(int count){    if(count <= 0){    throw new IllegalArgumentException("count must large than zero");    }    setState(count);//初始化资源数    }    public int tryAcquireShared(int reduceCount){    while(true){    int current = getState();    int newCount = current - reduceCount;    if(newCount < 0 || compareAndSetState(current, newCount)){    return newCount;    }    }    }        public boolean tryReleaseShared(int returnCount){    while(true){    int current = getState();    int newCount = current + returnCount;    if(compareAndSetState(current, newCount)){    return true;    }    }    }    }
一般锁实现Lock接口时,会在内部类中实现AbstractQueuedSynchronizer,并依赖该同步器去实现Lock

接口方法


原创粉丝点击