多线程进阶(下)

目录

一.JUC

二.线程安全的集合类

 三.死锁


一.JUC

这里的juc指的是java.util.concurrent(并发,多线程相关的),一个标准库中的类,下面是JUC里面的常见类:

Callable

这是一个interface,也是一种创建线程的方式,之前学的Runnable也是,但是这个不太适合让线程计算出一个结果这样的代码,例如创建一个线程让线程计算从1到1000相加的和,如果基于runnable实现的话,就会比较麻烦,还会陷入使线程陷入阻塞状态,而使用callable接口就可以很好的解决runnable这样不方便返回结果的情况,可以看一下callable的具体使用:


import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo28 {
    public static void main(String[] args) {
        Callable<Integer> callable = new Callable<Integer>() {//使用callable来描述一个任务,方便返回值
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 0; i <= 1000; i++) {
                    sum += i;
                }
                return sum;//这就可以返回值了
            }
        };
        //为了让线程执行callable里面的任务,使用构造方法还不行,还需要一个辅助的类
        FutureTask<Integer> task = new FutureTask<>(callable);
        //这就相当于是小票单号一样,因为任务可能不止一个,因此需要这样的"小票"来对应相关的任务,然后凭借这样的"小票"就可以取出任务,对上号,然后可以返回结果
        //创建线程来完成这里的计算任务
        Thread t = new Thread(task);//Thread并没有一个构造方法直接传入callable,因此需要上面的辅助类
        t.start();
        //如果线程没有执行结束,get就会阻塞,一直阻塞到任务完成了结果算出来,才会得到结果
        try {
            System.out.println(task.get());//凭小票得到结果
        } catch (InterruptedException e) {//阻塞过程被打断的异常
            e.printStackTrace();
        } catch (ExecutionException e) {//任务执行过程中出异常
            e.printStackTrace();
        }
    }
}

通过这样的创建线程方式,就会比runnable的简单不少,效率也有所提高

ReentrantLock

这个翻译过来是可重入锁,先介绍一下基本用法:


import java.util.concurrent.locks.ReentrantLock;
public class Demo29 {
    public static void main(String[] args) {
        ReentrantLock locker = new ReentrantLock();
        //加锁
        locker.lock();
        //如果这里有抛出异常的话,后面的unlock很有可能执行不到,然后导致死锁,因此这里一般的写法就是unlock放到finally里面保证一定能执行到
        //解锁
        locker.unlock();
        //这里将加锁和解锁区分开了
    }
}

 然后剩下的其他用法就和synchronized差不多类似了,都是当多个线程竞争同一个锁的时候就会阻塞

而我们之前学的synchronized也是可重入锁,那么和synchronized的区别是什么呢?

区别:

1.synchronized是一个关键字(背后的逻辑是JVM内部实现的),而ReentrantLock是一个标准库中的类(背后的逻辑是Java代码写的类)

2.synchronized不需要手动释放说,出了代码块,就会自己释放,而ReentrantLock必须要手动释放锁,而且不能忘记释放,否则会死锁

3.synchronized如果竞争锁失败,就会陷入阻塞等待状态,但是ReentrantLock除了阻塞等待这个之外,trylock,失败了就会直接返回(不会一直阻塞死等,或者等待多久,之后就可以会去做其他的事情,这就增加了锁的灵活性)

4.synchronized是一个非公平锁,而ReentrantLock提供了非公平锁和公平锁两个版本,在构造函数中,通过参数来指定是公平锁还是非公平锁,不写参数默认就是非公平锁

ReentrantLock locker1 = new ReentrantLock(true);//公平锁

这就是一个公平锁了

5.基于synchronized衍生出来的等待机制,是wait,notify,功能是相对有限的,

而基于ReentrantLock衍生出来的等待机制,是Condition类(条件变量),这个功能更丰富一些,这里了解一下就可以!

原子类也是这个类下面(import java.util.concurrent.atomic.AtomicInteger)的具体可以看我前面的文章:多线程进阶(上)_栋zzzz的博客-CSDN博客

线程池也是这个类下面(import java.util.concurrent.Executors)的具体可以看我前面的文章:

多线程案例_栋zzzz的博客-CSDN博客

semaphore(信号量)

这是一个更广义的锁,可以说锁就是信号量的一种特殊情况,叫做"二元信号量"

举个例子:比如一个停车场内的停车位一般都是右确定数目的,因此停车场入口一般都有标记当前空闲多少个车位,如果有车开进去,车位数就-1,如果有车开出去,车位数就+1,此时我们就认为这个标记牌就是信号量,描述了可用资源(车位)的个数,每次申请一个可用资源,计数器就-1(称为P操作),每次释放一个可用资源没计数器就+1(称为V操作),当信号量的计数为0了,再次进行P操作就会进入阻塞等待状态,而P操作在代码中的方法是acquire(申请),V操作是release(释放)

