线程安全 加锁机制

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其余线程不能进行访问直到该线程读取完,其余线程才可以使用。不会出现数据不一致或者数据污染。
线程不安全就是不提供数据访问保护,有可能出现多个线程前后更改数据形成所获得的数据是脏数据。html

摘自 http://blog.csdn.net/vking_wang/article/details/9952063java

 

Java线程 加锁机制:synchronized、Lock、Condition算法

一、synchronized

把代码块声明为 synchronized,有两个重要后果,一般是指该代码具备 原子性(atomicity)和 可见性(visibility)缓存

1.1 原子性

原子性意味着个时刻,只有一个线程可以执行一段代码,这段代码经过一个monitor object保护。从而防止多个线程在更新共享状态时相互冲突。安全

1.2 可见性

可见性则更为微妙,它要对付内存缓存和编译器优化的各类反常行为。它必须确保释放锁以前对共享数据作出的更改对于随后得到该锁的另外一个线程是可见的 。数据结构

做用:若是没有同步机制提供的这种可见性保证,线程看到的共享变量多是修改前的值或不一致的值,这将引起许多严重问题。多线程

原理:当对象获取锁时,它首先使本身的高速缓存无效,这样就能够保证直接从主内存中装入变量。 一样,在对象释放锁以前,它会刷新其高速缓存,强制使已作的任何更改都出如今主内存中。 这样,会保证在同一个锁上同步的两个线程看到在 synchronized 块内修改的变量的相同值。并发

 

通常来讲,线程以某种没必要让其余线程当即能够看到的方式(无论这些线程在寄存器中、在处理器特定的缓存中,仍是经过指令重排或者其余编译器优化),不受缓存变量值的约束,可是若是开发人员使用了同步,那么运行库将确保某一线程对变量所作的更新先于对现有synchronized 块所进行的更新,当进入由同一监控器(lock)保护的另外一个synchronized 块时,将马上能够看到这些对变量所作的更新。相似的规则也存在于volatile变量上。框架

——volatile只保证可见性,不保证原子性!dom

1.3 什么时候要同步?

可见性同步的基本规则是在如下状况中必须同步: 

  1. 读取上一次多是由另外一个线程写入的变量 
  2. 写入下一次可能由另外一个线程读取的变量

一致性同步:当修改多个相关值时,您想要其它线程原子地看到这组更改—— 要么看到所有更改,要么什么也看不到。

这适用于相关数据项(如粒子的位置和速率)和元数据项(如链表中包含的数据值和列表自身中的数据项的链)。

 

在某些状况中,您没必要用同步来将数据从一个线程传递到另外一个,由于 JVM 已经隐含地为您执行同步。这些状况包括:

  1. 由静态初始化器(在静态字段上或 static{} 块中的初始化器)
  2. 初始化数据时 
  3. 访问 final 字段时 ——final对象呢?
  4. 在建立线程以前建立对象时 
  5. 线程能够看见它将要处理的对象时

 

1.4 synchronize的限制

synchronized是不错,但它并不完美。它有一些功能性的限制:

  1. 它没法中断一个正在等候得到锁的线程;
  2. 也没法经过投票获得锁,若是不想等下去,也就无法获得锁;
  3. 同步还要求锁的释放只能在与得到锁所在的堆栈帧相同的堆栈帧中进行,多数状况下,这没问题(并且与异常处理交互得很好),可是,确实存在一些非块结构的锁定更合适的状况。

 

二、ReentrantLock

Java.util.concurrent.lock 中的Lock 框架是锁定的一个抽象,它容许把锁定的实现做为 Java 类,而不是做为语言的特性来实现。这就为Lock 的多种实现留下了空间,各类实现可能有不一样的调度算法、性能特性或者锁定语义。

ReentrantLock 类实现了Lock ,它拥有与synchronized 相同的并发性和内存语义,可是添加了相似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用状况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 能够花更少的时候来调度线程,把更多时间用在执行线程上。)

class Outputter1 {    
    private Lock lock = new ReentrantLock();// 锁对象    
  
    public void output(String name) {           
        lock.lock();      // 获得锁    
  
        try {    
            for(int i = 0; i < name.length(); i++) {    
                System.out.print(name.charAt(i));    
            }    
        } finally {    
            lock.unlock();// 释放锁    
        }    
    }    
}    

区别:

须要注意的是,用sychronized修饰的方法或者语句块在代码执行完以后锁自动释放,而是用Lock须要咱们手动释放锁,因此为了保证锁最终被释放(发生异常状况),要把互斥区放在try内,释放锁放在finally内!!

 

 

三、读写锁ReadWriteLock

上例中展现的是和synchronized相同的功能,那Lock的优点在哪里?

例如一个类对其内部共享数据data提供了get()和set()方法,若是用synchronized,则代码以下:

