java 并发编程实战 之 线程安全性
来源:互联网 发布:手机怎么给淘宝改评价 编辑:程序博客网 时间:2024/05/16 19:18
基础篇
第二章 线程安全性
在Java中同步的机制
volatile变量、显示的同步代码块(显示锁)、原子变量。
- 编写线程安全的代码的关键:利用以上三个机制合理控制对象共享的且是可变的状态(即类的field)的读写操作。
什么是线程安全性
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
当多个线程访问某个类时,1.不管运行环境采用何种调度方式或者线程将如何交替执行,2.并且在主代码中不需要额外的同步或协同,3.这个类都能表现出正确的行为,那么这个类就是线程安全的
无状态的对象一定是线程安全的
- 自身没有任何域也不包含任何其他对象的引用。
- 例: 无状态的servlet
package net.jcip.examples;import java.math.BigInteger;import javax.servlet.*;import net.jcip.annotations.*;/** * StatelessFactorizer * * A stateless servlet * * @author Brian Goetz and Tim Peierls */@ThreadSafepublic class StatelessFactorizer extends GenericServlet implements Servlet { public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); encodeIntoResponse(resp, factors); } void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) { } BigInteger extractFromRequest(ServletRequest req) { return new BigInteger("7"); } BigInteger[] factor(BigInteger i) { // Doesn't really factor return new BigInteger[] { i }; }}
原子性
原子性就是不可再分割,以count++
为例,它是一个包含读取-修改-写入三个操作的复合操作,并且count++
的结果的正确性依赖于这三个操作的执行顺序。倘若有AB两个线程同时执行该复合操作,有可能出现A读取-A修改-B读取-B修改-A写入-B写入的操作序列,这显然是错误的,除此之外还有很多错误的组合方式,我们唯一希望看到的序列如下:A读取-A修改-A写入, B读取-B修改-B写入 或者 B读取-B修改-B写入 ,A读取-A修改-A写入 。
package net.jcip.examples;import java.math.BigInteger;import javax.servlet.*;import net.jcip.annotations.*;/** * UnsafeCountingFactorizer * * Servlet that counts requests without the necessary synchronization * * @author Brian Goetz and Tim Peierls */@NotThreadSafepublic class UnsafeCountingFactorizer extends GenericServlet implements Servlet { private long count = 0; public long getCount() { return count; } public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); ++count; encodeIntoResponse(resp, factors); } void encodeIntoResponse(ServletResponse res, BigInteger[] factors) { } BigInteger extractFromRequest(ServletRequest req) { return new BigInteger("7"); } BigInteger[] factor(BigInteger i) { // Doesn't really factor return new BigInteger[] { i }; }}
竞态条件
如上面`count++`的例子,由于不切当的执行时序导致错误的执行结果。我们把这种情况称作**竞态条件**。最常见的静态条件就是**先检查后执行**。例如延迟加载:
package net.jcip.examples;import net.jcip.annotations.*;/** * LazyInitRace * * Race condition in lazy initialization * * @author Brian Goetz and Tim Peierls */@NotThreadSafepublic class LazyInitRace { private ExpensiveObject instance = null; public ExpensiveObject getInstance() { if (instance == null) instance = new ExpensiveObject(); return instance; }}class ExpensiveObject { }
上面的代码中instance
可能会被创建多次,而创建每一个实例的花销是特别大的,或者实例占用的资源是稀少的,比如数据库连接池、redis连接池。当然在某些需求中instance
需要保持唯一,这时候如果在并发环境中创建了多个实例,并被传递给其他对象,则会引发严重错误。
复合操作
假定有两个操作A、B,如果从执行A的线程来看,当另一个线程执行B的时候,要么B全部执行完,要么B完全不执行,那么A和B相对彼此来说是原子的。原子操作是指,对于访问同一个状态的所有操作来说,包括该操作本身,这个操作是一个以原子方式执行的操作。
与数据库中事务的原子性有异曲同工之意。事物之间互不影响,要么全部执行成功,要么全部执行失败。
- 使用
AtomicLong
来解决count++
的线程安全问题。
package net.jcip.examples;import java.math.BigInteger;import java.util.concurrent.atomic.*;import javax.servlet.*;import net.jcip.annotations.*;/** * CountingFactorizer * * Servlet that counts requests using AtomicLong * * @author Brian Goetz and Tim Peierls */@ThreadSafepublic class CountingFactorizer extends GenericServlet implements Servlet { private final AtomicLong count = new AtomicLong(0); public long getCount() { return count.get(); } public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); count.incrementAndGet(); encodeIntoResponse(resp, factors); } void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {} BigInteger extractFromRequest(ServletRequest req) {return null; } BigInteger[] factor(BigInteger i) { return null; }}
AtomicLong 变量能够保证,在多个线程对该变量操作时,能够保证个线程之间的操作是原子性的。
在实际情况中,应尽量使用线程安全的对象(例如 AtomicLong)
来管理对象的状态。
加锁机制
在上一个例子中我们使用`AtomicLong`解决了线程安全问题,
这是因为我们的Servlet
中只持有了一个状态。如果持有多个状态会怎样呢?我们希望将最后一次AtomicLong
因式分解的计算结果缓存起来。这里用到的是AtomicReference
(代替对象引用的线程安全类,后面会详细介绍各种原子变量。)在这里虽然能够保证对lastNumber
和lastFactors
上的操作是原子性的,但是它们两个之间存在着一对一的映射关系,当改变一个的时候,另一个也需要相应的做出改变,它们之间不是相互独立的,只保证它们两个相互独立的操作的原子性是不够的,而是需要保证 同时改变它们两个的一组操作 的原子性。
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状变量
package net.jcip.examples;import java.math.BigInteger;import java.util.concurrent.atomic.*;import javax.servlet.*;import net.jcip.annotations.*;/** * UnsafeCachingFactorizer * * Servlet that attempts to cache its last result without adequate atomicity * * @author Brian Goetz and Tim Peierls */@NotThreadSafepublic class UnsafeCachingFactorizer extends GenericServlet implements Servlet { private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>(); private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>(); 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); } } void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) { } BigInteger extractFromRequest(ServletRequest req) { return new BigInteger("7"); } BigInteger[] factor(BigInteger i) { // Doesn't really factor return new BigInteger[]{i}; }}
内置锁
内置锁机制:同步代码块 Synchronized Block,包括两部分,一个作为锁的对象引用,一个作为由这个锁保护的代码块。 - 同步代码块的锁 - 加在类的成员方法上时锁就是方法调用所在的对象,即`this`指向的对象,如下例中的`lastNumber`、`lastFactors`被`this`守护,即访问这两个状态的线程必须首先获取 以当前实例对象作为的锁 - 加在类的静态方法上的锁就是该类的Class对象。 - `Synchronized (lock) {}` 显示的制定锁,lock可以使任何对象。 > 每个java对象都可以作为一个实现同步的锁,这些锁被称为内置锁,或监视锁。
package net.jcip.examples;import java.math.BigInteger;import javax.servlet.*;import net.jcip.annotations.*;/** * SynchronizedFactorizer * * Servlet that caches last result, but with unnacceptably poor concurrency * * @author Brian Goetz and Tim Peierls */@ThreadSafepublic class SynchronizedFactorizer extends GenericServlet implements Servlet { @GuardedBy("this") private BigInteger lastNumber; @GuardedBy("this") private BigInteger[] lastFactors; public synchronized void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); if (i.equals(lastNumber)) encodeIntoResponse(resp, lastFactors); else { BigInteger[] factors = factor(i); lastNumber = i; lastFactors = factors; encodeIntoResponse(resp, factors); } } void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) { } BigInteger extractFromRequest(ServletRequest req) { return new BigInteger("7"); } BigInteger[] factor(BigInteger i) { // Doesn't really factor return new BigInteger[] { i }; }}
重入
> 当某个线程请求一个由其他线程持有的锁的时候,发出请求的线程就会阻塞,但是当线程再次请求他自己已经持有的锁的时候,会请求成功,这就是线程的重入。如果线程不是可重入的,下例代码将发生死锁。
public class Widget { public synchronized void doSomething(){ ... }}public class LoggingWidget extends Widget { public synchronized void doSomething(){ ... super.doSomething(); }}
用锁来保护状态
对于可能被多个线程同时访问的可变状态变量,在访问他时都需要持有同一个锁,在这种情况下我们称状态变量是由这个锁保护的。
- 见一个反例,下例中的
count++
虽然被synchronized
修饰,但它并不是线程安全的,欢迎大家留言来解释这个问题,提示:原因见上面用锁来保护状态
的解析。
public class Problem1 { public static int count; public static class TestThread implements Runnable{ @Override public void run() { doCount(); } private synchronized void doCount(){ count++; } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new TestThread(),"T1"); Thread t2 = new Thread(new TestThread(),"T2"); t1.start(); t2.start(); }}
每个共享的可变状态变量都应该只由一个锁来保护。
再举一个反例,下面的代码虽然使用了vector,但并不是线程安全的,为什么呢,因为在vector内部的synchronized方法 保证了,vector自己的域由内置锁也就是vector的这个实例守护,但是element 这个可变的状态变量却不是由vector的内置锁守护,所以其他线程是可以改变element变量的,从而形成竞态条件。
if(!vector.contains(element)) vector.add(element)
vector 的contains、add方法如下
public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; } public boolean contains(Object o) { return indexOf(o, 0) >= 0; } public synchronized int indexOf(Object o, int index) { if (o == null) { for (int i = index ; i < elementCount ; i++) if (elementData[i]==null) return i; } else { for (int i = index ; i < elementCount ; i++) if (o.equals(elementData[i])) return i; } return -1; }
活跃性与性能
过分使用或滥用Synchronized代码块,会导致严重的性能问题。上面的例子中的 SynchronizedFactorizer
他继承了Servlet,我们都知道Servlet是单例的,他需要为所有的请求提供服务,并且他的service
方法被synchronized修饰,这导致了所有的线程必须一个的按顺序执行,就像一大堆人坐缆车下山,只提供了一辆容纳一人的缆车,尤其是当下山的路线特别长的时候,下山的效率可想而知。为了解决不良并发问题,提出了缓存机制,代码如下。
package net.jcip.examples;import java.math.BigInteger;import javax.servlet.*;import net.jcip.annotations.*;/** * CachedFactorizer * <p/> * Servlet that caches its last request and result * * @author Brian Goetz and Tim Peierls */@ThreadSafepublic class CachedFactorizer extends GenericServlet implements Servlet { @GuardedBy("this") private BigInteger lastNumber; @GuardedBy("this") private BigInteger[] lastFactors; @GuardedBy("this") private long hits; @GuardedBy("this") 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 = i; lastFactors = factors.clone(); } } encodeIntoResponse(resp, factors); } void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) { } BigInteger extractFromRequest(ServletRequest req) { return new BigInteger("7"); } BigInteger[] factor(BigInteger i) { // Doesn't really factor return new BigInteger[]{i}; }}
当执行较长时间的计算或者无法快速完成的工作时(例,网络IO、或控制台IO),一定不要持有锁。
- java并发编程实战之线程安全性
- java 并发编程实战 之 线程安全性
- <Java 并发编程实战>抄书笔记之线程安全性
- 《Java并发编程实战》---线程安全性
- 《Java并发编程实战》---线程安全性---加锁
- java并发编程实战笔记-线程安全性
- java并发编程实战-线程安全性
- java并发编程实战:线程安全性笔记
- Java 并发编程之线程安全性
- JAVA并发编程之线程安全性
- java并发编程实践之线程安全性
- 《Java并发编程实战》---线程安全性---线程封闭
- 《Java并发编程实战》第二章 线程安全性 读书笔记
- 《Java并发编程实战》---线程安全性---对象的共享
- 《Java并发编程实战》---线程安全性---不可变性
- 《Java并发编程实战》---线程安全性---对象的发布
- Java并发编程实战笔记(一):线程安全性
- Java并发编程实战学习笔记(一)-线程安全性
- C语言逗号表达式
- 网络基础一 之七层协议
- dom-obj-event.html
- angular ng-model 无法获取值处理方式
- Entity Framework Code First约定
- java 并发编程实战 之 线程安全性
- dp sp px之间的区别与转换
- 生成对抗网络(GAN)
- 定时一秒和秒表
- storm模型
- SQL高级语句-SQL UNIQUE 约束,唯一标识数据库表中的每条记录。
- html框架之iframe和frame及frameset的相关属性介绍
- 【OpenCV】笔记(4) ——自带UI显示
- allegro 通孔焊盘制作