java必谈多线程(转)

原文地址:http://406657836.iteye.com/blog/1894307

 

程序语言作为开发软件的工具,与电脑沟通交流的工具。每个人都有选择工具的理由。我选择java的理由是:强规范,保证了很多低级错误,难以发觉的错误在编译期被检查;严格的内存管理,对内存的访问java是有严格的规范的,它可以防止数组下标越界,错误的内存访问;高性能的垃圾回收器,java在cms并发收集器出现后性能上了一个台阶,而且java虚拟机还在不断的发展进步,java的性能也会得到极大的提高,java的性能问题逐渐成了过去式;多线程,多线程是java优势所在,java语言在这方面也是下了很大的功夫,使java的多线程性能和安全性都如此强大。这些原因使我非常热爱在java语言上的学习,研究。 

本文主要介绍java多线程方面的知识。正如很多java程序员知道的那样,java多线程确实涉及到很多注意事项。这篇博文主要是引出一些话题,对他们进行讨论。如果要深入讨论的话,每一个话题都足以用几篇博文来讲解。下面是将要出现的一些话题: 

1,为什么要多线程(多线程的优势) 

2,多线程有什么问题(多线程要解决的问题) 

3,java对多线程的规范(java解决多线程问题对jvm作出的规范) 

4,java解决多线程问题的手段,包括(synchronized,ReentrantLock,volatile,final,ThreadLocal,栈封闭,JUC技术等)


关于第一个话题: 

要使用多线程的理由有很多,总体来说有如下(可能不全): 

1,尽量利用系统资源,减少任务完成时间。(如,在IO阻塞的时候可以让出cpu,分布式运算 mapreduce) 

2,多任务系统,有些事情需要并行执行。(如,需要响应用户中断请求) 

3,需要同时为更多人提供服务,不能因为一个长任务完全占用系统资源,使一些短任务也不能服务。(如,web服务器) 

4,一台服务器可以有很多cpu(单个cpu也可以支持超线程),所以多线程是必然的趋势。 


关于第二个话题: 

多线程下确实有很多不确定性。也很难像串行化执行那么调试程序。问题可能要一定的运行时间,一定的操作顺序才能产生。所以很多程序员在回避这个问题,很害怕去使用多线程。所以多线程要解决的第一个问题就是心理畏惧问题。对于技术上,多线程主要有两个问题要解决。这也是开发多线程程序时一定要思考的问题。 

1,共享数据的可见性(一个线程写入的数据,何时对于其他线程可见)。 

2,共享资源的竞争问题(多个线程同时请求一个资源,谁先谁后,如计数问题,转账问题等)。 

注意这里都有共享二字,也就是需要在多线程中同时操作资源才可能出问题。下面讨论下这两个问题产生的原因和情景。 

对于可见性问题: 

cpu的执行速度快的惊人,以致于计算机执行速度的瓶颈在IO上。所以除了cpu寄存器缓存外,还有L1 ,L2——在cpu上的独立缓存,L3——同一个cpu槽内共享的缓存。然后才是主存,磁盘,网络等。在cpu需要访问数据时,会依次沿着上面的缓存读写数据。就拿写来说,假设一份数据在主存中,它会被一层层加载到寄存器缓存中来,假设cpu0要改变这份数据的值,会先写到寄存器缓存中,这时候数据并不会马上去修改主存中的数据,如果第二个cpu又来读这份数据,它不会里面读到之前的改变值而是读到一个"脏"值。也就是第一个操作对于第二个操作不可见。而且这时候第二个cpu可能根据条件判断,对这个值做出一些处理。由于不可见,会引发判断是失效,丢失的更新等等问题。不可见问题还有可能由重排序引起。 

对于资源竞争问题: 

资源竞争问题其实很好理解,如对一个变量计数。由于计数要分为读取,改变(依赖原来值做修改),替换三个步骤。如果多个线程不加任何限制的执行这个三个步骤,就会使其它步骤失效。还有HashMap扩容问题,打印机这些只能独占资源等等。这些都是资源竞争的一些问题。资源竞争问题远没有这么简单,特别是在高并发下,还可能引发一些硬件层面的问题。这个也是多线程的难点。资源竞争问题也可能由重排序引起。 

关于第三个话题: 

java对多线程提出来了自己的一套JMM模型。这个模型主要是对可见性约定——happens-before原则。happens-before有如下约定: 


(1)同一个线程中的每条指令都happens-before于出现在其后的任何一个指令。 

(2)对一个监视器的解锁happens-before于每一个后续对同一个监视器的加锁。 

(3)对volatile字段的写入操作happens-before于后续这个字段的读操作。 

(4)对于final修饰的变量,对它的构造happens-before于对它的第一次访问之前。 

