Java多线程学习入门(二):Lock接口,线程间通信、线程不安全的容器

课程链接:JUC并发编程
JavaGuide
开始时间:2022-09-09

Lock接口

看看JavaGuide的描述

两者都是可重入锁

“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。

synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,==需要 lock() 和 unlock() ==方法配合 try/finally 语句块来完成)如果遇到异常,不放到finally里面,是不会释放锁的,所以我们可以通过查看它的源代码,来看它是如何实现的。

ReentrantLock 比 synchronized 增加了一些高级功能

相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:

等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。

可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。

可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。

Condition是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。

在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而synchronized关键字就相当于整个 Lock 对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。想通知谁就通知谁,notify只能通知一个,notifyAll又是全部唤醒,不能定制

如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。性能已不是选择标准

大量进程竞争时,Synchronized效率低很多
我们来用一下

package com.bupt.lock;

import java.util.concurrent.locks.ReentrantLock;

public class LSaleTicket {
    public static void main(String[] args) {
        LTicket ticket = new LTicket();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                try {
                    ticket.sale();
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                try {
                    Thread.sleep(500);
                    ticket.sale();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                try {
                    Thread.sleep(500);
                    ticket.sale();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "CC").start();
    }
}
class LTicket {
    //票数量
    private int number = 30;
    //创建可重人锁
    private final ReentrantLock lock = new ReentrantLock();//卖票方法

    public void sale() {
        //上锁
        lock.lock();//郸断是否有票
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + ":卖出1个" + "剩余:" + (--number));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //解锁
            lock.unlock();
        }
    }
}

稳稳当当

AA:卖出1个剩余:29
BB:卖出1个剩余:28
AA:卖出1个剩余:27
CC:卖出1个剩余:26
AA:卖出1个剩余:25
BB:卖出1个剩余:24
CC:卖出1个剩余:23
BB:卖出1个剩余:22
AA:卖出1个剩余:21
CC:卖出1个剩余:20
BB:卖出1个剩余:19
AA:卖出1个剩余:18
CC:卖出1个剩余:17
AA:卖出1个剩余:16
BB:卖出1个剩余:15
CC:卖出1个剩余:14
BB:卖出1个剩余:13
AA:卖出1个剩余:12
CC:卖出1个剩余:11
BB:卖出1个剩余:10
AA:卖出1个剩余:9
CC:卖出1个剩余:8
BB:卖出1个剩余:7
AA:卖出1个剩余:6
CC:卖出1个剩余:5
BB:卖出1个剩余:4
AA:卖出1个剩余:3
CC:卖出1个剩余:2
BB:卖出1个剩余:1
AA:卖出1个剩余:0

Process finished with exit code 0

注意锁代码块的方式不一样了

线程间通信

我们开两个线程,一个+1一个-1
让number值始终保持1 0两种状态

package com.bupt.syn;

public class ThreadDemo01 {
    public static void main(String[] args) {
        Share share = new Share();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(200);
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
    }
}

注意看,我们下面使用了synchronized修饰方法
并使用了wait和notify配套

class Share {
    private int number = 0;

    public synchronized void incr() throws InterruptedException {
        if (number != 0) {
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "::" + number);
        this.notifyAll();
    }

    public synchronized void decr() throws InterruptedException {
        if (number == 0) {
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "::" + number);
        this.notifyAll();
    }
}

这里我们可以看出sleep和wait的区别
上JavaGuide面经

两者最主要的区别在于:sleep() 方法没有释放锁,而 wait() 方法释放了锁 。 两者都可以暂停线程的执行。 wait()通常被用于线程间交互/通信,sleep() 通常被用于暂停执行。 wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep()> 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒

虚假唤醒

我们刚才是两个线程,一个增一个减
那我有四个线程,两个用来incr,两个用来decr会发生什么情况呢

new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "CC").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(200);
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "DD").start();

增加一个打印方法

public synchronized void incr() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "进入incr方法");
        if (number != 0) {
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "::" + number);
        this.notifyAll();
    }

    public synchronized void decr() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "进入decr方法");
        if (number == 0) {
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "::" + number);
        this.notifyAll();
    }
