线程同步基础

多个执行线程共享一个资源的情景,是最常见的并发编程情景之一。在并发应用中常常遇到这样的情景:多个线程读或者写相同的数据,或者访问相同的文件或者数据库连接。

为了防止这些共享资源可能出现的错误或数据不一致,我们必须实现一些机制来防止这些错误的发生。

为了解决这些问题,人们引入了临界区概念。临界区是一个可用以访问共享资源的代码块,这个代码块在同一时间内只允许一个线程执行。

为了帮助编程人员实行这个临界区,java提供了同步机制。当一个线程试图访问一个临界区时,它将使用一种同步机制来查看是不是已经有其他线程进入临界区。如果没有其他线程进入临界区,它就可以进入临界区;如果已经有线程进入了临界区,它就被同步机制挂起,直到进入的线程离开这个临界区。如果在等待进入临界区的线程不止一个,JVM会选择其中一个,其余的将继续等待。

java语言提供了两种基本同步机制:

1.synchronized关键字机制

2.Lock接口及其实现机制

一、synchronized关键字机制

1.同步方法
即有synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
代码如:
public synchronized void save(){}
对于实例的同步方法,使用this即当前实例对象。
对于静态的同步方法,使用当前类的字节码对象。
注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。

2.同步代码块
即有synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
代码如:
synchronized(object){}
Java中任意的对象都可以作为一个监听器(monitor),监听器可以被上锁和解锁,在线程同步中称为同步锁,且同步锁在同一时间只能被一个线程所持有。上面的obj对象就是一个同步锁,分析一下上面代码的执行过程:
1).一个线程执行到synchronized代码块,首先检查obj,如果obj为空,抛出NullPointerExpression异常;
2).如果obj不为空,线程尝试给监听器上锁,如果监听器已经被锁,则线程不能获取到锁,线程就被阻塞;
3).如果监听器没被锁,则线程将监听器上锁,并且持有该锁,然后执行代码块;
4).代码块正常执行结束或者非正常结束,监听器都将自动解锁;

线程同步锁对多个线程必须是互斥的,即多个线程需要使用同一个同步锁。代码中obj对象被多个线程共享,能够实现同步。
注:同步是一种高开销的操作,因此应该尽量减少同步的内容。
通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

两者的区别主要体现在同步锁上面。对于实例的同步方法,因为只能使用this来作为同步锁,如果一个类中需要使用到多个锁,为了避免锁的冲突,必然需要使用不同的对象,这时候同步方法不能满足需求,只能使用同步代码块(同步代码块可以传入任意对象);或者多个类中需要使用到同一个锁,这时候多个类的实例this显然是不同的,也只能使用同步代码块,传入同一个对象。

二、Lock接口及其实现机制

Lock接口及实现类提供了更多的好处:

1.支持更灵活的同步代码块结构。使用synchronized关键字时,只能在同一个synchronized块结构中获取和释放控制。Lock接口允许实现更复杂的临界区结构,即控制的获取和释放不出现在同一个块结构中。

2.相比synchronized关键字,Lock接口提供了更多的功能。其中一个新功能是tryLock()方法的实现。这个方法试图获取锁,如果锁已被其他线程获取,它将返回false,并继续往下执行代码。使用synchronized关键字时,如果线程A试图执行一个同步代码块,而线程B已在执行这个同步代码块,则线程A就会被挂起直到线程B运行完这个同步代码块。使用锁tryLock()方法,通过返回值将得知是否有其他线程正在使用这个锁保护的代码块。

3.Lock接口允许分离读和写操作,允许多个读线程和只有一个写线程。

4.相比synchronized关键字,Lock接口具有更好的性能。

 

锁的公平性
ReentrantLock和ReentrantReadWriteLock类的构造器都含有一个布尔参数fair,它允许你控制着两个类的行为。默认fair值为false,它称为非公平模式。
在非公平模式下,当有很多线程在等待锁时,锁将选择它们中的一个来访问临界区,这个选择是没有任何约束的。
如果fair值为true,则称为公平模式。
在公平模式下,当有很多线程在等待锁时,锁将选择它们中的一个来访问临界区,而且选择的是等待时间最长的。
这两种模式只适用于lock()和unlock()方法。而Lock接口的tryLock()方法没有将线程置于休眠,fair属性并不影响这个方法。

 

常用的线程类:

1)、ReentrantLock类:是一个可重入的互斥锁,重入锁是一种递归无阻塞的同步机制。ReentrantLock由最近成功获取锁,还没有释放的线程所拥有,当锁被另一个线程拥有时,调用lock的线程可以成功获取锁。如果锁已经被当前线程拥有,当前线程会立即返回。
a、防止重复执行(忽略重复触发)
ReentrantLock lock = new ReentrantLock();
public void getObject(){
    //如果已经被lock,则立即返回false不会等待,达到忽略操作的效果
    if (lock.tryLock()) {  
        try {  
            //操作  
        } finally {  
            lock.unlock();  
        }  
    }
}    

