JUC部分常见组件

目录

Callable接口的用法:

ReentrantLock可重入锁: 

 信号量Semaphore:

CountDownLatch:

 多线程使用ArrayList:

1.最直接的方法:手动加锁

2.使用Vector代替ArrayList,但是Vector是上古时期用到的东西,现如今已经不建议再使用

3.使用Collections.synchronizedList();对List进行包装

 4.使用CopyWriteArrayList写时拷贝:

 多线程使用哈希表:

1.使用HashTable:

2.使用ConcurrentHashMap:

和HashTable的核心区别:

其他区别:


Callable接口的用法:

Callable接口的用法,非常类似Runnable,都是描述一个任务,一个线程要干什么;

不同的是Runnable内的run方法返回void类型,有时候我们需要一个任务有一个实际结果,即有返回值

Callable内部描述任务的是call方法,其是有返回值的:

public class ThreadDemo2 {
    public static void main(String[] args) {
        //Callable是一个带泛型的接口泛型类型代表call返回值类型
        //同时Callable也是一个函数式接口
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 100; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        //现在任务创建好了,需要找一个线程执行这个任务
        //不能直接将callable作为线程的初始化参数,需要用FutureTask包装一下,后续方便获取返回值
        //FutureTask也是一个泛型类型,泛型代表了返回值类型
        FutureTask<Integer> task = new FutureTask<>(callable);
        Thread thread = new Thread(task);
        thread.start();
        int sum = 0;
        try {
            //获取返回值,get方法一定能确保获取到返回值,如果线程没执行完毕,那么main线程会在
            //get方法这里进行阻塞等待,类似于join方法的效果
            sum = task.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(sum);
    }
}

ReentrantLock可重入锁: 

通过实例化ReentrantLock可以创建一个可重入锁,其和synchronized的区别:

1.synchronized是关键字,其是基于代码块方式来控制加锁的,进代码块就加锁,出代码块就解锁,而ReentrantLock需要通过lock,unlock手动加锁解锁;

2.ReentrantLock提供了特殊的API:tryLock,也就是尝试加锁,如果当前能获取到锁,就返回true并且加锁,如果获取不到锁就返回false,也不会进入阻塞等待,也就是说,如果拿不到就不拿了

3.synchronized是一个非公平锁,也就说每个线程都有可能获取到锁,不遵循先来后到的规则,而ReentrantLock在实例化对象也就是构造方法中可以传入true或false选择是否为公平锁

4.synchronized是搭配wait,notify进行等待或者唤醒的,如果多个线程wait同一对象,notify是随机唤醒的,而ReentrantLock是搭配Condition这个类进行等待,唤醒的,而且根据不同的对象可以指定唤醒

public class ThreadDemo4 {
    public static void main1(String[] args) throws InterruptedException {
        //非公平锁
        ReentrantLock lock = new ReentrantLock(false);
        //创建condition对象用于后续的await和signal
        //这里每次创建的对象都是不一样的,方便我们后续能进行指定唤醒
        Condition condition = lock.newCondition();
        Thread thread = new Thread(() -> {
            lock.lock();
            try {
                //其和wait方法类似,都会先释放锁
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("唤醒Thread");
            lock.unlock();
        });
        thread.start();
        Thread.sleep(1000);
        //如果能获取到锁就加锁
        if (lock.tryLock()){
            //这里可以通过不同的对象signal进行指定的线程唤醒
            condition.signal();
            lock.unlock();
        }else {
            System.out.println(1);
            System.exit(0);
        }
    }
}

 

