java多线程(二) 之 线程安全性

来源:互联网 发布:软件系统维护收费标准 编辑:程序博客网 时间:2024/06/05 03:57

如果当多个线程访问同一个可变的状态变量时,没有使用合适的同步,那么程序就会出现错误.有三种方式修复这个错误:
1. 不在线程之间共享该状态变量
2. 将状态变量修改为不可变的变量
3. 在访问状态变量时,使用同步

一. 线程安全性
1.线程安全类:
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的.
无状态的类: 既不包含任何域,也不包含任何对其他类中域的引用.
例如:

public class StatelessFactorizer {    public void service(Request req,Response resp){        doing(req,resp);    }    public void doing(Request req, Response resp){        System.out.print(req);        System.out.println(resp);    }}

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

2.原子性
假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B.
线程不安全的统计方式:

public class UnsafeCountingFactorizer {    private long count;    public void service(Request req,Response resp){        ++count;        //doing(req,resp);    }    public long getCount() {        return count;    }}

当多个线程调用service的时候,由于执行时序问题,导致count的值会有偏差.
1) 竞态条件:在并发编程中,这种由于不恰当的执行时序而出现不正确的结果.
2) 延迟初始化中的竞态条件:

public class LazyInitRace {    private Object instance = null;    public Object getInstance(){        if(instance!=null)            return new Object();        return instance;    }}

3) 复合操作
先检查后执行;
读取–修改–写入等操作统称为复合操作

当在无状态的类中添加一个状态的时候,如果该状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的,在实际情况中,应该尽可能的使用现有的线程安全对象,来管理类的状态.
比如下:

public class CountingFactorizer {    private final AtomicLong count = new AtomicLong(0);    public long getCount(){        return count.get();    }    public void service(Request req,Response resp){        count.incrementAndGet();    }}

3.加锁机制:
有了原子操作,为什么还要采用加锁机制?
案例如下:

private final AtomicReference<Integer> lastNumber = new AtomicReference<>();private final AtomicReference<String> lastFactors = new AtomicReference<>();    private final AtomicLong count = new AtomicLong(0);    public void service(String s){        int i = (int) count.incrementAndGet();        String s1 = s+i;        lastNumber.set(i);        lastFactors.set(s1);        String s2 = s+lastNumber.get();        if(!lastFactors.get().equals(s2))            System.out.println(s2+"\t"+lastFactors.get());    }

测试代码:

public static void main(String[] args) {        final UnsafeCachingFactorizer cachingFactorizer = new UnsafeCachingFactorizer();        new Thread(){            public void run() {                for(int i = 0;i<10000;i++){                    cachingFactorizer.service("A");                }            };        }.start();        for(int i = 0;i<10000;i++){            cachingFactorizer.service("B");        }    }

测试结果:

A9  B11A43 B44B50 A50B58 A59A65 B65B70 A70B79 A79A85 B86B92 A93A98 B99B106    A108A116    B117

上面程序企图通过原子引用来实现统计每一次的输入,尽管这些原子引用本身都是线程安全的,但在UnsafeCachingFactorizer 中同样存在竞态条件.无法做到同时保证两个值同时获取和同时修改.

因此,当更新某一个变量的时候,需要在统一院子操作中对其他变量进行同时控制.
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量.

java提供了一种内置锁的机制来支持原子性:同步代码块synchronized block

1) 内置锁synchronized,相当于一种互斥锁

public synchronized void service(Request req,Response resp){        ...}

上面这种做法尽管实现了线程安全,但却非常极端,在同一时刻只有一个线程可以执行service,多个客户无法访问,服务的响应性,非常之低,无法令人接受,这是一个性能问题,而不是线程安全问题.

2) 重入
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞,然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功.

public class Widge {    public synchronized void doSomething(){        System.out.print("widge...doing");    }    public static void main(String[] args) {        new LoggingWidget().doSomething();    }}class LoggingWidget extends Widge{    public synchronized void doSomething(){        System.out.println("loggingWidget");        super.doSomething();    }}

获取锁的操作粒度是线程,而不是调用
当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数器置为1,如果同一个线程再次获取这个锁的时候,计数值将递增,而当线程退出同步代码块的时候,计数器会相应的递减.当计数值为0时,这个锁将被释放.

4.用锁来保护状态
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下我们称状态变量是由这个锁保护.
每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁.
对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护.

如果同步可以避免竞态条件问题,那么为什么不在每个方法声明时都使用关键字synchronized,事实上,如果不加区别的滥用synchronized,可能会导致程序中出现过多的同步,而且还并不足以保证复合操作都是原子的.
例如:

if(!vector.contains(element))    vector.add(element);

虽然contains和add都是原子方法,但是在上面的操作上仍然存在竞态条件.

5.活跃性与性能:
通常,在简单性与性能之间存在着相互制约因素.当实现某个同步策略时,一定不要盲目的为了性能而牺牲简单性(这有可能会破坏安全性)
当执行时间较长的计算或者无法快速完成的操作时(比如: 网络IO,控制台IO),一定不要持有锁

保证安全下的性能优化:

import java.util.ArrayList;import java.util.List;import java.util.concurrent.atomic.AtomicLong;public class CachedFactorizer {    private List<Integer> lastNumber = new ArrayList<>();    private List<String> lastFactors = new ArrayList<>();    private final AtomicLong count = new AtomicLong(-1);    //实现了,并非所有的操作都是必须同步,涉及到计算的时候,比如s1 = s+i;是可以不同步计算的.    //如果一个程序当中,计算非常复杂耗时的时候,给加上同步,往往是不理智的.    //很多时候,需要同步的,一般都会是取数据,和修改数据的时候,而不是计算.    public void service(String s){        int i = (int) count.incrementAndGet();        int j;        String s1 = s+i;        //添加元素的时候,将复合操作变为同步        synchronized (this) {            //通过局部变量j来确保获取数据的准确性            j = lastFactors.size();            lastNumber.add(i);            lastFactors.add(s1);        }        String s2 = null;        String s3 = null;        //获取的时候,将复合操作变为同步        synchronized (this) {            s2 = s+lastNumber.get(j);            s3 = lastFactors.get(j);        }               if(!s2.equals(s3))            System.out.println(s2+"\t"+s3);         }    public static void main(String[] args) {        final CachedFactorizer cachingFactorizer = new CachedFactorizer();        new Thread(){            public void run() {                for(int i = 0;i<10000;i++){                    cachingFactorizer.service("A");                }            };        }.start();        for(int i = 0;i<10000;i++){            cachingFactorizer.service("B");        }    }}
1 0