Java的多线程之同步篇三:同步阻塞、监视器、volatile、final、原子性、线程局部变量、锁测试与超时、读写锁

来源:互联网 发布:linux下解压rpm文件 编辑:程序博客网 时间:2024/06/11 18:13

一、同步阻塞

每一个Java对象有一个锁。线程可以通过调用同步方法获得锁。还有另一个机制可以获得锁,通过进入同步阻塞。

当线程进入如下形式的阻塞,将获得obj的锁。

synchronized (obj) {critical section}

有时会发现“特殊的”锁,如:

private Object lock = new Object();public void transfer(){synchronized(lock){...}}

在此,lock对象被创建仅仅是用来使用每个Java对象持有的锁。

       有时程序员使用一个对象的锁来实现额外的原子操作,实际上称为客户端锁定(clientside locking)。

例如:考虑Vector类,一个列表,它的方法是同步的。现在,假定在Vector<Double>中存储银行余额。实现如下:

public void transfer(Vector<Double> accounts, int from, int to, int amount) {accounts.set(from, accounts.get(from) - amount);accounts.set(to, accounts.get(to) + amount);}

        虽然Vector类的get和set方法时同步的,但是,并没有什么用。在get之后set之前,线程完全有可能被剥夺运行权。于是,另一个线程可能在相同的存储位置存入不同的值。但是可以如下修改:

public void transfer(Vector<Double> accounts, int from, int to, int amount) {synchronized(accounts){accounts.set(from, accounts.get(from) - amount);accounts.set(to, accounts.get(to) + amount);}}

            这个方法可以工作,但是它完全依赖于这样一个事实,Vector类对自己的所有可修改方案都使用内部锁。所以客户端锁定是非常脆弱的,不推荐使用。

二、监视器概念

       锁和条件是线程同步的强大工具,但是,严格来讲,它们不是面向对象的。多年来,研究人员努力寻找一种方法,可以在不需要程序员考虑如何加锁的情况下,就可以保证多线程的安全性。最成功的解决方案之一是监视器。

监视器特性如下:

1>监视器是只包含私有域的类。

2>每个监视器类的对象有一个相关的锁。

3>使用该锁对所有的方法进行加锁。

4>该锁可以有任意多个相关条件。

        Java设计者以不是很精确的方式采用了监视器概念,Java中的每一个对象有一个内部的锁和内部的条件。如果一个方法用synchronized关键字声明,那么,它表现的就像是一个监视器方法。通过调用wait/notifyAll/notify来访问条件变量。

        然而,在下述的3个方面Java对象不同于监视器,从而使得线程的安全性下降:

1>域不要求必须是private。

2>方法不要求必须是synchronized。

3>内部锁对客户是可用的。

三、Volatile域

有时,仅仅为了读写一个或两个实例域就使用同步,显得开销过大了。遗憾的是,现代的处理器与编译器,出错的可能性很大。

1>多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值。结果是,运行在不同处理器上的线程可能在同一个内存位置取到不同的值。

2>编译器可以改变指令执行的顺序以使吞吐量最大化。这种顺序上的变化不会改变代码语义,但是编译器假定内存的值仅仅在代码中有显示的修改指令时才会改变。然而,内存的值可以被另一个线程改变。

       这时,volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。

例如:使用锁来对实例域进行同步。

private boolean done;public synchronized boolean isDone() {return done;}public synchronized void setDone() {done = true;}

       或许使用内部锁不是个好主意,如果另一个线程已经对该对象加锁,isDone和setDone方法可能阻塞。如果注意到这个方面,一个线程可以为这一变量使用独立的Lock。但是,这也会带来许多麻烦。

在这种情况下,将域声明为volatile是合理的。

private volatile boolean done;public boolean isDone() {return done;}public void setDone() {done = true;}

注:volatile变量不能提供原子性。例如:方法

public void flipDone(){ done = !done; }   不能确保翻转域中的值。

因为在取值之后存值之前可能会修改数据。

四、final变量

除非使用锁或volatile修饰符,否则无法从多个线程安全地读取一个域。

还有一种情况可以安全地访问一个共享域,即这个域声明为final时。考虑一下声明:

final Map<String,Double> accounts = new HashMap<>();

