@CLSChen
2019-10-03T16:31:02.000000Z
字数 4802
阅读 810
未分类
最开始的计算机只能运行一个程序,操作系统的出现带来了进程,提高了资源利用率,公平性和便利性。而线程则进一步细分了进程。
计算机 -> 操作系统(进程) -> 线程
1 n n*m
但凡做事高效的人,总能在串行性和异步性之间找到合理的平衡,对于程序也是如此。
线程共享进程的变量等资源,如果没有明确的协同机制,那么当一个线程正在使用某个变量时,另一个线程可能同时访问这个变量,这将造成不可预测的结果。
发挥多处理器的强大能力
现在的计算机都是多核的,一个单线程的进程只能使用一个cpu资源,那么如果是双核的计算机,只能占有50%的资源,如果是100个处理器的系统上,将有99%的资源无法使用!
建模的简单性
将任务分成子任务,然后放在不同的线程中执行,比一个冗杂的大型任务更易于编写,错误更少,也更容易测试,每个工作流在一个单独的线程中运行,并在特定的同步位置进行交互。
异步事件的简化处理
框架中可能也会创建线程,JVM启动时,它将为JVM的内部任务,垃圾收集器,终结操作等创建后台线程,并创建一个主线程来运行main方法。
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
一个无状态类,只是加了一个count值,在每次调用这个类的时候count++,它就不是线程安全的了:count++并非一个原子操作,它包含了读取修改写入三个操作,每次操作都依赖于前面的状态。
在并发编程中,由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,被称为竞态条件。
public class LazyInitRace{
private ExpensiveObject instance = null;
public ExpensiveObjcet getInstance(){
if(instance == null) // 如果两个线程同时检查怎么办?
instance = new ExpensiveObject();
return instance;
}
}
如果两个线程同时执行getInstance方法,而且都觉得还未创建,又分别都创建了,那可能会得到两个instance!
这可不是我们想要的“单例模式”
光使用线程安全类是不够的,当更新某个变量时,需要在同一个原子操作中对其他变量同时进行更新。
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
每个共享的和可变的变量都应该只有一个锁来保护,从而使维护人员知道是哪一个锁。
当获取与对象关联的锁时,并不能组织其他线程访问该对象,只能阻止其他线程获得同一个锁,每个对象都有一个内置锁只是为了免去显式地创建锁对象。
一种常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步。在许多线程安全类中都是用了这种模式,如Vector和其他的同步集合类。
如果不加区别的滥用synchronized,可能会导致更多的同步。
if(!vector.contains(element))
vector.add(element);
虽然contains和add都是原子方法,但是这个操作中仍然存在竞态条件。
我们应该缩小同步代码块的作用范围,但是不要太小,不要将本应该是原子的操作拆分到多个同步代码块中。尽量将不影响共享状态且执行时间较长的操作从同步代码块中分离出去。
当执行时间较长的计算或者可能无法完成的操作时(网络IO操作,控制台IO操作),一定不要持有锁。
第二章强调了原子性,即不能有两个线程修改同一个变量。而同步还有另一个重要的方面:可见性。我们希望当一个确保一个线程修改了对象状态后,其他线程能够看到发生的状态变化。
public class NoVisibility{
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread{
public void run(){
while(!ready) // ready在被修改为true前线程阻塞
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args){
new ReaderThread().start();
number = 42;
ready = true;
}
}
程序有这么几种情况:
1.一直循环下去,主线程的两个修改一个都没看见。
2.输出0,只有ready的修改被看见了。因为重排序的原因,ready的修改在number之前。
3.输出42,两个都被看见了。
这三种情况是哪一种我们并无法知道,这就是非同步带来的可见性问题。
这是失效数据问题,当读线程读ready变量时,可能会看到一个已经失效的值。除非在每次访问变量时都使用同步,更糟糕的是,失效值可能不会同时出现,一个线程可能获得某个变量的最新值,而获得另一个变量的失效值。
Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但是long和double会将变量分成两个32位的变量,因此如果在多线程程序中使用共享且可变的long和double变量是不安全的,除非用volatile声明或者使用锁进行保护。
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行操作的线程都必须在同一个锁上同步。
发布就是使对象能够在当前作用域之外的代码中使用。
逸出就是指某个不该发布的对象被发布。
如果仅在单线程中访问数据,就不需要同步,这种技术被称为线程封闭(Thread Confinement)。这是实现线程安全性的最简单方式之一。
在Swing和JDBC中大量使用了线程封闭技术。
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全的发布:
在静态初始化函数中初始化一个对象引用。
将对象的引用保存到volatile类型的域或者AtomicReferance对象中。
将对象的引用保存到某个正确构造对象的final类型域中。
将对象的引用保存到一个由锁保护的域中。
在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。
对象的发布需求取决于它的可变性:
不可变对象可以通过任意机制来发布。
事实不可变对象必须通过安全方式来发布。
可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。
使用和共享对象的一些实用策略:
线程封闭,只读共享,线程安全共享,保护对象。
我们希望能用一些现有的线程安全组件组合起来,组合为更大规模的组件或程序,这些模式能够使得一个类更容易成为线程安全的,并且维护这些类时不会无意中破坏类的安全性保证。
如果对象的所有域都是基本类型的变量,这些域就是对象的全部状态。
如果对象的域引用了其他对象,那么该对象的状态将包含被引用对象的域。比如LinkedList的状态就包括该链表中所有节点对象的状态。