首页 > 代码库 > java5中的锁1

java5中的锁1

本文可作为传智播客《张孝祥-Java多线程与并发库高级应用》的学习笔记。

一个简单的例子

两个线程,一个不断打印a,一个不断打印b

public class LockTest {
    public static void main(String[] args){
        final Outputer outputer = new Outputer();
        new Thread(new Runnable(){
            @Override
            public void run() {
                while(true){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    outputer.output("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
                }                   
                
            }
        }).start();
        
        new Thread(new Runnable(){
            @Override
            public void run() {
                while(true){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    outputer.output("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
                }//a的数量与b的数量一致
                
            }
        }).start();
        
    }

    static class Outputer{
        public void output(String name){
            int len = name.length();
            try{
                for(int i=0;i<len;i++){
                    System.out.print(name.charAt(i));
                }
                System.out.println();
            }finally{

            }
        }
    }
}


最后的部分结果
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
b
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
为什么会这样?
很简单,在输出b的时候,还没有输出完,a线程(打印a的那个线程)已经抢到了控制权,开始打印a,等a线程将a输出完后,并且打印了一个回车后,b线程才抢回系统控制权,打印它上一次最后剩下的一个b。
要解决上面的问题很简单:
    static class Outputer{
        public synchronized void output(String name){
            int len = name.length();
            //.....
        }
    }
这样一来,我们就保证了Outputer类里的output方法是原子性的,不会有两个线程同时执行它。

就上面的例子而言我们是否还有更好的方法呢?
有。
java5中提供了一种更加面向对象的技术类解决多线程之间的互斥问题-----锁。
java.util.concurrent.locks Interface Lock
锁技术的核心就是Lock及它的实现类。

基本锁

上面的例子如果使用锁,代码如下
    static class Outputer{
        Lock lock = new ReentrantLock();
        
        public void output(String name){
            int len = name.length();
            lock.lock();      //标识1
            try{
                for(int i=0;i<len;i++){
                    System.out.print(name.charAt(i));
                }
                System.out.println();
            }finally{
                lock.unlock(); //标识2
            }
        }
线程a执行到上面代码的标识1处加锁,当线程a在输出字符a时,线程b也执行到了标识1处。此时线程b是不能获得锁的。它被阻塞到标识1处,直到线程a打印完之后在标识2处释放了锁。(线程a线程b共用一把锁,也就是Lock lock = new ReentrantLock())
另外为什么标识2出的释放锁放到了finally里,大家应该明白了吧。

读与写

上面的问题中output的主体(len是方法内部的局部变量,为每个线程自有,互不干涉)被全部互斥,它保证了任何时候,都只有一个线程执行标识1与标识2直接的代码。

但是我们得意识到:对共有数据的操作,基本可以分为两类,读与写。
对共有资源操作的时候,我们应该遵循三大准则:
1 当一个线程对资源进行写操作的时候,别的线程既不能对资源读也不能对资源写。
2 当一个线程对资源进行读操作的时候,别的线程不能对资源写。
3 当一个线程对资源进行读操作的时候,别的线程能对资源读。

一二准则保证了系统的正确性。第三准则能提高系统的性能。 毕竟多个线程对资源进行读操作是可以的。
看下面这个既有读又有写的例子。
public class ReadWriteLockTest {
    public static void main(String[] args) {
        final Queue3 q3 = new Queue3();
        for(int i=0;i<3;i++)
        {
            new Thread(){
                public void run(){
                    while(true){
                        q3.get();                        
                    }
                }
                
            }.start();

            new Thread(){
                public void run(){
                    while(true){
                        q3.put(new Random().nextInt(10000));
                    }
                }            
                
            }.start();
        }
        
    }
}

class Queue3{
    private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。

    public void get(){
        try {
            System.out.println(Thread.currentThread().getName() + " be ready to read data!");
            Thread.sleep((long)(Math.random()*1000));
            System.out.println(Thread.currentThread().getName() + "have read data :" + data);            
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public void put(Object data){
        try {
            System.out.println(Thread.currentThread().getName() + " be ready to write data!");                    
            Thread.sleep((long)(Math.random()*1000));
            this.data = data;        
            System.out.println(Thread.currentThread().getName() + " have write data: " + data);                    
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
结果如下
Thread-0 be ready to read data!
Thread-1 be ready to write data!
Thread-2 be ready to read data!
Thread-3 be ready to write data!
Thread-4 be ready to read data!
Thread-5 be ready to write data!
Thread-0have read data :null
Thread-0 be ready to read data!
Thread-3 have write data: 5280
Thread-3 be ready to write data!
Thread-1 have write data: 5839
Thread-1 be ready to write data!
Thread-4have read data :5839
我们可以看到 读中有写  写中有写 写中有读 完全乱套了。

我们试试个两个方法加上synchronized 结果如下
Thread-0 be ready to read data!
Thread-0have read data :null
Thread-5 be ready to write data!
Thread-5 have write data: 7931
Thread-5 be ready to write data!
Thread-5 have write data: 9564
Thread-5 be ready to write data!
Thread-5 have write data: 1203
Thread-5 be ready to write data!
Thread-5 have write data: 8870
Thread-4 be ready to read data!
Thread-4have read data :8870
Thread-3 be ready to write data!
Thread-3 have write data: 9334
Thread-3 be ready to write data!
Thread-3 have write data: 2680
Thread-3 be ready to write data!
Thread-3 have write data: 9948
Thread-3 be ready to write data!
Thread-3 have write data: 375
Thread-2 be ready to read data!
读与写完全互斥,读的时候不写,写的时候不读。满足一二准则。

读写锁

为了实现准则三,在java5中的出现了读写锁。
java.util.concurrent.locks Interface ReadWriteLock
ReadWriteLock有两个方法
Lock     readLock()   Returns the lock used for reading.
Lock     writeLock()  Returns the lock used for writing.
得到两种锁后,就可以调用锁的lock与unlock方法了。
一般使用它的子类ReentrantReadWriteLock来产生ReadWriteLock
其签名如下:
public class ReentrantReadWriteLock extends Object implements ReadWriteLock, Serializable
看看使用方法
class Queue3{
    private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
    ReadWriteLock rwl = new ReentrantReadWriteLock();
    public void get(){
        rwl.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "              be ready to read data!");
            Thread.sleep(20);
            System.out.println(Thread.currentThread().getName() + "       have read data :" + data);            
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            rwl.readLock().unlock();
        }
    }
    
    public void put(Object data){

        rwl.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + " be ready to write data!");                    
            Thread.sleep(20);
            this.data = data;        
            System.out.println(Thread.currentThread().getName() + " have write data: " + data);                    
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            rwl.writeLock().unlock();
        }
    }
}
结果如下
Thread-5 have write data: 7329
Thread-0              be ready to read data!
Thread-0       have read data :7329
Thread-1 be ready to write data!
Thread-1 have write data: 1361
Thread-2              be ready to read data!
Thread-4              be ready to read data!
Thread-0              be ready to read data!
Thread-2       have read data :1361
Thread-2              be ready to read data!
Thread-4       have read data :1361
我们可以看到 线程1的写是完全互斥的。

而线程2 4 0的读是可以同步进行的。

这是读写锁最简单的例子,下一节,我们看一个稍微复杂的,把读锁与写锁放到一个方法内的例子。

感谢glt

java5中的锁1