比较详细的并发编程细节分析

最近在系统的学习并发编程,整理出来详细的条理笔记,希望能够分享收获。

1. 最原始的加锁代码,加锁是指锁住了这个对象,一个线程在运行到这段代码的时候,在这个对象的堆内存区域写入了锁信息,其他线程运行到这段代码的时候,会访问这个对象,因为这块有synchronized关键字,就会访问锁信息,然后发现这个对象被锁住了,就会等。所以,锁住的永远都是对象,而不是代码块。

 

2. 专门用一个对象去当锁这事比较浪费,我们可以使用方法所在对象的本身来记录锁信息。一个对象的堆内存中记录了锁信息,并不是这个对象就无法被访问了,而是访问对象的代码中有synchronized关键字,它才会去考虑锁信息考虑异步同步问题。如果上述对象中还有其他没有synchronized关键字的方法,即使这个对象中增加了锁信息,其他方法还是可以正常使用的。

 

3. 为了写起来更简便,可以用如下方式,锁定的还是当前对象。

 

4. 静态的方法,锁定的是T.class这个对象

一个对象可以有多个锁吗?一个对象可以写入多少种的锁信息。比如说,一个线程运行到synchronized m这个方法这里,会在对象中写入锁信息, 另一个线程此时运行到synchronized mm这个方法这里,那么m方法增加的锁信息,会阻止mm方法继续运行吗?会。

对象锁是在一个类的对象上加的的锁,只有一把,不管有几个方法进行了同步。这些同步方法都共有一把锁,只要一个线程获得了这个对象锁,其他的线程就不能访问该对象的任何一个同步方法。但是如果是在同一个线程中,则会再次获取到这把锁。详情见下边7的分析。

 

5. 多线程之间的线程重入问题,即一个线程执行某个方法还没完事,另外一个线程就进入了这个方法。5个线程谁开始运行的并不一定。并不是thread0先start的就先运行,可能他愣了一下才获取到了CPU才运行的。代码运行为什么没有9?第一个线程执行到run方法的时候,先从内存中拿到了count这个值,然后拿到CPU的寄存器中运算得到了9,重新写回到内存中。在执行打印语句之前,被剥脱了CPU,然后第二个线程开始执行,从内存中拿到了9,计算后得到了8,之后第一个线程又获取到了CPU,继续执行打印,打印的时候再次去内存中拿取count的值,这时候已经是8了,所以打印出来了8. 而第二个线程在打印的时候,可能被第三个线程修改了,所以打印出了7,而第三个线程可能在第四个线程操作之前快速输出了,所以也打印出了7.

 

https://www.cnblogs.com/chihirotan/p/6486436.html 主内存与工作内存,上下文变量存在工作内存中。主内存是所有的线程所共享的,工作内存是每个线程自己有一个,不是共享的。

每条线程还有自己的工作内存(也认为是CPU寄存器中的缓冲区),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

原子性是指一个原子操作在cpu中不可以暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。原子操作保证了原子性问题。大致可以认为基础数据类型的访问和读写是具备原子性的. 但是x++等是由3个原子操作进行的。a.将变量x 值取出放在寄存器中 b.将将寄存器中的值+1 c.将寄存器中的值赋值给x. 什么是原子性,就是将寄存器中的值+1这种操作进行中,CPU不会被剥夺。

x值取出放到寄存器中这个操作用时极短,在一个线程占有CPU的时候,可能执行了特别多的操作。如果最后剥夺的时候刚刚好是放到寄存器的这个瞬间,那么这个寄存器中的值可能会被清空,或者说被下一个线程放进来的值覆盖。

当存在多线程时,如果工作内存A处理完还没来得及更新回主内存之前,工作内存B就从主内存中拉取了这个变量,那么很明显这个变量并不是最新的数据,会出现问题。 https://m.aliyun.com/zixun/wenjim/1233544.html

多线程中某个线程在运行时,是不断的从工作内存中拷贝内容到主内存的,并不是synchronize整个方法执行完才往回更新。

上述问题就有种可能,当拿到主内存count变量后放入到CPU中运行,从8变为了7,还没来得及从工作内存中写入到主内存中,这时候被剥夺了CPU,那么这个变量会存储在工作内存上下文中,当这个线程再次获取到CPU之后,直接拿取到存储在上下文的这个值,往主内存中的count中写入,这时候很可能其他线程已经改变了count的值,可能这时候已经是6了,但是这个线程用7覆盖了6,使count值再次变为了7.

