- 浏览: 2444463 次
- 性别:
- 来自: 杭州
文章分类
- 全部博客 (574)
- Book (62)
- Architecture (6)
- Java (39)
- Taobao (41)
- Distributed (4)
- Life (72)
- Database (7)
- Spring (16)
- Photography (15)
- Bicycle (41)
- Test (20)
- jBPM (8)
- Business (12)
- Movie (3)
- Ajax (15)
- Code (7)
- Eclipse (96)
- VIM (2)
- Music (6)
- Groovy (10)
- AutoHotKey (3)
- Dorado (10)
- Maven (7)
- Scrum (5)
- English (20)
- Financial (12)
- OSGi (3)
- Other (4)
- Tool (6)
- Browser (1)
- PPT (1)
- Project Management (4)
- Agile (6)
- Nosql (1)
- Search engine (6)
- Shell (2)
- Open Source (4)
- Storm (10)
- Guava (3)
- Baby (1)
- netty (1)
- Algorithm (1)
- Linux (1)
- Python (2)
最新评论
-
roy2011a:
https://github.com/ebottabi/sto ...
storm的序列化问题及与spring的结合方式 -
roy2011a:
能抗能打 写道哥们儿,你好!能共享下那个storm与sprin ...
storm的序列化问题及与spring的结合方式 -
Alick1:
兄弟,你之前是不是在深圳的正阳公司呆过啊?
storm的ack和fail -
liuleixwd:
先点个赞,写的非常好!有个问题请教下,如果我再bolt里不用e ...
storm的ack和fail -
yao-dd:
solr的facet查询
接上一篇
死锁
当每个人都拥有他人需要的资源, 并且等待其他人正在占有的资源, 如果大家一直占有资源, 直到获得自己需要却没被占用的资源, 那么就会产生死锁.
当一个线程永远占有一个锁, 而其他线程尝试去获得这个锁, 那么它们将永远被阻塞. 当线程占有锁L时, 想要获得锁M, 但是同时, 线程B持有M, 并尝试获得L, 两个线程将永远等待下去, 这种情况是死锁的最简单的形式.
当死锁的时候, 往往是遇到了最不幸的时候:在高负载之下.
如果所有线程以通用的固定次序获得锁, 程序就不会出现锁顺序死锁问题了.
动态的锁顺序死锁
这种情况举个例子比较好明白, 比如一个转账操作, 将账号A的钱转到账号B中, 然后在转账过程中顺序对账号A和账号B使用同步锁, 同时又出现账号B向账号A转账, 这时应为参数顺序导致死锁.
代码如下:
避免这种情况是强制指定顺序:
需要等待其他任务的结果是生成线程饥饿死锁的来源, 有界池和相互依赖的任务不能放在一起使用.
活锁(live lock)是线程中活跃度失败的另一种形式, 尽管没有被阻塞, 线程却仍然不能继续, 因为它不断重试相同的操作, 却总是失败. 活锁通常发生在消息处理应用程序中.
如果程序都不能保持现有的处理器处于忙碌的工作状态, 添加更多的处理器也无济于事. 线程通过分解应用程序, 总是让空闲的处理器进行未完成的工作, 从而保持所有的cpu热火朝天的工作.
可伸缩性指的是: 当增加计算资源的时候(比如增加额外CPU数量, 内存, 存储器, I/O宽带), 吞吐量和生产量能够得到相应的得到改进.
我们使用线程的重要原因之一是为了支配多处理器的能力, 我们必须保证问题被恰当地进行并行化分解, 并且我们的程序有效地使用了这种并行的潜能.
ReadWriteLock实现了一个多读少写的加锁规则: 只要没有修改, 那么多个读者可以并发的访问共享资源, 但是写者必须独占获得锁. 对于多数操作都为读操作的数据结构, ReadWriteLock比独占的锁具有更好的并发性.
单线程化的ConcurrentHashMap的性能要比同步的HashMap的性能稍好一些, 而且在并发应用中, 这种作用就十分明显了. ConcurrentHashMap的实现, 假定大多数常用的操作都是获得已存在的某个值, 因此它的优化是针对get操作. 提供更好的性能和并发性.
同步的Map实现中, 可伸缩性最主要的障碍在于整个Map存在 一个锁, 所以一次只有一个线程能够访问Map, 从另一个方面来说, ConcurrentHashMap并没有对成功的读操作加锁, 对写操作和真正需要锁的读操作使用了分离锁的方法, 因此, 多线程能够并发访问map, 而不是被阻塞.
关于救火的一个比喻
救火有两种解决方案: 一个是排成长队以传水救火, 另一个是一群人跑来跑去的救火. 在后一种方案中, 你对水源和火源都存在很大的竞争(结果是更少的水能够传递到灭火点), 还会造成更低的效率, 因为每一个工人都在不停的变换模式(注水, 跑步, 倒水, 跑步, 等等). 在第一种方案中, 从水源到燃烧的建筑物间的水流是不断的, 付出更少的体力却换得了更多的水传输过来, 每个工人只负责自己的一项工作.
高级主题
ReentrantLock实现了Lock接口, 提供了与synchronized相同的互斥和内存可见性的保证, 或者ReentrantLock的锁与进入synchronized块有相同的内存语义, 释放ReentrantLock锁与退出synchronized块有相同的意义.但是与synchronized相比, ReentrantLock为处理比可用的锁提供了更多的灵活性.
synchronized的一个缺点是不能中断正在等待获取锁的线程, 并且在请求锁失败的情况, 必须无限制的等待.
使用tryLock避免顺序死锁:
使用预定时间锁:
在内部锁不能满足使用时, ReentrantLock才被作为更高级的工具, 当你需要以下高级特性时, 才应该使用: 可定时的, 可轮询的与可中断的锁获取操作, 公平队列, 或者非块结构的锁, 否则请使用synchronized.
ReentrantLock实现了标准的互斥锁: 一次最多只有一个线程能够持有相同ReentrantLock. 但是互斥通常作为保护数据一致性的很强的加锁约束, 因此过分地限制了并发性. 互斥是保守的加锁策略, 避免了"写/写", "读/写"的重叠, 但是同样避开了"读/读"的重叠. 为了提高并发性, 提供了一个读写锁, 它允许一个资源能够被多个读者访问, 或者被一个写者访问, 两者不能同时进行.
ReadWriteLock同时暴露了两个Lock对象, 一个用来读, 一个用来写. 读取ReadWriteLock锁守护的数据, 你必须首先获得读取的锁, 当需要修改守护的数据时, 你必须首先获得写入的锁.
条件队列
条件队列就如同烤面包机上的面包已好的铃声, 如果你正在听着它, 当面包烤好后你可以立即注意到, 并且放下手头的事情开始品尝面包, 如果你没有听见它, 你会错过通知消息, 但是回到厨房后还是看到面包的状态, 如果已经烤完, 就取面包, 如果未烤完, 就再次监听铃声.
如果Object.wait()方法被调用表示"我要去休息了, 但是发生了需要关注的事情后叫醒我", 调用通知方法意味着"需要关注的事情发生了".
调用notify的结果是: JVM会从在这个队列中等待的众多线程中挑选出一个, 并把它唤醒; 而调用notifyAll会唤醒所有正在这个条件队列中等待的线程.调用notifyAll()的效率有时候非常低, 这主要是因为如果有10个线程在条件队列中等待, 调用notifyAll会唤醒每一个线程, 让它们去竞争锁, 然后它们中的大多数或者全部又回到休眠状态, 这意味着每一个激活单一线程执行的事件, 都会带来大量的上下文切换, 和大量竞争锁的请求.
Condition
Condition是一种广义的内部条件队列, 一个Condition和一个单独的Lock相关联, 调用Lock.newCondition()方法, 可以创建一个Condition. 每个Lock可以有任意数量的Condition对象. wait, notify, notifyAll在Condition中都有对等体:await, signal, signalAll, 而且一定要使用后者!
一个使用Condition的BoundedBuffer:
ReentrantLock和Semaphore这两个接口有很多共同点, 它们都扮演了"阀门"的角色, 每次允许有限数量的线程通过, 线程到达阀门之后, 可以允许通过(lock和acquire成功返回), 也可以等待(lock和acquire阻塞), 也可以被取消(tryLock和tryAcquire返回false, 指明在允许的时间内, 锁或者许可不可用), 更进一步, 它们允许可中断, 不可中断的, 可限时的请求尝试, 它们都允许选择公平, 非公平的等待线程队列.
CountDownLatch, ReentrantLock, SynchronousQueue和FutureTask都构建于共同的基类AbstractQueuedSynchronizer. AQS获取操作可能是独占的, 就像ReentrantLock一样, 也可能是非独占的, 就像Semaphore和CountDownLatch一样, 这取决于不同的Synchronizer.
基于AQS的闭锁
代码中通过AQS管理闭锁的状态:关闭0, 打开1。 await方法调用AQS的acqurieSharedInterruptibly, 后者随后请求OneShotLatch中的tryAcquireShared方法必须返回一个值来表明操作能否继续执行. 如果闭锁已经事先打开, tryAcquireShared会返回成功, 并允许线程通过; 否则他会返回一个值, 表明获取请求的尝试失败. acquireSharedInterruptibly方法处理失败的方式, 是把线程植入一个队列中, 该队列中的元素都是等待中的线程, asignal调用releaseShared, 进而导致tryReleaseShared被调用. tryReleasseShared的实现无条件地把闭锁的状态设置为打开, 通过返回值表明Synchronizer处于完全释放的状态.
原子变量与非阻塞同步机制
与基于锁的方案对比, 非阻塞算法的设计与实现要复杂的多. 但是他们可伸缩性和活跃度上占有很大的优势. 因为非阻塞算法可以让多个线程在竞争相同资源时不会发生阻塞, 所以它能在更精细化的层面上调整力度, 并能大大减少调度的开销. 进一步讲, 他们对死锁和其他活跃度问题具有免疫性
volatile变量与锁相比是更轻量的同步机制, 因为它不会引起上下文的切换和线程调度. 然而, volatile变量与锁相比有一定的局限性:尽管他们提供了相似的可见性保证, 但是它不能用于构造原子性的复合操作.
独占锁是一项悲观技术: 它假设最坏情况, 并且会通过获得正确的锁来避免其他线程更多打扰, 直到作出保证才能继续进行.
对于细粒度的操作, 有另外一种选择通常更加有效: 乐观的解决方法. 凭借新的方法, 我们可以指望不受打扰地完成更新, 这个方法依赖于冲突监测, 从而能判定更新过程中是否存在来自其他线程的干涉, 在冲突发生的情况下, 操作失败, 并会重试.
比较并交换(CompareAndSwap CAS)有三个操作数: 内存位置V, 旧的预期值A和新值B, 当且仅当V符合旧值A时, CAS用新值B原子化地更新V的值, 否则它什么都不会做. 在任何情况下都会返回V的真实值, CAS的意思是: 我认为V的值应该是A, 如果是, 那么将B值赋值给V, 若不是, 则不修改, 并告诉我应该为多少. CAS是一项乐观技术: 它抱着成功的希望进行更新, 并且如果另一个线程在上次检查后更新了该变量, 它能够发现错误.
当多个线程试图使用CAS同时更新相同的变量时, 其中一个会胜出, 并更新变量的值, 而其他的都会失败, 失败的线程更不会被挂起, 他们会被告知这次赛跑失利, 但是允许重试
CAS原理的一种简单实现:
CAS在阻塞计数器中的一种应用:
自增计数器遵循了经典形式:取得旧值, 根据它计算出新值(加1), 并使用CAS设定新值. 如果CAS白了, 立即重试该操作.
加锁的语法可能比较简洁, 但是JVM和OS管理锁的工作却并不简单.
锁与原子化随竞争程度的不同, 性能发生的改变表明了各自的优势和劣势. 在中低程度的竞争下, 原子化提供更好的伸缩性; 在高强度的竞争下, 锁能够很好的帮助我们避免竞争. 但是在单CPU系统中, CAS算法要胜于锁的算法, 因为CAS在单CPU的系统下总是成功的, 除非线程在读-改-写操作的中途被抢占.
非阻塞算法的所有特性: 一些任务的完成具有不确定性, 并可能必须重做.
基于非阻塞的Stack实现
CAS的基本模式: 如果更新失败就重试
构建非阻塞算法的窍门是: 缩小原子化的范围到唯一的变量.
基于非阻塞的链表
这里有一个窍门, 第一个就是即使是在多步更新中, 也要确保数据结构总能处于一致状态. 如果线程B到达时发现线程A在更新中, B可以分辨操作已部分完成, 并且知道不能立即开始自己的更新, 那么B就开始等待(反复检查重试)直到A完成更新, 这样两个线程就不会互相影响了.
死锁
当每个人都拥有他人需要的资源, 并且等待其他人正在占有的资源, 如果大家一直占有资源, 直到获得自己需要却没被占用的资源, 那么就会产生死锁.
当一个线程永远占有一个锁, 而其他线程尝试去获得这个锁, 那么它们将永远被阻塞. 当线程占有锁L时, 想要获得锁M, 但是同时, 线程B持有M, 并尝试获得L, 两个线程将永远等待下去, 这种情况是死锁的最简单的形式.
当死锁的时候, 往往是遇到了最不幸的时候:在高负载之下.
如果所有线程以通用的固定次序获得锁, 程序就不会出现锁顺序死锁问题了.
动态的锁顺序死锁
这种情况举个例子比较好明白, 比如一个转账操作, 将账号A的钱转到账号B中, 然后在转账过程中顺序对账号A和账号B使用同步锁, 同时又出现账号B向账号A转账, 这时应为参数顺序导致死锁.
代码如下:
public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) { synchronized (fromAccount) { synchronized (toAccount) { if (fromAccount.getBalance().compareTo(amount)<0) { throw new RuntimeException(); } else { fromAccount.debit(amount); toAccount.credit(amount); } } } }
避免这种情况是强制指定顺序:
public void transferMoney(final Account fromAccount, final Account toAccount, final DollarAmount amount) { class Helper { public void transfer() { if (fromAccount.getBalance().compareTo(amount) < 0) { throw new RuntimeException(); } else { fromAccount.debit(amount); toAccount.credit(amount); } } } // 通过唯一hashcode来统一锁的顺序, 如果account具有唯一键, 可以采用该键来作为顺序. int fromHash = System.identityHashCode(fromAccount); int toHash = System.identityHashCode(toAccount); if (fromHash < toHash) { synchronized (fromAccount) { synchronized (toAccount) { new Helper().transfer(); } } } else if (fromHash > toHash) { synchronized (toAccount) { synchronized (fromAccount) { new Helper().transfer(); } } } else { synchronized (tieLock) { // 针对fromAccount和toAccount具有相同的hashcode synchronized (fromAccount) { synchronized (toAccount) { new Helper().transfer(); } } } } }
需要等待其他任务的结果是生成线程饥饿死锁的来源, 有界池和相互依赖的任务不能放在一起使用.
活锁(live lock)是线程中活跃度失败的另一种形式, 尽管没有被阻塞, 线程却仍然不能继续, 因为它不断重试相同的操作, 却总是失败. 活锁通常发生在消息处理应用程序中.
如果程序都不能保持现有的处理器处于忙碌的工作状态, 添加更多的处理器也无济于事. 线程通过分解应用程序, 总是让空闲的处理器进行未完成的工作, 从而保持所有的cpu热火朝天的工作.
可伸缩性指的是: 当增加计算资源的时候(比如增加额外CPU数量, 内存, 存储器, I/O宽带), 吞吐量和生产量能够得到相应的得到改进.
我们使用线程的重要原因之一是为了支配多处理器的能力, 我们必须保证问题被恰当地进行并行化分解, 并且我们的程序有效地使用了这种并行的潜能.
ReadWriteLock实现了一个多读少写的加锁规则: 只要没有修改, 那么多个读者可以并发的访问共享资源, 但是写者必须独占获得锁. 对于多数操作都为读操作的数据结构, ReadWriteLock比独占的锁具有更好的并发性.
单线程化的ConcurrentHashMap的性能要比同步的HashMap的性能稍好一些, 而且在并发应用中, 这种作用就十分明显了. ConcurrentHashMap的实现, 假定大多数常用的操作都是获得已存在的某个值, 因此它的优化是针对get操作. 提供更好的性能和并发性.
同步的Map实现中, 可伸缩性最主要的障碍在于整个Map存在 一个锁, 所以一次只有一个线程能够访问Map, 从另一个方面来说, ConcurrentHashMap并没有对成功的读操作加锁, 对写操作和真正需要锁的读操作使用了分离锁的方法, 因此, 多线程能够并发访问map, 而不是被阻塞.
关于救火的一个比喻
救火有两种解决方案: 一个是排成长队以传水救火, 另一个是一群人跑来跑去的救火. 在后一种方案中, 你对水源和火源都存在很大的竞争(结果是更少的水能够传递到灭火点), 还会造成更低的效率, 因为每一个工人都在不停的变换模式(注水, 跑步, 倒水, 跑步, 等等). 在第一种方案中, 从水源到燃烧的建筑物间的水流是不断的, 付出更少的体力却换得了更多的水传输过来, 每个工人只负责自己的一项工作.
高级主题
ReentrantLock实现了Lock接口, 提供了与synchronized相同的互斥和内存可见性的保证, 或者ReentrantLock的锁与进入synchronized块有相同的内存语义, 释放ReentrantLock锁与退出synchronized块有相同的意义.但是与synchronized相比, ReentrantLock为处理比可用的锁提供了更多的灵活性.
synchronized的一个缺点是不能中断正在等待获取锁的线程, 并且在请求锁失败的情况, 必须无限制的等待.
使用tryLock避免顺序死锁:
public boolean transferMoney(final Account fromAccount, final Account toAccount, final DollarAmount amount, long timeout, TimeUnit unit) throws InterruptedException { long fixedDelay = 123; long randMod = 456; long stopTime = System.nanoTime() + unit.toNanos(timeout); while (true) { if (fromAccount.getLock().tryLock()) { // 如果不能获得锁就返回重试 try { if (toAccount.getLock().tryLock()) { try { if (fromAccount.getBalance().compareTo(amount) < 0) { throw new RuntimeException(); } else { fromAccount.debit(amount); toAccount.credit(amount); } } finally { toAccount.getLock().unlock(); } } } finally { fromAccount.getLock().unlock(); } } if (System.nanoTime() > stopTime) { // 重试了指定次数仍然无法获得锁则返回失败 return false; } Thread.sleep(fixedDelay + new Random().nextLong() % randMod); } }
使用预定时间锁:
public boolean trySendOneSharedLine(String message, long timeout, TimeUnit unit) throws InterruptedException { long nanosToLock = unit.toNanos(timeout) - estimatedNanosToSend(message); if (lock.tryLock(nanosToLock, TimeUnit.NANOSECONDS)) { return false; } try { return sendOnSharedLined(message); }finally { lock.unlock(); } }
在内部锁不能满足使用时, ReentrantLock才被作为更高级的工具, 当你需要以下高级特性时, 才应该使用: 可定时的, 可轮询的与可中断的锁获取操作, 公平队列, 或者非块结构的锁, 否则请使用synchronized.
ReentrantLock实现了标准的互斥锁: 一次最多只有一个线程能够持有相同ReentrantLock. 但是互斥通常作为保护数据一致性的很强的加锁约束, 因此过分地限制了并发性. 互斥是保守的加锁策略, 避免了"写/写", "读/写"的重叠, 但是同样避开了"读/读"的重叠. 为了提高并发性, 提供了一个读写锁, 它允许一个资源能够被多个读者访问, 或者被一个写者访问, 两者不能同时进行.
ReadWriteLock同时暴露了两个Lock对象, 一个用来读, 一个用来写. 读取ReadWriteLock锁守护的数据, 你必须首先获得读取的锁, 当需要修改守护的数据时, 你必须首先获得写入的锁.
public class ReadWriteMap<K, V> { private final Map<K, V> map; private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock w = lock.writeLock(); private final Lock r = lock.readLock(); 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(K key) { r.lock(); try { return map.get(key); } finally { r.unlock(); } } }
条件队列
条件队列就如同烤面包机上的面包已好的铃声, 如果你正在听着它, 当面包烤好后你可以立即注意到, 并且放下手头的事情开始品尝面包, 如果你没有听见它, 你会错过通知消息, 但是回到厨房后还是看到面包的状态, 如果已经烤完, 就取面包, 如果未烤完, 就再次监听铃声.
如果Object.wait()方法被调用表示"我要去休息了, 但是发生了需要关注的事情后叫醒我", 调用通知方法意味着"需要关注的事情发生了".
调用notify的结果是: JVM会从在这个队列中等待的众多线程中挑选出一个, 并把它唤醒; 而调用notifyAll会唤醒所有正在这个条件队列中等待的线程.调用notifyAll()的效率有时候非常低, 这主要是因为如果有10个线程在条件队列中等待, 调用notifyAll会唤醒每一个线程, 让它们去竞争锁, 然后它们中的大多数或者全部又回到休眠状态, 这意味着每一个激活单一线程执行的事件, 都会带来大量的上下文切换, 和大量竞争锁的请求.
Condition
Condition是一种广义的内部条件队列, 一个Condition和一个单独的Lock相关联, 调用Lock.newCondition()方法, 可以创建一个Condition. 每个Lock可以有任意数量的Condition对象. wait, notify, notifyAll在Condition中都有对等体:await, signal, signalAll, 而且一定要使用后者!
一个使用Condition的BoundedBuffer:
public class ConditionBoundedBuffer<T> { private static final int BUFFER_SIZE = 2; private final Lock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); private final T[] items = (T[]) new Object[BUFFER_SIZE]; private int tail, head, count; 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(); } } public static void main(String[] args) { final ConditionBoundedBuffer<String> cbb = new ConditionBoundedBuffer<String>(); new Thread(new Runnable() { @Override public void run() { try { cbb.take(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { cbb.put("haha"); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); // new Thread(new Runnable() { // // @Override // public void run() { // try { // cbb.take(); // } catch (InterruptedException e) { // e.printStackTrace(); // } // } // }).start(); new Thread(new Runnable() { @Override public void run() { try { cbb.put("haha"); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } }
ReentrantLock和Semaphore这两个接口有很多共同点, 它们都扮演了"阀门"的角色, 每次允许有限数量的线程通过, 线程到达阀门之后, 可以允许通过(lock和acquire成功返回), 也可以等待(lock和acquire阻塞), 也可以被取消(tryLock和tryAcquire返回false, 指明在允许的时间内, 锁或者许可不可用), 更进一步, 它们允许可中断, 不可中断的, 可限时的请求尝试, 它们都允许选择公平, 非公平的等待线程队列.
CountDownLatch, ReentrantLock, SynchronousQueue和FutureTask都构建于共同的基类AbstractQueuedSynchronizer. AQS获取操作可能是独占的, 就像ReentrantLock一样, 也可能是非独占的, 就像Semaphore和CountDownLatch一样, 这取决于不同的Synchronizer.
基于AQS的闭锁
public class OneShotLatch { private final Sync sync = new Sync(); private class Sync extends AbstractQueuedSynchronizer { @Override protected int tryAcquireShared(int arg) { return (getState() == 1) ? 1 : -1; } @Override protected boolean tryReleaseShared(int arg) { setState(1); return true; } } public void signal() { sync.releaseShared(0); } public void await() throws InterruptedException { sync.acquireInterruptibly(0); } }
代码中通过AQS管理闭锁的状态:关闭0, 打开1。 await方法调用AQS的acqurieSharedInterruptibly, 后者随后请求OneShotLatch中的tryAcquireShared方法必须返回一个值来表明操作能否继续执行. 如果闭锁已经事先打开, tryAcquireShared会返回成功, 并允许线程通过; 否则他会返回一个值, 表明获取请求的尝试失败. acquireSharedInterruptibly方法处理失败的方式, 是把线程植入一个队列中, 该队列中的元素都是等待中的线程, asignal调用releaseShared, 进而导致tryReleaseShared被调用. tryReleasseShared的实现无条件地把闭锁的状态设置为打开, 通过返回值表明Synchronizer处于完全释放的状态.
原子变量与非阻塞同步机制
与基于锁的方案对比, 非阻塞算法的设计与实现要复杂的多. 但是他们可伸缩性和活跃度上占有很大的优势. 因为非阻塞算法可以让多个线程在竞争相同资源时不会发生阻塞, 所以它能在更精细化的层面上调整力度, 并能大大减少调度的开销. 进一步讲, 他们对死锁和其他活跃度问题具有免疫性
volatile变量与锁相比是更轻量的同步机制, 因为它不会引起上下文的切换和线程调度. 然而, volatile变量与锁相比有一定的局限性:尽管他们提供了相似的可见性保证, 但是它不能用于构造原子性的复合操作.
独占锁是一项悲观技术: 它假设最坏情况, 并且会通过获得正确的锁来避免其他线程更多打扰, 直到作出保证才能继续进行.
对于细粒度的操作, 有另外一种选择通常更加有效: 乐观的解决方法. 凭借新的方法, 我们可以指望不受打扰地完成更新, 这个方法依赖于冲突监测, 从而能判定更新过程中是否存在来自其他线程的干涉, 在冲突发生的情况下, 操作失败, 并会重试.
比较并交换(CompareAndSwap CAS)有三个操作数: 内存位置V, 旧的预期值A和新值B, 当且仅当V符合旧值A时, CAS用新值B原子化地更新V的值, 否则它什么都不会做. 在任何情况下都会返回V的真实值, CAS的意思是: 我认为V的值应该是A, 如果是, 那么将B值赋值给V, 若不是, 则不修改, 并告诉我应该为多少. CAS是一项乐观技术: 它抱着成功的希望进行更新, 并且如果另一个线程在上次检查后更新了该变量, 它能够发现错误.
当多个线程试图使用CAS同时更新相同的变量时, 其中一个会胜出, 并更新变量的值, 而其他的都会失败, 失败的线程更不会被挂起, 他们会被告知这次赛跑失利, 但是允许重试
CAS原理的一种简单实现:
public class SimulateCAS { private int value; public synchronized int get() { return value; } public synchronized int compareAndSwap(int expectedValue, int newValue) { int oldValue = value; if (expectedValue == oldValue) { value = newValue; } return oldValue; } public synchronized boolean compareAndSet(int expectedValue, int newValue) { return (expectedValue == compareAndSwap(expectedValue, newValue)); } }
CAS在阻塞计数器中的一种应用:
public class CasCounter { private SimulateCAS value = new SimulateCAS(); 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; } }
自增计数器遵循了经典形式:取得旧值, 根据它计算出新值(加1), 并使用CAS设定新值. 如果CAS白了, 立即重试该操作.
加锁的语法可能比较简洁, 但是JVM和OS管理锁的工作却并不简单.
锁与原子化随竞争程度的不同, 性能发生的改变表明了各自的优势和劣势. 在中低程度的竞争下, 原子化提供更好的伸缩性; 在高强度的竞争下, 锁能够很好的帮助我们避免竞争. 但是在单CPU系统中, CAS算法要胜于锁的算法, 因为CAS在单CPU的系统下总是成功的, 除非线程在读-改-写操作的中途被抢占.
非阻塞算法的所有特性: 一些任务的完成具有不确定性, 并可能必须重做.
基于非阻塞的Stack实现
public class ConcurrentStack<E> { class Node<E> { E item; Node<E> next; public Node(E item) { this.item = item; } } AtomicReference<Node<E>> top = new AtomicReference<Node<E>>(); // 对栈顶的一个引用 public void push(E item) { Node<E> newHead = new Node<E>(item); Node<E> oldHead; do { oldHead = top.get(); newHead.next = oldHead; } while (!top.compareAndSet(oldHead, newHead)); } public E pop() { Node<E> oldHead; Node<E> newHead; do { oldHead = top.get(); if (oldHead == null) { return null; } newHead = oldHead.next; } while (!top.compareAndSet(oldHead, newHead)); return oldHead.item; } }
CAS的基本模式: 如果更新失败就重试
构建非阻塞算法的窍门是: 缩小原子化的范围到唯一的变量.
基于非阻塞的链表
public class LinkedQueue<E> { static class Node<E> { final E item; final AtomicReference<Node<E>> next; public Node() { this(null, null); } public Node(E item, Node<E> next) { this.item = item; this.next = new AtomicReference<Node<E>>(next); } } private final Node<E> dummy = new Node<E>(); private final AtomicReference<Node<E>> head = new AtomicReference<Node<E>>(dummy); private final AtomicReference<Node<E>> tail = new AtomicReference<Node<E>>(dummy); public boolean put(E item) { Node<E> newNode = new Node<E>(item, null); while (true) { Node<E> curTailNode = tail.get(); Node<E> tailNextNode = curTailNode.next.get(); if (curTailNode == tail.get()) { if (tailNextNode == null) { // 更新尾节点下一个节点 if (curTailNode.next.compareAndSet(null, newNode)) { // 更新成功, 将尾节点指向下一个节点 tail.compareAndSet(curTailNode, newNode); return true; } } else { // 在更新过程中, 发现尾节点的下一个节点被更新了, 将尾节点指向下一个节点 tail.compareAndSet(curTailNode, tailNextNode); } } } } public static void main(String[] args) { final LinkedQueue<String> queue = new LinkedQueue<String>(); new Thread(new Runnable() { @Override public void run() { queue.put("item1"); } }).start(); new Thread(new Runnable() { @Override public void run() { queue.put("item2"); } }).start(); } }
这里有一个窍门, 第一个就是即使是在多步更新中, 也要确保数据结构总能处于一致状态. 如果线程B到达时发现线程A在更新中, B可以分辨操作已部分完成, 并且知道不能立即开始自己的更新, 那么B就开始等待(反复检查重试)直到A完成更新, 这样两个线程就不会互相影响了.
发表评论
-
<异类>读书笔记
2013-03-06 07:54 0成功者能够获得更多的机会,从而能变得更为成功。税收愈减免,富人 ... -
《python学习手册》学习笔记
2013-03-11 22:25 3420python格式化传参数非常赞,用数字标明位置,值得java学 ... -
<万历十五年>读书笔记
2013-03-11 22:27 1529在网上下了一个电子书, 但是貌似跟万历十五年没啥关系, 都是讨 ... -
《鸟哥的linux私房菜》读书笔记(部分)
2013-03-11 22:27 2011x86是一种微机系统硬件架构,另一种是苹果的mac的架构 l ... -
《你的灯亮了吗》读书笔记
2013-03-06 07:20 1458这是一本原本写给程序员的书 本书的四个问题: 搞清问题的来源 ... -
《小狗钱钱》读书笔记
2013-03-06 07:17 1443一本非常不错的理财学习入门书, 以童话的形式, 儿童的思维方式 ... -
《我的奋斗》读书笔记
2012-04-14 22:03 2003文字写的很幽默, 故事也基本都是一些平常人的故事,看到了一个特 ... -
《Java Performance》书评
2012-01-15 18:32 2920原文: http://java.dzone.com/rev ... -
《程序员应该知道的97件事》读书笔记
2012-01-15 18:36 2339一本关于写代码的文 ... -
《影响力》读书笔记
2011-11-05 14:47 1797从书名上很可能以为 ... -
《浪潮之巅》读书笔记
2011-11-05 14:44 1336作为一个中国人通过分析硅谷高科技公司的一系列传奇, 总结出这 ... -
《黑客与画家》读书笔记
2011-11-05 13:37 1781以前看过《rework》, 觉得是每一个小型创业公司的创业宝 ... -
《乔布斯传》读书笔记
2011-10-18 08:53 2785在ipad上看完了这本书, 写的还不错, 里面没有无聊的八 ... -
《细说Java》读书笔记
2011-10-05 15:01 1947国人写的, 感觉是一 ... -
《敏捷估计与规划》读书笔记
2011-10-05 12:08 3140这本书断断续续看了很长时间, 内容非常不错, 基本涵盖了sc ... -
《怪诞心理学》读书笔记
2011-10-05 09:44 1778既然是怪诞, 那么整本书涉及的内容并不是我们平常司空见怪的一 ... -
《番茄工作法图解》读书笔记
2011-09-28 09:02 2349番茄工作法是时间管 ... -
《Java开发超级工具集》读书笔记
2011-09-28 08:59 2069"工欲善其事必先利其器", 在平时的开发 ... -
《敏捷迭代开发管理者指南》读书笔记
2011-09-24 13:09 2168这是一本关于迭代开发 ... -
《解析极限编程》读书笔记
2011-09-24 13:03 1743不知道是kent beck的语 ...
相关推荐
java并发编程实践笔记java并发编程实践笔记java并发编程实践笔记java并发编程实践笔记
java并发编程实战pdf学习笔记 总结了重要的知识点
java并发编程实践笔记资料.pdf
《Java并发编程实践》一书的个人读书笔记。主要列举包括各个章节的关键知识点,便于反复阅读和知识复习掌握。
Java是最先支持多线程的开发的语言之一,Java从一开始就支持了多线程能力,因此Java开发者能常遇到上面描述的...这也是我想为Java并发技术而写这篇系列的原因。作为对自己的笔记,和对其他Java开发的追随者都可获益的。
当调用 start 启动线程时 Java 虚拟机会调 用该类的 run方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我 们可以继承重写Thread 类,在其 start 方法中添加不断循环调用传递过来的 ...
的管理,这种分离还在不同事务间划分了自然的分界线,在程序出现错误时可以很方便地进行恢复,还有利于提高程序的并发性。围绕任务执行来管理应用程序时,第一步要指明一个清晰的任务边界,理想情况下,任务是 独立...
线程安全就是对共享的、可变的状态进行管理,对象的状态就是它的数据,换句话说就是在不可控制的并发访问中保护数据。
使用java.util.concurrent类库构造安全的并发应用程序的基础。共享其实就是某一线程的数据改变对其它线程可见,否则就会出现脏数据。
在实践中,委托是创建线程安全类最有效的策略之一:用已有的线程安全类来管理所 有状态即可。
java中没有提供任何机制,来安全是强迫线程停止手头的工作,Thread.stop和 Thread.suspend方法存在严重的缺陷,不能使用。程序不应该立即停止,应该采用中断这种协作机制来处理,正确的做法是:先清除当前进程中的...
Linux面试专题及答案+ActiveMQ消息中间件面试专题+Java基础面试题+MySQL性能优化的21个最佳实践+微服务面试专题及答案+深入理解java虚拟机+设计模式面试专题及答案+开源框架面试专题及答案+并发编程及答案+Spring...
Java 并发学习笔记: 进程和线程, 并发理论, 并发关键字, Lock 体系, 原子操作类, 发容器 & 并发工具, 线程池, 并发实践 Java是一种面向对象的编程语言,由Sun Microsystems于1995年推出。它是一种跨平台的...
Java语言从第一版本开始内置了对多线程的支持,这一点在当年是非常了不起的,但是当我们对并发编程有了更深刻的认识和更多的实践后,实现并发编程就有了更多的方案和更好的选择。本文是对并发编程的核心理论做了下小...
Java并发编程.pdf JAVA核心知识点整理.pdf Java高级架构知识点整理.pdf Java高级架构面试知识点整理.pdf JVM与性能优化知识点整理.pdf MySQL性能调优与架构设计解析文档.pdf Nginx入门到实战.pdf springCloud笔记....
学习线程介绍Java多线程学习PDF格式Java并发编程的艺术.pdf JAVA并发编程实践.pdf图解Java多线程设计模式-第2版.pdf代码code1是《 Java并发编程的艺术》的源码ThreadDesignPatterns是《图解Java多线程设计模式》第1...
Java并发体系知识导图笔记.xmind JVM和性能优化.xmind JVM面试专题及答案.pdf kafka知识导图笔记.xmind Kafka面试专题及答案.pdf Linux面试专题及答案.pdf memcached面试专题及答案.pdf MongoDB面试专题及答案.pdf ...
2022java面试题、JVM面试题、多线程面试题、并发编程、Redis面试题、MySQL面试题、Java2022面试题、Netty面试题 一、内容概览 本次分享的资源涵盖了Java面试的各个方面,从基础知识到高级技术,从数据库到框架应用...
《实战Java高并发程序设计》笔记和源码笔记《实战Java高并发程序设计》中有很多代码范例,适合初学者通过实践入门并发编程,这本书有个问题就是前面的代码都用JDK7,第六章开始又用JDK8了笔者精心制作相关笔记并整理...