发展史
没有操作系统,计算机从头到尾只执行一个程序,这个程序可以访问计算机所有的资源且每次只能运行一个
资源利用率、公平性、便利性
有了操作系统,出现了进程。操作系统负责为各个单独的进程分配各种资源如内存、文件句柄、安全证书等。
并在进程间采用套接字、信号处理器、共享内存、信号量、文件来共享&通信数据- 有了线程。允许进程存在多个程序控制流,线程共享进程范围的资源。线程的共享&通信需要更细粒度的机制
线程的优势
除了我们都知道的,UI响应更灵敏、异步事件的处理
可以发挥多处理器的能力,一个线程最多在一个处理器上运行,也就是如果是100个CPU,单线程程序有99%资源被浪费了
可以提高单处理器系统的吞吐率,当某线程等待I/O时,处理器空闲时另外一个线程可以继续运行
线程的风险
安全性问题:永远不发生糟糕的事情
/** * 多线程访问 操作一个非原子性的变量 * 得到结果具有随机性 * * @author by fengruicong on 16/8/10. */ public class MultiThreadTest1 { private static long count = 0; public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(5); for (int i = 0; i < 1000; i++) { service.execute(() -> System.out.print(getCount() + "\n")); } } public static long getCount() { return count++; } }
看上去count++是一个操作,但其实包含了三个:读count count+1 结果写入count
多线程执行getCount()时会出现得到的count值是一样的活跃性问题:某件正确的事情最终会发生
问题包括:死锁、饥饿、活锁等
性能问题:某件正确的事情尽快发生
问题包括:服务时间过长、响应不灵敏、吞吐率低、资源消耗过高、可伸缩性较低等
线程安全性
原子性
竞态条件:由于执行时序不一样出现的错误结果
1、如count++ 看起来是一个操作,但实际是“读取-修改-写入”的操作序列,结果状态依赖之前的状态,并且之前的状态是可能错误的
2、延迟初始化:目的是到用的时候才初始化对象
public class LazyInit{ private ExpensiveObject instance = null; public ExpensiveObject getInstance(){ if(instance == null){ instance = new ExpensiveObject(); } } }
这是常见的一种获取单例对象的写法,但是如果在多线程环境中,两个线程同时执行getInstance(),有可能出现多个结果:如两个线程各自得到一个ExpensiveObject实例 或者 两个线程得到同一个ExpensiveObject实例
3、复合操作
java.util.concurrent.atomic包里有些原子变量类如AtomicLong,实现了数值和对象引用上的原子状态转换,保证了代码的安全性
/** * 多线程访问 操作一个线程安全类 * * @author by fengruicong on 16/8/10. */ public class MultiThreadTest2 { private static AtomicLong count = new AtomicLong(0); public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(5); for (int i = 0; i < 1000; i++) { service.execute(() -> System.out.print(getCount().get() + "\n")); } } public static AtomicLong getCount() { return count.incrementAndGet(); } }
加锁机制
- 内置锁
synchronized修饰方法,即同步代码块、该同步代码块的锁就是调用方法的对象
sychronized修饰静态方法,class是锁
每个Java对象都可以用于实现同步锁,称为内置锁,线程在进入同步代码块时获得锁,退出同步代码块时释放锁。
获得内置锁唯一途径就是进入锁保护的代码块
每次只有一个线程执行内置锁代码块,所以由锁保护的代码块会以原子的方式执行
public synchronized void synMethod(){ } public static synchronized void synStaticMethod(){ }
参考 http://blog.csdn.net/virgoboy2004/article/details/7585182
- 重入
一个线程请求其他线程的锁时会阻塞,但是获得由自己持有的锁是没有问题的。
这代表着锁的操作粒度是“线程”不是“调用”
JVM实现重入的方法:为每一个锁关联一个计数值,当计数值为0,锁被释放。当线程请求锁时,JVM记录锁的持有者并计数值1,线程再次获取锁,计数值递增,线程退出时,计数值相应递减直至计数值为0释放锁
public class Widget{ public synchronized void doSth(){ ... } } public class LoggingWidget extends Widget{ public synchronized void doSth(){ System.out.println(toString()+": call doSth"); super.doSth(); } }
如果内置锁不可重入,上面这段代码会出现死锁
锁实现保护状态
synchronized可以实现单个操作的原子状态,多个原子操作合并为复合操作需要额外的锁条件和加锁机制
如果用同步协调对变量的访问,那所有访问这个变量的地方都需要使用同步,如果用锁协调的话,所有位置都必须使用同一个锁
活跃性和性能
需要在各种需求间找到平衡 安全性、简单、性能
当一个锁持有时间过长时,无论是执行什么操作,都会带来活跃性和性能的问题(耗时操作的操作不要持有锁)
线程间对象的共享
可见性
/** * 多线程 可见性 * 可能多种执行结果: * 1、打印 42 * 2、线程持续循环 ready对读线程一直不可见 * 3、打印 0 编译器重排序 * 4、 * * @author by fengruicong on 16/8/11. */ public class NoVisible { private static boolean ready; private static int number; private static class ReaderThread extends Thread { @Override public void run() { System.out.println(ready); while (!ready) { Thread.yield(); System.out.println(number); } } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } }
- 失效数据
非原子的64位操作
最低安全性:虽然得到的值失效,但是至少那个值是某个线程设置的值,而不是随机值
Java内存模型要求,变量的读写操作为原子操作,但是非volatile得64位变量long double,JVM会把该变量的读写分解为两个32位的操作,一旦读写操作在不同的线程,就会出现高32位和低32位不一致的问题
加锁和可见性
加锁不仅局限互斥性还有可见性。
volatile变量
- volatile变量对线程共享
- 编译器和运行时不会对它重排序
- 它不会被缓存在寄存器或者处理器看不见的其它地方
- 没有加锁 不会造成线程阻塞
- 只能保证内存可见性
读取volatile变量相当于进入同步块,写入volatile变量相当于退出同步块
volatile boolean asleep; ... while(asleep){ countSomeSheep(); }
* 注意:volatile变量无法保证原子性 如count++ *
对象发布和逸出
public static Set<Secret> knownSecrets; public void initialize(){ knownSecrets = new HashSet<Secret>(); }
当发布knownSecrets时同样会发布Secret对象 引起对象的逸出
class UnsafeStates{ private String[] states = new String[]{"AK","AL"}; public String[] getStates(){ return states; } }
这种方式发布states,由于所有调用者都可以修改数组内容,也就是states逸出了作用域,它本来是被定义为私有的
this引用的逸出
public class ThisEscape{ public ThisEscape(EventSource source){ source.register( new EventListener(Event e){ doSth(e); }); } }
当在构造函数使用多线程时,this引用会被多个线程共享,对象在没有完全构造完成时,新线程就可以看见它,导致逸出
改造:
public class SafeListener{ private final EventListener listener; private SafeListener(){ listener = new EventListener(){ public void onEvent(Event e){ doSth(e); } }; } public static SafeListener newInstance(EventSource source){ SafeListener safe = new SafeListener(); source.register(safe.listener); return safe; } }