b、同步执行,类似synchronized
ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁  
//ReentrantLock lock = new ReentrantLock(true); //公平锁  
public void getObject(){
    //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果  
    lock.lock();
    try {  
        //操作  
    } finally {  
        lock.unlock();  
    }
}
c、尝试等待执行
ReentrantLock lock = new ReentrantLock(true); //公平锁  
public void getObject(){
    try {  
        if (lock.tryLock(5, TimeUnit.SECONDS)) {      
            //如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行  
            try {  
                //操作  
            } finally {  
                lock.unlock();  
            }  
        }  
    } catch (InterruptedException e) {  
        e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException                   
    }
}

d、可中断锁的同步执行
ReentrantLock lock = new ReentrantLock(true); //公平锁  
public void getObject(){
    lock.lockInterruptibly();  
    try {  
        //操作  
    } catch (InterruptedException e) {  
        e.printStackTrace();  
    } finally {  
        lock.unlock();  
    }
}

ReadWriteLock类:读写锁,维护了一对相关的锁,一个用于只读操作,一个用于写入操作。只要没有writer,读取锁可以由多个reader线程同时保持。写入锁是独占的。
ReentrantReadWriteLock类:可重入读写锁,会使用两把锁来解决问题,一个读锁,一个写锁。
线程进入读锁的前提条件:
1).没有其他线程的写锁,
2).没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
线程进入写锁的前提条件:
1).没有其他线程的读锁
2).没有其他线程的写锁
ReentrantReadWriteLock和ReentrantLock的区别,它和后者都是单独的实现,彼此之间没有继承或实现的关系:
(a).重入方面其内部的WriteLock可以获取ReadLock,但是反过来ReadLock想要获得WriteLock则永远都不要想。
(b).WriteLock可以降级为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有。反过来ReadLock想要升级为WriteLock则不可能。
(c).ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。
(d).不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致。
(e).WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。
读写锁的例子:
import java.util.Random;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
    public static void main(String[] args) {
        Queue3 q3 = new Queue3();
        for(int i=0;i<3;i++){
            new Thread(){
                public void run(){
                    while(true){
                        q3.get();                        
                    }
                }
            }.start();
        }
        for(int i=0;i<3;i++){        
            new Thread(){
                public void run(){
                    while(true){
                        q3.put(new Random().nextInt(10000));
                    }
                }            
            }.start();    
        }
    }
}
 
class Queue3{
    private Object data = null;//共享数据,只能有一个线程能写该数据,但可以有多个线程同时读该数据。
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    public void get(){
        rwl.readLock().lock();//上读锁,其他线程只能读不能写
        System.out.println(Thread.currentThread().getName() + " be ready to read data!");
        try {
            Thread.sleep((long)(Math.random()*1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "have read data :" + data);        
        rwl.readLock().unlock(); //释放读锁,最好放在finnaly里面
    }
   
    public void put(Object data){
        rwl.writeLock().lock();//上写锁,不允许其他线程读也不允许写
        System.out.println(Thread.currentThread().getName() + " be ready to write data!");                    
        try {
            Thread.sleep((long)(Math.random()*1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.data = data;        
        System.out.println(Thread.currentThread().getName() + " have write data: " + data);                    
        rwl.writeLock().unlock();//释放写锁    
    }
}

在锁中使用多条件

一个锁可能关联一个或多个条件,这些条件通过Condition接口声明。目的是允许线程获取锁并且查看等待的某一个条件是否满足,如果不满足就挂起直到某个线程唤醒它们。Condition接口提供了挂起线程和唤起线程的机制。

Condition是在java1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。
Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
Conditon中的await()对应Object的wait();
Condition中的signal()对应Object的notify();
Condition中的signalAll()对应Object的notifyAll()。
代码示例:
import java.util.concurrent.locks.Condition;  
import java.util.concurrent.locks.ReentrantLock;  
 
 
public class Main {  
    public static void main(String[] args) {  
        final ReentrantLock reentrantLock = new ReentrantLock();  
        final Condition condition = reentrantLock.newCondition();  
 
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                reentrantLock.lock();  
                System.out.println(Thread.currentThread().getName() + "拿到锁了");  
                System.out.println(Thread.currentThread().getName() + "等待信号");  
                try {  
                    condition.await();  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
 
                System.out.println(Thread.currentThread().getName() + "拿到信号");  
 
                reentrantLock.unlock();  
            }  
        }, "线程1").start();  
 
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                reentrantLock.lock();  
                System.out.println(Thread.currentThread().getName() + "拿到锁了");  
 
                try {  
                    Thread.sleep(3000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
 
                System.out.println(Thread.currentThread().getName() + "发出信号");  
                condition.signalAll();  
 
                reentrantLock.unlock();  
            }  
        }, "线程2").start();  
    }  

 

posted @ 2017-11-22 17:38  牛头人  阅读(186)  评论(0编辑  收藏  举报