线程安全:当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用者代码不必作其他的协调,这个类的行为仍 然是正确的,那么称这个类是线程安全的。
编写线程安全的代码, 本质上就是管理对状态的访问,而且通常都是共享的, 可变的状态.通俗的说, 一个对象的状态就是他的数据.
所谓共享就是指一个变量可以被多个线程访问, 所谓可变是指变量的值在其生命周期内可以改变, 而真正要做到线程安全是在不可控的并发访问中保护数据
一个对象是否应该是线程安全取决于它是否被多个线程访问.
无论如何, 只要有多于一个的线程访问给定的状态变量, 而其中某个线程会写入该变量, 此时必须使用同步来协调线程对该变量的访问.
最简单的保证数据的线程安全:
- 不要跨线程共享变量
- 使状态变量为不可变
- 在任何访问状态变量的时候使用同步.
锁:
synchronized方法的锁就是该方法所从属的对象本身,而在静态方法中是class上获取。
竞争条件最常见的一种竞争条件是"检查再运行(check-then-act)",:使用一个潜在的过期值作为下一步操作的依据.
检查再运行: 你观察到一些事情为真, 然后基于你的观察去执行一些动作, 但事实上, 从观察到执行操作的这段时间内, 观察结果可能已经无效了, 从而引发错误.
惰性初始化中的竞争条件:
惰性初始化的目的是延迟对象的初始化,直到程序真正使用它,同时确保它只初始化一次。如下:
public class LazyInitrace{
private Object instance=null;
public Object getinstance(){
if(instance==null)
instance=new Object();
return instance;
}
}
线程A和B同时执行程序,A看到instance==null,所以实例化一个新对象,B也在检查instance是否为空,此时的instance是否为空依赖于时序,这是无法预期的,如果instance为null,这时两个getinstance方法的调用者得到不同的结果,而我们期望的是同样的结果。
原子操作
假设有操作a和b, 如果从执行a的线程的角度看, 当其他线程执行b时, 要么b全部执行完成, 要么一点也没有执行, 那么a和b就互为原子操作.
自增操作(value++),不是原子操作,它的过程是:先读取value的值,然后自增,最后再重新写入。如:
public classs first{
private long count=0;
public long getcount(){
return count;
}
public void method(){
count++;
//·····
}
}
而在java.util.concurrent.atomic包中包括了原子变量类,改写如下:
public classs first{
private final AtomicLong count=new AtomicLong(0);
public long getcount(){
return count;
}
public void method(){
count.incrementAndGet();
//·····
}
}
把long类型的换成AtomicLong类型的,可以确保所有访问计数器的操作都是原子的,incrementAndGet()方法不但是计数器递增,而且还会返回递增后的值。
为了保证线程安全, "检查再运行"操作和"读-改-写"操作必须是原子操作.
用锁来保护共享状态,复合操作会在完整的运行期间占有锁,以确保其行为是原子的。
即使获得与对象关联的锁也不能阻止其他线程访问这个对象——获得对象的锁后,唯一可以做的事情就是防止其他线程再获得相同的锁。
锁的可重入特点
线程在试图获得它自己占有的锁时, 请求线程将会成功, 重进入意味着所有的请求都是基于"每线程", 而不是基于"每调用".
可重入的例子:
- public class Supper{
- public synchronized void doSomething(){...}
- }
- public class Child extends Super{
- public synchronized void doSomething(){
- ...
- super.doSomething();
- }
- }
如果内部锁是不可重入的, 那么super.doSomething()将永远也无法得到Super的锁, 因为锁已经被子类占用了, 而可重入则可以避免这种死锁.
因为父类和子类的dosomething方法都是synchronized,都会在处理时试图获得父类的锁,但子类改写了父类的方法,获得锁,当执行到super.dosomething()时,由于子类获得了锁,所以此句将等待锁,但永远都无法获得锁。
如同线程安全类Vector,它 的每一个方法都是同步的,但不确保在Vector上执行的复合操作是原子的,如:
if(!vector.contains(element))
vector.add(element);
虽然contains()和add()方法都是原子的,
但这种“缺少即加入(put-if-absent)”操作的过程中仍然存在竞争条件。虽然同步方法确保了不可分割操作的原子性,但把多个操作整合到一个复合操作时,还是需要额外的锁。
共享对象
同步同样还有一个重要而微妙的方面: 内存可见性. 我们不仅希望能够避免一个线程修改其他线程正在使用的对象的状态, 而且希望确保当一个线程修改了对象的状态之后, 他线程能够真正看到改变.
共享只读:可以被多个线程不用同步的访问,但是没有线程能修改他,也就是:不可变对象和高效不可变对象。
共享线程安全:对象在自身内部进行同步,其他线程无需额外同步,通过公用接口可以安全访问。
3.1.0volatile
volatile类型,它确保对一个变量的更新以可预见的方式告知其它线程,相当于get()+set()。
锁不仅是关于同步与互斥的, 也是关于内存可见的, 为了保证所有线程都能看到共享的, 可变变量的最新值, 读取和写入线程必须使用公共的锁进行同步.
volatile变量相对于synchronized而言, 只是轻量级的同步机制:加锁可以保证可见性与原子性; volatile变量只保证可见性.从内存可见性的角度来看:写入volatile变量就像退出同步块, 读取volatile变量就像进入同步块(理解).
正确使用volatile的方式包括:
用于确保它们所引用的对象状态的可见性或者用于标识重要的生命周期事件(如初始化或关闭)的发生。
如下就师范了一种volatile的典型应用:检查状态标记,以确定是否退出一个循环。
volatile boolean asleep;
···
while(!asleep)
countsheep();
上述例子中的asleep标记就必须为volatile类型,否则执行检查的线程不会在注意到asleep已被其他线程修改,当然也可以用锁来替换volatile。
3.1.1过期数据
过期数据就是一个线程访问了一个变量先前写入的值,称为过期值。如:
public class First{
private int value;
public int get(){
return value;
}
public void set(int value){
this.value=value;
}
}
如果一个线程调用了set,而另一个线程此时正在调用get,它就可能看不到更新的数据了。我们可以同步化getter和setter,使之成为线程安全的,如:
public class First{
private int value;
public synchronized int get(){
return value;
}
public synchronized void set(int value){
this.value=value;
}
}
仅仅是同步的setter是不够的:因为调用get的线程仍然能够看见过期值。
只要线程被跨线程共享,就进行恰当的同步。
3.1.3锁和可见性
内置锁(synchronized块)可以用来确保一个线程以某种可预见的方法看到另一个线程的影响,换句话说就是当B执行到与A相同的锁监视的同步块时,A在同步块中或者之前所做的每件事对B都是可见的,这就是是说当A执行到一个同步块中,线程B也进入了被同一个锁监视的同步块中,这时可以保证,在锁释放之前对A可见的变量的值,B获得锁之后同样也是可见的。
当访问一个共享变量时,为什么要求所有线程用同一个锁进行同步——为了保证一个线程对变量的写入,其他线程都可见。即 锁是可见的也是同步的。
3.14非原子的64为操作
JVM允许将64位的读写划分两个32位的操作, 如果读写发生在不同的线程, 这样情况读取一个非volatile类型long就可能会出现得到一个值的高32位和另一个值的低32位,即使你不关心过期数据,但仅仅在多线程程序中使用共享的、可变的long和double(64位)也是不安全的,除非将它们生命为volatile类型或者用锁保护起来。
3.2发布与逸出
对象的发布:使这个对象能够被当前范围之外的代码所引用。
对象的逸出:对象在未准备好时就将它发布。
允许内部可变的数据逸出:
class Unsafe{
private String[] state=new String[]{"a","b","c"};
public Stirng[] getstate(){
return state;
}
}
以这种方式发布会出问题。state本应该是私有的数据,事实上已经变成公有的了,任何一个调用者都可以修改它的内容,数组state已经逸出了它所属的范围。发布一个对象,同样也发布了该对象所有非私有域所引用的对象,在一个已经发布的对象中,那些非私有域的引用链和方法调用链中可获得对象也都会被发布。
隐式地允许this引用逸出:
public class Thisescape{
public Thisescape(EventSource source){
source.registerListener(
new EventListener(){
public void onEvent(Event e){
dosomething();
}
}
);
}
}
发布的内部EventListener实例是一个封装的Thisescape实例,所以当发布
EventListener时,也无条件地发布了封装Thisescape的实例,因为内引类的实例包含了对封装实例隐含的引用。
一个导致this引用在构造期间逸出的常见错误就是在构造函数中启动一个线程。
为了避免不正确的创建,可以使用一个私有的构造函数和一个公共的工厂方法。如:
public class Safelistener{
private final EventListener listener;
public Safelistener(){
listener=new EventListener(){
public void onEvent(Event e){
dosomething()
}
}
}
public static Safelistener newInstance(EventSource source){
Safelistener safe=new Safelistener();
source.registerListener(safe.listener);
return safe;
}
}
3.4、不可变对象
不可变对象:创建后状态不能被修改的对象。
高效不可变对象:在技术上不是不可变,但发布后但在发布后不会被修改的对象,这些对象可以在任何线程中安全发布。
可变对象:要安全发布,同时也得是线程安全或被锁保护的
无状态对象:它不包含域也没有引用其他类的域。
无状态安全对象永远是线程安全的
3.53安全发布的模式
安全的发布对象引用:
.静态初始化器中的初始化引用
.引用存到volatile或AtomicReference字段中
.引用存储到正确创建的final域
.引用存储到synchronized正确保护的域中
线程安全容器的内部同步意义:
即满足上述最后一条原则。如果线程A将对象置入某一个线程安全容器,随后线程B重新获得X,这时可以保证B所看到的X的状态正是A所设置(更新)的。
线程安全库提供了如下的线程安全保证:
。置入Hashtable、synchronizedMap、ConcurrentMap中的主键以及键值,会安全地发布到可以从Map获得它们的任意线程中,无论是直接获得还是通过迭代器(Iterator)
。置入Vector、CopyOnWriteArrayList、CopyOnArraySet、synchronizedList、synchronizedSet中的元素,会安全地发布到可以从容器中获得它们的任意线程中
。置入BlockingQueue、ConcurrentLinkedQueue 的元素,会安全地发布到可以从队列中获得它们的任意线程中
通常最简单和最安全的方式就是使用静态初始化器:
public static Holder holder=new Holder(42);