其他线程会在构造函数完成构造之后才看到这个accounts变量。

      如果不使用final,就不能保证其他线程看到的是accounts更新后的值,它们可能都只是看到null,而不是新构造的HashMap。

当然,对这个映射表的操作并不是线程安全的。如果多个线程在读写这个映射表,仍然需要进行同步。

五、原子性

假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为volatile。

java.util.concurrent.atomic包中有很多类使用了很高效的机器级指令(而不是使用锁)来保证其他操作的原子性。

应用程序员不应该使用这些类,它们仅供那些开发并发工具的系统程序员使用。

六、死锁

七、线程局部变量

在线程间共享变量有风险,有时可以避免使用共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。例如,SimpleDateFormat类不是线程安全的。假设有一个静态变量:

public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

如果两个线程都执行以下操作:

String dateStamp = dateFormat.format(new Date());

结果可能混乱,因为dateFormat使用的内部数据结构可能会被并发的访问所破坏。当然可以使用同步,但开销很大;或者也可以在需要时构造一个局部SimpleDateFormat对象,不过这也太浪费了。


为每个线程构造一个实例,可以使用一下代码:

public static final ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>() {@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");}};
要访问具体的格式化方法,可以调用:

String dateStamp = dateFormat.get().format(new Date());

在一个给定线程中首次调用get时,会调用initialValue方法。在此之后,get方法会返回属于当前线程的那个实例。

       在多个线程中使用随机数也存在类似的问题。java.util.Random类是线程安全的。但是如果多个线程需要等待一个共性的随机数生成器,这会很低效。
       可以使用ThreadLocal辅助类为各个线程提供一个单独的生成器,不过JavaSE7还另外提供了一个便利类。只需要做一下调用:

int random = ThreadLocalRandom.current().nextInt(upperBound);

       ThreadLocalRandom.current()调用会返回特定于当前线程的Random实例。

八、锁测试与超时

线程在调用lock方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。应该更加谨慎申请锁。

tryLock方法试图申请一个锁,在成功获得锁后返回true,否则,立即返回false,而且线程可以立即离开去做其他的事情。

if (myLock.tryLock()) {try {...} finally{myLock.unlock();}}else        //do something else
可以调用tryLock时,使用超时参数。

       lock方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁,那么,lock方法就无法终止。

       然而,如果调用带有超时参数的tryLock,那么如果线程在等待期间被中断,将抛出InterruptedException异常。这是一个非常有用的特性,因为允许程序打破死锁。

       也可以调用lockInterruptibly方法,它就相当于一个超时设为无限的tryLock方法。

       在等待一个条件时,也可以提供一个超时。

       如果一个线程被另一个线程通过调用signalAll或signal激活,或者超时时限已达到,或者线程被中断,那么await方法将返回。

       如果等待的线程被中断,await方法将抛出一个InterruptedException异常。

常用方法:tryLock()   

                 尝试获得锁而没有发生阻塞;如果成功返回真。这个方法会抢夺可用的锁,即使该锁有公平加锁策略,即便其他线程已经等待很久也是如此。

                 tryLock(long time,TimeUnit unit)

                 尝试获得锁,阻塞时间不会超过给定的值;如果成功返回true;

                 await(long time,TimeUnit unit)

                 进入该条件的等待集,直到线程从等待集中移出或等待了指定的时间之后才解除阻塞。如果因为等待时间到了而返回就返回false,否则返回true。

九、读/写锁

java.util.concurrent.locks包定义了两个锁类,ReentrantLock和ReentrantReadWriteLock类。

       如果多个线程从一个数据结构读取数据而很少线程修改其中的数据的话,后者十分有用。在这种情况下,允许对读者线程共享访问是合适的。当然,写者线程依然必须互斥访问的。

       下面是使用读写锁的必要步骤:

1>构造一个ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

2>抽取读锁和写锁:

private Lock readLock = rwl.readLock();

private Lock writeLock = rwl.writeLock();

3>对所有的获取方法加读锁。

4>对所有的修改方法加写锁。

常用方法: Lock readLock() 得到一个可以被多个读操作共用的读锁,但会排斥所有的写操作。

                  Lock writeLock() 得到一个写锁,排斥所有其他的都操作和写操作。

0 0
原创粉丝点击