class syncData {        
    private int data;// 共享数据        
    public synchronized void set(int data) {    
        System.out.println(Thread.currentThread().getName() + "准备写入数据");    
        try {    
            Thread.sleep(20);    
        } catch (InterruptedException e) {    
            e.printStackTrace();    
        }    
        this.data = data;    
        System.out.println(Thread.currentThread().getName() + "写入" + this.data);    
    }       
    public synchronized  void get() {    
        System.out.println(Thread.currentThread().getName() + "准备读取数据");    
        try {    
            Thread.sleep(20);    
        } catch (InterruptedException e) {    
            e.printStackTrace();    
        }    
        System.out.println(Thread.currentThread().getName() + "读取" + this.data);    
    }    
}    

而后写个测试类来用多个线程分别读写这个共享数据:

 

public static void main(String[] args) {    
//        final Data data = new Data();    
          final syncData data = new syncData();    
//        final RwLockData data = new RwLockData();    
          
        //写入  
        for (int i = 0; i < 3; i++) {    
            Thread t = new Thread(new Runnable() {    
              @Override  
              public void run() {    
                    for (int j = 0; j < 5; j++) {    
                        data.set(new Random().nextInt(30));    
                    }    
                }    
            });  
            t.setName("Thread-W" + i);  
            t.start();  
        }    
        //读取  
        for (int i = 0; i < 3; i++) {    
            Thread t = new Thread(new Runnable() {    
              @Override  
              public void run() {    
                    for (int j = 0; j < 5; j++) {    
                        data.get();    
                    }    
                }    
            });    
            t.setName("Thread-R" + i);  
            t.start();  
        }    
    }    

运行结果:

Thread-W0准备写入数据  
Thread-W0写入0  
Thread-W0准备写入数据  
Thread-W0写入1  
Thread-R1准备读取数据  
Thread-R1读取1  
Thread-R1准备读取数据  
Thread-R1读取1  
Thread-R1准备读取数据  
Thread-R1读取1  
Thread-R1准备读取数据  
Thread-R1读取1  
Thread-R1准备读取数据  
Thread-R1读取1  
Thread-R2准备读取数据  
Thread-R2读取1  
Thread-R2准备读取数据  
Thread-R2读取1  
Thread-R2准备读取数据  
Thread-R2读取1  
Thread-R2准备读取数据  
Thread-R2读取1  
Thread-R2准备读取数据  
Thread-R2读取1  
Thread-R0准备读取数据 //R0和R2能够同时读取,不该该互斥!  
Thread-R0读取1  
Thread-R0准备读取数据  
Thread-R0读取1  
Thread-R0准备读取数据  
Thread-R0读取1  
Thread-R0准备读取数据  
Thread-R0读取1  
Thread-R0准备读取数据  
Thread-R0读取1  
Thread-W1准备写入数据  
Thread-W1写入18  
Thread-W1准备写入数据  
Thread-W1写入16  
Thread-W1准备写入数据  
Thread-W1写入19  
Thread-W1准备写入数据  
Thread-W1写入21  
Thread-W1准备写入数据  
Thread-W1写入4  
Thread-W2准备写入数据  
Thread-W2写入10  
Thread-W2准备写入数据  
Thread-W2写入4  
Thread-W2准备写入数据  
Thread-W2写入1  
Thread-W2准备写入数据  
Thread-W2写入14  
Thread-W2准备写入数据  
Thread-W2写入2  
Thread-W0准备写入数据  
Thread-W0写入4  
Thread-W0准备写入数据  
Thread-W0写入20  
Thread-W0准备写入数据  
Thread-W0写入29  

如今一切都看起来很好!各个线程互不干扰!等等。。读取线程和写入线程互不干扰是正常的,可是两个读取线程是否须要互不干扰??

对!读取线程不该该互斥!

咱们能够用读写锁ReadWriteLock实现:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class Data {        
    private int data;// 共享数据    
    private ReadWriteLock rwl = new ReentrantReadWriteLock();       
    public void set(int data) {    
        rwl.writeLock().lock();// 取到写锁    
        try {    
            System.out.println(Thread.currentThread().getName() + "准备写入数据");    
            try {    
                Thread.sleep(20);    
            } catch (InterruptedException e) {    
                e.printStackTrace();    
            }    
            this.data = data;    
            System.out.println(Thread.currentThread().getName() + "写入" + this.data);    
        } finally {    
            rwl.writeLock().unlock();// 释放写锁    
        }    
    }       
  
    public void get() {    
        rwl.readLock().lock();// 取到读锁    
        try {    
            System.out.println(Thread.currentThread().getName() + "准备读取数据");    
            try {    
                Thread.sleep(20);    
            } catch (InterruptedException e) {    
                e.printStackTrace();    
            }    
            System.out.println(Thread.currentThread().getName() + "读取" + this.data);    
        } finally {    
            rwl.readLock().unlock();// 释放读锁    
        }    
    }    
}    

测试结果:

