程序语言作为开发软件的工具,与电脑沟通交流的工具。每个人都有选择工具的理由。我选择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于启动线程里面的动作。

)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的内容很多在这里不单独讨论,有兴趣的同学可以看这篇博客,

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