Java并发编程之Lock锁详解与实战

如果你不相信努力和时光,那么成果就会是第一个选择辜负你的。不要去否定你自己的过去,也不要用你的过去牵扯你现在的努力和对未来的展望。不是因为拥有希望你才去努力,而是去努力了,你才有可能看到希望的光芒。Java并发编程之Lock锁详解与实战,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

1.1 简介

java.util.concurrent.locks.Lock 是一个类似于synchronized 块的线程同步机制。但是Lock比synchronized 块更加灵活。Lock是个接口,有个实现类是ReentrantLock。

1.2 Lock和syncronized的区别
  • synchronized是Java语言的关键字。Lock是一个类。
  • synchronized不需要用户去手动释放锁,发生异常或者线程结束时自动释放锁;Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
  • lock可以配置公平策略,实现线程按照先后顺序获取锁。
  • 提供了trylock方法 可以试图获取锁,获取到或获取不到时,返回不同的返回值 让程序可以灵活处理。
  • lock()和unlock()可以在不同的方法中执行,可以实现同一个线程在上一个方法中lock()在后续的其他方法中unlock(),比syncronized灵活的多。
1.3 Lock接口抽象方法
Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}

void lock():获取锁,如果锁不可用,则出于线程调度的目的,当前线程将被禁用,并且在获取锁之前处于休眠状态。

Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}

boolean tryLock():如果锁可用立即返回true,如果锁不可用立即返回false。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException:如果锁可用,则此方法立即返回true。 如果该锁不可用,则当前线程将出于线程调度目的而被禁用并处于休眠状态,直到发生以下三种情况之一为止:当前线程获取到该锁;当前线程被其他线程中断,并且支持中断获取锁;经过指定的等待时间如果获得了锁,则返回true,没获取到锁返回false。
void unlock():释放锁。释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

1.4 ReentrantLock

重入锁也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。避免死锁问题的,synchronized也可重入。
synchronized重入测试:

package com.weige.javaskillpoint;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
class JavaSkillPointApplicationTests {

    public synchronized void eat() {
        log.info("小明正在认真的吃饭:"+ "synchronized eat");
        drinkWater();
    }
    public synchronized void drinkWater() {
        log.info("小明吃饭到一半噎着了,马上喝水:"+ "synchronized drinkWater");
    }
    public static void main(String[] args) {
        JavaSkillPointApplicationTests reentrantDemo = new JavaSkillPointApplicationTests();
        reentrantDemo.eat();
    }
}

在这里插入图片描述
ReentrantLock重入测试:

package com.weige.javaskillpoint;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j
@SpringBootTest
class JavaSkillPointApplicationTests {

    public static void main(String[] args) {
        ReentrantDemo reentrantDemo = new ReentrantDemo();
        reentrantDemo.eat();
    }

}
@Slf4j
class ReentrantDemo implements Runnable {
    Lock lock = new ReentrantLock();
    @Override
    public void run() {
        eat();
    }
    public void eat() {
        try {
            lock.lock();
            log.info("lock加锁:" + "小明正在认真的吃饭");
            drinkWater();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();// 必须在finally中释放
        }
    }

