【Java之多线程】JUC常见知识点全面总结

2年前 (2022) 程序员胖胖胖虎阿
176 0 0

JUC全称为java.util.concurrent,其中,concurrent这个包里包含了很多和多线程并发相关的操作,同样也是面试中的高频考点,下面博主就带大家学习学习这部分内容吧!

【Java之多线程】JUC常见知识点全面总结

JUC

  • 一. ReentrantLock
    • 1. 理解
    • 2. 用法
    • 3. 与synchronized区别
    • 4. 总结
  • 二. 原子类
    • 1. 理解
    • 2. 常见的原子类
    • 3. 常见的方法
  • 三. 线程池
    • 1. 为什么要引入线程池
    • 2. 引入线程池的好处
    • 3. 创建线程池的方法
      • (1)ThreadPoolExecutor
      • (2) Executors
  • 四. 信号量Semaphore
    • 1. 定义
    • 2. 作用
    • 3. 用法示例
  • 五. CountDownLatch
    • 1. 理解
    • 2. 用法
  • 六. 高频面试题
    • 1. 进程间通信有哪几种方式
    • 2. 线程同步的方式有哪些
    • 3. 为什么有了synchronized还需要JUC底下的 lock
    • 4. 简单说一下什么是信号量
    • 5. 解释一下 ThreadPoolExecutor 构造方法的参数的含义

一. ReentrantLock

1. 理解

  1. 之前我们讨论的可重入锁,翻译成英文就是ReentrantLock,大部分情况下这个英文单词要理解成这一锁特性,但少数情况下要理解成一个类
  2. 和 synchronized 定位类似,都是用来实现互斥效果,用来保证线程安全,同时这个锁是可重入的

2. 用法

下面我们来看一段代码实现两个线程分别对一个变量count累加操作:

public class Test {
    static class Counter{
        public int count=0;
        public void increase(){
            count++;
        }
    }

    public static void main(String[] args) {
        Counter counter=new Counter();
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter.count);
    }
}

经过之前的学习,我们认为此方法打印count是线程不安全的,不会每次都很准确地打印10000:
第一次运行
【Java之多线程】JUC常见知识点全面总结
第二次运行
【Java之多线程】JUC常见知识点全面总结
之前我们学过的解决方法是使用synchronized保证线程的安全性,代码如下:

static class Counter{
        public int count=0;
        synchronized public void increase(){
            count++;
        }
    }

改动部分如上图所示(其他部分一样),打印结果如下:【Java之多线程】JUC常见知识点全面总结
但此时我们可以通过创建ReentrantLock这一对象对其实现加锁,完整代码如下:

import java.util.concurrent.locks.ReentrantLock;

public class Test {
    static class Counter {
        public int count;
        public ReentrantLock locker = new ReentrantLock();

        public void increase() {
            locker.lock();
            count++;
            locker.unlock();

        }
    }
    public static void main(String[] args) {
        Counter counter=new Counter();
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    counter.increase();
                }
            }
        };
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter.count);
    }
}

打印结果如下:
【Java之多线程】JUC常见知识点全面总结

3. 与synchronized区别

那么,与synchronized同样都能对其实现加锁功能,这两者有什么区别呢?

  1. ReentrantLock把加锁和解锁拆成了两个方法,确实存在遗忘解锁的风险,但可以让代码变得更加灵活,可以把加锁和解锁的代码分别放到两个方法之中
  2. synchronized在申请锁失败时,代码会死等。而ReentrantLock 可以通过trylock这个方法等待一段时间就放弃,不会浪费时间
  3. synchronized是非公平锁,而ReentrantLock默认是非公平锁。但可以通过构造方法传入一个 true 开启公平锁模式
  4. ReentrantLock 有更强大的唤醒机制,synchronized 是通过 Object 的 wait / notify 方法实现等待唤醒过程的,每次唤醒的是一个随机等待的线程。而ReentrantLock搭配 Condition 类实现等待-唤醒,可以更精确控制唤醒某个指定的线程。

