多线程(基础3)

目录

一,callable接口

1.理解

2.与Runnable的区别与联系

3.Callable创建线程 

二,CountDownLatch

1.理解

2.构造

3.使用场景

4.实现

三,Semaphore

1.理解

2.构造

3.实现

四,线程安全的集合类

1.线程安全的集合类

2.List(在多线程中不能使用ArrayList,LinkedList)

五,ConcurrentHashMap

1.HashMap—HashTable—ConcurrentHashMap区别

2.ConcurrentHashMap的底层原理

3.加锁细节 

1)对Node进行volatile修饰

2)put元素的细节(cas)

3)put元素的细节(加锁)

4.扩容

六,死锁

1.什么是死锁

2.产生死锁的必要条件

3.如何避免死锁


一,callable接口

1.理解

  • Callable 和 Runnable 相对, 都是描述一个 "任务". Callable 描述的是带有返回值的任务, Runnable描述的是不带返回值的任务.
  • Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
  • FutureTask 就可以负责这个等待结果出来的工作 

2.与Runnable的区别与联系

  • Callable 是一个 interface . 相当于把线程封装了一个 "返回值". 方便程序猿借助多线程的方式计算结果
  • Callable 和 Runnable 相对, 都是描述一个 "任务". Callable 描述的是带有返回值的任务, Runnable描述的是不带返回值的任务.
  • Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
  • FutureTask 就可以负责这个等待结果出来的工作

3.Callable创建线程 

具体步骤:

1)定义一个Callable对象,重写带返回值的call方法

2)创建一个FutureTask未来任务对象

3)将任务交给线程执行

4)接收返回值,当前线程阻塞等待FutureTask任务执行完成,并获取结果

package day20220325;

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

public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {

            @Override
            public Integer call() throws Exception {
                int i = 1;
                i = i + 1;
                return  i;
            }
        };
        FutureTask<Integer> task = new FutureTask<>(callable);
        Thread thread = new Thread(task);
        thread.start();
        System.out.println(task.get());
    }
}

二,CountDownLatch

1.理解

  • 同时等待 N 个任务执行结束.
  • 好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩

2.构造

  • 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
  • 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
  • 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了. 

3.使用场景

 等待多个线程全部执行完,在执行某个任务(只能减少,不能增加)

4.实现

package day20220325;

import java.util.concurrent.CountDownLatch;

public class MyCountDownLatch {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch count = new CountDownLatch(10);
        for(int i = 0;i < 10;i++){
            int j = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(j);
                    count.countDown();
                }
            }).start();
        }
        count.await();
        System.out.println("main");
    }
}

三,Semaphore

1.理解

  • 信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器.
  • Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用

理解信号量

可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.

当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)

当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)

如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.

2.构造

3.实现

package day20220325;

import java.util.concurrent.Semaphore;

public class MySemaphore {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(4);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("申请资源");
                    semaphore.acquire();
                    System.out.println("我获取到资源了");
                    Thread.sleep(1000);
                    System.out.println("我释放资源了");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 20; i++) {
            Thread t = new Thread(runnable);
            t.start();
        }
    }
}

四,线程安全的集合类

1.线程安全的集合类

Vector,HashTable,Stack,ConcurrentHashMap

2.List(在多线程中不能使用ArrayList,LinkedList)

  • 同步的List

五,ConcurrentHashMap

1.HashMap—HashTable—ConcurrentHashMap区别

HashMap 是线程不安全的,效率高;HashTable 是线程安全的,效率低。

ConcurrentHashMap 可以做到既是线程安全的,同时也可以有很高的效率,得益于使用了分段锁

2.ConcurrentHashMap的底层原理

  • 是基于哈希表实现的
  • 底层数据结构是Node节点(本质上是一个单向链表),存放的是Node<K,V>数组,对Node键值对进行volatile修饰,并且采用了cas+synchronized来保证线程安全
  • 采用数组+链表/红黑树的结构才保存数据
  • 当达到存放数据的阈值之后,会进行扩容

3.加锁细节 

1)对Node进行volatile修饰

对于读读操作来说本身就是具有原子性,使用volatile修饰,可以保证可见性,顺序性,因此,就保证了线程安全

2)put元素的细节(cas)

如果在进行数据插入时,并且当前桶的元素为空时,就进行cas+自旋的方式进行插入元素,确保不会因为加锁释放锁消耗太多的时间(对于桶空的情况来说,证明线程冲突比较小,符合cas)

3)put元素的细节(加锁)

对于真正的桶不空的情况来说,就要考虑加锁了,但加锁只对Node(头节点)位置进行加锁,插入的方式就是头插,因此只用对头节点进行加锁

4.扩容

如果数据已经超过了(插入数据的阈值,负载因子*容量),就需要进行扩容

六,死锁

1.什么是死锁

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止所产生的bug

2.产生死锁的必要条件

  • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路

简单点说

锁的层面:锁本身必须互斥,并且不能被抢占

线程方面:线程不执行完不释放已经得到的锁,并且,还不会主动的放弃,只会死等

3.如何避免死锁

  • 破坏产生死锁的任意一个条件都行
  • 一般是从循环等待解决,多个线程约定好,都以某一个顺序进行执行就可以了

4.如何检测死锁

在jconsole中提供了检测死锁的方式

猜你喜欢

转载自blog.csdn.net/qq_54773998/article/details/124085466