Java 线程锁机制 -Synchronized Lock 互斥锁 读写锁
什么是互斥锁?
在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁。
如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都被编程就绪状态, 第一个变为就绪状态的线程又执行加锁操作,那么其他的线程又会进入等待。 在这种方式下,只有一个线程能够访问被互斥锁保护的资源。
什么是共享锁?
互斥锁要求只能有一个线程访问被保护的资源,共享锁从字面来看也即是允许多个线程共同访问资源。
什么是读写锁?
读写锁既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的。
读写锁有三种状态:读加锁状态、写加锁状态和不加锁状态
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
---------------------
作者:weixin_33882443
来源:CSDN
原文:https://blog.csdn.net/weixin_33882443/article/details/87331842
版权声明:本文为博主原创文章,转载请附上博文链接!
(1)synchronized 是互斥锁;
(2)ReentrantLock 顾名思义 :可重入锁
(3)ReadWriteLock :读写锁
读写锁特点:
a)多个读者可以同时进行读
b)写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
c)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
1、synchronized
把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility)。
(1) 原子性
原子性意味着个时刻,只有一个线程能够执行一段代码,这段代码通过一个monitor object保护。从而防止多个线程在更新共享状态时相互冲突。
(2) 可见性
可见性则更为微妙,它要对付内存缓存和编译器优化的各种反常行为。它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 。
作用:如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
一般来说,线程以某种不必让其他线程立即可以看到的方式(不管这些线程在寄存器中、在处理器特定的缓存中,还是通过指令重排或者其他编译器优化),不受缓存变量值的约束,但是如果开发人员使用了同步,那么运行库将确保某一线程对变量所做的更新先于对现有synchronized
块所进行的更新,当进入由同一监控器(lock)保护的另一个synchronized
块时,将立刻可以看到这些对变量所做的更新。类似的规则也存在于volatile
变量上。
(3)synchronize的限制
synchronized是不错,但它并不完美。它有一些功能性的限制:
- 它无法中断一个正在等候获得锁的线程;
- 也无法通过投票得到锁,如果不想等下去,也就没法得到锁;
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内!!
3、读写锁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 是使用读-写锁定的理想候选者。
4、线程间通信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的功能
优点:
假设缓存队列中已经存满,那么阻塞的肯定是写线程,唤醒的肯定是读线程,相反,阻塞的肯定是读线程,唤醒的肯定是写线程。