Thread-W1准备写入数据  
Thread-W1写入9  
Thread-W1准备写入数据  
Thread-W1写入24  
Thread-W1准备写入数据  
Thread-W1写入12  
Thread-W0准备写入数据  
Thread-W0写入22  
Thread-W0准备写入数据  
Thread-W0写入15  
Thread-W0准备写入数据  
Thread-W0写入6  
Thread-W0准备写入数据  
Thread-W0写入13  
Thread-W0准备写入数据  
Thread-W0写入0  
Thread-W2准备写入数据  
Thread-W2写入23  
Thread-W2准备写入数据  
Thread-W2写入24  
Thread-W2准备写入数据  
Thread-W2写入24  
Thread-W2准备写入数据  
Thread-W2写入17  
Thread-W2准备写入数据  
Thread-W2写入11  
Thread-R2准备读取数据  
Thread-R1准备读取数据  
Thread-R0准备读取数据  
Thread-R0读取11  
Thread-R1读取11  
Thread-R2读取11  
Thread-W1准备写入数据  
Thread-W1写入18  
Thread-W1准备写入数据  
Thread-W1写入1  
Thread-R0准备读取数据  
Thread-R2准备读取数据  
Thread-R1准备读取数据  
Thread-R2读取1  
Thread-R2准备读取数据  
Thread-R1读取1  
Thread-R0读取1  
Thread-R1准备读取数据  
Thread-R0准备读取数据  
Thread-R0读取1  
Thread-R2读取1  
Thread-R2准备读取数据  
Thread-R1读取1  
Thread-R0准备读取数据  
Thread-R1准备读取数据  
Thread-R0读取1  
Thread-R2读取1  
Thread-R1读取1  
Thread-R0准备读取数据  
Thread-R1准备读取数据  
Thread-R2准备读取数据  
Thread-R1读取1  
Thread-R2读取1  
Thread-R0读取1  

 

与互斥锁定相比,读-写锁定容许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)能够修改共享数据,但在许多状况下,任何数量的线程能够同时读取共享数据(reader 线程)

 

从理论上讲,与互斥锁定相比,使用读-写锁定所容许的并发性加强将带来更大的性能提升。

在实践中,只有在多处理器上而且只在访问模式适用于共享数据时,才能彻底实现并发性加强。——例如,某个最初用数据填充而且以后不常常对其进行修改的 collection,由于常常对其进行搜索(好比搜索某种目录),因此这样的 collection 是使用读-写锁定的理想候选者。

 

四、线程间通讯Condition

Condition能够替代传统的线程间通讯,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()。

——为何方法名不直接叫wait()/notify()/nofityAll()?由于Object的这几个方法是final的,不可重写!

 

传统线程的通讯方式,Condition均可以实现。

注意,Condition是被绑定到Lock上的,要建立一个Lock的Condition必须用newCondition()方法。

 

Condition的强大之处在于它能够为多个线程间创建不一样的Condition

看JDK文档中的一个例子:假定有一个绑定的缓冲区,它支持 put 和 take 方法。若是试图在空的缓冲区上执行take 操做,则在某一个项变得可用以前,线程将一直阻塞;若是试图在满的缓冲区上执行 put 操做,则在有空间变得可用以前,线程将一直阻塞。咱们喜欢在单独的等待 set 中保存put 线程和take 线程,这样就能够在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可使用两个Condition 实例来作到这一点。

——其实就是java.util.concurrent.ArrayBlockingQueue的功能

class BoundedBuffer {  
  final Lock lock = new ReentrantLock();          //锁对象  
  final Condition notFull  = lock.newCondition(); //写线程锁  
  final Condition notEmpty = lock.newCondition(); //读线程锁  
  
  final Object[] items = new Object[100];//缓存队列  
  int putptr;  //写索引  
  int takeptr; //读索引  
  int count;   //队列中数据数目  
  
  //
  public void put(Object x) throws InterruptedException {  
    lock.lock(); //锁定  
    try {  
      // 若是队列满,则阻塞<写线程>  
      while (count == items.length) {  
        notFull.await();   
      }  
      // 写入队列,并更新写索引  
      items[putptr] = x;   
      if (++putptr == items.length) putptr = 0;   
      ++count;  
  
      // 唤醒<读线程>  
      notEmpty.signal();   
    } finally {   
      lock.unlock();//解除锁定   
    }   
  }  
  
  //
  public Object take() throws InterruptedException {   
    lock.lock(); //锁定   
    try {  
      // 若是队列空,则阻塞<读线程>  
      while (count == 0) {  
         notEmpty.await();  
      }  
  
      //读取队列,并更新读索引  
      Object x = items[takeptr];   
      if (++takeptr == items.length) takeptr = 0;  
      --count;  
  
      // 唤醒<写线程>  
      notFull.signal();   
      return x;   
    } finally {   
      lock.unlock();//解除锁定   
    }   
  }   

优势:假设缓存队列中已经存满,那么阻塞的确定是写线程,唤醒的确定是读线程,相反,阻塞的确定是读线程,唤醒的确定是写线程。那么假设只有一个Condition会有什么效果呢?缓存队列中已经存满,这个Lock不知道唤醒的是读线程仍是写线程了,若是唤醒的是读线程,皆大欢喜,若是唤醒的是写线程,那么线程刚被唤醒,又被阻塞了,这时又去唤醒,这样就浪费了不少时间。

相关文章
相关标签/搜索