什么是线程安全


        线程安全定义:

       当多个线程访问一个类的时候,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方法代码不必做其他的协调,这个类的行为任然是正确的,那么称这个类是线程安全的。

        所有的例子都是一个servlet用来进行因数分解,通过request传入一个数字,然后service方法调用factor方法进行因数分解。

       1. 无状态的对象永远线程安全

public 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 };
    }
}
        这个类没有一个私有或公共的变量,是一个无状态的类。因为多个线程进入service方法,所有的service方法内的局部变量都封装在各个线程的栈中,只有本线程可以访问,所以,这个类是线程安全的。

        2. 原子性

@NotThreadSafe
public 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 };
    }
}
          我们在第一个例子上,添加了一个功能计数。每调一次service方法,count加1。很可惜,这个类不是线程安全的,因为++count不是一个原子操作,这是一个读-改-写三个步骤的操作。这样我们就很容易理解这个类为什么不是线程安全的了,线程A读到count值为1,然后修改为2。在线程A将count的值写回去之前,线程B读取了count的值。结果是线程A将count的值改为2,线程B也将count的值改为2。至于什么是原子操作,我也没看懂,定义如下:

        假设有操作A和B,如果从执行A的线程的角度看,当其他线程执行B的时候,要么B全部执行完成,要么一点也没有执行,这样A和B互为原子操作。一个原子操作是指:该操作对于所有的操作,包括它自己,都满足前面描述的状态。(我已经晕了)

        让count加一操作变成原子操作的代码如下:

@ThreadSafe
public 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是什么鬼,这个大家去问度娘吧。简单说,它在读-改-写的第三步写操作时还会回去查看值是否已经被改变,如果被改变,重新加载。(可以百度搜索java cas)


     3. 锁

      我们上面所说的自增操作,通过atomic类变成了原子操作,因此类也就变成线程安全的了。但是如果有多个全局变量,并且这多个全局变量的值具有关联性。场景如下:我们想实现应对连续两个客户请求相同的数字进行因数分解,于是我们缓存了最新的计算结果

@NotThreadSafe
public 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 };
	}
这个类不是线程安全的,因为lastNumber和lastFactors这两个变量是有关系的,不是独立的。我们的atomic只能保证他们自身的操作是线程安全,无法保证两个一起是线程安全的。假设,lastNumber被线程A修改为了10,而lastFactors还是旧值9的因数分解结果,那线程B刚好请求数字10的因数分解结果,就拿到了9的因数分解结果。

        这里我们很容易想到用synchronized关键字来给service方法加锁,我就不贴代码了,这样做的确能够解决问题,但是service方法一次只能进入一个线程了,性能存在很大问题。我们在做并发编程的时候,一定要兼顾性能,最好的方式如下所示:

@ThreadSafe
public 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};
    }
}
          把锁打碎在方法里面,第一个synchronized块保护着检查再运行的操作,另一个保证缓存的number和factor同步更新。提醒一点,在加锁期间,不要进行耗时操作,我们把锁移到方法里面,主要目的就是将那些耗时操作剔除到同步块之外。






发布了55 篇原创文章 · 获赞 5 · 访问量 7万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章