美文网首页
java并发--线程安全

java并发--线程安全

作者: w1992wishes | 来源:发表于2017-08-31 17:01 被阅读6次

    处理器的时钟频率已经很难再提高,想要提升计算机的性能,一个很明显的趋势是使用多处理器。
    因为程序调度的基本单元是线程,一个单线程程序一次只能运行在一个处理器上,在双处理器系统,就浪费了一般的CPU资源,在100个CPU系统中,就浪费了99%的CPU资源。而使用多线程编程则能够充分利用处理器资源,提高吞吐量。
    多线程的使用并不是绝对有利的,它同时也引入了单线程环境不存在的安全性问题,在多线程环境中,因为竞争条件的存在,在没有充分同步的情况下,多线程中的各个操作的顺序是不可预测的,有时甚至让人惊讶。
    在设计良好的应用程序使用多线程,能够获得不错的性能收益,但多线程仍会带来一定程度的性能开销。上下文切换,当调度程序时挂起正在运行的线程,另一个线程开始运行--这在多线程系统中是很频繁的,会带来很大的性能消耗;保存和恢复线程执行的上下文,会让CPU的时间花费在对线程的调度而不是运行上。当线程共享数据时,需要使用同步机制,这回限制编译器的优化。
    ......

    什么是线程安全

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

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

    public class StatelessServlet implements Servlet{
        public void service(ServletRequest req, ServletResponse res){
            Integer i = extractFromRequest(req);
            Integer[] factors = factor(i);
            encodeIntoResponse(res, factors);
        }
    }
    

    以一段伪代码来说明。
    StatelessServlet是一个无状态的servlet,因为它不包含域也没有引用其他类的域。线程之间不共享状态,一个请求过来,会唯一地存在本地变量,这些变量保存在线程的栈中,只有执行线程才能访问,不会影响同一个servlet的其他请求线程。
    因为线程访问无状态对象的行为,不会影响其他线程访问该对象的正确性,因此无状态对象是线程安全的。

    原子性

    给上面的伪代码加上计数功能,如下:

    public class UnsafeCountServlet implements Servlet{
        private long count = 0; 
        
        public long getCount(){return count;}
        
        public void service(ServletRequest req, ServletResponse res){
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            ++count;
            encodeIntoResponse(res, factors);
        }
    }
    

    此时的UnsafeCountServlet是一个有状态的类,在多线程环境中不再是线程安全的,因为++count不是一个原子操作,而是三个离散操作的的简写:获取当前值,加1,返回新值,是一个“读-改-写”操作。
    这样的操作在多线程环境中很容易出现问题,加入初始值为0,某个时刻线程一读取到值0,此时线程调度,另一个线程二也读到0,然后加1,返回新值1,再切回线程一,加1,返回新值1,这就缺少了一次自增。
    出现这样的错误是因为:竞争条件的存在。

    竞争条件

    当计算的正确性依赖于运行时的时序或者多线程的交替时,就会产生竞争条件。最常见的竞争条件就是“检查再运行”。
    下面是一个惰性初始化的例子:

    public class LazyInitClass{
        private ExpensiveObject instance = null;
        
        public ExpensiveObject getInstance(){
            if(instance == null){
                instance = new ExpensiveObject();
            }
            return instance;
        }
    }
    

    LazyInitClass的竞争条件会破坏其正确性。假如两个线程A和B同时执行getInstance(),A看到instance为null,执行初始化,此时B也在检查instance是否为null,而instance是否为null,依赖于时序,是无法预期的,如果B检查也为null,则线程B也会执行初始化,得到两个不同的对象,和惰性初始化只初始化一次是矛盾的。

    使用线程安全对象管理类的全部状态,可以维护类的线程安全性

    将UnsafeCountServlet改造一下,代码如下:

    public class SafeCountServlet{
        private final AtomicLong count = new AtomicLong(0);
        
        public long getCount(){return count.get();}
        
        public void service(ServletRequest req, ServletResponse res){
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            count.incrementAndGet();
            encodeIntoResponse(res, factors);
        }
    } 
    

    java.util.concurrent.atomic包中包括了原子变量类,这些类用来实现数字和对象引用的原子状态转换,把long换成AtomicLong,可以确保所有访问计数器状态的操作都是原子的,计数器是线程安全的了,而计数器的状态就是SafeCountServlet的状态,所以SafeCountServlet也变成了线程安全的。

    使用锁可以维护类的线程安全性

    使用线程安全对象管理类的全部状态,可以维护类的线程安全性,但如果类中存在多个实例域,即有多个状态,仅仅加入更多的线程安全的状态变量时不够的。
    为了保护状态的一致性,要在单一的原子操作中更新相互关联的的状态变量。
    java提供了锁可以保护类的线程安全性。

    相关文章

      网友评论

          本文标题:java并发--线程安全

          本文链接:https://www.haomeiwen.com/subject/nqaqjxtx.html