Java+Threads+多线程参考手册


1         同步
                如何同步多个线程对共享资源的访问是多线程编程中最基本的问题之一。当多个线程并发访问共享数据时会出现数据处于计算中间状态或者不一致的问题,从而影响到程序的正确运行。我们通常把这种情况叫做竞争条件(race condition),把并发访问共享数据的代码叫做关键区域(critical section)。同步就是使得多个线程顺序进入关键区域从而避免竞争条件的发生。

1.1       Synchronized关键字
               Synchronized是Java多线程编程中最常用的关键字。所有的Java 对象都有自己唯一的隐式同步锁。该锁只能同时被一个线程获得,其他试图获得该锁的线程都会被阻塞在对象的等待队列中直到获得该锁的线程释放锁才能继续工作。Synchronized关键字通常有两种用法。当Synchronized关键字用于类方法定义中时,表示所有调用该方法的线程都必须获得当前对象的锁。这种方式比较简单,但是同步的粒度比较大,当一个线程要执行某个对象的同步方法的时候,必须同时没有任何其他线程在执行该对象的任一同步方法。此外,同步方法中的所有代码均在同步块中,获得锁的线程必须在执行完所有的代码离开该方法后才会释放锁,这些代码中可能只有一部分涉及到对共享资源(例如成员变量)的访问需要同步,其余则不需要,那么这样粗粒度的同步显然增加了其他线程的等待时间。Synchronized的另一种 用法允许作用在某个对象上,并且只同步一段代码而不是整个方法。

 

synchronized (object)  {

 // 需要同步的代码

}

 

这里synchronized所作用的对象可以是类的某个成员变量,也可以是这个类对象(用this表示)。这种用法使得程序员可以根据需要同步不同的成员变量,而不总是当前类对象,提高了灵活性。

 值得一提的是,并不是只有对象才有锁,类本身也有自己的锁,这使得static方法同样可以用synchronized来修饰。访问同步static方法的线程需要获得类的同步锁才能继续执行。

1.2       Volatile关键字
                在Java内存模型中每个线程拥有自己的本地存储(例如寄存器),并且允许线程拥有变量值的拷贝。这使得本来不需要同步的一些原子操作,例如boolean成员变量存储和读取也变得不安全。设想我们有个叫做done的boolean成员变量和一个当done为true时才会停止的循环,该循环由后台线程执行,另一个UI线程等待用户输入,用户按下某个按钮以后会把done设成true从而终止循环。由于UI线程自己本地拥有done的拷贝,用户在按下按钮时只是把自己本地的done设成了true而没有及时更新主内存中的done,所以后台线程由于看不到done的改变而不会终止。即使主内存中的done变化了,后台线程也会因为自己本地的变量值没有及时更新而没有察觉到done的变化。解决这一问题的方法之一是为done提供synchronized的setter和getter方法,这是因为获得同步锁会迫使所有变量的值从临时存储(寄存器)写会主内存。除此之外,Java提供了一个解决这个问题更为优雅的方法:Volatile关键字。每次使用volatile变量,JVM都会保证从主内存中读取它的值;同样每次修改volatile变量,JVM都会把值写回到主内存中。

                Volatile适用的场景比较严格,必须很清楚地看到volatile只是告诉JVM对于该变量的读写必须每次都在主内存中进行而禁止使用临时的拷贝来优化,它只是出于JVM特殊的内存模型的需要,并没有同步的功能。因此只有对volatile变量进行的原子操作(读取和赋值)才是线程安全的,像自增++自减--这样包含多个命令的操作仍然需要其它的同步措施。

                另一个需要注意的的地方是当用volatile修饰数组的时候,它只是说数组的引用是volatile的,而数组中的元素还是和普通变量一样,可能被JVM优化,我们无法为数组中的元素加上volatile修饰。解决上述问题的方法是使用Atomic变量。作为使用volatile修饰数组的一个例子,可以参考java.util.concurrent.CopyOnWriteArrayList。它的add操作是通过复制原来的数组并把新元素添加到新数组末尾然后再把内部数组引用变量指向新数组来实现的,因此数组变量经常会被修改,需要使用volatile。

1.3       显式锁Lock
                尽管synchronized关键字可以解决大多数同步问题,J2SE5.0还是引入了Lock接口。相比使用synchronized关键字获取对象隐式的同步锁,我们称Lock为显式锁。使用显式锁的一个显而易见的好处是它不再属于某个对象,从而可以在多个对象之间共享它。Lock接口有lock()和unlock()两个方法,使用它们和使用synchronized关键字类似,在进入需要同步的代码之前调用lock,在离开同步代码块时调用unlock。通常unlock会被放在finally中以保证即使同步代码块中有异常发生,锁仍然可以被释放。

                和使用synchronized关键字和lock()方法总是把未能获得锁的线程阻塞不同,Lock接口还提供了非阻塞的tryLock()方法。调用tryLock方法的线程如果未能获得锁会立刻返回false,线程可以继续执行其他代码而避免等待,这为程序员提供了更多自由。

                Lock接口还提供了一个newCondition () 方法,它返回一个Condition对象。Condition对象的作用和Object用于线程通知的wait-notify机制相同。

1.4       信号量Semaphore
                有时候我们有多个相同的共享资源可以同时被多个线程使用。我们希望在锁的基础上加上一个计数器,根据资源的个数来初始化这个计数器,每次成功的lock操作都会使计数器的值减去1,只要计数器的值不为零就表示还有资源可以使用,lock操作就能成功。每次unlock操作都会给这个计数器加1。只有当计数器的值为0的时候lock操作才会阻塞当前线程。这就是Java中的信号量Semaphore。

                Semaphore类提供的方法和Lock接口非常类似,当把信号量的资源个数设置成1时,信号量就退化为普通的锁。

1.5       读写锁ReadWriteLock
                对共享资源的访问通常可以分为读取和写入。在有些应用场景中读取可能需要花费较长时间,我们需要使用互斥锁来阻止并发的写入操作以保证数据的一致性。但是对于并发的读取线程其实并不需要使用同步。事实上只有使数据发生变化的操作才需要同步,我们希望有一种方法可以把读取和写入区分开来,读取和写入的操作之间是互斥的,但是多个读取操作可以同时进行,这样可以有效提高读取密集型程序的性能。J2SE5.0提供了ReadWriteLock接口并提供了实现该接口的ReentrantReadWriteLock类:

 

public interface ReadWriteLock {

    Lock readLock();

    Lock writeLock();

}

从接口方法中不难看出读写锁中包含读锁和写锁。实现类ReentrantReadWriteLock为我们提供了更多便捷的方法来使用读写锁,例如isWriteLocked可以用来检测是否被写锁定。

2         线程通知
                除了同步锁,Java Object还有两个可用于线程间通知的同步方法wait和notify。调用对象wait方法的线程会被阻塞在该对象的等待队列中直到其他线程调用notify方法来唤醒它。每次notify调用只能唤醒一个在等待队列中的线程,notifyAll方法可以唤醒所有在该对象等待队列中的线程。

  • 1
  • 2
  • 下一页

相关内容