Java 线程锁机制 -Synchronized Lock 互斥锁 读写锁

(1)synchronized 是互斥锁;html

(2)ReentrantLock 顾名思义 :可重入锁java

(3)ReadWriteLock :读写锁算法

读写锁特色:缓存

a)多个读者能够同时进行读
b)写者必须互斥(只容许一个写者写,也不能读者写者同时进行)
c)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)数据结构

 

一、synchronized

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

(1) 原子性并发

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

 (2)  可见性dom

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

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

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

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

(3)synchronize的限制

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

  1. 它没法中断一个正在等候得到锁的线程;
  2. 也没法经过投票获得锁,若是不想等下去,也就无法获得锁; 

二、ReentrantLock (可重入锁)

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

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

 1 class Outputter1 {    
 2     private Lock lock = new ReentrantLock();// 锁对象    
 3   
 4     public void output(String name) {           
 5         lock.lock();      // 获得锁    
 6   
 7         try {    
 8             for(int i = 0; i < name.length(); i++) {    
 9                 System.out.print(name.charAt(i));    
10             }    
11         } finally {    
12             lock.unlock();// 释放锁    
13         }    
14     }    
15 }

区别:

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

三、读写锁ReadWriteLock

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

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

 1 class syncData {        
 2     private int data;// 共享数据        
 3     public synchronized void set(int data) {    
 4         System.out.println(Thread.currentThread().getName() + "准备写入数据");    
 5         try {    
 6             Thread.sleep(20);    
 7         } catch (InterruptedException e) {    
 8             e.printStackTrace();    
 9         }    
10         this.data = data;    
11         System.out.println(Thread.currentThread().getName() + "写入" + this.data);    
12     }       
13     public synchronized  void get() {    
14         System.out.println(Thread.currentThread().getName() + "准备读取数据");    
15         try {    
16             Thread.sleep(20);    
17         } catch (InterruptedException e) {    
18             e.printStackTrace();    
19         }    
20         System.out.println(Thread.currentThread().getName() + "读取" + this.data);    
21     }    
22 }

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

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();  
        }    
    }

运行结果: 

 1 Thread-R2准备读取数据  
 2 Thread-R2读取1  
 3 Thread-R2准备读取数据  
 4 Thread-R2读取1  
 5 Thread-R2准备读取数据  
 6 Thread-R2读取1  
 7 Thread-R2准备读取数据  
 8 Thread-R2读取1  
 9 Thread-R0准备读取数据 //R0和R2能够同时读取,不该该互斥!  
10 Thread-R0读取1  
11 Thread-R0准备读取数据  
12 Thread-R0读取1  
13 Thread-R0准备读取数据  
14 Thread-R0读取1  
15 Thread-R0准备读取数据

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

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

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

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

 1 class Data {        
 2     private int data;// 共享数据    
 3     private ReadWriteLock rwl = new ReentrantReadWriteLock();       
 4     public void set(int data) {    
 5         rwl.writeLock().lock();// 取到写锁    
 6         try {    
 7             System.out.println(Thread.currentThread().getName() + "准备写入数据");    
 8             try {    
 9                 Thread.sleep(20);    
10             } catch (InterruptedException e) {    
11                 e.printStackTrace();    
12             }    
13             this.data = data;    
14             System.out.println(Thread.currentThread().getName() + "写入" + this.data);    
15         } finally {    
16             rwl.writeLock().unlock();// 释放写锁    
17         }    
18     }       
19   
20     public void get() {    
21         rwl.readLock().lock();// 取到读锁    
22         try {    
23             System.out.println(Thread.currentThread().getName() + "准备读取数据");    
24             try {    
25                 Thread.sleep(20);    
26             } catch (InterruptedException e) {    
27                 e.printStackTrace();    
28             }    
29             System.out.println(Thread.currentThread().getName() + "读取" + this.data);    
30         } finally {    
31             rwl.readLock().unlock();// 释放读锁    
32         }    
33     }    
34 }

测试结果:

 1 Thread-W1准备写入数据  
 2 Thread-W1写入9  
 3 Thread-W1准备写入数据  
 4 Thread-W1写入24  
 5 Thread-W1准备写入数据  
 6 Thread-W1写入12  
 7 Thread-W0准备写入数据  
 8 Thread-W0写入22  
 9 Thread-W0准备写入数据  
10 Thread-W0写入15  
11 Thread-W0准备写入数据  
12 Thread-W0写入6  
13 Thread-W0准备写入数据  
14 Thread-W0写入13  
15 Thread-W0准备写入数据  
16 Thread-W0写入0  
17 Thread-W2准备写入数据  
18 Thread-W2写入23  
19 Thread-W2准备写入数据  
20 Thread-W2写入24  
21 Thread-W2准备写入数据  
22 Thread-W2写入24  
23 Thread-W2准备写入数据  
24 Thread-W2写入17  
25 Thread-W2准备写入数据  
26 Thread-W2写入11  
27 Thread-R2准备读取数据  
28 Thread-R1准备读取数据  
29 Thread-R0准备读取数据  
30 Thread-R0读取11  
31 Thread-R1读取11  
32 Thread-R2读取11  
33 Thread-W1准备写入数据  
34 Thread-W1写入18  
35 Thread-W1准备写入数据  
36 Thread-W1写入1  
37 Thread-R0准备读取数据  
38 Thread-R2准备读取数据  
39 Thread-R1准备读取数据  
40 Thread-R2读取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的功能

优势:假设缓存队列中已经存满,那么阻塞的确定是写线程,唤醒的确定是读线程,相反,阻塞的确定是读线程,唤醒的确定是写线程。

相关文章
相关标签/搜索