第一章 基础知识(线程安全)
线程安全的核心问题:处理对象状态的问题。如果要处理的对象是无状态的(不变性),或者可以避免多个线程共享的(线程封闭),那么我们可以放心,这个对象可能是线程安全的。当无法避免,必须要共享这个对象状态给多线程访问时,这时候才用到线程同步的一系列技术(锁)。
对象的状态:指存储在状态变量(eg:实例或者静态域)中的数据。
处理多线程的大方向:如果能避免多线程问题就避免多线程问题,实在避免不了了再处理。
处理多线程问题可以考虑的几个点:
1、能否做成无状态的不变对象,无状态是线程安全的。(不变性)
2、能否线程封闭(线程封闭)
3、采用何种同步技术(锁)
多线程中需要了解的三个特性:
原子性:sychronized
可见性:sychronized、volatile、final
有序性:sychronized、volatile
内置锁:sychronized(原子性,可见性,有序性)
因为是阻塞的,虽然简单,性能却非常低。
重入:某个线程试图获得一个已经由他自己持有的锁,那么这个请求就会成功。重入避免了死锁情况的发生。
发布与逸出:
发布:对象能够在当前作用域之外的代码中使用。(发布内部对象可能会破坏封装性,并使得程序难以维持不变性条件)
逸出:当某个不应该发布的对象被发布时,称为逸出。
发布对象的方法:
1、将对象的引用保存到一个公有的静态变量中(其他对象可以间接发布)
public class Test {
public static List<People> list;
public Test(){
list = new ArrayList<People>();
}
}
2、非私有方法中返回一个引用,发布返回的对象。
public class Test {
private String[] strs = {"AB","BA"};
public String[] getStrs() {
return strs;
}
}
3、将对象引用传递给外部方法
当把一个对象传递给外部方法时,就相当于发布了这个对象
public class Test {
public void get(Object obj){ //obj对象逸出
...
}
}
this引用逸出:
1、内部类中this引用逸出
public class Outer {
private String str="Outer's string";
public class Inner{
public void write(){
System.out.println(Outer.this.str);
}
}
public static void main(String[] args) {
Outer out = new Outer();
Outer.Inner in = out.new Inner();
in.write();
}
}
2、构造函数中this引用逸出
public class ThisEscape {
private Thread t;
public ThisEscape(){
System.out.println(this);
t = new Thread(){
public void run(){
System.out.println(ThisEscape.this);
}
};
t.start();
// do something
}
public static void main(String[] args){
ThisEscape a = new ThisEscape();
}
}
this引用被线程t共享,故线程t的发布将导致ThisEscape对象的发布,由于ThisEscape对象被发布时还未构造完成,这将导致ThisEscape对象逸出(在构造函数中创建线程是可以的,但是不要在构造函数执行完之前启动线程)
在构造函数中调用一个可改写的实例方法时,也会导致同样的问题
解决方式(私有构造函数+公有工厂方法):
public class ThisEscape {
private final Thread t;
private ThisEscape(){
t = new Thread(){
public void run(){
// do something
}
};
// do something
}
public static ThisEscape getInstance(){
ThisEscape thisEscape = new ThisEscape();
thisEscape.t.start();
return thisEscape;
}
}
(1)无状态(不可变)
不变性:某个对象在被创建之后其状态就不能被修改,这个对象就是不可变对象。
即使一个对象是线程安全的不可变对象,指向这个对象的引用也可能不是线程安全的。
final修饰的变量,不可修改。
final修饰的方法,不可重写。
final修饰的类,不可继承。
(2)线程封闭
1、程序控制线程封闭
2、栈封闭
使用局部变量。局部变量是保存在线程栈中的,对该线程可见,是线程安全的
3、ThreadLocal类
ThreadLocal机制本质上是程序控制线程封闭,只不过是Java本身帮忙处理了。来看Java的Thread类和ThreadLocal类
1. Thread线程类维护了一个ThreadLocalMap的实例变量
2. ThreadLocalMap就是一个Map结构
3. ThreadLocal的set方法取到当前线程,拿到当前线程的threadLocalMap对象,然后把ThreadLocal对象作为key,把要放入的值作为value,放到Map
4. ThreadLocal的get方法取到当前线程,拿到当前线程的threadLocalMap对象,然后把ThreadLocal对象作为key,拿到对应的value.
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
public class ThreadLocal<T> {
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
}
ThreadLocal的设计很简单,就是给线程对象设置了一个内部的Map,可以放置一些数据。JVM从底层保证了Thread对象之间不会看到对方的数据。
使用ThreadLocal前提是给每个ThreadLocal保存一个单独的对象,这个对象不能是在多个ThreadLocal共享的,否则这个对象也是线程不安全的。
Structs2就用了ThreadLocal来保存每个请求的数据,用了线程封闭的思想。但是ThreadLocal的缺点也显而易见,必须保存多个副本,采用空间换取效率。
(3)采用内置锁来保护对象状态
锁的对象是实例或者Class
锁的同步性和互斥性会带来性能问题,可以采用合理优化缩小同步代码块的范围来稍微提高性能。
当执行时间较长的计算或者可能无法快速完成的操作时(eg网络I/O或者控制台I/O),一定不要持有锁。
对象的组合
解决问题:
不希望每次线程访问都去分析是否线程安全,通过将一些现有的线程安全组件组合起来形成线程安全的程序。
1、设计线程安全的类三个基本要素
找出构成对象状态的所有变量
找出约束状态变量的不变性条件
建立对象状态的并发访问管理策略
2、实例封闭来确保线程安全(同步锁)
Java监视器模式:把对象所有的可变状态封装起来,并由对象自己的内置锁来保护。
3、线程安全性委托
我们经常会使用ConcurrentHashMap这样的并发对象,它们是java提供的并发变成对象,它们在内部实现了同步机制,所以它们是线程安全的,我们可以把不安全对象Map的访问完全交给它来委托管理,这样通过final类型的处理,我们只需要通过Collections的浅拷贝实现Map的线程安全。
4、在现有的线程安全类中添加功能
客户端加锁:需要明确是哪个对象需要加锁(使用哪个对象的客户端代码)
组合:
小结:
多线程并发编程核心是处理对象状态。这些对象有一个特点是线程不安全的:共享和可变
有三种办法:
1、避免可变,将对象设置为无状态的,不可变的。
2、避免共享,可以用线程封闭方法,ThreadLocal,给不同的线程分配不同的副本。用空间换取时间。ThreadLocal接口有4种方法:set(Object value),get(),remove(),initialValue()。
3、实在没有办法避免多线程并发的问题,则需要考虑哪些变量声明为volitaile,哪些变量需要用锁保护,哪些锁保护哪些变量,哪些操作必须是原子操作。是否可以采用线程安全对象组合的方式构建线程安全,组合是否需要考虑客户端加锁的办法。
在确保线程安全时需要注意的三大特性:原子性,可见性,有序性
在对象操作的时候注意发布与逸出问题。