    public void drinkWater() {
        try {
            lock.lock();
            log.info("lock加锁:" + "小明吃饭到一半噎着了,马上喝水");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

在这里插入图片描述

同一个线程,首先在eat方法中获取锁,然后调用drinkWater方法,drinkWater方法中重复获取同一个锁。两个方法都执行成功。

1.5 NonReentrantLock

不可重入锁:

package com.weige.javaskillpoint;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j
@SpringBootTest
class JavaSkillPointApplicationTests {

    public static void main(String[] args) {
        NonLockDemo nonLockDemo = new NonLockDemo();
        nonLockDemo.eat();
    }

}

@Slf4j
class NonLockDemo implements Runnable {

    Lock lock = new NonReentrantLock();

    @Override
    public void run() {
        eat();
    }

    public void eat() {
        try {
            lock.lock();
            log.info("lock加锁:" + "小明正在认真的吃饭");
            drinkWater();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();// 必须在finally中释放
        }
    }

    public void drinkWater() {
        try {
            lock.lock();
            log.info("lock加锁:" + "小明吃饭到一半噎着了,马上喝水");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

class NonReentrantLock extends ReentrantLock {
    @Override
    public void lock() {
        if (isHeldByCurrentThread()) {
            throw new RuntimeException("当前线程已经持有锁,不可重入");
        }
        super.lock();
    }
}

在这里插入图片描述
同一个线程先调用eat方法并获取到锁后继续调用drinkWater方法,此时set方法还未执行所得释放,在drinkWater方法中尝试获取锁时返回false。

1.6 ReentrantReadWriteLock

读写锁,可以分别获取读锁或写锁。也就是说将数据的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。读锁使用共享模式;写锁使用独占模式;读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁。
writeLock():获取写锁。
readLock():获取读锁。
执行三个线程进行读写操作,并设置一个屏障,线程依次准备就绪后未获取锁之前都在等待,当第三个线程执行 cyclicBarrier.await();后屏障解除,三个线程同时执行。

package com.weige.javaskillpoint;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@Slf4j
@SpringBootTest
class JavaSkillPointApplicationTests {

    public static void main(String[] args) {

        WriteAndReadLockTest.start();
    }

}
@Slf4j
class WriteAndReadLockTest {
    private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

    // 参数解释
    // corePoolSize 表示线程池的常驻核心线程数。如果设置为 0,则表示在没有任何任务时,销毁线程池;如果大于 0,即使没有任务时也会保证线程池的线程数量等于此值。但需要注意,此值如果设置的比较小,则会频繁的创建和销毁线程(创建和销毁的原因会在本课时的下半部分讲到);如果设置的比较大,则会浪费系统资源,所以开发者需要根据自己的实际业务来调整此值。
    // maximumPoolSize 表示线程池在任务最多时,最大可以创建的线程数。官方规定此值必须大于 0,也必须大于等于 corePoolSize,此值只有在任务比较多,且不能存放在任务队列时,才会用到。
    // keepAliveTime 表示线程的存活时间,当线程池空闲时并且超过了此时间,多余的线程就会销毁,直到线程池中的线程数量销毁的等于 corePoolSize 为止,如果 maximumPoolSize 等于 corePoolSize,那么线程池在空闲的时候也不会销毁任何线程。
    // unit 表示存活时间的单位,它是配合 keepAliveTime 参数共同使用的。
    // workQueue 表示线程池执行的任务队列,当线程池的所有线程都在处理任务时,如果来了新任务就会缓存到此任务队列中排队等待执行。
    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10,
            60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());

    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
    private static int i = 1999;

    public static void start() {
        threadPoolExecutor.execute(()->{
            read(Thread.currentThread());
        });
        threadPoolExecutor.execute(()->{
            write(Thread.currentThread());
        });
        threadPoolExecutor.execute(()->{
            read(Thread.currentThread());
        });
        threadPoolExecutor.shutdown();
    }

    // cyclicBarrier.await()解释
    // 在CyclicBarrier上等待的线程数量达到parties,则所有线程被释放,继续执行。
    // 当前线程被中断,则抛出InterruptedException异常,并停止等待,继续执行。
    // 其他等待的线程被中断,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
    // 其他等待的线程超时,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
    // 其他线程调用CyclicBarrier.reset()方法,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。

    private static void read(Thread thread) {
        try {
            cyclicBarrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
        reentrantReadWriteLock.readLock().lock();
        try {
            log.info("读线程 "+ thread.getName() + " 开始执行, i=" + i);
            Thread.sleep(1000);
            log.info(thread.getName() +"读取结束!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            reentrantReadWriteLock.readLock().unlock();

        }
    }
    private static void write(Thread thread) {
        try {
            cyclicBarrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
        reentrantReadWriteLock.writeLock().lock();
        try {
            i++;
            log.info("写线程 "+ thread.getName() + " is doing, i=" + i);
            log.info(thread.getName() +"写入结束!");
        } finally {
            reentrantReadWriteLock.writeLock().unlock();
        }
    }
}

在这里插入图片描述
线程1先获取到了读锁,因为读锁时可以共享的,所有线程3也可以获取到读锁,线程1、3读操作完成后将读锁释放后,线程2才能获取到写锁并开始执行写操作。

1.7 公平锁与非公平锁

公平锁:就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
非公平锁:比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
实现:
ReentrantLock:模式是非公平锁。也可通过构造方法创建公平锁。

package com.weige.javaskillpoint;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.locks.ReentrantLock;

@Slf4j
@SpringBootTest
class JavaSkillPointApplicationTests {

    public static void main(String[] args) {

        // 通过构造方法 传true则为公平锁
        ReentrantLock reentrantLock = new ReentrantLock(true);

          // 默认为非公平锁
//        public ReentrantLock() {
//            sync = new ReentrantLock.NonfairSync();
//        }

//        public ReentrantLock( boolean fair){
//            sync = fair ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync();
//        }
    }
}

ReentrantReadWriteLock:默认是非公平锁,也可以通过构造方法创建公平锁。

package com.weige.javaskillpoint;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.locks.ReentrantReadWriteLock;

@Slf4j
@SpringBootTest
class JavaSkillPointApplicationTests {

    public static void main(String[] args) {

        // 通过构造方法 传true则为公平锁
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);

          // 默认为非公平锁
//        public ReentrantReadWriteLock() {
//            this(false);
//        }

//         public ReentrantReadWriteLock(boolean fair) {
//            sync = fair ? new ReentrantReadWriteLock.FairSync() : new ReentrantReadWriteLock.NonfairSync();
//            readerLock = new ReentrantReadWriteLock.ReadLock(this);
//            writerLock = new ReentrantReadWriteLock.WriteLock(this);
//        }
    }
}

优缺点:非公平锁性能高于公平锁性能。首先,在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。而且,非公平锁能更充分的利用cpu的时间片,尽量的减少cpu空闲的状态时间。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/234913.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!