6. 加入synchronize关键字,使方法具有原子性,不会再出现重复

 

7. 在同一个线程中,可以多次获取到这把锁,也就是说,synchronize方法调用synchronize方法并不会死锁住。

 

8. 在非同一个线程中,如果前一个线程获取到了锁,那么下一个线程无法执行任何一个synchronize方法,都要等。

 

9. 多线程中某个线程在运行时,是不断的从工作内存中拷贝内容到主内存的,并不是synchronize整个方法执行完才往回更新。程序在执行过程中,如果出现异常,默认情况锁会被释放。在一个web-app中,多个servlet线程共同访问一个资源,一个线程在运行到一半的时候发送异常,这时候早已经把部分处理的结果写入到了主内存,此时锁会被释放,其他线程会进入同步区域,会访问到产生异常时生成的数据,有可能造成数据不一致的现象。

 

10. 下图中m方法并不会结束。running=false并没有起作用

 

11. 加入volatile关键字可以使m方法结束。

或者在死循环中加入一些逻辑也可以使m方法结束

 

 

CPU并不是一直读工作内存中的值,也可能抽空去读一下主内存的值来更新一下。如果while中不存在任何代码的话,该线程在每次获取到CPU在执行的时候就一直在死循环,没有时间去读取一下主内存。如果while中有一些逻辑,在执行过程中就有很大的几率去抽空读取一下主内存。一般来讲,都有机会的,因为真实情况下很少有空死循环的情况。

 

volatile的作用是通知作用,就是一个变量如果被标记了volatile,那么这个变量被改变后,会主动通知所有的线程,告诉他们工作内存中的变量已经过期了,去重新获取一个,这时候所有线程都会主动的去主内容中获取到变量刷新工作内存中的变量值。

 

12. volatile可以保证读取的都是最新的,但是不能保证写回到主内存的时候不会覆盖新的值。

这里的结果并不是100000,而是90527或者其他小于100000的数。因为这里有10个线程并发,第一个线程从主内存取出的count值为300,volatile关键字只能保证第一个线程的值为最新,比如这时候有人更新了count值为320,那么第一个线程会得到通知然后更新自己的值为320. 但是第一个线程已经完成了自己的计算,结果为301,但是现在count的值为320,第一个线程不会关注这个,会直接给他覆盖了,最后为301,所以会损失一些和值。

所以说,volatile不会替代synchronize的作用。

 

13. 在某些多线程情况下,一个线程要根据另外一个线程的某个操作结果才能继续执行,这时候可以使用volatile加上第二个线程的死循环轮询结果。但是这样资源太浪费。可以使用wait, notify方法。

只有在synchronize代码块中使用lock, notify才有效果,因为这有在这个区域,才会去查看锁信息。

 

14. 使用synchronize关键字在多进程竞争资源的时候属于非公平锁,比如说有5个进程,第一个进程在释放锁之后,锁被其他哪个进程获取到是不确定的,有可能有的等了一分钟,有的等了一小时了,而CPU却移交给了等了一分钟的。这不公平。但是这样效率最高,调度器不会进行计算哪个等的时间长。

公平锁就是谁等的时间长,让谁得到那把锁。ReentranLock为true的时候便是一个公平锁

 

在上述代码中,大部分时间是t1与t2线程交替执行的,之所以看到开始的时候都是t2在执行,是因为那时候t1还没有启动,当t1启动了,跟t2开始争夺资源了,那么会平均分给t1,t2相同的机会去执行,谁等的时间长谁就执行。

如果上述代码用synchronize关键字,那么就是如下的结果。因为t1与t2获取CPU的机会不一定,所以可能t1执行了,下一次还是t1获取到CPU执行。

 

 

11. ThreadLocal是用来存放每个线程自己变量的方式。线程局部变量。如果不用threadlocal,两个线程访问同一个对象,那么只能上锁。如果用threadlocal,那么互相之间不会产生任何影响。Spring,Hibernate中都大量使用了threadlocal,提高了效率。

返回值为null,在第二个线程中无法获取到第一个线程中存放的对象。

 

