多线程——JUC

本文介绍了Java并发编程中的关键概念,包括ReentrantLock的使用,如加锁、解锁和条件对象,对比了它与synchronized的区别。还讨论了Callable接口,用于创建带返回值的线程任务,并展示了与FutureTask的配合使用。此外,提到了原子类如AtomicInteger的优势以及信号量Semaphore在资源管理中的应用。最后,讨论了死锁的原因和解决策略。
摘要由CSDN通过智能技术生成

JUC是java.util.concurrent包,由jdk1.5引进。

1.ReentrantLock

可重入互斥锁,和Sychronized定位类似,都是用来实现互斥效果,保证线程安全的。

ReentrantLock的方法:
1.lock():加锁操作,如果获取不到锁就死等。
2.trylock(超时时间):加锁操作,如果等待一段时间,获取不到锁之后就直接放弃加锁。
3.unlock():解锁操作。

1.ReentrantLock的休眠唤醒

和Sychronized休眠和唤醒不一样,需要创建Condition对象来进行休眠与唤醒。ReentrantLock是有条件的休眠和唤醒,对应的对象唤醒对应的线程。

   // 创建一个休眠和唤醒的条件
        Condition condition = reentrantLock.newCondition();

        // 等待,类似于locker.wait();
        condition.await();
        // 唤醒,类似于locker.notify();
        condition.signal();
        // 唤醒所有,类似于locker.notifyAll();
        condition.signalAll();



        // 锁对象
        Object locker = new Object();
        // 等待,并释放锁资源
        locker.wait();

        // 唤醒之前等待的线程
        locker.notify();
        // 唤醒所有
        locker.notifyAll();

2.ReentrantLock与Sychronized的区别

  1. Sychronized退出代码块就自动释放锁,ReentrantLock退出代码块需要手动释放锁,注意需要用try catch来处理加锁的代码(防止忘记释放锁形成死锁)。

  1. Sychronized只支持非公平锁,ReentrantLock自持公平锁也支持非公平锁,可以通过构造方法传入true来开启公平锁模式

  1. ReentrantLock可以根据不同条件来唤醒和休眠

  1. Sychronized在申请失败时,会一直等待锁资源,ReentrantLock在申请失败时,会等待一段时间,超时会放弃申请

  1. Sychronized是一个关键字,是JVM内部实现的,ReentrantLock是标准库中的一个类,由java层面实现

3.如何选择哪个锁

  • 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.

  • 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.

  • 如果需要使用公平锁, 使用 ReentrantLock.

2.Callable接口

Callable是一个interface,相当于把线程封装了一个返回值,方便借助多线程的方式进行计算。

其中Callable是搭配FutureTask一起使用,FutureTask是用来获取Callable 的执行结果的


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

public class Demo_1101_Callable {
    public static void main(String[] args) {
        // 通过Callable定义线程的任务
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                // 这里是具体要处理的任务
                System.out.println("执行运算..");
                int a = 10 + 20;
                // 等待3秒返回
                TimeUnit.SECONDS.sleep(3);
//                throw new Exception("任务执行过程中出现异常");
                return a;
            }
        };

        // Callable要配合FutureTask一起使用,FutureTask是用来获取Callable的执行结果的
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        // FutureTask当做构造参数传入Thread的构造方法里
        Thread thread = new Thread(futureTask);
        // 启动线程
        thread.start();

        try {
            // 阻塞等待Callable任务的执行结果
            Integer result = futureTask.get();
            System.out.println("线程的执行结果:" + result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
            System.out.println("捕获到任务异常:" + e.getMessage());
        }

    }
}

同样是描述一个任务,Runnable与Callable 的区别:

1.Callable要实现call()方法,是有返回值,而Runnable要实现run()方法, 没有返回值。
2.Callable的call()方法可以抛出异常,Runnable的run()方法不能抛出异常。
3.Callable 要搭配FutureTask来使用,获取结果的时候需要使用get()方法。
4.两者都是描述线程任务的接口