4. 总结

  1. 大部分情况下使用 synchronized就足够了
  2. 锁竞争激烈的时候,使用ReentrantLock , 搭配 trylock 方法可以更灵活地控制加锁的行为,而不是死等。
  3. 如果需要使用公平锁, 使用 ReentrantLock

二. 原子类

1. 理解

保证线程安全不一定非得加锁,当然也可以用原子类,从java1.5开始,jdk提供了java.util.concurrent.atomic包,这个包内包含一系列的原子操作类,提供了一种用法简单,性能高效,线程安全的更新一个变量的方式。其内部通常以CAS方式实现,因此性能通常比加锁实现i++要高很多,具体使用方法如下(上述例子)

  public AtomicInteger count = new AtomicInteger(0);

        public void increase() {
            count.getAndIncrement();
        }

这里只展示改动后的代码,其打印结果如下:
【Java之多线程】JUC常见知识点全面总结

2. 常见的原子类

  1. AtomicBoolean
  2. AtomicInteger
  3. AtomicIntegerArray
  4. AtomicLong
  5. AtomicReference
  6. AtomicStampedReference

3. 常见的方法

AtomicInteger 举例,常见方法有

  1. addAndGet(int delta); 相当于 i += delta;
  2. decrementAndGet(); 相当于–i;
  3. getAndDecrement(); 相当于i–;
  4. incrementAndGet(); 相当于++i;
  5. getAndIncrement(); 相当于i++;

【Java之多线程】JUC常见知识点全面总结

三. 线程池

1. 为什么要引入线程池

解决并发编程的方案一般是靠多进程的,但是进程开销的资源是非常大的,因此我们进一步地引入了多线程。虽然创建销毁线程比创建销毁进程看起来似乎更轻量了,但是在频繁创建毁线程的时还是会比较低效。线程池就是为了解决这个问题。如果某个线程不再使用了,并不是真正把线程释放,而是放到一个 "池子"中。当我们需要使用多线程的时候,直接从之前创建好的池子中取出一个就行了,当我们不用的时候,直接把这个线程放回池子中即可。

2. 引入线程池的好处

  1. 当我们不用线程池的时候,频繁地创建或者销毁线程涉及到用户态和内核态的来回切换,从用户态切换到内核态会创建出对应的PCB(进程控制块,英文是Processing Control Block),这样会消耗大量的系统资源,而且效率还会比较低。
  2. 当我们引入线程池后,相当于只在用户态完成各种操作,这样代码执行效率和系统开销会大大优化

3. 创建线程池的方法

(1)ThreadPoolExecutor

使用Java标准库中的ThreadPoolExecutor方式创建,但需注意里面各自的参数代表的含义,使用起来相对而言比较复杂。
构造方法
【Java之多线程】JUC常见知识点全面总结
为了更好地理解每个参数的具体含义,大家可以利用空闲时间去jdk的官方文档学习学习,对自己是非常有帮助的:【Java之多线程】JUC常见知识点全面总结

(2) Executors

使用 Executors 这个类创建,这个类相当于一个工厂类,通过这个工厂类中的一些方法,就可以创建出不同风格的线程池实例了。
部分方法

  1. Executors.newFixedThreadPool:创建一个固定大小的线程池
  2. Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
  3. Executors.newSingleThreadExecutor:创建出只包含一个线程数的线程池,它可以保证先进先出的执行顺序。
  4. Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池(放入的任务能够过一会再执行)
  5. Executors.newSingleThreadScheduledExecutor:创建出具有一个单线程并且可以执行延迟任务的线程池

用法示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 20; i++) {
            service.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
    }
}

运行结果:
【Java之多线程】JUC常见知识点全面总结

四. 信号量Semaphore

1. 定义

