• 22-08-27 西安 JUC(01)线程安全synchronized、Lock锁、线程通信


    线程安全问题

    资源:服务器中的一个文件或者是某个接口

    并行:多个请求同一个时间点访问不同的资源
    并发:多个请求同一个时间点访问同一个资源(春运抢票、电商秒杀、限量抢购)

    Lock锁 [jdk1.5新特性]

    1、悲观锁与乐观锁

    悲观锁

    对于悲观锁来说,它总是认为每次访问共享资源时会发⽣冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同⼀时间只能有⼀个线程在执⾏。

    只要添加了悲观锁,并发的多个线程只能等持有锁的线程释放锁才可以获取到锁使用,同时只能有一个线程使用锁。

    悲观锁场景使用

    悲观锁多⽤于”写多读少“的环境,避免频繁失败和重试影响性能

    乐观锁

    乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执⾏,⽆需加锁也⽆需等待。⽽⼀旦多个线程发⽣冲突,乐观锁通常是使⽤⼀种称为CAS的技术来保证线程执⾏的安全性

    查询数据没有限制,但是查询数据时会一起查询它的版本号

    如果需要更新数据 更新时先检查版本号是否一致 如果一致更新数据 不一致更新失败

    乐观锁场景使用

    乐观锁多⽤于“读多写少“的环境,避免频繁加锁影响性能


    2、Lock接口

    Lock锁是代码级别的锁 需要我们手动创建对象,调用方法完成加锁释放锁 使用复杂 但是灵活

    lock()

    lock:用来获取锁,如果锁被其他线程获取,处于等待状态

    使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生

    --------------------------------------------------------------------------------------

    tryLock()

    1. tryLock方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被
    2. 其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

    --------------------------------------------------------------------------------------

    unlock()

    释放锁,一定要在finally块中释放


    3、实现类ReentrantLock

    Lock是一个接口,常用的实现类:可重入锁 ReentrantLock

    ReentrantLock特点:

    可重入的、可公平可不公平,可响应中断的,悲观的排他的锁,需要手动控制锁的添加、释放。使用灵活 代码层面的锁

    ReentrantLock使用场景(适合写多读少),悲观锁的特征:你读我加锁,你写我也加锁

    这些特点具体表现在:

    1.需要自己创建对象调用方法加锁 释放锁

    2.可重入性:持有锁的业务方法 可以调用其他的需要相同锁的方法

    3.公平性:所谓公平锁,也就是在锁上等待时间最长的线程将获得锁的使用权

    1. 默认 new ReentrantLock() 是非公平锁
    2. //默认底层初始化一个不公平的同步器
    3. new ReentrantLock() 和new ReentrantLock(false)一样
    4. //创建一个公平锁对象,处理任务时 并发的线程按照排序顺序来执行
    5. ReentrantLock lock = new ReentrantLock(true);

    4.可以响应中断[超时]::获取锁或者释放锁超过指定时间后 可以中断操作,一定程度上可以避免死锁

    1. //返回获取锁是否成功
    2. boolean b = lock.tryLock(2, TimeUnit.SECONDS);

    测试ReentrantLock是可重入锁

    这部分我自己都觉得写的多余了,但又怕读者不能理解可重入锁。。就写上吧,万一以后我自己忘了啥玩意是可重入锁

    1. class Ticket{
    2. private Integer number = 20;
    3. private ReentrantLock lock = new ReentrantLock();
    4. public void sale(){
    5. lock.lock();
    6. if (number <= 0) {
    7. System.out.println("票已售罄!");
    8. lock.unlock();
    9. return;
    10. }
    11. try {
    12. Thread.sleep(200);
    13. // 调用check方法测试锁的可重入性
    14. this.check();
    15. number--;
    16. System.out.println(Thread.currentThread().getName() + "买票成功,当前剩余:" + number);
    17. } catch (InterruptedException e) {
    18. e.printStackTrace();
    19. } finally {
    20. lock.unlock();
    21. }
    22. }
    23. /**
    24. * 为了测试可重入锁,添加检查余票方法
    25. */
    26. public void check(){
    27. lock.lock();
    28. System.out.println("网络支付安全...");
    29. lock.unlock();
    30. }
    31. }

    多线程测试类,我就不拿出来了,,,控制台运行结果,足以证明ReentrantLock是可重入锁

    测试ReentrantLock是公平锁

    公平就是谁排队时间最长谁先执行获取锁,上面也说了怎么去搞一个公平锁

    创建一个公平锁对象,处理任务时 并发的线程按照排序顺序来执行

    ReentrantLock lock = new ReentrantLock(true); 

    把上面测试可重入锁的代码稍微改一下,改动方式如下

    1. class Ticket {
    2. private Integer number = 20;
    3. private ReentrantLock lock = new ReentrantLock(true);
    4. public void sale() {
    5. lock.lock();
    6. try {
    7. if (number <= 0) {
    8. System.out.println("票已售罄!");
    9. // lock.unlock();
    10. return;
    11. }
    12. Thread.sleep(200);
    13. number--;
    14. System.out.println(Thread.currentThread().getName() + "买票成功,当前剩余:" + number);
    15. } catch (InterruptedException e) {
    16. e.printStackTrace();
    17. } finally {
    18. lock.unlock();
    19. }
    20. }
    21. }

    测试类,这里我就犯了一个毛病,,,,刚开始把For循环写外面了,难怪测试不出来 

    1. public class LockDemo {
    2. public static void main(String[] args) {
    3. Ticket ticket = new Ticket();
    4. new Thread(() -> {
    5. for (int i = 0; i < 20; i++) {
    6. try {
    7. ticket.sale();
    8. } catch (Exception e) {
    9. e.printStackTrace();
    10. }
    11. }
    12. }, "AA").start();
    13. new Thread(() -> {
    14. for (int i = 0; i < 20; i++) {
    15. try {
    16. ticket.sale();
    17. } catch (Exception e) {
    18. e.printStackTrace();
    19. }
    20. }
    21. }, "BB").start();
    22. }
    23. }

    控制台测试结果:

    测试ReentrantLock是可响应中断的

    tryLock方法来实现,可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果。

    true表示获取锁成功,false表示获取锁失败。我们可以将这种方法用来解决死锁问题。

    1. public void sale() {
    2. // lock.lock();
    3. try {
    4. boolean b = lock.tryLock(1000, TimeUnit.MILLISECONDS);//尝试获取锁
    5. if (!b) {
    6. System.out.println(Thread.currentThread().getName() + "获取锁失败");
    7. return;
    8. }
    9. if (number <= 0) {
    10. System.out.println("票已售罄!");
    11. lock.unlock();
    12. return;
    13. }
    14. Thread.sleep(2000);
    15. number--;
    16. System.out.println(Thread.currentThread().getName() + "买票成功,当前剩余:" + number);
    17. } catch (InterruptedException e) {
    18. e.printStackTrace();
    19. } finally {
    20. lock.unlock();
    21. }
    22. }

    这会报错,,,

    原因就在那个finally上了,都获取锁失败了吗,但是finally还要求你释放锁,我都没锁怎么释放锁。。。

    然后老师冷不丁的来了这么一句:刚开始觉得???,后来觉得还是能跟这个案例扯上关系的

    1. Lock获取锁成功,底层通过一个int类型的state属性代表锁获取的次数
    2. 0代表空闲 >0代表锁使用的次数 值不能为负数

    4、ReentrantReadWriteLock 可重入读写锁

    ReentrantReadWirteLock实现了ReadWirteLock接口,并未实现Lock接口

    1. public class ReentrantReadWriteLock
    2. implements ReadWriteLock, java.io.Serializable {
    3. }
    4. //ReadWriteLock接口只有这俩个方法
    5. public interface ReadWriteLock {
    6. Lock readLock();//获取读锁
    7. Lock writeLock();//获取写锁
    8. }

    引入问题:synchronized或者ReentrantLock。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。

    现实中有这样一种场景:在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了

    ReentrantReadWriteLock(读写锁)。读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞

    针对并发较大 同时有部分写操作 和大量的读操作,希望只有读的时候不使用悲观锁,使用共享锁一旦有写的操作 所有的未执行的读都会阻塞 等所有的写操作执行结束后读才可以获取锁继续执行

    常用的场景:并发的缓存读写管理

    2个线程读读不互斥,读写和写写都是互斥的,也就是说一个线程读的时候,另外一个线程不能写

    读写锁的注意点

    重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待

    重入时降级支持:即持有写锁的情况下去获取读锁

    演示不加读写锁时的问题

    缓存管理类MyCache,一个写缓存的方法 一个是读缓存的方法

    1. class MyCache{
    2. private volatile Map cache= new HashMap<>();
    3. public void put(String key, String value){
    4. try {
    5. System.out.println(Thread.currentThread().getName() + " 开始写入!");
    6. Thread.sleep(300);
    7. cache.put(key, value);
    8. System.out.println(Thread.currentThread().getName() + " 写入成功!");
    9. } catch (InterruptedException e) {
    10. e.printStackTrace();
    11. } finally {
    12. }
    13. }
    14. public void get(String key){
    15. try {
    16. System.out.println(Thread.currentThread().getName() + " 开始读出!");
    17. Thread.sleep(300);
    18. String value = cache.get(key);
    19. System.out.println(Thread.currentThread().getName() + " 读出成功!" + value);
    20. } catch (InterruptedException e) {
    21. e.printStackTrace();
    22. } finally {
    23. }
    24. }
    25. }
    26. public class ReentrantReadWriteLockDemo {
    27. public static void main(String[] args) {
    28. MyCache cache = new MyCache();
    29. for (int i = 1; i <= 5; i++) {
    30. String num = String.valueOf(i);
    31. // 开启5个写线程
    32. new Thread(()->{
    33. cache.put(num, num);
    34. }, num).start();
    35. }
    36. for (int i = 1; i <= 5; i++) {
    37. String num = String.valueOf(i);
    38. // 开启5个读线程
    39. new Thread(()->{
    40. cache.get(num);
    41. }, num).start();
    42. }
    43. }
    44. }

    控制台打印如下:

    解决:加读写锁 ReentrantReadWriteLock

    读的时候跟不加锁是一样的,那为什么加锁?

    为了写的时候阻塞所有的读

    1. class MyCache{
    2. private volatile Map cache= new HashMap<>();
    3. // 加入读写锁
    4. ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    5. public void put(String key, String value){
    6. // 加写锁
    7. rwl.writeLock().lock();
    8. try {
    9. System.out.println(Thread.currentThread().getName() + " 开始写入!");
    10. Thread.sleep(300);
    11. cache.put(key, value);
    12. System.out.println(Thread.currentThread().getName() + " 写入成功!");
    13. } catch (InterruptedException e) {
    14. e.printStackTrace();
    15. } finally {
    16. // 释放写锁
    17. rwl.writeLock().unlock();
    18. }
    19. }
    20. public void get(String key){
    21. // 加入读锁
    22. rwl.readLock().lock();
    23. try {
    24. System.out.println(Thread.currentThread().getName() + " 开始读出!");
    25. Thread.sleep(300);
    26. String value = cache.get(key);
    27. System.out.println(Thread.currentThread().getName() + " 读出成功!" + value);
    28. } catch (InterruptedException e) {
    29. e.printStackTrace();
    30. } finally {
    31. // 释放读锁
    32. rwl.readLock().unlock();
    33. }
    34. }
    35. }

    运行结构不再有null值


    5、Synchronized和ReentrantLock区别

    (1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。

    (2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

    (3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断。

    (4)synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。

    (5)性能上来说,在资源竞争不激烈的情形下,Lock性能稍微比synchronized差点(编译程序通常会尽可能的进行优化synchronized)。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。


    ThreadLocal 线程本地变量

    java.lang.ThreadLocal
    
    1. public class ThreadLocal {
    2. }

    ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本,让每个线程绑定自己的值从而避免了线程安全问题

    ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

    1. 多个线程如何才能不争抢
    2. 1.加入synchronized或者Lock控制资源的访问顺序
    3. 2.人手一份,大家各自安好,没必要抢夺

    1、ThreadLocal的5个方法

    ThreadLocal 类最常⽤的就是 set ⽅法和 get ⽅法。

    ThreadLocal变量初始化

    该变量是每个线程自带的。推荐使用withInitial()方法

    1. public static ThreadLocal withInitial(Supplier supplier) {
    2. return new SuppliedThreadLocal<>(supplier);
    3. }

    如果使用initialValue()

    1. protected T initialValue() {
    2. return null;
    3. }

    需要使用匿名内部类


    2、ThreadLocal编码规范

    ali编码规范中要求如下

    那么我们做一个演示

    使用完,不清理ThreadLocal的变量

    1. class MyData {
    2. //一个ThreadLocal类型的成员变量
    3. ThreadLocal threadLocalField = ThreadLocal.withInitial(() -> 0);
    4. public void add() {
    5. //每调用一次该方法,给线程专属变量+1
    6. threadLocalField.set(1 + threadLocalField.get());
    7. }
    8. }
    9. public class ThreadLocalDemo {
    10. public static void main(String[] args) {
    11. MyData myData = new MyData();
    12. ExecutorService threadpool = Executors.newFixedThreadPool(3);
    13. //来10个任务,让该线程池去处理
    14. for (int i = 1; i <= 10; i++) {
    15. threadpool.submit(()->{
    16. Integer beforeINT = myData.threadLocalField.get();
    17. myData.add();
    18. Integer afterINT = myData.threadLocalField.get();
    19. System.out.println("当前处理任务的线程是"+Thread.currentThread().getName()+"\t"+"beforeINT: "+beforeINT+"\t afterINT: "+afterINT);
    20. });
    21. }
    22. threadpool.shutdown();
    23. }
    24. }

    看的出来,线程1的ThreadLocal变量值一直在增加,这回造成业务逻辑问题和内存泄漏

    使用完,清理ThreadLocal的变量

    1. public static void main(String[] args) {
    2. MyData myData = new MyData();
    3. ExecutorService threadpool = Executors.newFixedThreadPool(3);
    4. //来10个任务,让该线程池去处理
    5. for (int i = 1; i <= 10; i++) {
    6. threadpool.submit(() -> {
    7. try {
    8. Integer beforeINT = myData.threadLocalField.get();
    9. myData.add();
    10. Integer afterINT = myData.threadLocalField.get();
    11. System.out.println("当前处理任务的线程是" + Thread.currentThread().getName() + "\t" + "beforeINT: " + beforeINT + "\t afterINT: " + afterINT);
    12. } finally {
    13. //每个线程处理完任务之后,都要回收ThreadLocal变量,避免造成内存泄漏
    14. myData.threadLocalField.remove();
    15. }
    16. });
    17. }
    18. threadpool.shutdown();
    19. }

    ThreadLocal变量的值不再累加,符合效果 

    ====

    建议ThreadLocal对象使用static修饰

    ThreadLocali能实现了线程的数据隔离,不在于它自己本身,而在于Thread的ThreadLocalMap。所以,ThreadLocali可以只初始化一次,只分配一块存储空间就足以了,没必要作为成员变量多次被初始化。

    阿里手册


    3、ThreadLocalMap

    为什么TheadLocal线程安全?

    Thread类中有一个ThreadLocalMap类型的变量,每个线程人手一份,不和别人共用,该map

    只被持有它的线程访问,故不存在线程安全以及锁的问题

    ThreadLocalMapThreadLocal的静态内部类

    ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题

    以当前ThreadLocal对象作为键,给定的数据作为值设置到当前线程对象的ThreadLocalMap


    4、set、get、remove方法

     set方法

    1. public void set(T value) {
    2. Thread t = Thread.currentThread();//获取当前线程对象
    3. ThreadLocalMap map = getMap(t);//得到线程的Map,也就是这个属性threadLocals
    4. if (map != null)
    5. map.set(this, value);//将当前的threadLocal对象作为键 传入的参数作为值存入
    6. else
    7. createMap(t, value);
    8. }
    9. void createMap(Thread t, T firstValue) {
    10. t.threadLocals = new ThreadLocalMap(this, firstValue);
    11. }

    =========  后续通过ThreadLocal获取属性值

    get方法 

    1. public T get() {
    2. Thread t = Thread.currentThread();
    3. ThreadLocalMap map = getMap(t);
    4. if (map != null) {
    5. ThreadLocalMap.Entry e = map.getEntry(this);
    6. if (e != null) {
    7. @SuppressWarnings("unchecked")
    8. T result = (T)e.value;
    9. return result;
    10. }
    11. }
    12. return setInitialValue();
    13. }

    ====================  移除数据

     remove方法

    1. public void remove() {
    2. ThreadLocalMap m = getMap(Thread.currentThread());
    3. if (m != null)
    4. m.remove(this);
    5. }

    5、ThreadLocalMap与WeakReference

    内存泄漏:不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露

    ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题

    1. 只被弱引用关联的对象只能生存到下一次垃圾收集发生为止;在系统GC时,只要发现弱引用,
    2. 无论内存是否足够都会回收掉只被弱引用关联的对象。
    1. static class ThreadLocalMap {
    2. static class Entry extends WeakReference> {
    3. /** The value associated with this ThreadLocal. */
    4. Object value;
    5. Entry(ThreadLocal k, Object v) {
    6. super(k);
    7. value = v;
    8. }
    9. }


    线程通信

    线程间通信是通过 线程竞争关系+ 线程等待释放锁+唤醒等待 来完成线程的通信

    synchornized和Lock加锁的方式 锁定资源都不同 所以不能通用

    synchronized线程间通信

    • synchronized 保证多个线程访问的资源有竞争关系
    • wait/noityAll+ 业务中的判断 保证满足条件的线程才会执行

    多线程模板

    1.线程操作资源类

    2.线程间的协作(通信)

             判断、干活、通知(唤醒)


    1、线程通信简单案例

    两个线程操作一个初始值为0的变量,实现一个线程对变量增加1,一个线程对变量减少1,交替10轮

    这个案例之所以能成功,原因在于AA线程和BB线程都在争抢同一个对象shareDataOne ,存在竞争关系。

    AA获取到后,就去给number+1,假设此时number=0,AA会把number+1,因为synchronized是非公平锁,所以AA可能又抢到了,此时number=1,AA就在this.wait那等着,并且释放锁,BB拿到锁之后对number-1,并且唤醒所有在对象shareDataOne等待的线程,AA就醒了,继续往下走。。。把number再+1.。。。

    1. public class ShareDataOne {
    2. private Integer number = 0;
    3. /**
    4. * 增加1
    5. */
    6. public synchronized void increment() throws InterruptedException {
    7. //判断
    8. if(number!=0){
    9. //让该线程等着,唤醒后从wait下一行开始执行,所以wait下一行是没有return的
    10. this.wait();
    11. }
    12. //干活
    13. number++;
    14. System.out.println(Thread.currentThread().getName() + ": " + number);
    15. //通知:唤醒此对象上等待的其他线程
    16. this.notifyAll();
    17. }
    18. /**
    19. * 减少1
    20. */
    21. public synchronized void decrement() throws InterruptedException {
    22. //判断
    23. if(number!=1){
    24. //让该线程等着,唤醒后从wait下一行开始执行,所以wait下一行是没有return的
    25. this.wait();
    26. }
    27. //干活
    28. number--;
    29. System.out.println(Thread.currentThread().getName() + ": " + number);
    30. //通知:唤醒此对象上等待的其他线程
    31. this.notifyAll();
    32. }
    33. }

    测试类如下:

    1. public class NotifyWaitDemo {
    2. public static void main(String[] args) {
    3. ShareDataOne shareDataOne = new ShareDataOne();
    4. new Thread(()->{
    5. for (int i = 0; i < 10; i++) {
    6. try {
    7. shareDataOne.increment();
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }
    12. }, "AA").start();
    13. new Thread(()->{
    14. for (int i = 0; i < 10; i++) {
    15. try {
    16. shareDataOne.decrement();
    17. } catch (InterruptedException e) {
    18. e.printStackTrace();
    19. }
    20. }
    21. }, "BB").start();
    22. }
    23. }

    测试结果也是对的,交替打印

    2、虚假唤醒问题案例

    资源类不改动,我们改变测试类代码、

    再加CC和DD线程

    1. public class NotifyWaitDemo {
    2. public static void main(String[] args) {
    3. ShareDataOne shareDataOne = new ShareDataOne();
    4. new Thread(()->{
    5. for (int i = 0; i < 10; i++) {
    6. try {
    7. shareDataOne.increment();
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }
    12. }, "AA").start();
    13. new Thread(()->{
    14. for (int i = 0; i < 10; i++) {
    15. try {
    16. shareDataOne.decrement();
    17. } catch (InterruptedException e) {
    18. e.printStackTrace();
    19. }
    20. }
    21. }, "BB").start();
    22. new Thread(()->{
    23. for (int i = 0; i < 10; i++) {
    24. try {
    25. shareDataOne.increment();
    26. } catch (InterruptedException e) {
    27. e.printStackTrace();
    28. }
    29. }
    30. }, "CC").start();
    31. new Thread(()->{
    32. for (int i = 0; i < 10; i++) {
    33. try {
    34. shareDataOne.decrement();
    35. } catch (InterruptedException e) {
    36. e.printStackTrace();
    37. }
    38. }
    39. }, "DD").start();
    40. }
    41. }

    打印结果,依然会有概率是,10101010...。

    但是,多执行几次,也会出现错乱的现象,也就是虚假唤醒问题

    分析为什么会出现虚假唤醒问题

    消费者被唤醒后是从wait()方法(被阻塞的地方)后面执行,而不是重新从同步块开头。

    解决线程唤醒问题很简单

    判断时,不能用if,改用while

    if只判断一次,while是只要唤醒就要拉回来再判断一次。

    1. //判断
    2. while(number!=0){
    3. //让该线程等着,唤醒后从wait下一行开始执行,所以wait下一行是没有return的
    4. this.wait();
    5. }
    6. //判断
    7. while(number!=1){
    8. //让该线程等着,唤醒后从wait下一行开始执行,所以wait下一行是没有return的
    9. this.wait();
    10. }

    测试,控制台打印结果,就会交替打印1和0了


    ReentrantLock线程通信

    ReentrantLock: 它是通过同一个锁对象的state属性值 判断锁是否被使用,

    多个线程如果希望通过Lock来加锁保证竞争关系,他们需要使用同一个lock对象,哪一个能够将lock对象的state属性值从0改为1 哪一个就可以获取锁成功

    注:只能有一个线程修改state成功,会把等待的线程放在队列(AQS)

    此时线程间的通信需要使用的方法:跟上面那一套(synchronized)不一样

    线程间通信需要使用Lock的newCondition创建的Condition对象的:

    await()、signal()/signalAll() 控制线程间的通信

    Condition对象每个线程拥有自己单独的一个,才可以精确控制当前线程释放锁等待 或者被唤醒

    1. public class ShareDataOne {
    2. private Integer number = 0;
    3. Lock lock = new ReentrantLock(); // 初始化lock锁
    4. //必须使用正在加锁的锁对象创建Condition对象
    5. Condition c_add = lock.newCondition();
    6. Condition c_sub = lock.newCondition();
    7. /**
    8. * 打印0
    9. */
    10. public void print0() {
    11. lock.lock(); // 加锁
    12. try {
    13. // 1. 判断
    14. while (number != 0) {
    15. // c_add让当前线程等待
    16. c_add.await();
    17. }
    18. // 2. 干活
    19. System.out.print(number++ + "\t");
    20. // 3. 通知 唤醒的是使用c_sub等待的线程
    21. //condition对象 每个线程需要自己单独的一个,才可以精确控制当前线程释放锁等待 或者被唤醒
    22. c_sub.signal();
    23. } catch (InterruptedException e) {
    24. e.printStackTrace();
    25. } finally {
    26. lock.unlock();
    27. }
    28. }
    29. /**
    30. * 打印1
    31. */
    32. public void print1() {
    33. lock.lock();
    34. try {
    35. // 1. 判断
    36. while (number != 1) {
    37. c_sub.await();
    38. }
    39. // 2. 干活
    40. System.out.print(number-- + "\t");
    41. // 3. 通知
    42. //condition对象 每个线程需要自己单独的一个,才可以精确控制当前线程释放锁等待 或者被唤醒
    43. c_add.signalAll();
    44. } catch (InterruptedException e) {
    45. e.printStackTrace();
    46. } finally {
    47. lock.unlock();
    48. }
    49. }
    50. }

    测试类:

    1. public class NotifyWaitDemo {
    2. public static void main(String[] args) {
    3. ShareDataOne shareDataOne = new ShareDataOne();
    4. new Thread(()->{
    5. for (int i = 0; i < 10; i++) {
    6. shareDataOne.print0();
    7. }
    8. },"AA").start();
    9. new Thread(()->{
    10. for (int i = 0; i < 10; i++) {
    11. shareDataOne.print1();
    12. }
    13. },"BB").start();
    14. }
    15. }

    控制台打印效果 


    LockSupport

    LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

    1、LockSupport工具类

    LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。

    8个方法


    2、park()和unpark()

    LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程

    LockSupport和每个使用它的线程都有一个许可(permit)关联。每个线程都有一个相关的permit且permit最多只有一个,重复调用unpark也不会积累凭证

    ==========

    park()

    permit许可证默认设有,所以一开始调park()方法当前线程就会阻塞,直到别的线程给当前线程的发放permit,park方法才会被唤醒。

    调用了UnSafe类

    1. public static void park() {
    2. UNSAFE.park(false, 0L);
    3. }

    ==========

    unpark()

    调用unpark(thread)方法后,就会将thread线程的许可证permit发放,会自动唤醒park线程,即之前阻塞中的LockSupport.park()方法会立即返回。

    调用了UnSafe类

    1. public static void unpark(Thread thread) {
    2. if (thread != null)
    3. UNSAFE.unpark(thread);
    4. }

    =======

    为什么可以突破wait/notify的原有调用顺序?

    因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

    为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

    因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。


    3、LockSupport实现线程间通信

    实现线程间等待唤醒的3种方式

    方式1:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程

    方式2:使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程

    方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

    不管是方式1还是方式2都有如下2个限制

    • 线程先要获得并持有锁,等待唤醒方法必须在锁块(synchronized或Iock)中
    • 必须要先等待后唤醒,线程才能够被唤醒
    1. public class LockSupportDemo {
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread t1 = new Thread(()->{
    4. System.out.println(Thread.currentThread().getName()+"\t----come in");
    5. LockSupport.park();
    6. System.out.println(Thread.currentThread().getName()+"\t----被唤醒");
    7. },"t1");
    8. t1.start();
    9. //休眠1s
    10. TimeUnit.SECONDS.sleep(1);
    11. new Thread(()->{
    12. LockSupport.unpark(t1);
    13. System.out.println(Thread.currentThread().getName()+"\t----发出通知");
    14. },"t2").start();
    15. }
    16. }


    死锁问题排查

    1、死锁产生的原因

    死锁:两个或者两个以上线程在执行过程中,因为争夺资源而造成一种互相等待的观象,如果没有外力干涉,他们无法再执行下去

     A持有A锁,B持有B锁,都试图获取对方的锁

    造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。

    1.一个资源每次只能被一个线程使用
    2.一个线程在阻塞等待某个资源时,不释放已占有资源
    3.一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
    4.若干线程形成头尾相接的循环等待资源关系

    而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。


    2、死锁的几种情况

    1、使用synchronized

    两个线程在持有锁的情况下去获取对方的锁(业务中没有合理的使用锁、释放锁)

    1. /*
    2. 死锁:
    3. A线程和B线程 互相持有锁
    4. 各自在加锁的代码中有需要获取对方的锁导致死锁
    5. */
    6. public class JucDemo04DeadLock {
    7. Object obj1 = new Object();
    8. Object obj2 = new Object();
    9. public static void main(String[] args) {
    10. JucDemo04DeadLock deadLock = new JucDemo04DeadLock();
    11. new Thread(()->{
    12. deadLock.a();
    13. },"AA").start();
    14. new Thread(()->{
    15. deadLock.b();
    16. },"BB").start();
    17. }
    18. public void a(){
    19. synchronized (obj1){
    20. System.out.println(Thread.currentThread().getName()+"获取到obj1的锁 正在执行");
    21. try {
    22. Thread.sleep(1000);
    23. } catch (InterruptedException e) {
    24. e.printStackTrace();
    25. }
    26. synchronized (obj2){
    27. System.out.println(Thread.currentThread().getName()+"获取到obj2的锁 正在执行");
    28. }
    29. }
    30. }
    31. public void b(){
    32. synchronized (obj2){
    33. System.out.println(Thread.currentThread().getName()+"获取到obj2的锁 正在执行");
    34. try {
    35. Thread.sleep(1500);
    36. } catch (InterruptedException e) {
    37. e.printStackTrace();
    38. }
    39. synchronized (obj1){
    40. System.out.println(Thread.currentThread().getName()+"获取到obj1的锁 正在执行");
    41. }
    42. }
    43. }
    44. }

    控制台打印效果如下:停不下来了。。。直是红色方块 

    2、使用reentrantLock时:

    为了防止虚假唤醒使用了while死循环判断某个flag变量的状态值

        - 如果一个线程执行完任务后没有修改flag变量的值 会导致死循环
        - 如果忘了通过signal或者使用错了condition对象signal都会导致死锁
        - 如果lock忘记unlock也会导致死锁


    3、如何查看死锁

    1. 可以通过jstack命令来进行查看,jstack/jvisualvm命令中会显示发生了死锁的线程
    2. 或者两个线程去操作数据库时,数据库发生了死锁,这是可以查询数据库的死锁情况

    判断是否出现死锁

    如果你打不开terminal窗口,就试着改一下这个。。深受其害

    jps : 查看所有的java进程   

    jps -l   //定位进程号

     注意是:小L

    jstack:查看某个java进程的线程堆栈信息+线程的状态

    jstack 进程号  //找到死锁的问题

    可是可是在实际开发报错是这样的。。。程序就是卡住在那一块了,用jstack也是照样卡住。。。

    线程的状态

    • NEW 未启动的。

    • RUNNABLE 运行中。

    • BLOCKED 受阻塞并等待监视器锁。

    • WATING 等待另一个线程执行特定操作。

    • TIMED_WATING 有时限的等待另一个线程的特定操作。

    • TERMINATED 已退出的。


    4、死锁的解决方案

    如果是synchronized

    jps+jstack 可以查看到死锁的线程 排查代码问题

    如果是reentrantLock

    需要我们自己判断长时间等待的线程,到代码中找业务问题


    5、开发中如何避免死锁

    1.要注意加锁顺序,保证每个线程按同样的顺序进行加锁
    2.要注意加锁时限,可以针对锁设置一个超时时间
    3.要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决

    ===============

    使用synchronized时:

    因为它不可响应中断,一定不要写复杂的嵌套锁的获取

    使用ReentrantLock

    每次加锁必须伴随一次释放锁,释放锁建议写在finally中,还可以使用tryLock尝试获取锁,可以响应中断,获取失败超时

  • 相关阅读:
    内部类~锁~访问修饰符
    oralce迁移到达梦:初始化参数怎么确定?
    动态内存申请
    将dumpbin从VS中抠出来,并使用dumpbin查看exe和dll库的依赖关系
    HTTPS 加密全过程
    2D槽道流
    LeetCode_栈_困难_227.基本计算器(不含乘除)
    metersphere 接口自动化
    夜神安卓7导入charles证书
    数据结构实验之队列(文末附完整代码)
  • 原文地址:https://blog.csdn.net/m0_56799642/article/details/126558326