锁就可以视为"二元信号量",锁的可用资源就一个,计数器要么为1,要么为0,而信号量就把锁推广到了一般情况,可用资源更多的时候该如何处理:

import java.util.concurrent.Semaphore;
public class Demo30 {
    public static void main(String[] args) throws InterruptedException {
        //初始化的值表示可用资源有4个
        Semaphore semaphore = new Semaphore(4);
        //申请资源,P操作
        semaphore.acquire();//还可用申请或释放多个资源(加参数),当资源申请完了再次申请的话就会曹成阻塞效果,释放也是一样的
        //释放资源,V操作
        semaphore.release();
    }
}

CountDownLatch

可以理解为一个"终点线"一样,假设有一场百米赛跑,而这个就可以理解为是一个终点线,当所有的选手都冲过终点线的时候,这场比赛才会结束!这样的场景,在开发中也是存在的,例如多线程下载(把一个文件拆成多个部分,每个线程负责下载其中的一个部分,当所有的线程都完成自己的下载才算下载完成,这样就可以提高整个文件的下载效率),利用代码还原一下这场百米赛跑:

CountDownLatch的基本方法:

countDown:给每个线程里面去调用,就表示到达终点了

await:是给等待线程去调用,当所有的线程都到达终点了,await就从阻塞状态中返回,就表示任务完成了

import java.util.concurrent.CountDownLatch;
public class Demo31 {
    public static void main(String[] args) {
        //构造函数的参数表示这里有几个选手参赛
        CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            Thread t  = new Thread(()->{//多个线程调度是随机的
                try {
                    Thread.sleep(3000);
                    System.out.println(Thread.currentThread().getName() + "到达终点");
                    latch.countDown();//表示这个选手到达终点
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            });
            t.start();
        }
        //裁判需要等待所有的选手都通过终点线才会结束这场比赛
        //当这些线程没有执行完的时候,await就会是阻塞状态,所有的线程执行完了,await才会返回
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("比赛结束");
    }
}

调用countDown的次数和构造参数的个数一致时,就会返回await

二.线程安全的集合类

像Vector,HashTable,Stack这样的几个都是线程安全的,那么在多线程环境下如何使用不安全的ArrayList:

1.可以自主给关键步骤加锁

2.可以使用标准库里面的synchronizedList方法,这是基于synchronized进行线程同步的List,这里面的关键步骤都会锁:Collection.synchronized(new ArrayList),但是这个就不像自主加锁那样的灵活性了

3.使用CopyOnWriteArrayList,这是写时拷贝(在修改的时候会创建一份副本出来,就把ArrayList复制到副本,然后修改副本里面的内容,修改完成之后再让副本转正,还原回去(这样做的好处就是修改的同事对于读操作是没有任何影响的,读的时候优先读旧的版本))的容器,这个适合于读多写少的情况,也适合数据小的情况(更新配置数据,经常会用到这种类似的操作)

多线程使用队列,类似于LinkedBlockingQueue这样的,我前面都是介绍过的这些线程安全的队列,这里就不细说了

多线程环境下使用哈希表

HashMap本身是线程不安全的,这是不能直接在多线程环境下使用的,因此我们有两种解决方案:

1.HashTable(不推荐)

HashTable就是主要给关键方法加锁

这是直接针对方法加锁,也就是针对this加锁,当有多个线程来访问HashTable的时候,无论啥样的操作,都会产生锁竞争,这样的设计就会导致锁竞争的概率非常大,效率就会比较低! 

2.ConcurrentHashMap(推荐)

优点:

1.ConcurrentHashMap就是将每一个链表的头结点就加上一把锁,锁的竞争就会减小,效率也会提高

 2.另外ConcurrentHashMap只是针对写操作加锁了,读操作是没有加锁的,只是使用了volatile,避免内存可见性的问题

3.ConcurrentHashMap中更广泛的使用了CAS,进一步提高了效率(比如维护size的操作)

4.ConcurrentHashMap针对扩容进行了巧妙的化整为零(如果元素多了,链表长度变长,就会影响哈希表的效率,因此就需要扩容,而对于之前的扩容操作就会创建一个更大的数组,把之前旧的元素全部搬运过去,这样的操作是非常耗时的,HashTable就是这样做,ConcurrentHashMap就会每次操作只搬运一点点,通过多次的操作完成整个搬运的过程,会同事维护一个新的HashMap和一个旧的,查找的时候急需要查找旧的也要查找新的,插入的时候只插入新的,直到搬运完成之后,就会销毁旧的HashMap)

 三.死锁

 最后就是死锁,这里就不过多介绍了,具体可以看我前面关于synchronized里面介绍的死锁内容,各种死锁都包含了:线程加锁关键字_栋zzzz的博客-CSDN博客

以上就是所有内容啦,感谢支持!!

猜你喜欢

转载自blog.csdn.net/qq_58266033/article/details/123910621