并发基础知识

线程和锁的作用类似铆钉和工字梁在土木工程中的作用。

编写线程安全的代码,核心在于对其状态访问的操作进行管理,特别是对共享的和可变的状态的访问。

共享意味着多个线程同时访问;可变意味着变量的值在其生命周期内可以改变。重点在于控制代码不出现 一些不可控的并发访问。


 

一个对象是否需要线程安全,主要是取决于它是否需要被多个线程访问。

当有多个线程访问某个状态变量并且其中有一个线程执行写入操作的时候,必须使用同步机制协同这些线程对变量的访问。

同步的方法:synchronized,volatile,Lock以及原子变量。

如果没有合适的方法来进行同步一般有三种方法来修复:

1.在各个线程之间不共享这个变量

2.将状态变量变成一个不可变的变量

3.在访问状态变量的时候使用同步机制

设计线程安全的类时,良好的面向对象技术,不可修改性,以及明晰的不变性规范都能起到一定的帮助作用。


 

面向对象中的抽象和封装会有时候降低程序的性能


线程安全性:某个类的行为与规范完全一致;当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,主调代码中不需要任何的额外同步或者协同,这个类表现出正确的行为,那么就称这个类是安全的。

无状态对象一定是线程安全的。

无状态对象指:它不包含任何域,也不包含任何对其他类中域的引用。


原子性:一个不可分割的操作

不满足原子性就会出现一个竞态条件的问题:由于不恰当的执行时序而出现不正确的结果。

复合操作:为了保证原子性,“先检查后执行(延迟初始化)”以及“读取——修改——写入(递增)”这种操作必须是一个不可分割的原子性操作。这种操作称之为复合操作。

如果在一个类中有多个类变量,那么原子性就不再简单针对某一个单一的变量,所有在整体的状态都进行维护。

内置锁:Synchronized Block ,以Synchronized作为关键字来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码库的锁就是方法调用所在的对象,静态方法以Class对象锁。

Synchronized (SmartCat){
   //访问或修改由锁保护的共享状态  
}

每个Java对象都可以用做一个实现同步的锁(也叫内置锁或者监视器锁),当线程A(SmartCat)需要进入方法Test的时候,线程A就会获得一个锁,线程B去进入方法Test的时候就会被拒绝,因为线程A正在Test方法中


 

重入:当某个线程请求一个自己已经获得过锁的方法的时候就会直接进入。

重入意味着锁的操作的颗粒度是“线程”而不是“调用”。重入的一种实现方法就是为锁关联一个获得计数值和一个所有者线程,进入的时候记录下线程以及对应的计数值。


用锁来保护状态:由于锁能使其保护的代码路径以串行的形式来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。只要始终遵守这些协议,就能确保状态的一致性。仅仅将复合操作封装到一个同步代码块中是不够的,如果使用同步来协调对某个变量的访问,那么在访问这个变量的所有位置上都需要使用同步,而且,当使用锁来协调某个变量的访问时,在访问变量的所有位置上都需要使用同一个锁。
对象的内置锁与其装填之间没有内在的关联,当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁之后,只能阻止其他线程获得同一个锁。你需要自行构造加锁协议或同步策略来实现对共享状态的安全访问,并且在程序中自始至终地使用它们。

常见的加锁约定是,将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径都进行同步,当某一个变量由锁来保护时,意味着每次访问这个变量时都需要首先获得锁,这样就确保同一时刻只有一个线程可以访问这个变量。


对象的共享:要编写正确的并发程序,关键问题在于:在访问共享的可变状态时需要正确的进行管理,同步代码块和同步方法可以确保以原子性或者确定临界区的方法执行,但Synchronized还有一个作用就是内存可见性,我们不仅希望防止某个线程正在使用对象状态而另一个对象进行了改变。

可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性。这种安全性使用绝大部分变量,除64位,64位的读写操作是需要被分成两个进行执行的


volatile变量:volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile变量的时候总会返回一个最新的值。常用于保持内存可见性和防止指令重排序

写入volatile变量相当于退出同步代码块,而读取volatile变量就相当于进入同步代码块。