AA进入incr方法
AA::1
AA进入incr方法
CC进入incr方法
DD进入decr方法
DD::0
BB进入decr方法
CC::1
AA::2
BB::1
DD进入decr方法
DD::0

Process finished with exit code 0

我从中取了一段有问题的输出
我们可以看到,A第一次抢到,number变为1
A又抢到了,但得进入wait状态
C抢到,因为number值不符合要求
所以让个D
D给他减了后,notifyAll了
此时B进来了,进来还是得卡着
C拿到时间片,可以加1了,因为if完了就继续往下了
A拿到时间片,可以加1了,因为if完了就继续往下了,因此number为2
如果我们三个线程为incr
会发现更明显

CC进入incr方法
AA::8
EE::9
CC::10
EE进入incr方法
AA进入incr方法
CC进入incr方法
BB进入decr方法
BB::9
CC::10
AA::11
EE::12
DD进入decr方法
DD::11
BB进入decr方法
BB::10

Process finished with exit code 0

怎么解决呢,我们刚刚用的if,改为while就好,让他不能只判断1次就走
就ok啦!

使用Lock完成上述功能

package com.bupt.lock;

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

public class ThreadDemo02 {
    public static void main(String[] args) {
        Share share = new Share();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.incr();
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "CC").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(200);
                    share.decr();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    share.decr();
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "DD").start();
    }
}
class Share {
    private int number = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void incr() {
        System.out.println(Thread.currentThread().getName() + "进入incr方法");
        lock.lock();
        try {
            while (number != 0) {
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decr() {
        System.out.println(Thread.currentThread().getName() + "进入decr方法");
        lock.lock();
        try {
            while (number != 1) {
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}


AA进入incr方法
CC进入incr方法
AA::1
DD进入decr方法
DD::0
CC::1
CC进入incr方法
BB进入decr方法
AA进入incr方法
DD进入decr方法
BB::0
AA::1
DD::0
CC::1
CC进入incr方法
AA进入incr方法
BB进入decr方法
DD进入decr方法
BB::0
CC::1
DD::0
AA::1
DD进入decr方法
BB进入decr方法
CC进入incr方法
AA进入incr方法
DD::0
CC::1
BB::0
AA::1
DD进入decr方法
AA进入incr方法
BB进入decr方法
CC进入incr方法
DD::0
AA::1
BB::0
CC::1
CC进入incr方法
BB进入decr方法
BB::0
AA进入incr方法
DD进入decr方法
CC::1
DD::0
AA::1
CC进入incr方法
AA进入incr方法
BB进入decr方法
DD进入decr方法
BB::0
CC::1
DD::0
AA::1
BB进入decr方法
DD进入decr方法
AA进入incr方法
CC进入incr方法
BB::0
AA::1
DD::0
CC::1
DD进入decr方法
BB进入decr方法
CC进入incr方法
AA进入incr方法
DD::0
CC::1
BB::0
AA::1
AA进入incr方法
BB进入decr方法
CC进入incr方法
DD进入decr方法
BB::0
CC::1
DD::0
AA::1
BB进入decr方法
BB::0

线程间定制化通信

在这里插入图片描述
我们简化一下,AA打印2次,BB3次,CC4次
一共循环5轮

package com.bupt.lock;

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

public class ThreadDemo03 {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();
        new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                try {
                    shareResource.print2(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();
        new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                try {
                    shareResource.print3(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
        new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                try {
                    shareResource.print4(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "CC").start();
    }
}
class ShareResource {
    private int flag = 1;
    private Lock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();

    public void print2(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 1) {
                c1.await();
            }
            for (int i = 1; i <= 2; i++) {
                System.out.println(Thread.currentThread().getName() + "::" + i + "轮数" + loop);
            }
            flag = 2;
            c2.signal();
        } finally {
            lock.unlock();
        }
    }

    public void print3(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 2) {
                c2.await();
            }
            for (int i = 1; i <= 3; i++) {
                System.out.println(Thread.currentThread().getName() + "::" + i + "轮数" + loop);
            }
            flag = 3;
            c3.signal();
        } finally {
            lock.unlock();
        }
    }

    public void print4(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 3) {
                c3.await();
            }
            for (int i = 1; i <= 4; i++) {
                System.out.println(Thread.currentThread().getName() + "::" + i + "轮数" + loop);
            }
            flag = 1;
            c1.signal();
        } finally {
            lock.unlock();
        }
    }
}

使用condition和signal的组合实现选择性通知

演示ArrayList线程不安全

package com.bupt.lock;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class ThreadDemo4 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

我们每次添加每次打印,当我循环次数高了之后,会抛异常

[null, b05d1fb7, ffe95624, 58575748, c8d72808, 79149fe8, d9048626, 69763908, 49d45b8e, 5ed5dff0, 80ddd97f, ea7c43c3]
[null, b05d1fb7, ffe95624, 58575748, c8d72808, 79149fe8, d9048626, 69763908, 49d45b8e, 5ed5dff0, 80ddd97f]
[null, b05d1fb7, ffe95624, 58575748, c8d72808, 79149fe8, d9048626, 69763908, 49d45b8e, 5ed5dff0]
[null, b05d1fb7, ffe95624, 58575748, c8d72808, 79149fe8, d9048626]
[null, b05d1fb7, ffe95624, 58575748, c8d72808]
[null, b05d1fb7, ffe95624, 58575748, c8d72808, 79149fe8, d9048626, 69763908, 49d45b8e]
[null, b05d1fb7, ffe95624, 58575748, c8d72808, 79149fe8]
[null, b05d1fb7, ffe95624, 58575748, c8d72808, 79149fe8, d9048626, 69763908]
[null, b05d1fb7, ffe95624, 58575748, c8d72808]
[null, b05d1fb7, ffe95624, 58575748, c8d72808, 79149fe8, d9048626, 69763908, 49d45b8e, 5ed5dff0, 80ddd97f, ea7c43c3, c74468dc, d0bba8d3, 3b6c15c8]
[null, b05d1fb7, ffe95624, 58575748, c8d72808, 79149fe8, d9048626, 69763908, 49d45b8e, 5ed5dff0, 80ddd97f, ea7c43c3, c74468dc, d0bba8d3]
Exception in thread "2" Exception in thread "7" Exception in thread "0" Exception in thread "1" java.util.ConcurrentModificationException

有可能来不及扩容,就继续add导致数组越界

  • 解决方案一
List<String> list = new Vector<>();

add上加了synchronized
但是这种古老实现类已经不怎么用了,不过确实是线程安全的

  • 解决方案二
List<String> list = Collections.synchronizedList(new ArrayList<>());
  • 解决方案三
List<String> list = new CopyOnWriteArrayList<>();

在这里插入图片描述
看看源码
做了一次复制

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

验证HashSet和HashMap

果然
喜提线程不安全

Set<String> set = new HashSet<>();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
thread "10" Exception in thread "7" Exception in thread "0" java.util.ConcurrentModificationException

容易出现数据覆盖的问题

假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

解决方案

Set<String> set = new CopyOnWriteArraySet<>(new HashSet<>());
  • CopyOnWrite(简称COW,中文意思是:写入时复制)就是在进行写操作时,先复制要改变的对象,对副本进行写操作,完成对副本的操作后,把原有对象的引用指向副本对象。
  • CopyOnWrite采用了读写分离的思想解决了线程安全且支持读多写少等问题
  • CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
  • 内存占用问题,产生了两个容器

测试一下HashMap的安全性问题

Map<String, String> map = new HashMap<>();
        for (int i = 0; i < 130; i++) {
            String key = String.valueOf(i);
            new Thread(() -> {
                map.put(key, UUID.randomUUID().toString().substring(0, 8));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }

喜提异常
解决方案

Map<String, String> map = new ConcurrentHashMap<>();

原理分析

Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。

synchronized实现同步的基础:Java中的每一个对象都可以作为锁具体表现为以下3种形式。

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的class对象。
  • 对于同步方法块,锁是synchonized括号里配置的对象

结束时间:2022-09-10

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值