重温《并发编程实战》---线程安全性

1.多个线程访问的可变变量,如果没有使用合适的同步,那么程序就会出现错误。

请注意2个关键字:

a.)多个线程。

b.)可变变量。

因此为了不出现错误,可以针对a.)不在线程之间共享该变量,针对b.)将该变量设置为不可变变量。c.)使用同步。

2.只有当类中仅包含自己的状态时(即不包含其他对象的状态),线程安全类采样才有意义。

3.线程安全性:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么称这个类是线程安全的。

4.无状态对象一定是安全的:

我们在Servlet中可以看到很多无状态类,它即不包含任何域,也不包含任何其他类中域的引用,他的临时状态(包括方法参数和局部变量只保存在线程私有的虚拟机栈上的局部变量表中,因为这些都是临时状态,是线程私有的,其他的线程访问不到另一个线程的临时状态,所以无状态对象一定是线程安全的。

 

大多数servlet都是无状态的,这样会降低实现Servlet线程安全性的复杂性。

 

5.竞态条件:

当某个计算的结果取决于多个线程交替执行时序的时候,那么就会发生竞态条件。

基于一种可能失效的观察结果来做出判断或执行某个计算。

最典型的静态条件的类型就是”先检查后执行”,即根据一个可能已经失效的结果来做出判断或者进行计算,你的判断和计算也会是错的。

典型的竞态条件情况:

a.)先检查后执行:(最典型的是延迟初始化):比如你想判断一个对象是否为空,如果是空就生成1个这个对象即

private Objcet object;

If(object == null){

Object = new Object();

}

这种延迟初始化的本意是将对象的初始化操作延迟到实际使用时,并且只初始化1次。

然而我们实际的结果则需要取决于不可预测的时序,假如A线程看到object为空,如果切换到B时,仍然看到为空,那么应该最后其实你实例化了2个对象。

 

     b.)“读取-修改-写入操作”:最典型的是递增或递减计数器,结果需要依赖之前的结果, 读到之前的结果取决于顺序。

6.关于状态数量的线程安全:

当一个无状态类中添加了1个状态,此时状态数量由0变成1的时候,如果这个新增的状态是由线程安全的对象来管理(比如AtomicLong),这个类仍然是线程安全的。

当你的状态数量变成多个的时候,即使你的状态是由线程安全的对象来管理,这个类依旧不一定是线程安全的类。

当多个状态变量相互产生约束和依赖的时候,当你更新其中一个状态时,你要确保相关的状态变量在同一次操作中完成更新,这样才能保证线程安全。

因此,要保证状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。

 

7.原子性和可见性:

Java内置锁机制和其他同步机制的两个重要特性就是:原子性和可见性。

8,Java锁机制的原子性(同步代码块):

同步代码块包括两部分:

a.)作为锁的对象引用,synchronized(作为锁的对象引用)

b.)锁保护的代码块。

需要注意的是,静态的synchronized方法以Class对象作为锁,因为实例对象是在初始化的时候生成,而不是在静态加载的时候生成。

锁是互斥锁,保证了原子性。

缺点:只有一个线程可以执行相关方法,服务器响应比较低,性能体验比较差。

9.锁的重入:

Java中的synchronized同步块是可重入的。这意味着如果一个java线程进入了代码中的synchronized同步块,并因此获得了该同步块使用的同步对象对应的管程上的锁,那么这个线程可以进入由同一个管程对象所同步的另一个java代码块。下面是一个例子:

 

 

public class Reentrant{

2    public synchronized outer(){

        inner();

    }

    public synchronized inner(){

        //do something

}

注意outer()inner()都被声明为synchronized,这在Java中和synchronized(this)块等效。如果一个线程调用了outer(),在outer()里调用inner()就没有什么问题,因为这两个方法(代码块)都由同一个管程对象(”this”)所同步。如果一个线程已经拥有了一个管程对象上的锁那么它就有权访问被这个管程对象同步的所有代码块。这就是可重入。线程可以进入任何一个它已经拥有的锁所同步着的代码块。

public class Test {

public static void main(String[]args){

Thread t = new Thread(new Runnable() {

@Override

public void run() {

S s = new S();

s.doc();

System.out.println("测试线程阻塞否");

}

});

t.start();

}

}

class F{

public synchronized void doc(){

System.out.println("f");

}

}

class S extends F{

@Override

public synchronized void doc() {

System.out.println("s");

super.doc();

}

}

如果锁不可以重入,那么调用doc方法时已经有锁了,再调用super.doc()无法获得锁,线程将阻赛,实际却没有,说明同一个锁是可以重入的。

10.结论:如果用同步来协调对某个变量的访问,那么在访问这个变量的所有位置上都需要使用同步。

11.我们的每个对象都有1个自己的内置锁,但是内置锁与其状态之间没有内在的关联。内置锁为我们免去了显示创建锁对象,每一个共享的和可变的变量都应该只由1个锁来保护,从而使维护人员知道应该是哪一个锁。

 

12.同步:同步这个术语包括4个概念:

a.) synchronized

b.) volatile类型变量

c.) 显示锁

d.) 原子变量

13.不变性条件的锁机制:

我们的类中可能会有不变性条件,当我们的不变性条件“涉及”多个状态变量的时候,为了不破坏这个不变性条件(目的),我们需要在单个原子操作中访问或者更新这些变量(实现理论),所以在不变性条件中的每个变量必须由同一个锁来保护(实现方法)

14.复合操作(多个单个操作)的锁机制:

例如:

If(!vectoer.contains(element)){

vector.add(element);

}

虽然addcontains都是单个原子操作,但是我们整个过程是由这两个原子操作完成的。

synchronized方法可以确保单个操作的原子性,但是如果把多个操作合并为一个复合操作,则需要额外的加锁机制。

15.编程结论:应该尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去。

猜你喜欢

转载自blog.csdn.net/mypromise_tfs/article/details/72772685