并发编程之线程安全lock
并发编程之线程安全lock
为什么需要lock接口锁
提供了编程级别的锁接口,锁实现,可以灵活使用、扩展
lock类层次结构
自jdk1.5后,增加的JUC包下的locks包包含的类如下:
AQS
AbstractOwnableSynchronizer:提供独占模式同步的线程所有者;
AbstractQueuedSynchronizer:抽象的队列同步器;
AbstractQueuedLongSynchronizer:将AbstractQueuedSynchronizer的状态int改为long类型的抽象的队列同步器;
ReentrantLock
特性:独占锁;支持公平锁、非公平锁两种模式;可重入锁
ReentrantReadWriteLock
维护一对关联锁,一个用于读操作,一个用于写操作;读锁可以有多个线程同时持有,写锁时排他锁
适用场景:适合读取线程比写入线程多的场景,改进互斥锁的性能,比如:集合的并发线程安全性改造、缓存组件。
锁降级指的是写锁降级成为读锁。把持住当前拥有的写锁的同时,再获取到读锁,随后释放写锁的过程。写锁是线程独占,读锁是共享,所以写- >读是降级。(读->写,是不能实现的)
lock类方法
lock接口方法
- Acquires the lock 获取锁
void lock();
- Acquires the lock unless the current thread is interrupted 获取锁,除非当前线程被打断
void lockInterruptibly() throws InterruptedException;
- Acquires the lock only if it is free at the time of invocation 仅在调用时锁是空闲的情况下才获取锁(尝试获取锁)
boolean tryLock();
//带时间限制的尝试获取锁,超过指定时间无法获取锁,则继续执行
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
- Releases the lock 释放锁
void unlock();
lock锁使用
lock锁使用
JUC包中的ReentrantLock类中给我们提供了使用lock锁的实例,如下:
class X {private final ReentrantLock lock = new ReentrantLock();// ...public void m() {lock.lock(); // block until condition holdstry {// ... method body} finally {lock.unlock()}}}}
建议:正如上边的实例,在使用lock锁时,正确的姿势:配合try{}finally{}一块使用,保证获取锁后,在不使用锁时将锁能够及时的释放:lock.lock lock.unlock
public static void main(String[] args) {
// test1();test2();}/****/private static void test1() {lock.lock();System.out.println("当前线程"+Thread.currentThread().getName()+"获的锁的次数为:"+lock.getHoldCount());;if( lock.tryLock()){System.out.println("尝试获得锁成功");}System.out.println("当前线程"+Thread.currentThread().getName()+"获的锁的次数为:"+lock.getHoldCount());;try{System.out.println("获取了锁");}finally {lock.unlock();System.out.println("当前线程"+Thread.currentThread().getName()+"释放一次锁,目前获的锁的次数为:"+lock.getHoldCount());lock.unlock();System.out.println("当前线程"+Thread.currentThread().getName()+"释放一次锁,目前获的锁的次数为:"+lock.getHoldCount());}}public static void test2(){lock.lock();try {final Thread thread = new Thread(new Runnable() {@Overridepublic void run() {try {System.out.println("当前线程" + Thread.currentThread().getName() + "开始获得锁");lock.lockInterruptibly();//子线程可以被打断,执行finallylock.lock();//子线程不可被打断,一直阻塞等待System.out.println("当前线程" + Thread.currentThread().getName() + "结束获得锁");} catch (Exception e) {e.printStackTrace();} finally {System.out.println("当前线程" + Thread.currentThread().getName() + "释放锁");lock.unlock();}}});thread.start();System.out.println("当前线程" + Thread.currentThread().getName() + " 开始打断interrupt子线程"+thread.getName());thread.interrupt();} finally {
// lock.unlock();}}
}
自定义锁
自定义实现Lock
public class MyLock implements Lock {//当前锁的拥有者private AtomicReference<Thread> owner = new AtomicReference<>();//等待队列private LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();@Overridepublic boolean tryLock() {return owner.compareAndSet(null,Thread.currentThread());}@Overridepublic void lock() {while (!tryLock()){waiters.offer(Thread.currentThread());LockSupport.park();waiters.remove(Thread.currentThread());}}public void unlock() {if(owner.compareAndSet(Thread.currentThread(),null)){waiters.forEach(thead -> {LockSupport.unpark(thead);});}}@Overridepublic void lockInterruptibly() throws InterruptedException {}@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException {return false;}@Overridepublic Condition newCondition() {return null;}
}
使用自定义实现的lock锁
public class MyLockDemo {MyLock lock = new MyLock();public int i =0 ;public static void main(String[] args) {for (int i=0;i<200;i++){test2(i+1);}}public static void test2(int n){MyLockDemo demo = new MyLockDemo();List<Thread> list = new ArrayList<>();for (int i=0;i<200;i++){final Thread thread = new Thread(new Runnable() {@Overridepublic void run() {demo.add();}});thread.start();list.add(thread);}for (Thread thread:list){try {thread.join();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("第 "+n+" 次:"+demo.i);}public void add(){lock.lock();try{i++;}finally {lock.unlock();}}
}
ReentrantReadWritLock使用
Demo5_ReadWrite01的实例,是一个读和写都用一把锁,也就是共同争抢一把锁,这样效率太低了,代码如下:
/*** 既有读,又有写时, 读也需要加锁。* 一个写(独占),多个读(共享),多个读不能相互影响,读写之间互斥。*/
public class Demo5_ReadWrite01 {long i = 0;Lock lock = new ReentrantLock();//为什么读也要加锁???public void read() {lock.lock();try {long a = i;System.out.println(Thread.currentThread().getName()+" 读到了值"+a);}finally {lock.unlock();}}public void write() {lock.lock();try {i++;System.out.println(Thread.currentThread().getName()+" 修改值:"+i);}finally {lock.unlock();}}public static void main(String[] args) throws InterruptedException {final Demo5_ReadWrite01 demo = new Demo5_ReadWrite01();List<Thread> list = new ArrayList<Thread>();for (int i=0; i<=10; i++){int n = i;String threadName = n == 0 ? "writeThread" : "readThread"+n;Thread th = new Thread(()->{long starttime = System.currentTimeMillis();while (System.currentTimeMillis() - starttime < 1000) {if (n==0) {demo.write();}else{demo.read();}}}, threadName);list.add(th);th.start();}for(Thread th : list) {th.join();}}}
由于读和写都通争抢一把锁,效率太低了,因此使用读写锁ReentrantReadWriteLock改进,读一把锁,写一把锁,代码Demo6_ReadWrite02,如下:
public class Demo6_ReadWrite02 {volatile long i = 0;ReadWriteLock rwLock = new ReentrantReadWriteLock();//为什么读也要加锁???public void read() {// 获得读锁,读锁共享rwLock.readLock().lock();try {long a = i;System.out.println(Thread.currentThread().getName()+" 读到了值"+a);}finally {// 释放读锁rwLock.readLock().unlock();}}public void write() {// 获取写锁,写锁排他rwLock.writeLock().lock();try {i++;System.out.println(Thread.currentThread().getName()+" 修改值:"+i);}finally {// 释放写锁rwLock.writeLock().unlock();}}public static void main(String[] args) throws InterruptedException {final Demo5_ReadWrite01 demo = new Demo5_ReadWrite01();List<Thread> list = new ArrayList<Thread>();for (int i=0;i<=10; i++){int n = i;String threadName = n == 0 ? "writeThread" : "readThread"+n;Thread th = new Thread(()->{long starttime = System.currentTimeMillis();while (System.currentTimeMillis() - starttime < 1000) {if (n==0) {demo.write();}else{demo.read();}}}, threadName);list.add(th);th.start();}for(Thread th : list) {th.join();}}}
ReentrantReadWritLock锁降级
ReentrantReadWritLock:读写锁包含:读锁ReadLock、写锁WriteLock
/*** 如果缓存中存在的,则从缓存中获取;* 如果缓存中不存在的,则从数据库中获取*/
public class CacheDemo {public static void main(String[] args) {System.out.println(new CacheDemo().getData("admin"));System.out.println(new CacheDemo().getData("admin"));}static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();private static volatile boolean isCache ;private String getData(String key) {String date = "";//获取读锁rwLock.readLock().lock();try {//缓存中有if(isCache){date = Redis.map.get(key);}else{//缓存中没有,从数据库获取//..........................//从数据库获取//释放读锁rwLock.readLock().unlock();//获取写锁rwLock.writeLock().lock();try {//判断缓存中是否有if(isCache){date = Redis.map.get(key);}else {date = DBService.getData();Redis.map.put(key,date);isCache = true;}rwLock.readLock().lock();//锁降级、由写锁变为读锁}finally {rwLock.writeLock().unlock();}}return date;}finally {//释放读锁rwLock.readLock().unlock();}}
}
class DBService{static String getData(){System.out.println("开始查询数据库。。。");return "{name:张三,age:18}";}
}
class Redis{public static Map<String,String> map = new HashMap<>();
}
Condition
用于替代wait/notify。
Object中的wait(),notify(),notifyAll()方法是和synchronized配合使用的,可以唤醒一个或者全部(单个等待集);
Condition是需要与Lock配合使用的,提供多个等待集合,更精确的控制(底层是park/unpark机制);
同步锁的本质-排队
同步的方式:独享锁 – 单个队列窗口, 共享锁 – 多个队列窗口
抢锁的方式: 插队抢(不公平锁)、先来后到抢锁(公平锁)
没抢到锁的处理方式: 快速尝试多次(CAS自旋锁)、阻塞等待
唤醒阻塞线程的方式(叫号器):全部通知、通知下一个
AQS抽象队列同步器
提供了对资源占用、释放,线程的等待、唤醒等等接口和具体实现。
可以用在各种需要控制资源争用的场景中。(ReentrantLock/CountDownLatch/Semphore)
acquire、 acquireShared : 定义了资源争用的逻辑,如果没拿到,则等待。
tryAcquire、 tryAcquireShared : 实际执行占用资源的操作,如何判定一个由使用者具体去实现。
release、 releaseShared : 定义释放资源的逻辑,释放之后,通知后续节点进行争抢。
tryRelease、 tryReleaseShared: 实际执行资源释放的操作,具体的AQS使用者去实现。
资源占用流程
线程封闭
线程封闭概念
多线程中访问共享可变数据时,涉及到线程间数据同步的问题。
并不是所有的时候,都要用到共享数据,若数据都被封闭在各自的线程中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭
线程封闭实现
ThreadLocal实现了线程封闭,它是java中一种特殊的变量;它是一个线程级别变量,每个线程都拥有一个ThreaLocal,就是每个线程都拥有自己独立的一个变量,不存在竞争,在并发模式下是绝对安全的变量。
用法:ThreadLocal var = new ThreadLocal();
会自动在每一个线程上创建一个T的副本,副本之间彼此独立,互不影响。可以用ThreadLocal存储一些参数,以便在线程中多个方法中使用,用来代替方法传参的做法
栈封闭
局部变量的固有属性之一就是封闭在线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。
流程
[外链图片转存中…(img-YNyxQVpp-1747910139323)]
线程封闭
线程封闭概念
多线程中访问共享可变数据时,涉及到线程间数据同步的问题。
并不是所有的时候,都要用到共享数据,若数据都被封闭在各自的线程中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭
线程封闭实现
ThreadLocal实现了线程封闭,它是java中一种特殊的变量;它是一个线程级别变量,每个线程都拥有一个ThreaLocal,就是每个线程都拥有自己独立的一个变量,不存在竞争,在并发模式下是绝对安全的变量。
用法:ThreadLocal var = new ThreadLocal();
会自动在每一个线程上创建一个T的副本,副本之间彼此独立,互不影响。可以用ThreadLocal存储一些参数,以便在线程中多个方法中使用,用来代替方法传参的做法
栈封闭
局部变量的固有属性之一就是封闭在线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。