volatile变量的经典用法就是:标识

volatile boolean asleep;
....
    while(!asleep)
        countSomeSheep();

volatile的原理是:

当对volatile变量进行写操作的时候,JVM会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议

缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

所以,如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。

所以在当且满足以下所有条件的时候才回去使用volatile:

1.对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值

2.该变量不会与其他状态一起纳入不变性条件中

3.在访问变量时不需要加锁


 

发布对象: 有具体的三种情况,如果在对象在那个构造完成之前就发布该对象,就会破坏线程安全性

1.将一个指向该对象的引用保存到其他代码可以访问到的地方

2.在某一个非私有的方法返回该对象的引用

3.将引用传递到其他类的方法中

要注意的是在腹部一个对象的时候可能会间接的发布掉其他的对象,如下例子

class UnsafeStates{
  private String[] states = new String(){
        "XXX","XAXAX".....
    };
  publib String[] getStates(){
    return states;
    }  
}
//——————————————————————————————————————————————————————————————————————————————————————————————————————————
//下面这个例子会发布出ThisEscape实例本身,所以不要的构造函数的时候,不小心的使用到了this进行实体类的发布,可以考虑使用工厂模式进行防止this引用逸出。
//当构造的过程中,this引用进行了逸出,this引用会被新创建的线程看见并共享(因为线程类其实是一个内部类)
public class ThisEscape{
  public ThisEscape(EventSiyrce source){
  source.registerListener{
    new EventListener(){
        public void OnEvent(Event e){
        doSomething(e);
        }
      }
    }
  }
}

states本身应该是一个私有的方法,在通过getStates的方法就可以得到state对应的地址,所以当发布一个对象的时候,该对象的非私有域中引用的所有对象同样会被发布。

所以在写代码的时候必须要注意到这个点,你发布的对象里面包含一些本身不应该被发出去的内容,这一点必须被假设到。


线程封闭:共享的线程是需要进行共享的,封闭了自然不需要对进行同步的操作。

最常见的就是JDBC中的Connection对象的线程封闭,在Connection被返回到池子之前是不会给到其他的线程的。

当然使用局部变量或者ThreadLocal类也可以完成我们的需求。


Ad—hoc线程封闭:维护线程封闭性的职责完全由程序实现来承担

这个其实是很困难的,因为目前没有方法能将Java对象封闭到某目标线程上。因为我了解的 好像内存地址和线程之间无法通过Java语言进行关联。


栈封闭:栈封闭是我们编程当中遇到的最多的线程封闭。什么是栈封闭呢?简单的说就是局部变量。多个线程访问一个方法,此方法中的局部变量都会被拷贝一分儿到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。

public class Snippet {
    public int loadTheArk(Collection<Animal> candidates) {
        SortedSet<Animal> animals;
        int numPairs = 0;
        Animal candidate = null;
    
        // animals被封闭在方法中,不要使它们逸出!
        animals = new TreeSet<Animal>(new SpeciesGenderComparator());
        animals.addAll(candidates);
        for (Animal a : animals) {
            if (candidate == null || !candidate.isPotentialMate(a))
                candidate = a;
            else {
                ark.load(new AnimalPair(candidate, a));
                ++numPairs;
                candidate = null;
            }
        }
        return numPairs;
    }
}

ThreadLocal类:也是封闭的一种方法,ThreadLocal提供了get与set等访问接口或方法,可以为每个使用该变量的线程都存有一份独立的副本。


不变性:满足同步需求的另一种方法就是:使用不可变对象

不可变对象一定是线程安全的,不可变对象满足以下三类:

1.对象创建以后其状态就不能修改

2.对象的所有域都是final类型

3.对象是正确创建的(在对象的创建期间。this引用没有逸出)

但是不可能对象的内部依然是可以使用方法来进行变化的,例如

private final Set<XXX> xxx = new HasgSet<XXX>();
public XXXX(){
xxx.add(aaaa);
}

不过final域的作用还有一个就是确保初始化过程的安全性,从而可以不受限制地访问不可变对象

 

posted @ 2020-08-01 21:02  smartcat994  阅读(118)  评论(0编辑  收藏  举报