概念篇
1: 为什么需要多线程
在程序处理多任务时,在单线程下,如果其中一个任务被阻塞了(比如IO操作),那么所有的任务都无法前进,知道被阻塞的任务进行前进,将浪费很大的系统资源。
那么解决方案就是某一项任务暂时不适用资源时,就将资源释放出来,供其他的任务适用,这种方式就是多线程。
2:多线程有什么问题
多线程能极大程度上利用空闲的资源。如果多个线程之间共享了某一份数据,如果不进行同步,共享的数据将被破坏。
因此多线程编程的难点就是如何保证对共享数据的同步。这也是多线程中唯一的一个问题,该问题可引发其他许许多多的后续问题和技术,比如:死锁、非阻塞算法等等。
3:Java的内存模型
平台的存储模型:
每个线程、寄存器都有自己的内存空间,因此对于同一存储位置(比如:没有同步的变量),允许不同的线程看到
不同的值。
重排序:
在一个没有进行同步的程序中,线程之间的执行先后顺序是没有保障的。那么线程将变量的值从变量的存储位置拷贝到线程的内存的时机也是各不相同的。
Java的内存模型:
happens-before : 如果动作B要看到动作A执行后的结果,那么称为A happends-before B,即A发生在B之前。
如果没有对两个动作定义了happens-before的关系,那么JVM就可以对他们进行任意的重排序。
happens-before:常见的有:
程序次序:单线程的代码是按代码的前后顺序执行
访问同步的变量
volatile写操作happens-before在其完成后的所有读操作。
4:volatile变量
volatile关键字指的是,线程每次读取volatile的变量的值,都是重新从变动的存储位置读取,因此能保证是最新写入的值。
和锁的区别在于:加锁(这里以排它锁举例)读取时,别的线程不能对这个变量进行操作;而volatile则没有这层限制。
因此volatile变量不能用于类似自增的操作,因此自增的操作是:取当前值A,加1,将A+1的值付给变量。由于volatile只保证取值是最新写入的值,那么就可能发生取完最新值A后,别的线程就可能对该变量写了很多次值,因此volatile变量不适于自增操作。
volatile只能用于保证可见性(读的都是最新写入的数据),不能保证原子性。原子性的操作就需要锁或者CAS.
5:发布对象
发布对象:如果该对象能够被别的代码(别的线程)访问到,那它就是被发布出去了。
发布出去的对象需要考虑:别人是否会该其对象的内部结构。
不好的实践:
返回一个数组
返回匿名内部类,隐藏了对外部类的引用,同时也是一种很隐秘的内存泄露方式。
6:不需要同步的数据
不需要同步的数据:在多线程,不加锁的情况下不会被破坏的数据。
1:不可变对象:比如String,Date,int等。
注意final对象只表示引用所指向的对象地址不会改变,不表示对象的内部状态不变。final对象不一定是不可变对象。
2:只在单线程中使用的变量:
方法中不返回的局部变量;存放于ThreadLocal中的变量;
7:线程安全:
线程安全:类所提供的每一个方法在多线程情况下是不会损坏数据时,则表示线程是安全的。
注意:使用线程安全的方法,不一定就是安全的。组合使用两个线程安全的方法时,在两个线程安全的方法之间别的线程可能破坏数据。
if(vector.contains()){
vector.add().
}
8:死锁与open call:
如果线程1获得锁A,正在申请锁B,而同时线程2获得了锁B,正在申请锁A。这将发生死锁。
open call : 当一个方法中不获得锁时,称为open call。
为什么需要open call:
两个原因:
1:因为外部一个方法可能需要多个内部的方法完成,如果多个内部的方法都持有锁时,性能较低。
2:为了更好的避免死锁,如果你加了锁而调用方的另一些方法也加了同样的锁,那么就容易造成死锁。
因此一个可采取的方法是:
1:尽可能的使用open call
2:一定要加锁时,不同的类里的方法使用不同的锁机制、锁标识,同一类中要按顺序加锁。