12. 容器的同步异步问题。如下代码会出问题,可能会出现票卖重复,或者有异常抛出。因为ArrayList容器,remove方法不具有原子性,就是说一个线程对容器进行remove的过程中,现在容器含有一个元素,remove进行到一半,可能其他线程对这个容器进行了操作,比如说另外的线程动作较快,也挪作了一个,那么第一个remove动作就会抛出超出索引异常。或者说这种情况(该情况下不考虑volatile,因为中间有逻辑,认为工作内存与主内存中的tickets容器一致),remove过程就是先从容器中拿出一个对象,然后卖出票,最后再标记会容器减去一个元素。第一个线程remove的时候,要先从容器中取出一个,但是现在remove动作还没完成,此时第二个线程来了,判断容器中依然有一个元素,这时候它也拿到了同一个元素,然后卖出,这时候第一个线程也卖出,所以就出现了票卖重复了的现象。

 

13. 现在讲List改成Vectore,这是一个同步的容器,也就是线程安全的容器。但是下边的代码依然有问题。问题出现在判断与操作分离了。可能第一个线程判断了之后,在remove之前,被其他线程remove了。size与remove都是原子性的操作,但是两个原子性的操作中间有可能被其他线程进入。remove,size方法是原子性的,就是说一个线程在访问容器这个方法的时候,对象会被锁定,不会被其他线程干扰。

 

 

14. 改成加锁的代码后,不会再抛出异常,因为判断与操作这两个原子性的操作加上了锁,变成了一这个原子性的操作。业务逻辑上没有了问题,但是运行的时候明显感觉到了速度变慢了。因为加锁会造成性能上的损失。

 

15. 解决性能问题,就要使用并发容器。Queue的方式,从设计思想上,就抛弃了先判断size再remove的方式,它是直接往外拿,拿到了就是有,没有拿到就是容器中没有了。所以判断与拿出操作为一个大的原子操作,即poll方法。

 

16. ConcurrentMap, CopyOnWriteList, SynchronizedList, ArrayBlockingQueue, ConcurrentQueue, TransferQueue.

 

17. 接下来是线程池相关。Executor执行器,不同的实现可以有不同的执行方式,多线程执行,或者是简单的方法调用。

 

18. 多线程中用到Runnable接口,也有Callable接口。前者没有返回值,后者有返回值。当你需要新的线程运行完有返回值给你,那么就用Callable. ExcutorService中可以往里边扔任务,第一种是excute方法,执行Runnable任务,第二种是submit方法,执行Callable任务。ExecutorService就是一个线程池,然后往里扔了6个任务,ExecutorService会启动5个线程来解决这些任务。如果是5个线程执行1000个任务,内部会使用BlockingQueue, 等一个线程执行完了,再去拿任务执行。

 

线程池的好处是,ExecutorService对象一直存在的,任务执行完了,那个线程是一直都在,下次有任务再进来,就直接执行。启动与关闭线程,都是消耗资源的,而且消耗都比较大。那么有了线程池的概念,就能大大的提高性能。线程池适用于经常要有新的线程创建的情景。比如说Web应用的内部实现。

 

19. FutureTask是一种阻塞时的多线程的实现。在一个新的线程中执行Furetask中的内容,在主线程中调用get方法,这时候会一直等待新线程中的工作完毕与值的返回。FutureTask内部包装了Callable.

 

20. ParalleComputing. Future, 就是ExecutorService线程池调用callable之后返回的结果。调用其get方法的时候也会阻塞。这个阻塞指的是,主线程会一直等待线程池中Futrue中有结果返回,才会继续向下执行。 所以这就是为什么20000个数在划分的时候,因为前边的数小,所以分到的数更多,这样才能保证,4个线程差不多同时结束。如果某个线程执行时间太长,那么主线程运行到那里阻塞的时间就长。

 

21. ScheduledPool用于执行定时任务,类似于C#中的Timer. 两者都是在一定的设定时间内,启动新的线程来执行一段代码。不同点在于,Timer是每次都会启动一个新的线程,比较耗时。而ScheduledPool是复用已经存在的线程。

 

22. ParallelStream API与线程池关系不大。它是容器带有的方法,在使用foreach的时候,默认使用多线程去处理每一个元素,这里比较适用于容器量比较大,然后处理时间比较长的情况。当然我们也可以自己去分割容器,然后自己用线程池去实现。但是既然容器自己带了这种方法,我们用起来更简单一些。

 

猜你喜欢

转载自blog.csdn.net/sundacheng1989/article/details/91811375