• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
xiaoyaovo
博客园    首页    新随笔    联系   管理    订阅  订阅
JUC(Java.util.concurrent)常见组件

这里写目录标题

    • ReentrantLock
    • semaphore
    • CountDownLatch
    • 原子类
    • 线程安全的集合

ReentrantLock

ReentrantLock 和 synchronized 一样,也是一个可重入锁。

  • ReentrantLock 使用方法
import java.util.concurrent.locks.ReentrantLock;

/**
两个线程操作一个数
*/
public class MyReentrantLock {

    static int count;
    static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                reentrantLock.lock();
                for (int i = 0; i < 100000; i++) {
                    count++;
                }
                reentrantLock.unlock();
            }
        };
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                reentrantLock.lock();
                for (int i = 0; i < 100000; i++) {
                    count++;
                }
                reentrantLock.unlock();
            }
        };
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count);
    }

}

  • 区别(参考 ReenTrantLock 与synchronized 区别)
  1. ReenTrantLock 是 JDK(java 源码包) 实现的,而 synchronized 是关键字,通过 JVM 底层源码(c++) 实现;
  2. ReenTrantLock 自己实现加锁(lock)和解锁(unlock),而 synchronized 是全套服务,加锁解锁自己实现;
  3. ReenTrantLock 中有 tryLock ,当线程阻塞后,可以手动实现等待时间,或者直接返回(tryLock() 方法中可以设置等待时间,或者不设置,因此可以避免进入内核态的阻塞状态,线程进入内核态就是不可控的,往往进入内核态意味着效率更低。所以设计锁就是避免线程进入内核态)(为什么有 synchronized 还要有 ReenTrantLock 的原因);

semaphore

semaphore(信号量),简单点来理解,信号量就是一个计数器,表示可用资源的个数。

怎么来理解这个 计数器 呢?
类似于停车场入口显示的车位剩余量,当一辆车离开停车场,那么车位剩余量+1,如果一辆车进入停车场,那么车位剩余量-1,如果车位剩余量为 0 ,那么车不能挺进去。
semaphore 也是这样,涉及到两个操作
P :申请一个可用资源(可用资源-1,也就是剩余车位数量-1)
V :释放一个可用资源(可用资源+1,也就是剩余车位数量+1)
如果信号量为 0 ,也就是可用资源个数是 0,那么就会阻塞,直到其他线程释放资源。
注意:而 信号量 + 1 或者 - 1 操作都是 原子性 的。

基于 信号量 的理解,因此可以设计一个锁

  1. 信号量只有 0 和 1;
  2. 如果一个线程进行 P 操作,那么信号量 -1,此时如果其他线程想使用此资源,需要阻塞等待;
  3. 如果线程释放了资源,那么信号量 +1,此时阻塞线程可以申请资源。

也可以实现一个 阻塞队列
需要三个 semaphore,一个代表锁,一个代表资源的个数,一个代表空位的个数;

CountDownLatch

类似于一个计数器,但是有不同,举个例子。

比如下载一个游戏,需要多个线程下载。那么线程 1 下载好了当前的文件,而其他线程还没下载好,因此并不算完全下载成功,等待所有线程都下载成功后,才算真的下载完成。

原子类

在这里插入图片描述
原子类能够保证操作数据时 CPU 的指令是原子的。

  • 示例
import java.util.concurrent.atomic.AtomicInteger;

public class MyAtomic {
    static AtomicInteger atomicInteger = new AtomicInteger();
    static int num = 0;

    /**
     * 执行 5 次自增对比结果
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 5; i++) {
            doIncrement();
            System.out.println("atomicInteger"+i + "= " + atomicInteger);
            System.out.println("num"+i + "= " + num);
            System.out.println("--------分割线---------");
            atomicInteger = new AtomicInteger();
            num = 0;
        }
    }

    /**
     * 创建两个线程对一个数进行修改
     * @throws InterruptedException
     */
    public static void doIncrement() throws InterruptedException {
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    atomicInteger.incrementAndGet();
                    num++;
                }
            }
        };
        thread1.start();
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    atomicInteger.incrementAndGet();
                    num++;
                }
            }
        };
        thread2.start();
        thread1.join();
        thread2.join();
    }
}


  • 结果
atomicInteger1= 100001
num1= 63105
--------分割线---------
atomicInteger2= 100001
num2= 91873
--------分割线---------
atomicInteger3= 100001
num3= 91147
--------分割线---------
atomicInteger4= 100001
num4= 92904
--------分割线---------
atomicInteger5= 100001
num5= 88765

由结果得知,原子类 AtomicInteger 是线程安全的

线程安全的集合

平时刷题用的一些集合都是线程不安全的,例如

  1. ArrayList
  2. LinkedList
  3. HashSet
  4. HashMap

当然也有安全的集合,例如
5. Stack
6. Vector
7. HashTable(目前没用到过)

针对线程不安全的集合,在 Java.util.concurrent 这个包中包含了许多线程安全的集合(标记了部分)
在这里插入图片描述
当然,也可以自己实现线程安全的集合,那就是通过加 synchronized 关键字。而 HashTable 就是使用了这个方法,保证线程安全的。

而为什么有了 HashTable 还要有 concurrentHashMap ?(HashTable 和 concurrentHashMap 的区别)
直接加 synchronized 关键字必然会导致效率很低,而 concurrentHashMap 是通过什么方式保证线程安全呢?

  1. 每个链表/红黑树加锁的方式来保证线程安全(这种分法是 jdk1.8之后的分法,而之前是几个链表/红黑树加一个锁),也就是说,如果线程之间修改的不是同一个数据,那么就不会导致线程不安全。
  2. 针对读操作,不加锁,虽然可能会导致一个线程正在修改数据的时候,另外一个线程刚好读这个数据,导致线程不安全。当然可以用读写锁来进行优化。
  3. 内部广泛使用 CAS 操作来提高效率,例如获取元素个数的时候,没加锁,直接 CAS;
  4. 针对扩容进行了优化,假如某一个线程触发了扩容,那么当其他线程进行操作的时候,就会一起进行扩容。而 HashTable 则是触发扩容的线程单独进行扩容。
posted on 2021-08-21 21:46  豆本豆红枣豆奶  阅读(10)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3