3.原子类

原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个:

  • AtomicBoolean

  • AtomicInteger

  • AtomicIntegerArray

  • AtomicLong

  • AtomicReference

  • AtomicStampedReference

以 AtomicInteger 举例,常见方法有:

  • addAndGet(int delta); i += delta;

  • decrementAndGet(); --i;

  • getAndDecrement(); i--;

  • incrementAndGet(); ++i;

  • getAndIncrement(); i++;

4.信号量——Semaphore

计数信号量。 从概念上讲,信号量保持一组许可。 如果有必要,每个 acquire()都会阻止,直到有许可证,然后接受。 每个 release()都添加了许可证,可能会释放阻止收购者。 但是,没有使用实际的许可对象; Semaphore只保留可用数量并相应地采取行动。信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源

通俗的来讲,信号量用来表示资源的数量,本质上就是一个计数器。acquire()申请资源,release()释放资源。例如创建多个线程去申请资源,就会不断地申请资源和时释放资源:


import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class Demo_1202_Semaphore {
    public static void main(String[] args) {
        // 创建信号量的对象, 并指定可用资源数量
        Semaphore semaphore = new Semaphore(5);

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "开始申请资源...");
                try {
                    // 申请资源
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "=====申请资源成功.");
                    // 持有一会
                    TimeUnit.SECONDS.sleep(1);
                    // 释放资源
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + "释放资源.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 创建多个线程去申请资源
        for (int i = 0; i < 20; i++) {
            Thread thread = new Thread(runnable);
            // 启动线程
            thread.start();
        }
    }
}

5.CountDonwLatch

同时等待N个任务执行结束。就例如跑步比赛,等所有人撞线之后,才开始公布成绩。
  • 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.

  • 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.

  • 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了


import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class Demo_1203_CountDownLatch {
    public static void main(String[] args) throws InterruptedException {
        // 多少个选手参赛
        CountDownLatch countDownLatch = new CountDownLatch(10);

        System.out.println("========= 所有选手各就各位 =============");
        // 创建线程表示参赛选手
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "出发...");
                try {
                    // 休眠一段时间,模拟从起点到终点的过程
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "到达终点.");
                // 用这个方法来表示选手到达终点
                countDownLatch.countDown();

            });
            // 启动线程,相当于开始比赛
            thread.start();
        }
        // 等待比赛结束
        countDownLatch.await();
        // 当计数变为 0 的时候,继续执行后续操作
        System.out.println("========== 比赛结束 ===========");

    }
}

6.线程安全的集合类

在多线程的情况下,多个线程对同一个变量进行写操作的时候,就会出现线程不安全。

1.多线程环境使用ArrayList

使用一个普通的ArrayList在多线程环境下的操作:

package com.lihui;

import java.util.ArrayList;
import java.util.List;

public class Demo_1204 {
    public static void main(String[] args) {
        // 创建一个普通集合类
        List<Integer> array = new ArrayList<>();

        // 创建10个线程
        for (int i = 0; i < 10; i++) {
            int j = i;
            // 在每个线程中分别对这个集合进行写入和读取
            Thread thread = new Thread(() -> {
                // 写
                array.add(j);
                // 读
                System.out.println(array);
            });
            thread.start();
        }
    }
}

由于线程不安全,无法预知结果会是怎么样。

使用一个线程安全的Collections.synchronizedList(new ArrayList)来实现同样操作:

synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List.
synchronizedList 的关键操作上都带有 synchronized
package com.lihui;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Demo_1205 {
    public static void main(String[] args) {
        // 创建一个普通集合类
        List<Integer> array = new ArrayList<>();
        // 把一个普通的集合类转换成一个线程安全的集合类
        List<Integer> list = Collections.synchronizedList(array);

        // 创建10个线程
        for (int i = 0; i < 10; i++) {
            int j = i;
            // 在每个线程中分别对这个集合进行写入和读取
            Thread thread = new Thread(() -> {
                // 写
                list.add(j);
                // 读
                System.out.println(list);
            });
            thread.start();
        }
    }
}

