本系列为本人在研读相关技术书籍后所总结之精华,希望能对大家有所帮助,有兴趣的可以加我好友,大家共同学习进步!
不同的程序都在单独的进程中运行,操作系统为各个独立的进程分配包括内存在内的各种资源,对大多数操作系统来说,都是以线程为基本调度单位。
第二章、线程安全性
原子操作:
public class P5 {
public class UnsafeSequence{
private int value;
public int getNext(){
return value++;//改操作不保证原子性
}
}
}
虽然操作count++看上去只是一个操作,但其并非原子的,以为它并不会作为一个不可分割的操作来执行,它包含了读取——修改——写入
什么是原子方式?
假定有两个操作A和B,如果从执行A的线程来看,当个另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说就是原子的。
竞态条件:
由于不正确的时序而出现不正确的结果
最常见的竞态条件:先检查后执行(基于一种可能失效的观察结果判断)
以单例模式中的懒汉模式和饿汉模式为例子,首先单例模式的·核心要点在于只允许创建一个对象,相应地优点便是占用内存资源少。
代码特点:1.私有的从属于类的对象属性 2.私有的构造方法 3.公开的get对象方法
public class P17 {
private static P17 p17= new P17();//饿汉模式,线程安全
private P17(){};
public P17 getP17(){
return p17;
}
}
class Lazy{//懒汉模式线程不安全,必须加锁
private static Lazy lazy;
private Lazy(){};
public synchronized Lazy getLazy() {
if (lazy == null) {
lazy = new Lazy();
}
return lazy;
}
}
在懒汉模式中
if (lazy == null) {
lazy = new Lazy();
}
便是先检查后执行,若不加锁将可能出现多线程下创建多个实例的情况,这也就违背了单例模式的初衷。
内置锁:
java中提供了一种内置的锁机制来支持原子性:synchronize关键字:
1.同步代码块:包括两部分,锁的对象引用 和 锁保护的代码块
synchronize (lock){
//由锁保护的内容
}
2.用synchronize修饰的方法:本质上就是横跨整个方法体的同步代码块,锁的对象就是调用方法的对象
锁的获得与释放:
释放锁:1.执行完代码块内容,正常退出 2.抛出异常
获得锁:获得内置锁的唯一途径就是进入这个锁保护的同步代码块或方法
并发环境中的原子性:
一组语句作为一个不可分割的单元被执行
可重入锁:
重入意味着获取锁的操作粒度是“线程”而不是“调用”。
重入的一种实现方法:
为每个锁关联一个获取计数值和一个所有者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数值置为1.如果同一个线程再次获取这个锁,计数值将递增,当线程退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。
//P21
public class Widget{
public synchronized void doSomething(){
//...
}
}
class loggingWidget extends Widget{
public synchronized void doSomething() {
System.out.println(toString()+": calling doSomething");
super.doSomething();
}
}
重入进一步提升了加锁行为的封装性,如果没有可重入锁,那么以上代码将产生死锁,重入避免了这种死锁情况的发生。
用锁来保护状态:
当某个变量用锁来保护时,意味着在每次访问这个变量时都需要首先获得锁,这样就确保了在同一时刻只有一个线程可以访问这个变量。当类的不变性条件涉及多个状态变量时,那么还有另一个需求:在不变性条件中的每个变量都必须由同一个锁来保护。因此可以在单个原子操作中访问或更新这些变量,从而确保不变性条件不被破坏。
if ( ! vector.contains(element) )
vector.add(element) ;
我们知道vector是线程安全的,contains和add方法都是原子方法,但以上操作仍存在竞态条件。虽然synchroniz方法可以确保单个操作的原子性,但如果要把多个操作合并为一个复合操作,还是需要额外的加锁机制。
活跃性与性能
通过缩小同步代码块的作用范围,我们可以确保并发性,又保证效率。但注意同步块不要过小(出现线程安全问题)。不要将本应是原子的操作拆分到多个同步代码块中(在获取与释放锁等操作上都需要一定的开销,因此如果将同步代码块分解得过细会影响性能)