多线程初步小结

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.解决示例

java,c#,c++ 的消费者和生产者模式。

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则打开门放所有等待队列中的线程到就绪队列。

 

posted @ 2019-06-13 07:25  琴鸟  阅读(179)  评论(0编辑  收藏  举报