java并发编程实战总结2

一、显示锁 1. Lock 和 ReentrantLock

(1) 简述:与内置锁机制不同的是,Lock提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁方法都是显示的。

public interface Lock {
    //获取锁
    void lock();
    //如果当前线程未被中断,则获取锁。
    void lockInterruptibly() throws InterruptedException;
    //仅在调用时锁为空闲状态才获取锁
    tryLock();
    //如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。
    tryLock(long time,TimeUnit unit);
    //返回绑定此Lock实例的新Condition实例。
    Condition newCondition();
}

ps:lock方法获取不到锁则阻塞,tryLock获取不到锁不阻塞直接返回false。

(2) lock 编码规范:

Lock lock = new Reentrantlock();
...
lock.lock();
try {
    ...
} finally {
    lock.unlock();
}

ps:在使用lock是,比较好的做法是在finally代码块里释放锁,如果没有释放锁很可能程序运行不下去,造成死锁。这也是使用synchronized比使用lock简单的一个点。

(3) 定时锁和轮询锁:
简述:在使用内置锁是,锁一旦获取是不能取消的,在死锁时,恢复程序的唯一方法是重新启动应用程序,而防止死锁的唯一方法就是在构造程序时避免出现不一致的锁顺序。可定时的与可轮询的锁提供了另一种选择:避免死锁的发生。如果不能获得所有需要的锁,那么可以使用定时的或可轮询的锁获取方式,从而使你重新获得控制权,它会释放已经获得的锁,然后重新尝试获取所有锁。
一个轮询锁避免死锁的示例:

