0.为什么需要多线程
cpu太快,其他硬件太慢,如网络,硬盘等。所以开多个线程,进程,让cpu在等待网络的时候也可以做其他线程。
这样就会出现多线程访问同一数据的竞争问题,所以需要把访问共享数据的代码块做成线程安全的。
注意访问共享数据需要锁住,而访问耗时的网络等必须在锁之外。否则变成了类似单线程,没有意义。
1.多线程存在什么问题
数据竞争:如 i++ ,链表的删除和插入多线程同时进行。
竞态条件:如 if(i==0){xxx这里i可能已经变化了。还在执行某些操作xx}
2.问题归纳一下,为什么会有这些问题
非原子操作和指令重排序,常出现于先检查后执行的情况。
3.如何解决
加锁,让一段逻辑成为一个原子操作。常见的是把 共享数据全部放入一个类中来管理。
3.1所以我们需要有个机制来让线程阻塞和激活。这个就是lock和unlock.
lock 必须要硬件支持,因为 检测是否有线程进入,有如何,没如何,这个逻辑必须是原子的。不然自己都不是原子,如何去锁定一段代码为原子操作。
lock让线程挂起。unlock让阻塞线程都去抢锁。只要保证锁定的地方是必须锁定的,也就是指只锁定读写共享数据的部分代码。那么就已经就性能极限了。
等等,因为我们是解决数据共享问题。必然有插入和读取,我们就是保护读写之间的安全性。
那么关于多线程, 还发现一种特殊情况。比如共享数据空了, 那么读线程还是会去获得锁,不过逻辑上我们应该会让他立即放锁,不作任何处理。但是这个不是最优的处理。应该让他没有数据之前别再去请求了。
3.2所以我们condition。让满足某个条件的线程不再放入到阻塞队伍中。而是放到等待队伍中。当然我们也必须要对应的线程来唤醒。nitify 或者 singal .
注意unlock,只让lock的线程去抢cpu。而notify 是让等待的队列去抢cpu。lock和wait分别形成了2个不同的队列。一般是要先nofity再 unlock.
还有一点需要注意 wait之后会放锁,自己进入等待,有信号后,必须先获得锁。所以隐含语意就是 xx.await 等价于 xx.unlock. xx.等待唤醒信号 xx.lock .
而一般获得锁需要再次判断条件,因为notify不能保证之前条件还是成立,而且获得锁之后是继续执行wait后的语句。 所以一般是while(xxx){xxx.await} ,用一个循环,让它唤醒后再次检测,算是固定写法。
理论上至此好像没有任何问题了。
但是需求是多样的。比如读写问题,需要读可以多线程访问。
所以我们要把把lock的作用扩大化。就是lock本来是判断是否有线程进入,我们用lock作为逻辑太小用了。
我们应该用lock锁住自定义数据,让这些自定义数据来组成我们的逻辑。
如一下逻辑,但是系统已经提供了读写锁。而且实现原理应该不是一样。所以没有必要自己写。
3.3如果确实需要更复杂的逻辑,如读锁,那么我们可以让lock。锁住2个变量,分为为读的线程个数,和写的线程个数,处理后,立即放锁。 也就是把实际读数据的部分不上锁,而锁住一些自定义数据来实现逻辑。
那么理论上好像可以满足任何需求了
3.4通用的做法。就是把多线程下,会共享的数据放入到一个类中。
再类中,写线程安全的方法,提供给外部使用。
要注意的一点就是,竞态条件。所以这个必须人工去检测,所有涉及到共享变量的地方,看看是否存在 先判断,后处理的情况。
如果有,必须把整段逻辑放入某个方法中,提供给外部使用。
public synchronized void lockRead() throws InterruptedException { while (writers > 0 || writeRequests > 0) { wait(); } readers++; }
4.解决示例
5.解决方案是否有其他问题。
死锁等问题。一般是需要资源的全部获取。所以对资源的获取进行固定排序是一个方式。
记住sleep, yield。不会放锁。所以处理满数据的时候,让写线程sleep ,是不会让读线程获得cpu的。必须wait.放锁,并进入等待队列。或者直接跳过代码进入unlock.
6.小结。
之前写的互斥量的理解。https://www.cnblogs.com/lsfv/p/6284735.html
7. lock.unlock. await. signle.
lock.unlock为什么是一等公民?因为lock.unlock 是由系统控制的,只要有cpu空,就会随时排到你。起码他可以排队。别人unlok后。他就可以参与lock.
await. signle.为什么是二等公民,因为await. signle是由对象控制的。首先要一个对象获得锁才能唤醒你们。操作系统无法唤醒他们。 已经丧失了排队的资格,unlock了。只能由一等公民去唤醒他。
当一个线程尝试着lock一个同步对象的时候,该线程就在就绪队列中排队。
一旦unlock没人拥有该同步对象,就绪队列中的线程就可以占有该同步对象。这也是我们平时最经常用的lock方法。
为了其他的同步目的,占有同步对象的线程也可以暂时放弃同步对象,并把自己流放到等待队列中去。这就是Monitor.Wait。由于该线程放弃了同步对象,其他在就绪队列的排队者就可以进而拥有同步对象。
比起就绪队列来说,在等待队列中排队的线程更像是二等公民:他们不能自动得到同步对象,甚至不能自动升舱到就绪队列。而Monitor.Pulse的作用就是开一次门,使得一个正在等待队列中的线程升舱到就绪队列;
相应的Monitor.PulseAll则打开门放所有等待队列中的线程到就绪队列。