这就是我们预期的结果。

CopyOnWriteArrayList(写时复制技术)

其中原理为:如果有一个集合,在此时需要修改其中的数据,但它不会在原始集合修改,而是将原始集合复制一份,在新的集合修改,在此期间若有读操作,还是在原始集合进行读取,等待新集合写操作完成之后将数据集更新,此后使用新的数据集。
  • 优点:对于读多写少的环境下效率非常之高,不需要加锁竞争

  • 缺点:会消耗很多的资源,也不能保证第一时间读到最新的数据

2.多线程环境使用队列

  1. ArrayBlockingQueue 基于数组实现的阻塞队列

  1. LinkedBlockingQueue基于链表实现的阻塞队列

  1. PriorityBlockingQueue基于堆实现的带优先级的阻塞队列

  1. TransferQueue 最多只包含一个元素的阻塞队列

3.多线程环境使用哈希表

1.HashTable

HashTable是线程安全的,

但是这相当于直接给HashTable对象本身加锁

  • 如果多个线程访问一个HashTable就会造成冲突

  • size属性也是通过Sychronized控制的,比较慢

  • 一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低

2.ConcurrentHashMap

相比于 Hashtable 做出了一系列的改进和优化:

  • 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然

是用 synchronized, 但是不是锁整个对象, 而是 "锁桶" (用每个链表的头结点作为锁对象), 大大降

低了锁冲突的概率.

  • 充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.

  • 优化了扩容方式: 化整为零

  1. 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.

  1. 扩容期间, 新老数组同时存在.

  1. 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小

  1. 部分元素.

  1. 搬完最后一个元素再把老数组删掉.

  1. 这个期间, 插入只往新数组加.

  1. 这个期间, 查找需要同时查新数组和老数组

7.死锁

在多线程中最严重的问题之一,死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

典型案例——哲学家就餐问题。
假如五个哲学家吃一盘面,每两个人之间有一根筷子,规定:必须有两根筷子才可以吃面。此时五个人立马拿起筷子,但是每个人只有一根筷子,谁也不让谁,然后就形成僵局。这就是死锁。

造成死锁的原因

  • 互斥使用:当资源被一个线程占用之后,其他线程就不能使用这个资源。

  • 不可抢占:资源请求者不能从资源拥有者的手中抢占当前资源,只能等待拥有这释放之后再使用

  • 请求和保持:当线程已经获取到锁A,还要继续获取锁B

  • 循环等待:线程1等待线程2释放锁,线程2等待线程3释放锁,线程3等待线程1释放锁,此时形成一个回路。

以上四条是形成死锁的四个原因,必须同时满足,才可以形成死锁,也就是说只要打破一条,就不可能形成死锁

解决死锁问题

由于互斥访问不可抢占是锁的基本特性,所以不能打破。

请求和保持:这个在于代码的设计与实现,可以改变

循环等待:这个是最容打破的条件而改变死锁

打破循环等待来解锁死锁

规定锁的编号,让使用锁时按照编号,保证不会出现回路,造成死锁。

可能产生回路的代码:

Object locker1 = new Object();
    Object locker2 = new Object();

    Thread t1 = new Thread(() ->{
        synchronized (locker1){
            synchronized (locker2){
            }
        }
    });
    t1.start();
    Thread t2 = new Thread(() ->{
        synchronized (locker2){
            synchronized (locker1){

            }
        }
    });
    t2.start();

不会产生回路的代码:

Object locker1 = new Object();
    Object locker2 = new Object();

    Thread t1 = new Thread(() ->{
        synchronized (locker1){
            synchronized (locker2){
            }
        }
    });
    t1.start();
    Thread t2 = new Thread(() ->{
        synchronized (locker1){
            synchronized (locker2){

            }
        }
    });
    t2.start();

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值