(5)Thread.start()的调用会happens-before于启动线程里面的动作。 

(6)Thread中的所有动作都happens-before于其他线程检查到此线程结束或者Thread.join()中返回或者Thread.isAlive()==false。 

(7)一个线程A调用另一个另一个线程B的interrupt()都happens-before于线程A发现B被A中断(B抛出异常或者A检测到B的isInterrupted()或者interrupted())。 

(8)一个对象构造函数的结束happens-before与该对象的finalizer的开始 

(9)如果A动作happens-before于B动作,而B动作happens-before与C动作,那么A动作happens-before于C动作 

这些happens-before的约定解决了共享变量的可见性问题。对于共享资源竞争性主要是靠有锁的synchronized和lock,和无锁的cas操作保证。 

关于第四个话题: 

被final修饰的变量,是不可变对象(如果是一个引用类型,只是引用不可变,其值任然可变)。所以通过final修饰共享变量,可以大大减少多线程访问的复杂度。因为不可变对象一定是线程安全的。构造不可变对象是一个好的编程习惯。 

栈封闭,其实就是说没有共享变量,把所有变量的分配都在本线程的方法栈内使用。不传递给其他线程使用。这其实是回避了多线程共享问题,很多时候我们仍然需要共享的。 

ThreadLocal,这是另一个回避多线程共享变量的技术(只对本线程共享)。它把对象绑定到线程本地,Thread上的ThreadLocalMap中。这是一个弱键引用的map,键是ThreadLocal,value是具体本地线程共享的对象。所以这样的对象有两种方式被回收。第一ThreadLocal实例失去了强引用,在gc时可能会把它回收,第二这个线程失去了强引用,随着线程的回收而回收。所以ThreadLocal是不会内存溢出的。使用remove去移除不需要的变量是一种好习惯。如果线程长时间运行ThreadLocal通常也是不会失去强引用的,所以这里的内容得不到回收。 

volatile变量主要是对可见性的约束,即它的写入总能立即被下一个读取的线程看到。但是它不保证原子操作。 

synchronized是在对象或类上的内置锁。在synchronized作用域内的代码不能被重排序,并且读取共享变量能够看到它的最新状态,写入共享变量能够对持有这个锁的其它对象立即可见。 

ReentrantLock提供了与synchronized相同的功能。它们的主要区别是 

1,ReentrantLock必须自己释放锁。通常在finally释放。 

2,ReentrantLock可以通过参数控制多线程访问的公平性。 

3,ReentrantLock可以通过tryLock(),非阻塞的获得锁,也可以在指定等待时间内获得锁。synchronized一旦进入锁竞争就一定得等到锁,所以更容易产生死锁。 

4,ReentrantLock可以响应中断,而synchronized是无法响应中断的。这一点很重要!一个好的线程设计都需要响应中断。 

JUC的包很多主要是一些多线程工具。这里主要说下cas操作。像AtomicXXXX主要是利用cas操作,而不是锁。所以它的效率会高于有锁的同步。cas操作又主要是又cpu的硬件支持。JUC的内容很多在这里不单独讨论,有兴趣的同学可以看这篇博客,http://blog.csdn.net/xieyuooo/article/details/8594713 


关于多线程的讨论暂时就到此了,如有任何纰漏还请多多指出,谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当谈到Java多线程的实例时,有很多常见的应用场景和示例可以提供。以下是一些常见的Java多线程实例: 1. 线程池:线程池是一种管理和复用线程的机制,它可以提高线程的利用率和性能。通过使用线程池,可以创建一个固定数量的线程,然后将任务提交给线程池进行执行。这样可以避免频繁地创建和销毁线程,提高系统的响应速度。 2. 生产者-消费者模型:生产者-消费者模型是一种常见的并发编程模型,其中生产者线程负责生成数据,而消费者线程负责处理数据。通过使用多线程,可以实现生产者和消费者之间的并发执行,提高系统的吞吐量和效率。 3. 并行计算:在某些情况下,需要同时执行多个任务来提高计算速度。通过使用多线程,可以将一个大任务分解成多个小任务,并行执行这些小任务,最后将结果合并起来得到最终结果。这种方式可以显著提高计算速度。 4. 定时任务:在某些情况下,需要定期执行一些任务,比如定时备份数据、定时发送邮件等。通过使用多线程和定时器,可以实现定时任务的自动执行。 5. 并发容器:Java提供了一些并发容器,如ConcurrentHashMap、ConcurrentLinkedQueue等,它们可以在多线程环境下安全地进行读写操作。通过使用这些并发容器,可以实现高效的并发编程。 以上只是一些常见的Java多线程实例,实际上还有很多其他的应用场景和示例。希望以上回答对您有所帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值