 信号量Semaphore:

信号量本质是记录当前资源可用数量

P操作(acquire):申请资源,计数器 -1;

V操作(release):释放资源,计数器+1;

如果计数器已经是0,继续申请资源就会阻塞等待

其实synchronized和ReentrantLock都是一种信号量,但其计数器的值只在1和0之间反复横跳,1代表有资源可用,即未上锁,0代表无资源可用,即已经有线程在用锁,反过来说,其实信号量也是一种锁,允许多个线程同时上锁

public class ThreadDemo6 {
    public static void main(String[] args) {
        //创建4份资源,即最多四个线程共用同一把锁;false代表非公平锁
        Semaphore semaphore = new Semaphore(4,false);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    //P操作
                    semaphore.acquire();
                    System.out.println("我获取到资源了");
                    Thread.sleep(1000);
                    //V操作
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 20; i++) {
            Thread t = new Thread(runnable);
            t.start();
        }
        //通过运行,几乎是每四个一组进行打印,这就代表了最多4个线程共用一把锁
    }
}

CountDownLatch:

一般用于一个线程等待固定数量的其他线程全部执行完特定任务后再执行

举个例子:

主线程创建一个CountDownLatch对象,构造方法写5代表接下来要等待5个线程完成任务

5个线程分别执行各自的操作

主线程是使用await方法来阻塞等待所有的任务完成

5个线程每个线程执行完,都调用一次countDown方法

await会暗中计算又几个countDown被调用了,当5个线程都调用过了之后,此时主线程的await就解除阻塞,可以进行后续工作了

public class ThreadDemo7 {
    public static void main(String[] args) throws InterruptedException {
        //创建countdownlatch对象,5代表有5个线程要先完成任务
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 1; i <= 5; i++) {
            int number = i;
            Thread thread = new Thread(() -> {
                try {
                    Thread.sleep(number * 1000);
                    //一次countDownLatch
                    //达到一定次数后,主线程的阻塞就解除
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            thread.start();
        }
        //await会后台计算目前countDownLatch了几次,达到5次后就解除阻塞
        countDownLatch.await();
        System.out.println("结束");
    }
}

 多线程使用ArrayList:

为了保证线程安全:

1.最直接的方法:手动加锁

2.使用Vector代替ArrayList,但是Vector是上古时期用到的东西,现如今已经不建议再使用

3.使用Collections.synchronizedList();对List进行包装

让list成为返回的对象的一个成员常量,其成员方法大多都是被synchronized修饰的

但是使用Collections.synchronizedList()并不意味着一定线程安全,到底线程安不安全还需结合来看

如:

public class ThreadDemo8 {
    public static void main(String[] args) throws InterruptedException {
        //将ArrayList进行包装
        List<Integer> list = Collections.synchronizedList(new ArrayList<>());
        list.add(0);
        //让两个线程分别对list下标为0的元素进行5W次自增
        Thread thread1 = new Thread(() -> {
            for (int i = 1; i <= 50000; i++) {
                list.set(0,list.get(0) + 1);
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 1; i <= 50000; i++) {
                list.set(0,list.get(0) + 1);
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        //输出结果
        System.out.println(list.get(0));
    }
}

通过多次运行我们发现结果几乎都是不一样的,并且不是稳定的10W,这是因为在循环里的这个操作先运行get方法,然后运行set方法,这俩之间有个空隙,正好锁释放了,所以是线程不安全的;

 4.使用CopyWriteArrayList写时拷贝:

 

如果是多线程读,由于读本身就是线程安全的,所以没事

如果某个线程尝试修改这个数组,就会触发写时拷贝

写时拷贝内部的部分方法也是加锁了的,不过用的不是synchronized而是ReentrantLock

同理与Collections.synchronizedList();并不是使用了就一定是线程安全的,到底线程安全不安全还要结合代码的特点

 多线程使用哈希表:

HashMap是线程不安全的;

1.使用HashTable:

可以使用HashTable其在关键方法上直接使用了synchronized修饰,相当于直接对整个哈希表进行加锁,但我们知道哈希表是数组加链表的形式组成的,有时候put两个对象的哈希码根部不同,不会存在线程不安全问题,但HashTabe仍会加锁,这就降低了并发效率,同时HashTable是一个上古版本的东西,其和HashMap不仅只有synchronized修饰方法这一差别,比如HashTable是不可以树化的,所以我们并不推荐使用HashTable

2.使用ConcurrentHashMap:

和HashTable的核心区别:

ConcurrentHashMap不只有一把锁,其每个链表的头节点都作为一个锁对象,每次进行操作,都只是针对链表的头节点进行加锁,操作不同的链表就不会存在锁冲突,通过这样的优化就会大大降低所冲突的概率,从而导致加锁开锁的开销很小,甚至只是使用了偏向锁

以一段伪代码展示逻辑:

void put (String key, String value){
    /先找到对应链表的头节点
    int index = hashCode(key);
    Node head = getHead(index);
    synchronized(head){
        //执行链表插入节点操作    
    }
}

 

而HashTable是对于整个哈希表加锁,操作完全不相干的两个链表也会上锁导致阻塞等待,效率比较低

注意:上述的ConcurrentHashMap的特点是在jdk1.8之后的特性,在1.8版本之前ConcurrentHashMap采用的是分段锁,也就是几个链表共用一把锁,但是这个设定并不合理,效率并不高代码也麻烦

其他区别:

1.ConcurrentHashMap更充分的利用了CAS,有的操作,比如获取/更新元素个数,就是直接使用的CAS(锁策略与CAS_zyz04的博客-CSDN博客)

2.ConcurrentHashMap优化了扩容策略,对于HashTable如果元素过多,并且受限于负载因子,在某次put的时候就会涉及到扩容,扩容需要申请内存空间并且要搬运元素,但是如果搬运元素过多,那么就会导致那次put特别的卡顿。

而ConcurrentHashMap的扩容方式并不会一次性把所有的元素搬运到新的空间中,而是每次只搬运一部分,此时相当于有两份哈希表,此时再插入元素是向新的hash表中插入,查找元素会新表旧表一起查,并且在每次操作中都搬运一部分元素

虽然这样的方式浪费了部分空间,但是起码不会导致卡顿

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值