摘要:发布的对象内部状态可能会破坏封装性,使程序难以维持不变性条件。不变性线程安全性是不可变对象的固有属性之一。可变对象必须通过安全方式来发布,并且必须是线程安全的或者有某个锁保护起来。
线程的优缺点
线程是系统调度的基本单位。线程如果使用得当,可以有效地降低程序的开发和维护等成本,同时提升复杂应用程序的性能。多线程程序可以通过提高处理器资源的利用率来提升系统的吞吐率。与此同时,在线程的使用开发过程中,也存在着诸多需要考虑的风险。
安全性:有合理的同步下,多线程的并发随机执行使线程安全性变得复杂,如++i。
活跃性:在多线程中,常因为缺少资源而处于阻塞状态,当某个操作不幸造成无限循环,无法继续执行下去的时候,就会发生活跃性问题。
性能:线程总会带来程序的运行时开销,多线程中,当频繁地出现上下文切换操作时,将会带来极大的开销。
线程安全性线程安全的问题着重于解决如何对状态访问操作进行管理,特别是对共享和可变的状态。共享意味着可多个线程同时访问;可变即在变量在其生命周期内可以被改变;状态就是由某个类中的成员变量(Field)。
一个无状态的对象一定是线程安全的。因为它没有可被改变的东西。
public class LoginServlet implements Servlet { public void service(ServletRequest req, ServletResponse resp) { System.out.println("无状态Servlet,安全的类,没有字段可操作"); } }原子性
正如我们熟知的 ++i操作,它包含了三个独立的“读取-修改-写入”操作序列,显然是一个复合操作。为此java提供了原子变量来解决 ++i这类问题。当状态只是一个的时候,完全可以胜任所有的情况,但当一个对象拥有两个及以上的状态时,仍然存在着需要思考的复合操作,尽管状态都使用原子变量。如下:
public class UnsafeCachingFactorizer implements Servlet { private final AtomicReferencelastNumber = new AtomicReference (); private final AtomicReference lastFactors = new AtomicReference (); public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); if (i.equals(lastNumber.get())) { encodeIntoResponse(resp, lastFactors.get()); } else { BigInteger[] factors = factor(i); lastNumber.set(i); lastFactors.set(factors); encodeIntoResponse(resp, factors); } } } // lastNumber lastFactors 虽然都是原子的,但是 if-else 是复合操作,属“先验条件”
既然是复合操作,最直接,简单的方式就是使用synchronized将这个方法同步起来。这种方式能到达预期效果,但效率十分低下。
既然提到synchronized加锁同步,那么就必须知道 锁的特点:
锁是可以重入的。即子类的同步方法可以调用本类或父类的同步方法。
同一时刻,只有一个线程能够访问对象中的同步方法。
静态方法的锁是 类;普通方法的锁是 对象本身。
回顾上面的代码,一个方法体中,只要涉及了多个状态的时候,就一定需要同步整个方法吗?答案是否定的,同步只是为了让多步操作为原子性,即对复合操作同步即可,因此需要明确的便是哪些操作是复合操作。如下:
public class CachedFactorizer implements Servlet { private BigInteger lastNumber; private BigInteger[] lastFactors; private long hits; private long cacheHits; public synchronized long getHits() { return hits; } public synchronized double getCacheHitRatio() { return (double) cacheHits / (double) hits; } public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = null; synchronized (this) { ++hits; if (i.equals(lastNumber)) { ++cacheHits; factors = lastFactors.clone(); } } if (factors == null) { factors = factor(i); synchronized (this) { lastNumber = 1; lastFactors = factors.clone(); } } encodeIntoResponse(reqsp, factors); } }// 两个synchronized分别同步独立的复合操作。对象共享
重排序:当一个线程修改对象状态后,其他线程没有看见修改后的状态,这种现象称为“重排序”。
java内存模型允许编译器对操作顺序进行重排序,并将数据缓存在寄存器中。当缺乏同步的情况下,每一个线程在独立的缓存中使用缓存的数据,并不知道主存中的数据已被更改。这就涉及到内存可见性的问题。
可见性内存可见性:同步的另一个重要的方面。我们不仅希望防止多个线程同时操作对象状态,而且还希望确保某一个线程修改了状态后,能被其他线程看见变化。
volatile:使用 synchronized可以实现内存可见,但java提供了一种稍弱的更轻量级得同步机制volatile变量。在访问volatile变量时不会执行加锁操作,因此不会产生线程阻塞。即便如此还是不能过度使用volatile,当且仅当能简化代码的实现以及对同步策略的验证时,才考虑使用它。
发布与逸出发布指:使对象能够在当前作用于之外的代码中使用。即对象引用能被其他对象持有。发布的对象内部状态可能会破坏封装性,使程序难以维持不变性条件。
逸出指:当某个不应该发布的对象被发布时,这种情况被称为逸出。
// 正确发布:对象引用放置公有静态域中,所有类和线程都可见 class CarFactory { public static Setcars; private CarFactory() { cars = new HashSet (); } // 私有,外部无法获取 CarFactory的引用 public static Car void newInstance() { Car car = new Car("碰碰车"); cars.put(car); return car; } // 使用方法来获取 car }
// 逸出 class Person { private String[] foods = new String[] {"土豆"}; public Person(Event event) { person.registListener { new EventListener() { public void onEvent(Event e) { doSomething(e); } } } }// 隐式逸出了this,外界得到了Person的引用 并且 EventListener也获取了Person的引用。 public String[] getFoods() { return foods; }// 对发布的私有 foods,外界还是可以修改foods内部值 }线程封闭
将可变的数据仅放置在单线程中操作的技术,称之为发线程封闭。
栈封闭:只能通过局部变量才能访问对象。局部变量的固有属性之一就是封装在执行线程中,它们位于执行线程的栈中,其他线程无法访问这个栈,即只在一个方法内创建和使用对象。
public int test(Person p) { int num = 0; PersonHolder holder = new PersonHolder(); Person newPerson = deepCopy(p); Person woman = holder.getLove(newPerson); newPerson.setWomen(person); num++; return num; // 基本类型没有引用,对象创建和修改都没有逸出本方法 }
ThreadLocal类:ThreadLocal能够使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了 get、set等访问接口的方法,这些方法为每一个使用该变量的线程都存有一份独立的副本,因get总是返回由当前执行线程在调用set时设置的最新值。
private static ThreadLocalconnectionHolder = new ThreadLocal () { public Connection initialValue() { return DriverManager.getConnection(DB_URL); } }; public static Connection getConnection() { return connectionHolder.get(); }
当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用ThreadLocal。不变性
线程安全性是不可变对象的固有属性之一。不可变对象一定是线程安全的,它们的不变性条件是由构造函数创建的,只要它们的状态不可变。
// 在可变对象基础上构建不可变类 public final class ThreadStooges { private final Setstooges = new HashSet (); public ThreadStooges() { stooges.add("Moe"); stooges.add("Larry"); } public boolean isStooge(String name) { return stooges.contains(name); } }// 没有提供可修改状态的方式,尽管使用了Set可变集合,但被private final修饰着
对象不可变的条件
安全发布对象创建以后其状态就不能修改。
对象的所有域都是final类型。
对象是正确创建的(在对象的创建期间,this引用没有逸出)
任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。
// 安全的 Holder类 class Holder { private int n; public Holder(int n) { this.n = n; } } public class SessionHolder { // 错误的发布,导致 Holder不安全 public Holder holder; public void init() { holder = new Holder(10); } }// 当初始化 holder的时候,holder.n会被先默认初始化为 0,然后构造函数才初始化为 10;在并发情况下,可能会有线程在默认初始化 与 构造初始化中,获取到 n 值为 0, 而不是 10;
要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式安全发布:
在静态初始化函数中初始化一个对象引用。
将对象的引用保存到 volatitle 类型的域或者 AtomicReferance 对象中。
将对象的引用保存到某个正确构造对象的 final 类型域中。
将对象的引用保存到一个由锁保护的域中。
在线程并发容器中的安全发布:
通过将一个键或者值放入 Hashtable、synchronizedMap 或者 ConsurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)。
通过将某个元素放入 Vector、 CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或 synchronizedSet中,可以将元素安全地发布到任何从这些容器中访问该元素的线程。
通过将某个元素放入 BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。
通常,要发布一个静态构造的对象,最简单、安全的方式就是使用静态的初始化器。如public static Holder holder = new Holder(10)。如果对象在发布后状态不会被修改(则称为事实不可变对象),那么在没有额外的同步情况下,任何线程都可以安全地使用被安全发布的不可变对象。
对象的发布需求取决于它的可变性:
不可变对象可以通过任意机制来发布。
事实不可变对象必须通过安全方式来发布。
可变对象必须通过安全方式来发布,并且必须是线程安全的或者有某个锁保护起来。
在并发程序中使用和共享对象时可采用的策略:
线程封闭。将对象封闭在线程中,如在方法中创建和修改局部对象。
只读共享。
线程安全共享。对象内部实现同步,使用公有接口来访问。
保护对象。使用特定的锁来保护对象。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/73777.html
摘要:当我们希望能界定这二者之间的区别时,我们将第一种称为纯粹的函数式编程,后者称为函数式编程。函数式编程我们的准则是,被称为函数式的函数或方法都只能修改本地变量。另一种观点支持引用透明的函数式编程,认为方法不应该有对外部可见的对象修改。 一、实现和维护系统 1.共享的可变数据 如果一个方法既不修改它内嵌类的状态,也不修改其他对象的状态,使用return返回所有的计算结果,那么我们称其为纯粹...
摘要:之前,使用匿名类给苹果排序的代码是的,这段代码看上去并不是那么的清晰明了,使用表达式改进后或者是不得不承认,代码看起来跟清晰了。这是由泛型接口内部实现方式造成的。 # Lambda表达式在《Java8实战》中第三章主要讲的是Lambda表达式,在上一章节的笔记中我们利用了行为参数化来因对不断变化的需求,最后我们也使用到了Lambda,通过表达式为我们简化了很多代码从而极大地提高了我们的...
摘要:利用前面所述的方法,这个例子可以用方法引用改写成下面的样子构造函数引用对于一个现有构造函数,你可以利用它的名称和关键字来创建它的一个引用。 第三章 Lambda表达式 函数式接口 函数式接口就是只定义一个抽象方法的接口,哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。 常用函数式接口 showImg(https://segmentfault.com/img...
摘要:上下文比如,接受它传递的方法的参数,或者接受它的值得局部变量中表达式需要类型称为目标类型。但局部变量必须显示的声明,或实际上就算。换句话说,表达式只能捕获指派给它们的局部变量一次。注捕获实例变量可以被看作捕获最终局部变量。 由于第三章的内容比较多,而且为了让大家更好的了解Lambda表达式的使用,也写了一些相关的实例,可以在Github或者码云上拉取读书笔记的代码进行参考。 类型检查、...
摘要:线程允许同一个进程中同时存在多个程序控制流。线程也被称为轻量级进程。现代操作系统中,都是以线程为基本的调度单位,而不是进程。 并发简史 在早期的计算机中不包含操作系统,从头至尾都只执行一个程序,并且这个程序能访问计算机所有资源。操作系统的出现使得计算机每次能运行多个程序,并且不同的程序都在单独的进程中运行:操作系统为各个独立执行的进程分配内存、文件句柄、安全证书等。不同进程之间通过一些...
阅读 1058·2021-11-24 09:39
阅读 1290·2021-11-18 13:18
阅读 2382·2021-11-15 11:38
阅读 1802·2021-09-26 09:47
阅读 1592·2021-09-22 15:09
阅读 1609·2021-09-03 10:29
阅读 1470·2019-08-29 17:28
阅读 2938·2019-08-29 16:30