`

并发编程回顾:synchronized与Lock

 
阅读更多

原先多线程并发编程的学习笔记和代码整理一下贴上来。

---------------------------------

synchronized与Lock

Java中提供了2种线程同步的方式,一种是Java语言级的同步原语synchronized关键字,另一种是使用javase5中新提供的java.util.concurrent.locks.Lock相关接口。

一、synchronized

关于synchronized,大家应该都比较熟悉了,从最早的java规范开始就包含了这个关键字。

1、synchronized作用域:

(1)是某个对象实例范围。synchronized防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。但不同对象实例的synchronized方法是不相干扰的,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法。 

(2)是某个类的范围。防止多个线程同时访问这个类中的synchronized static方法,它可以对类的所有对象实例起作用。

2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中。用法是: synchronized(this){/*区块*/},它的作用域是当前对象;如果在另一个对象上同步synchronized(other){/*区块*/},就要确保所有相关线程都在同一个对象上同步。

当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。

当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。

3、synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){}。在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法。

二、Lock相关接口

对于Lock接口,JDK文档描述如下:

Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。

Lock接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁,从而支持使用这种技术。

Lock对象必须被显示的创建、锁定和释放,这是与synchronized的区别之一。

1、Lock接口与ReentrantLock

使用ReentrantLock所需代码比synchronized要多,通常为解决一些特殊问题,如:

synchronized关键字不能尝试着获取锁且最终获取锁会失败,或者尝试着获取锁然后放弃它。

ReentrantLock允许你尝试获取锁并但最终并未获取,synchronized若获取不到锁则会一直阻塞。

举例:首先定义一个task,使用ReentrantLock作为锁。

class LockTask{
	private Lock lock=new ReentrantLock();
	public Lock getLock(){
		return lock;
	}
	public void lockA(){
		boolean locked=lock.tryLock();
		try{
			System.out.println("isLocked:"+locked);
		}finally{
			if(locked){
				lock.unlock();
			}
		}
	}
	public void lockB(){
		boolean locked=false;
		try {
			locked=lock.tryLock(5, TimeUnit.SECONDS);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		try{
			System.out.println("isLocked(5s)="+locked);
		}finally{
			if(locked){
				lock.unlock();
			}
		}
	}
}

注意lockA和lockB中的tryLock方法,仅在调用时锁未被另一个线程保持的情况下,才获取该锁。如果锁被另一个线程保持,则此方法将立即返回false。 其中lockB设置了tryLock的时间为5s。注意一定要在finally中释放锁。

下面测试时,会打开一个线程获取先lock住锁,之后再调用lockA和B方法。其中Thread.currentThread().join(1000);保证lock操作一定会执行完。

final LockTask task=new LockTask();
task.lockA();
task.lockB();
new Thread(){
	{setDaemon(true);}
	public void run(){
		task.getLock().lock();
		System.out.println("lock!");
	}
}.start();
Thread.currentThread().join(1000);
task.lockA();
task.lockB();
查看输出:
isLocked:true
isLocked(5s)=true
lock!
isLocked:false
isLocked(5s)=false
可见当已经有线程拿到锁时,再次尝试获取锁会失败(返回false),而synchronized则会一直阻塞等待。

2、ReadWriteLock接口与ReentrantReadWriteLock

java.util.concurrent.locks包中另一个接口是ReadWriteLock读写锁。

关于ReadWriteLock,JDK文档中描述:

ReadWriteLock维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有writer,读取锁可以由多个reader线程同时保持。写入锁是独占的。

意思就是没有写锁时,读锁可以由多个线程保持,而有写锁只能由一个线程持有。

举例:

定义一个类包含2个方法,read和write。read方法获取读锁,write方法获取写锁。

class ReadWrite{
    int data = 1;
    ReadWriteLock rwl = new ReentrantReadWriteLock();
    public void read(){
        rwl.readLock().lock();
        try{
            System.out.println(Thread.currentThread().getName()+" reading...");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName()+" read:" + data);
        }catch(InterruptedException e){
            e.printStackTrace();
        }finally{
            rwl.readLock().unlock();
        }
    }
    public void write(int data){
        rwl.writeLock().lock();
        try{
            System.out.println(Thread.currentThread().getName()+" writing...");
            this.data = data;
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName()+" written:" + data);
        }catch(InterruptedException e){
            e.printStackTrace();
        }finally{
            rwl.writeLock().unlock();
            Thread.yield();//让步
        }
    }
}

上面程序中,让read方法持有锁1s,且只进行读操作打印data值,write方法持有锁的时间稍短一些0.1s,同时进行写操作给data赋值。

进行读写操作:

此处启动3个线程,一个循环写入data随机数,另外2个线程循环读取data值。

final ReadWrite readWrite = new ReadWrite();
new Thread(){
        public void run(){
                while(true){
                    readWrite.write(new Random().nextInt(1000));
                }
        }
}.start();
Thread.sleep(1000);
for(int i=0;i<2;i++){
         new Thread(){
                public void run() {
                    while(true){
                        readWrite.read();
                    }
                }
         }.start();
}
程序运行后一段时间,可以看到,写数据的时候不能读,读数据的时候不能写,但可以同时2个线程在读。

所以说,写锁是独占的,读锁可以由多个线程保持。

另外,JDK文档中提到另外一个示例:

在使用某些种类的 Collection时,可以使用ReentrantReadWriteLock来提高并发性。通常,在预期 collection很大,读取者线程访问它的次数多于写入者线程,并且entail操作的开销高于同步开销时,这很值得一试。以下是一个使用 TreeMap的类,预期它很大,并且能被同时访问。

class RWDictionary {
    private final Map<String, Data> m = new TreeMap<String, Data>();
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock r = rwl.readLock();
    private final Lock w = rwl.writeLock();
    public Data get(String key) {
        r.lock();
        try { return m.get(key); }
        finally { r.unlock(); }
    }
    public String[] allKeys() {
        r.lock();
        try { return m.keySet().toArray(); }
        finally { r.unlock(); }
    }
    public Data put(String key, Data value) {
        w.lock();
        try { return m.put(key, value); }
        finally { w.unlock(); }
    }
    public void clear() {
        w.lock();
        try { m.clear(); }
        finally { w.unlock(); }
    }
}
使用读锁get数据和获取key,使用写锁put数据和清除map。注意要unlock。

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics