2.线程的安全性
2.1什么是线程安全
在多个线程访问的时候,程序还能“正确”,那就是线程安全的。
无状态(可以理解为没有字段的类)的对象一定是线程安全的。
2.2 原子性
典型的例子,多线程状态下的i++是不安全的。因为i++其实是分很多步骤实现的,多个线程的执行过程可能会相互混乱。
竞态条件(Race Conditions)
线程与线程之间需要依赖于执行顺序来保证执行结果的正确性。那么就会发生竞态条件。例如在A线程中设置一个值,然后通过另外一信号变量通知给另外一个线程来读。这种协调是非常错误的。
关于重排序,参考这里:http://ifeve.com/java-memory-model-2/
最常见的例子:“先检查后执行”
在非同步的多线程环境下,你所看到的不一定是真实的,因为下一秒就可能已经不是这样的了!
书中举了个例子:
你和朋友约好了在某个地方的星巴克见面,但是去了之后才发现有两家星巴克,A和B,你在星巴克A里没有发现朋友,那么此时你的朋友可能:
1,在星巴克B家
2,在星巴克A与B的路上
如果你离开星巴克A,去找别的地方找,那么:你离开星巴克A的大门的下一秒,你手头的“朋友不在星巴克A”的信息就有可能失效!,因为在你从正门出去的下一秒,你的朋友就可能从后门进来了。
2.3 加锁机制
synchronize关键字表示内置锁,它根据当前的使用位置来自动创建一个锁对象,因此在使用的时候不用明确地指出用什么锁。
在普通成员方法中的synchronize表示用this表示锁对象
而在静态方法中的synchronize表示用class来作为锁对象
参考:https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.4.3.6
重入
同步方法的递归和子类调用父类方法时是可以重入的。不会产生死锁。
重入的实现机制:
在锁的信息中有这样的信息
{thread01,1}
表示thread01获取了1次这个锁。当thread01需要再次获取锁时,记录如下:
{thread01,2}
在方法执行完毕之后,锁数量会往下减,当数量为0时则释放该锁。
3.对象的共享
3.1 可见性
怎样让线程A的行结果对线程B可见? 加锁!
用锁来保证一个写、另外一个读按期望的顺序执行,才能实现写的结果会被读到。仅靠代码执行顺序是不行的!
volatile变量
Java提供的一种同步机制,用来保证volatile修饰的变量,在一个线程中被修改之后,另外的线程也能看到。
内部机制是volatile变量不会进行重排也不会缓存到寄存器。
注意事项:
1.volatile只保证可见性,但不保证原子性,所以不要应用于多线程“写”的场景中
2.多个volatile其实单独的,不构成整体同步
使用volatile时一定要特别小心,一般用于简单的信号传递,稍微复杂一点儿的还是用锁吧:
public class VolatileTest2 { public static void main(String[] args) { VolatileTest2 test = new VolatileTest2(); test.start(); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } test.stop(); } volatile boolean stop = false; public void start() { new Thread(new Runnable() { @Override public void run() { while(!stop) { System.out.println("running..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } public void stop() { stop = true; } }
3.2 发布与逸出(Escape)
你需要弄清楚,那些对外公开的数据是否真的需要对外公开,因为一旦公开,外部就可以访问,那么你就不得不考虑多线程的问题了。
另外一些时候,你可能会不小心把数据对外公开了,比如在构造函数中创建匿名内部类,或者启动一个线程。不要这么做的原因是,构造函数函数中会把this暴露给内部类或者线程。而“this”本身其实还可能没ready好呢!
(解决办法:使用工程方法;先定义好线程,另外增加一个方法来start这个线程)
3.3 线程封闭
当线程不需要进行数据共享,那么把数据封闭在线程中,是保证线程安全很好的办法。程序设计中应该避免数据逸出到其他线程中。
比如可以使用局部变量或者ThreadLocal来存储线程数据。
ad-hoc线程封闭
就是指完全由程序来实现线程间数据的封闭,不要这样做。
栈封闭
可以理解为基本类型的局部变量是封装在执行的栈里的,不会破坏栈的封闭性。
ThreadLocal
它是一个实现线程封闭的很好的工具类。可以把ThreadLocal<T>看成Map<Thread,Object>,但是它底层的实现其实不是这样的。ThreadLocal的数据实际存储在了关联的Thread对象中。在Thread结束时,这些数据会被回收掉。
注意:不是所有全局参数都需要存储在ThreadLocal中,有人甚至把函数调用的参数都存在这里,这样做会导致程序可读性降低,增加程序耦合度,不便于维护。
3.4 不变性
不可变的数据一定是线程安全的,所以在避免数据状态出问题时,可以想办法让它的状态不可变(如果能不变)。
使用final+volatile实现线程安全
final的特点