信号量Semaphore一般用来表示可用资源的个数,相当于一个计数器,可类比生活中停车场牌子上面显示的停车场剩余车位数量。

  1. 当有车开进去的时候, 就相当于申请一个可用资源,可用车位就 -1 (这个称为信号量的 P 操作)
  2. 当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)
  3. 如果计数器的值已经为 0 了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源(计数器的值是大于等于0的)

2. 作用

  1. 在创建信号量的时候,可以给定一个初始值(可用资源个数),当可用资源个数用完时,就会阻塞等待,以确保线程安全
  2. 若把信号量的初始值设成1,则计数器的值只能取0或1了,此时把这个信号量称为二元信号量,和锁的功能类似,有加锁(没法申请资源)和解锁状态(可以申请资源)

3. 用法示例

下面我们创建15个线程,给定初始资源量为3个,然后先尝试申请资源(acquire),申请完资源后再休眠1秒,然后释放资源(release):

mport java.util.concurrent.Semaphore;

public class Test {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("准备申请资源");
                    semaphore.acquire();
                    System.out.println("申请资源成功");
                    // 申请到资源之后休眠1秒
                    Thread.sleep(1000);
                    semaphore.release();
                    // 释放资源
                    System.out.println("释放资源完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 创建15个线程,让这 15 个线程来分别去尝试申请资源
        for (int i = 0; i < 15; i++) {
            Thread t = new Thread(runnable);
            t.start();
        }
    }
}

运行结果如下:【Java之多线程】JUC常见知识点全面总结
可以看到,由于资源数为3,所以前3个线程申请资源后很容易成功,而之后的线程就没有资源可以申请了,只能等到前3个线程把资源释放出来后再申请
信号量相当于是锁的升级版本,锁只能控制一个资源的有无,而信号量可以控制很多个资源的有无

五. CountDownLatch

1. 理解

用于同时等待N个任务结束,就好比百米赛跑一样,只有当所有选手都到位之后,哨声响了之后才能同时出发开始跑步,当所有选手都通过终点时才会公布成绩。

2. 用法

我们创建10个线程同时开始执行一个任务,每个任务执行完后记录一下,都调用 latch.countDown()方法。在CountDownLatch 内部的计数器同时自减。再创建一个主线程,其中使用 latch.await(); 阻塞等待至所有任务执行完毕(此时计数器为0)
用法示例

import java.util.concurrent.CountDownLatch;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("任务开始");
                try {
                    Thread.sleep((long) (Math.random() * 10000));//生成随机数
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                latch.countDown();
                System.out.println("任务完成!");
            }
        };
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(runnable);
            t.start();
        }
        latch.await();
        System.out.println("所有任务结束");
    }
}

打印结果如下:【Java之多线程】JUC常见知识点全面总结

六. 高频面试题

1. 进程间通信有哪几种方式

  1. 网络(socket)
  2. 文件
  3. 管道
  4. 消息队列(操作系统内核提供的方式)
  5. 信号量(操作系统内核提供的方式,有别于本篇讲的Java标准库提供的方式)
  6. 信号

2. 线程同步的方式有哪些

首先,线程同步就是协同步调,按预定的先后次序进行运行,即靠哪个线程先获得到CUP的执行权谁就先执行

  1. 阻塞队列
  2. synchronized
  3. ReentrantLock
  4. Semaphore
  5. volatile等等

3. 为什么有了synchronized还需要JUC底下的 lock

详见与synchronized区别部分(学会用自己的话总结

4. 简单说一下什么是信号量

详见信号量的定义部分(学会用自己的话总结

5. 解释一下 ThreadPoolExecutor 构造方法的参数的含义

详见上文中所涉及的jdk官方文档,这个参数虽然比较多,但大致上可以通过英文推测出其大意,此时就需要我们平常多记记了!

版权声明:程序员胖胖胖虎阿 发表于 2022年9月10日 下午11:08。
转载请注明:【Java之多线程】JUC常见知识点全面总结 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...