public boolean transferMoney(Account fromAcct,Account toAcct,DollarAmount amount,long timeout,TimeUnit unit) throws InsufficientFundsException,InterruptedException{
    ...

while(true) {
    if(fromAcct.lock.tryLock()) {
        try {
            if(toAcct.lock.tryLock()) {
                ..
            } 
        } finally {
                toAcct.lock.unlock();
        }
    } finally {
        fromAcct.lock.unlock();
    }
}

ps:要获取就同时获取两个锁,如果获取不到,则释放锁其中一个锁,然后重新获取两个锁。

一个定时锁的示例:

public boolean trySendOnSharedLine(String message,long timeout,TimeUnit unit) throws InterruptedException {
    long nanosToLock = unit.toNanos(timeout) - estimatedNanosToSend(message);
    if(!lock.tryLock(nanosToLock,NANOSSENCONDS)) {
        return false;
    }
    try {
        return sendOnSharedLine(message);
    } finally {
        lock.unlock();
    }
}

ps:定时的tryLock如果一段时间获取不到锁则自己中断阻塞。

(4) 可中断的锁获取的方式:
简述:lockInterruptibly方法能够在获得锁的同时保持对中断的响应,并且由于它包含在Lock中,因此无法创建其他类型的不可中断的阻塞机制。定时的tryLock同样能响应中断,因此当需要实现一个定时的和可中断的锁取消操作时,可以使用tryLock方法。
一个可中断的获取操作实例:

public boolean sendOnSharedLine(String message) throws InterruptedException {
    lock.lockInterruptibly();
    try{
        return cancellableSendOnSharedLine(message);
    } finally {
        lock.unLock();
    }
}

private boolean cancellableSendSharedLine(String message) throws InterruptedException {...}

ps:可以调用线程的中断方法,中断等待在该锁上的线程。

  1. 内置锁性能的提升:
    java6 使用了改进后的算法来管理内置锁,与在ReentrantLock中使用的算法类似,该算法有效的提高了可伸缩性。java6中在竞争激烈的情况下,内置锁sychronized代码块和ReentrantLock的性能基本接近。

  2. 锁的公平性和非公平性:
    在公平性的锁中,如果有另一个线程持有这个锁或者有其他线程在队列中等待这个锁,那么新发出请求的线程将被放入队列。在非公平性锁中,如果发出请求的同时锁变为可用状态,那么这个线程将跳过队列中等待的线程直接获得锁(插队),只有当锁被某个线程持有时,新发出的请求的线程才会被放入队列中。
    ps:(1)大多数情况下,非公平性锁的性能要高于公平性锁的性能。其主要原因是在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。(2)当持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么应该使用公平锁,在这些情况下,“插队”带来的吞吐量则不显著。

  3. synchronized和ReentrantLock之间的选择:
    ReentrantLock在加锁和内存上提供的语义与内置锁相同,此外它还提供了一些其他功能,包括定时等待,可中断的锁等待,公平性,以及实现非块结构的加锁。然而ReentrantLock的危险性比同步机制要高,如果忘记在finally块中调用unlock,那么虽然代码表面上正常运行,但实际上已经埋下了一颗定时炸弹,并可能伤及其他代码。仅内置锁不能满足需求时,才可以考虑使用ReentrantLock.
    ps:在一些内置锁无法满足需求时,ReentrantLock可以作为一种高级工具,当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还应该优先使用synchronized。ReentrantLock的非块结构仍然意味着,获取锁的操作不能与特定的栈帧关联起来,而内置锁则可以。

  4. 读写锁:
    ReentrantLock实现了一种标准的互斥锁,每次最多只有一个线程能持有ReentrantLock,但对于维护数据的完整性来说,互斥通常是一种过硬的加锁规则,因此也就不必要的限制了并发性。互斥是一种保守的加锁策略,虽然可以避免“写写”冲突和“读写”冲突,但同样也避免了“读读”冲突。因此读写锁则允许读读操作,一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。

    public interface ReadWriteLock {
        Lock readLock();
        Lock wirteLock();
    }
    

ps:(1)尽管这两个锁看上去是彼此独立的,但是读取锁和写入锁只是读-写对象的不同视图。(2)在实际情况中,对于多处理器系统被频繁读取的数据结构,读-写锁能提高性能。而在其他情况下,读-写锁的性能要被独占锁要差,因为读写锁的实现成本更高。
读写锁实现map的并发性:

public class ReadWriteMap<K,V> {
    private final Map<K,V> map;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock r = lock.readLock();
    private lock w = lock.writeLock();

    public ReadWriteMap(Map<K,V> map) {
        this.map = map;
    }

    public V put(K key, V value) {
        w.lock();
        try {
            return map.put(key,value);
        } finally {
            w.unlock();
        }
    }

    public V get(Object key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }
}

6.条件队列:
传统的队列的元素是一个个的数据,而与之不同的是,条件队列中的元素是一个个正在等待相关条件的线程。正如某个java对象都可以作为一个锁,每个对象同样可以作为作为一个条件队列,并且Object中的wait、notify和notify All方法就构成了内部队列的API。对象的内置锁与其内部条件队列是相互关联的,要调用对象X中的条件队列的任何一个方法,必须持有对象X上的锁。
ps:(1)Object.wait会自动释放锁,并请求操作系统挂起当前线程。(2) 锁对象和条件队列对象必须是同一个对象。

7.notify 和 notifyAll:
在条件队列API中有两个发出通知的方法,即 notify和 notifyAll。无论调用哪一个,都必须持有与条件队列对象相关联的锁。notify操作会选在条件队列中的一个线程唤醒,其他线程则无法得到唤醒通知。而notifyAll则会唤醒所有等待在条件队列中的线程来竞争锁。使用notify单一的同时可能发生信号丢失问题。
ps:普遍认可的做法是优先使用notifyAll而不是notify。虽然notifyAll可能比notify更低效,但却更容易确保类的行为是正确的。

8.Lock 和 Condition:
一个Condition和一个Lock关联在一起,就像一个条件队列和一个内置锁关联在一起一样。要创建一个Condition,可以在相关联的Lock上调用 Lock.newCondition方法。正如Lock比内置加锁提供了更为丰富的功能,Condition同样比内置队列提供了更丰富的功能:每个锁上可存在多个等待,条件等待可以是可中断或不可中断的、基于时限的等待,以及公平的或非公平的队列操作。与内置条件队列不同的是,对于每个Lock,可以由任意数量的Condition对象。Condition对象继承了先关Lock的公平性,对于公平的锁,线程依然会按照FIFO的顺序从Condition.await中释放。
使用显示条件变量的有界缓存:

public class ConditionBoundedBuffer<T> {
    protected final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Conditino notEmpty = lock.newCondition();
    private final T[] items = (T[])new Object(BUFFER_SIZE);

    public void put(T x) throws InterruptedException {
        lock.lock();
        try {
            while(count == items.length) {
                notFull.await();
            }
            items[tail]  = x;
            if(++tail == items.length) {
                tail = 0;
            }
            ++count;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while(count == 0) {
                notEmpty.await();
            }
            T x = items[head];
            items[head] = null;
            if(++head == items.length) {
                head = 0;
            }
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

ps:在使用显示的Condition和内置条件队列之间进行选择时,与在ReentrantLock和synchronized之间进行选择时一样的:如果需要一些高级功能,例如使用公平队列的队列操作或者在每个锁上对应多个等待线程集,那么应该优先使用Condition而不是内置队列。

9.AbstractQueuedSynchronizer:

AQS负责管理同步类中的状态,它管理了一个整数状态信息,可以通过getState,setState以及compareAndSetState等protected类型方法类进行操作。这个整数可以用于表示任意状态。例如ReentrantLock用它来表示所有线程已经重复获取该锁的次数,Semaphore用它来表示剩余许可的数量,FutrueTask用它来表示任务的状态(尚未开始,正在运行,已经完成或已取消)。

10.硬件对并发的支持:
在针对多处理器操作而设计的处理器中提供了一些特殊指令,用于管理对共享数据的并发访问。在早期的处理器中支持原子的测试并设置(Test-and-Set),获取并递增(Fetch-and-Increment)以及交换(Swap)等指令。这些指令足以实现各种互斥体,而这些互斥体又可以实现一些复杂的并发对象。现在,几乎所有现代处理器中都包含了某种形式的原子 读-改-写指令,例如比较并交换(Compare-and-Swap)或者关联加载/条件存储(Load-Linked/Store-Conditional)。操作系统和JVM使用这些指令来实现锁和并发的数据结构,但在java5.0之前,在java类中还不能直接使用这些指令。

11.CAS (Compare-and-Swap) :
基于CAS实现非阻塞技术器:

public class CasCounter {
    private SimulatedCAS value;

    public int getValue() {
        return value.get();
    }

    public int increment() {
        int v;
        do {    
            v = value.get();
        }
        while(v != value.compareAndSwap(v,v+1));
        return v+1;
    }
}

ps:初看起来,基于CAS的技术器似乎比基于锁的计数器在性能上更差一些,因为它需要执行更多的操作和更复杂的控制流,并且还依赖似乎复杂的CAS操作。但实际上,当竞争程度不高时,基于CAS的技术器在性能上远远超过基于锁的计数器,而在没有竞争时甚至更高。

12.非阻塞算法:

如果某种算法中,一个线程的失败或挂起不会导致其他线程也失败或挂起,那么这种算法就被称为非阻塞算法。如果在算法的每个步骤都存在某个线程能够执行下去,那么这种算法也称为无锁算法(Lock-free)算法。如果在算法中仅将CAS用于协调线程之间的操作,并且能正确的实现,那么他即是一种无阻塞算法,又是一种无锁算法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值