Thinking in Java---多线程学习笔记(2)

来源:互联网 发布:搜衣服的软件 编辑:程序博客网 时间:2024/06/12 07:46

多线程中的一个核心问题就是对共享资源的访问问题。因为不能准确的知道一个线程在何时运行,所以如果多个线程对共享资源进行修改的化,结果可能就出错了。解决这一冲突的基本思路就是当一个资源被一个任务使用时,在其上加锁;这样其它的任务就不能再访问这个资源,直到上面的锁打开;这样就可以实现一个序列化的访问共享资源。Java中提供了多种对访问共享资源的临界区代码进行加锁的方法,下面对这些方法进行一个归纳总结。
下面先给出一段多线程并发访问的代码:

package lkl;public abstract class IntGenerator {    private volatile boolean canceled= false;    public abstract int next();    public void cancel(){canceled = true;}    public boolean isCanceled(){return canceled;}}public class EvenGenerator extends IntGenerator{    private int currentEvenValue =0;    public synchronized int next(){          ++currentEvenValue; ///可能在这发生不正确的中断        //Thread.currentThread().yield();//如果我们通过yield()来强化这种错误,则几乎每次都会出现错误        ++currentEvenValue;        return currentEvenValue;    }    public static void main(String[] args){        EvenChecker.test(new EvenGenerator());    }}package lkl;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class EvenChecker implements Runnable {    private IntGenerator generator;    private final int id;    public EvenChecker(IntGenerator g , int ident){        generator =g;        id = ident;    }    public void run(){        while(!generator.isCanceled()){            int val = generator.next();            if(val%2 !=0){                System.out.println(val +" not even");                generator.cancel();            }        }    }    public static void test(IntGenerator gp , int count){        ExecutorService exec = Executors.newCachedThreadPool();        for(int i=0; i<count ; i++)             exec.execute(new EvenChecker(gp,i));        exec.shutdown();    }    public static void test(IntGenerator gp){        test(gp,10);    }}
//输出结果:1383 not even1387 not even1385 not even1381 not even

这段代码的逻辑提供一个产生偶数的对象,这个对象有一个变量初始化为0,然后每次调用next(),这个变量都会自加两次;我们开了几个线程对这个对象不断的调用next()函数,退出的条件是检测到这个变量为奇数。如果以一般的眼光看,这些线程应该会无限运行下去,但真正的结果是有很高的一个频率会退出。而问题正如注释所示,可能在两次自加期间出现中断,这样就会出现错误的结果。如果我们调用yield()方法强化这种随机的中断,则每次调用都会出错。下面我们要使用几种不同的加锁方式来修复这个问题,总的来说可以分成两类:一是对整个方法进行加锁,二是只对临界区代码进行加锁。

一.对整个方法进行加锁
要想对整个方法进行加锁,那么使用synchronized关键字进行修饰是最简单有效的方法;synchronized关键字可以包装在当前线程访问资源时,其它试图访问这个资源的线程阻塞。使用synchronized对共享资源的访问进行控制的一般逻辑是,先将共享资源包装进一个对象,然后把所有操作这个资源的函数都声明成synchronized类型。另外还有注意的是我们一定要将共享的域声明成private的,这样sychronzied才可以正确的作用。针对每个类,也有一个锁(作为类的Class对象的一个部分),所以synchronized static 方法就可以在类的范围内防止对static数据的并发访问。
对于上面的代码我们只需要用synchronized对修改共享变量的next()函数进行修饰,就可以解决问题了:

private int currentEvenValue =0;    public synchronized int next(){  ///使用synchronized关键字修饰,包装互斥        ++currentEvenValue; ///可能在这发生不正确的中断        //使用synchronized修饰后,就算调用yield()方法也不会出问题        Thread.currentThread().yield();        ++currentEvenValue;        return currentEvenValue;    }

除了使用synchronized关键字进行加锁外,还可以自己显式的加锁和解锁;这依靠与concurrent类库中的Lock类。常用的Lock类的子类是ReentrantLock,这个类允许你尝试获取但是最终未获得锁,这样如果其它人已经取得这个锁,那么你就可以离开去执行一些其它的事情,而不是在这里阻塞了。使用Lock对象一般来说比synchronized要写更多的代码,但是也更具有灵活性一些。使用Lock对象进行锁定时,一般要用try{}finally{}语句,以保证锁正确的释放。使用Lock改写上面的代码如下:

package lkl;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;import java.util.concurrent.*;///使用Lock对象进行显式的互斥//这种方式加大代码量,但是也更加的灵活public class MutexEvenGenerator extends IntGenerator{    private int currentEventValue = 0;    private Lock lock = new ReentrantLock();    public  int next(){        lock.lock(); ///加锁,一次只允许一个线程进入临界区        try{            ++currentEventValue;            Thread.yield();  //yield()是静态方法            ++currentEventValue;            return currentEventValue;        }        finally{            lock.unlock(); ///finally语句保证一定能解锁,不会出现死锁现象        }    }    public static void main(String[] args){        EvenChecker.test(new MutexEvenGenerator());    }}

二.只对临界区进行加锁
临界区指的是访问共享资源的那一段代码。上面的所有的加锁方式都是对整个方法进行的加锁,但是有时候我们可以只是需要防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。很容易想到这样做的好处是可以提高效率。同样的也有两种方法可以实现临界区的访问控制,使用Lock对象控制临界区和上面的示例没有什么不同,只是加锁和解锁的位置稍有不同而已。使用synchronized对临界区进行控制,则必须要传入一个对象才行,这个对象的锁被用来控制临界区的代码的同步。具体的格式如下:

synchronized(syncObject){//The code can be accessed//by only one task at a time}

这也被称为同步控制块;在进入这段代码前,必须得到synObject对象的锁,如果其它线程已经得到这个锁,那么就只有等到锁被释放以后,才能进入临界区。因为synchronized借助一个对象的锁,所以我们可以实现两个任务可以同时进入同一个对象,只要这个对象的方法是在不同的锁上同步的即可。如下面的代码所示:

package lkl;///synchronized同时对多个对象加锁的情况class DualSynch{    private Object syncObject = new Object();    public synchronized void f(){        for(int i=0; i<5; i++){            System.out.println("f()");            Thread.yield();        }    }    public void g(){        //可以将SyncObject改成this试试        //this表示的是当前对象        synchronized(syncObject){            for(int i=0;i<5;i++){                System.out.println("g()");                Thread.yield();            }        }    }}public class SyncObject {    public static void main(String[] args){        final DualSynch ds = new DualSynch();        new Thread(){            public void run(){                ds.f();            }        }.start();        ds.g();    }/*        g()        f()        f()        f()        f()        f()        g()        g()        g()        g()    */}
1 0