JavaEE初阶---《JUC 并发编程完全指南:组件用法、原理剖析与面试应答》

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


前言

上节博客我们详细学习了不同的锁入乐观锁VS悲观锁、轻量级锁VS重量级锁、自旋锁VS挂起等待锁、公平锁VS不公平锁、读写锁、偏向锁。还有synchronized的原理这些多线程锁策略。下面我们就要学习


提示:以下是本篇文章正文内容,下面案例可供参考

一、JUC(java.util.concurrent) 是什么

JUC(java.util.concurrent)是Java标准库中专门用于处理并发编程的工具包,提供了一系列线程安全的类、接口和工具,简化多线程开发。它解决了原生线程操作的复杂性,支持高性能、高并发的任务处理。

1.JUC(java.util.concurrent)的常见类

Callable接口

Callable是一个interface。相当于把线程封装了一个"返回值"。方便程序员借助多线程的方式计算结果。

代码示例:创建一个线程计算1+2+3+......+1000,不使用Callable版本。

java 复制代码
public class demo {
    public static int sum =0;
    public static Object locker = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
           int result =0;
            for (int i = 0; i <= 100; i++) {
                result+=i;
            }
            synchronized (locker){
                sum = result;
                locker.notify();
            }
        });
        t.start();
        synchronized (locker){
            while (sum==0){
                locker.wait();
            }
            System.out.println(sum);
        }
    }

可以看到,上述代码需要一系列的加锁和wait notify操作,操作复杂,容易出错。

代码示例:创建一个线程计算1+2+3+......+1000,使用Callable版本。

java 复制代码
class demo{
public static void main(String[] args) throws ExecutionException, InterruptedException {
        //此处Callable只是定义了一个"带有返回值的任务。
        //并没有真的在执行,还是需要Thread类来执行。
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result =0;
                for (int i = 0; i <= 100; i++) {
                    result+=i;
                }
                return result;
            }
        };
        FutureTask task = new FutureTask<>(callable);
        Thread t = new Thread(task);
        t.start();
        //get操作过程就是获取到FutureTask的返回值,这个返回值就来自于callable的call方法
        //get可能会阻塞,如果当前线程执行完毕,get拿到返回结果。
        //如果当前线程还没执行完毕,get一定会阻塞。
        System.out.println(task.get());
    }
}

可以看到,使用Callable和FutureTask之后,代码简化了很多,也不必手动写线程同步代码了。

理解Callable

Callable和Runnable相对,都是描述一个"任务"。Callable描述的是带有返回值的任务,Runnable描述的是不带返回值的任务。

Callable通常需要搭配FutureTask来使用。FutureTask用来保存Callable 的返回结果。因为Callable往往是在另一个线程中执行的,啥时候执行完并不确定。

FutureTask就可以负责这个等待结果出来的工作。

理解FutureTask

想象去吃麻辣烫,当餐点好后,后厨就开始做了。同时前台会给你一张"小票"。这个小票就是TutureTask。后面我们可以随时凭这张小票查看自己的这份麻辣烫做出来了吗。

ReentrantTask

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

ReentrantLock也是可重入锁。"Reentrant"这个单词的原意就是"可重入"。

ReentrantLock的用法:

  • lock():加锁,如果获取不到锁就死等。
  • trylock(超时时间):加锁,如果获取不到锁,等待一定的时间之后就放弃加锁。
  • unlock():解锁。
java 复制代码
public class demo28 {
    private static  int count =0;
    public static void main(String[] args) {
        ReentrantLock locker = new ReentrantLock(true);
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    locker.lock();
                    try {
                        count++;
                    }finally {
                        locker.unlock();
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    locker.lock();
                    try{
                        count++;
                    }finally {
                        locker.unlock();
                    }
                }
            }
        });
        t.start();
        t2.start();
        try {
                t.join();
                t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("final count:"+count);
    }
}

ReentrantLock和synchronized的区别:

  • synchronized是一个关键字,是JVM内部实现的(大概率是基于C++实现)。ReentrantLock是标准库的一个类,在JVM外实现的(基于Java实现的)。
  • synchronized使用时不需要手动释放锁。ReentrantLock使用时需要手动释放。使用起来更灵活,但是也容易遗漏unlock。
  • synchronized在申请锁失败时,会死等。ReentrantLock可以通过trylock的方式等待一段时间就放弃。
  • synchronized是非公平锁,ReentrantLock默认是非公平锁。可以通过构造方法闯入一个true开启公平锁模式。
  • 更强大的唤醒机制。synchronized是通过Object的wait/notify实现等待-唤醒。每次唤醒的是一个随机等待的线程。ReentrantLock搭配Condition类实现等待-唤醒。可以更精确控制唤醒某个指定的线程。

如何选择使用哪个锁?

  • 锁竞争不激烈的时候,使用synchronized,效率更高,自动释放更方便。
  • 锁竞争激烈的时候,使用ReentrantLock,搭配trylock更灵活控制加锁的行为,而不是死等。
  • 如果需要使用公平锁,使用ReentrantLock。

原子类

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

  • AtomicBoolean
  • AtomicInteger
  • AtomicIntegerArray
  • AtomicLong
  • AtomicReference
  • AtomicStampedReference

以AtomicInteger举例,常见方法有

java 复制代码
public class demo29 {
    public  static AtomicInteger atomicInteger = new AtomicInteger();
    public static void main(String[] args) {
        atomicInteger.addAndGet(9);//i+=9;
        atomicInteger.decrementAndGet();//--i;
        atomicInteger.getAndIncrement();//i--
        atomicInteger.incrementAndGet();//++i;
        atomicInteger.getAndIncrement();//i++
    }
}

线程池

虽然创建销毁线程比创建销毁进程更轻量,但是在频繁创建销魂线程的时候还是会比低效。线程池就是为了解决这个问题。如果某个线程不再使用了,并不是真正把线程释放了,而是放到一个"池子"中,下次如果需要用到线程就直接从池子中取,不必通过系统来创建了。

ExecutorService和Executors

代码示例:

  • ExevutorService表示一个线程池实例。
  • Executors是一个工厂类,能够创建出几种不同风格的线程池。
  • ExecutorService的submit方法能够向线程池中提交若干个任务。
java 复制代码
public class demo29 {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("passion");
                }
            });
    }

Executors创建线程池的几种方式

  • newFixedThreadPool:创建固定线程数的线程池
  • newCachedThreadPool:创建线程池数目动态增长的线程池。
  • newSingleThreadExecutor:创建只包含单个线程的线程池。
  • newScheduleThreadPool:设定延迟时间后执行命令,或者定期执行命令,是进阶版的Timer。

Executors本质上是ThreadPoolExecutor类的封装。

ThreadPoolExecutor

ThreadPoolExecutor提供了更多的可选参数,可以进一步细化线程池行为的设定。

ThreadPoolExecutor的构造方法

理解ThreadPoolExecutor构造方法的参数

把创建一个线程池想象成开个公司,每个员工相当于一个线程。

  • corePoolSize:核心线程数量。即正式员工的数量。
  • maximumPoolSize:最大线程数。即正式员工+临时员工的数量。
  • keepAliveTime:非核心线程允许存活的时间。即临时工允许的空闲时间。
  • unit:keepaliveTime的时间单位,是秒,分钟,还是其他值。
  • workQueue:创建线程的工厂,参与具体的创建线程工作。
  • RejectedExecutionHandler:拒绝策略,如果任务量超出公司的负荷了记下来该怎么处理。
    • 1.AbortPolicy(): 超过负荷, 直接抛出异常.
      2.CallerRunsPolicy(): 调⽤者负责处理
      3.DiscardOldestPolicy(): 丢弃队列中最⽼的任务.
      4.DiscardPolicy(): 丢弃新来的任务

代码示例

java 复制代码
public class demo29 {
    public static void main(String[] args) {
        ExecutorService pool = new ThreadPoolExecutor(1,2,1000, TimeUnit.MILLISECONDS,
                new SynchronousQueue<Runnable>(), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 3; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("passion");
                }
            });
        }
    }
}

线程池的工作流程

二、信号量Semaphore

信号量,用来表示"可用资源的个数"。本质上就是一个计数器。

理解信号量

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

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

Semaphore的PV操作中的加减计数器操作都是原子的,可以在多线程环境下直接使用。

java 复制代码
public class demo30 {
    public static void main(String[] args) throws InterruptedException {
        //指定可用资源的个数是"4"
        Semaphore semaphore = new Semaphore(4);
        semaphore.acquire();
        System.out.println("进行一次P操作");
        semaphore.acquire();
        System.out.println("进行一次P操作");
        semaphore.acquire();
        System.out.println("进行一次P操作");
        semaphore.acquire();
        System.out.println("进行一次P操作");
    }
}
java 复制代码
public class demo30 {
    private  static  int count =0;
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(1);
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {

                    for (int i = 0; i < 50000; i++) {
                        try {
                            semaphore.acquire();
                            count++;
                            semaphore.release();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    try {
                        semaphore.acquire();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    count++;
                    semaphore.release();
                }
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("count="+count);
    }
}

CountDownLatch

同时等待N个任务结束。

好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。

java 复制代码
public class demo31 {
    public static void main(String[] args) throws InterruptedException {
        // 现在把整个任务拆成 10 个部分. 每个部分视为是一个 "子任务".
        // 可以把这 10 个子任务丢到线程池中, 让线程池执行.
        // 当然也可以安排 10 个独立的线程执行.
        //构造方法也可以传入的10表示任务的个数
        CountDownLatch latch = new CountDownLatch(10);
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            int ret =i;
            executorService.submit(()->{
                System.out.println("子任务开始执行:"+ret);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("子任务结束执行:"+ret);
                latch.countDown();
            });
        }
        //这个方法阻塞等待所有的任务结束
        //此处的a=>all
        latch.await();
        System.out.println("所有任务执行完毕!!");
        executorService.shutdown();
    }
}

相关面试题

1.线程同步的方式有哪些?

synchronized、ReentrantLock、Semaphore等都可以用于线程同步。

2.为什么有了synchronized还需要JUC下的lock?

以juc的ReentrantLock为例

ReentrantLock和synchronized的区别:

  • synchronized是一个关键字,是JVM内部实现的(大概率是基于C++实现)。ReentrantLock是标准库的一个类,在JVM外实现的(基于Java实现的)。
  • synchronized使用时不需要手动释放锁。ReentrantLock使用时需要手动释放。使用起来更灵活,但是也容易遗漏unlock。
  • synchronized在申请锁失败时,会死等。ReentrantLock可以通过trylock的方式等待一段时间就放弃。
  • synchronized是非公平锁,ReentrantLock默认是非公平锁。可以通过构造方法闯入一个true开启公平锁模式。
  • 更强大的唤醒机制。synchronized是通过Object的wait/notify实现等待-唤醒。每次唤醒的是一个随机等待的线程。ReentrantLock搭配Condition类实现等待-唤醒。可以更精确控制唤醒某个指定的线程。

如何选择使用哪个锁?

  • 锁竞争不激烈的时候,使用synchronized,效率更高,自动释放更方便。
  • 锁竞争激烈的时候,使用ReentrantLock,搭配trylock更灵活控制加锁的行为,而不是死等。
  • 如果需要使用公平锁,使用ReentrantLock。

3.AtomicInteger的实现原理是什么?

基于CAS机制,伪代码如下:

java 复制代码
class AtomicInteger {
private int value;
public int getAndIncrement(){
int oldval = value;
while(CAS(value,oldval,oldval+1) !=true){
oldval = value;
}
return oldval;
}
}

4.信号量听说过吗?之前都用在过哪些场景下?

信号量,用来表示"可用资源的个数"。本质上就是一个计数器。

使用信号量可以实现"共享锁",比如某个资源允许3个线程同时使用,那么就可以使用P操作作为加锁,V操作作为解锁,前三个线程的P操作都能顺利返回,后续线程再进行P操作就会阻塞等待,直到前面的线程执行了V操作。

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

参考上面的ThreadPoolExecutor章节

线程安全的集合类

原来的集合类,大部分都不是线程安全的。

Vector,Stack,HashTable,是线程安全的(不建议用),其他的集合类不是线程安全的。

多线程环境使用ArrayList

1.自己使用同步机制(synchronized或者ReentrantLock)

前面做过很多相关的讨论了,此处不再展开。

2.Collections.synchronizedList(new ArrayList);

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

3.使用CopyOnWriteArrayList

CopyOnWrite容器即写时复制的容器。

  • 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素。
  • 添加完元素之后,再将原容器的引用指向新的容器。

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。

所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

优点:

在读多写少的场景下,性能很高,不需要加锁竞争。

缺点:

1.占用内存较多。

2.新写的数据不能被第一时间读取到。

多线程环境使用队列

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

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

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

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

多线程环境下使用哈希表

HashMap本身不是线程安全的。

在多线程环境下使用哈希表可以使用:

  • Hashtable
  • ConcurrentHashMap
1)Hashtable

只是把简单的把关键方法加上了synchronized关键字。

这相当于直接针对Hashtable对象本身加锁。

  • 如果多线程访问同一个Hashtable就会直接造成锁冲突。
  • size属性也是通过synchronized来控制同步,也是比较慢的。
  • 一旦触发扩容,就由该线程完成整个扩容过程。这个过程会涉及到大量的元素拷贝,效率会非常低。
  • 一个Hashtable只有一把锁,两个线程访问Hashtable中的任意数据都会出现锁竞争。
2)ConcurrentHashMap

相比于Hashtable做出了一系列的改进和优化。以Java1.8为例

  • 读操作没有加锁(但是使用了volat保证从内存读取结果),只对写操作进行加锁。加锁的方式仍然是用synchronized,但是却不是锁整个对象,而是"锁桶"(用每个链表的头节点作为锁对象),大大降低了锁冲突的概率。
  • 充分利用CAS特性,比如size属性通过CAS来更新。避免出现重量级锁的情况。
  • 优化了扩容方式:化整为零:
  1. 发现需要扩容的线程,只需要创建一个新的数组,同时只搬几个元素过去。
  2. 扩容期间,新老数组同时存在。
  3. 后续每个来操作ConcurrentHashMap的线程,都会参与搬家的过程。每个操作复杂搬运一小部分元素。
  4. 搬完最后一个元素在把老数组删掉。
  5. 这个期间,插入只往新数组加。
  6. 这个期间,查找需要同时查新数组和老数组。

ConcurrentHashMap每个哈希桶都有一把锁,

只有两个线程访问的恰好是同一个哈希桶上的数据才出现锁冲突。

相关面试题

1.ConcurrentHashMap的读是否要加锁,为什么?

读操作没有加锁,目的是为了进一步降低冲突的概率。为了保证读到刚修改的数据,搭配了volatile关键字。

2.介绍下ConcurrentHashMap的锁分段技术?

这个是Java1.7中采取的技术。Java1.8中已经不再使用了。简单的说就是把若干个哈希桶分成一个"段",针对每个段分别加锁。

目的也是为了降低锁竞争的概率。当线程访问的数据恰好在同一个段上的时候,才触发锁竞争。

3.ConcurrentHashMap在jdk1.8做了哪些优化?

取消了分段锁,直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象)。

将原来数组+链表的实现方式改进成数组+链表/红黑树的范式,当链表较长的时候(大于等于8个元素)就转换成红黑树。

4.Hashtable和HashMap、ConcurrentHashMap之间的区别?

HashMap:线程不安全。key允许为null。

Hashtable:线程安全,使用synchronized锁Hashtable对象,效率较低,key不允许为null。

ConcurrentHashMap:线程安全。使用synchronized锁每个链表头结点,锁冲突概率低,充分利用CAS机制。优化了扩容方式。key不允许为null。

死锁

死锁是什么

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

举个栗子理解死锁

假设你和我去餐厅吃饭,桌子上只有 1副刀叉 和 1副筷子 (资源有限)。我们各自有不同的饮食习惯:

  • 南:必须先用刀叉吃牛排,吃完牛排后才能用筷子吃面条

  • 我 :必须先用筷子吃面条,吃完面条后才能用刀叉吃牛排

死锁发生过程 :

  1. 我们同时到达餐厅,开始用餐

  2. 南 眼疾手快,先抢到了 刀叉 (持有资源A)

  3. 我 反应迅速,先抢到了 筷子 (持有资源B)

  4. 现在:

  • 南 需要 筷子 吃面条,但筷子被我拿着

  • 我 需要 刀叉 吃牛排,但刀叉被你拿着

  1. 我们都不肯放下自己手里的餐具(资源不能被强制剥夺)

  2. 结果:我们俩都只能干坐着,谁也吃不上饭------ 这就是死锁

死锁是一种严重的BUG!导致一个程序的线程"卡死",无法正常工作!

如何避免死锁

死锁产生的四个必要条件:

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

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

其中最容易破环的就是"循环等待"。

破除循环等待

最常用的一种死锁阻止技术就是锁排序。假设由N个线程尝试获取M把锁,就可以针对M把锁进行编号(1,2,3......M)。

N个线程尝试获取锁的时候,都按照固定的编号由小到大顺序来获取锁。这样就可以避免环路等待。

代码示例:

这是死锁代码

java 复制代码
public class demo32 {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
           synchronized (locker1){
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
               synchronized (locker2){
                   System.out.println("我想直接入侵那个骗我钱的服务器");
               }
           }
        });
        Thread t2 = new Thread(()->{
           synchronized (locker2){
               synchronized (locker1){
                   System.out.println("不行我要先把病毒植入他们的服务器");
               }
           }
        });
        t1.start();
        t2.start();
    }
}

下面是通过破解循环等待从而解除死锁的代码

约定好先后期locker1,在获取locker2,就不会环路等待

java 复制代码
public class demo32 {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2){
                    System.out.println("我想直接入侵那个骗我钱的服务器");
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker1){
                synchronized (locker2){
                    System.out.println("不行我要先把病毒植入他们的服务器");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

相关面试题

1.谈谈死锁是什么,如何避免死锁,避免算法?实际解决过没有?

死锁是什么

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

举个栗子理解死锁

假设你和我去餐厅吃饭,桌子上只有 1副刀叉 和 1副筷子 (资源有限)。我们各自有不同的饮食习惯:

  • 南:必须先用刀叉吃牛排,吃完牛排后才能用筷子吃面条

  • 我 :必须先用筷子吃面条,吃完面条后才能用刀叉吃牛排

死锁发生过程 :

  1. 我们同时到达餐厅,开始用餐

  2. 南 眼疾手快,先抢到了 刀叉 (持有资源A)

  3. 我 反应迅速,先抢到了 筷子 (持有资源B)

  4. 现在:

  • 南 需要 筷子 吃面条,但筷子被我拿着

  • 我 需要 刀叉 吃牛排,但刀叉被你拿着

  1. 我们都不肯放下自己手里的餐具(资源不能被强制剥夺)

  2. 结果:我们俩都只能干坐着,谁也吃不上饭------ 这就是死锁

死锁是一种严重的BUG!导致一个程序的线程"卡死",无法正常工作!

如何避免死锁

死锁产生的四个必要条件:

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

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

其中最容易破环的就是"循环等待"。

破除循环等待

最常用的一种死锁阻止技术就是锁排序。假设由N个线程尝试获取M把锁,就可以针对M把锁进行编号(1,2,3......M)。

N个线程尝试获取锁的时候,都按照固定的编号由小到大顺序来获取锁。这样就可以避免环路等待。

代码示例:

这是死锁代码

java 复制代码
public class demo32 {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
           synchronized (locker1){
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
               synchronized (locker2){
                   System.out.println("我想直接入侵那个骗我钱的服务器");
               }
           }
        });
        Thread t2 = new Thread(()->{
           synchronized (locker2){
               synchronized (locker1){
                   System.out.println("不行我要先把病毒植入他们的服务器");
               }
           }
        });
        t1.start();
        t2.start();
    }
}

下面是通过破解循环等待从而解除死锁的代码

约定好先后期locker1,在获取locker2,就不会环路等待

java 复制代码
public class demo32 {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2){
                    System.out.println("我想直接入侵那个骗我钱的服务器");
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker1){
                synchronized (locker2){
                    System.out.println("不行我要先把病毒植入他们的服务器");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

其他常见面试题

1.谈谈volatile关键字的用法?

volatile能够保证内存可见性。强制从主内存中读取数据。此时如果有其他线程修改被volatile修饰的变量,可以第一时间读取到最新的值。

2.Java多线程是如何实现数据共享的?

JVM把内存分成了这几个区域:

方法区,堆区,栈区,程序计数器。

其中堆区这个内存区域是多个线程之间共享的。只要把某个数据放到堆内存中,就可以让多个线程都能访问到。

3.Java创建线程池的接口是什么?参数LinkedBlockingQueue的作用是什么?

创建线程池主要有两种方式:

  • 通过Executors工厂类创建,创建方式比较简单,但是定制能力有限。
  • 通过ThreadPoolExecutor创建,创建方式比较复杂,但是定制能力抢。

LinkedBlockingQueue表示线程池的任务队列。用户通过submit/excute向这个任务队列中添加任务,再由线程池中的工作线程来执行任务。

4.Java线程共有几种状态?状态之间怎么切换的?

  • NEW:安排了工作,还未开始行动,新创建的线程,还没有调用start方法时处在这个状态。
  • RUNNABLE:可工作的。又可以分成正在工作中和即将开始工作。调用start方法之后,并正在CPU运行/在即将准备运行的状态。
  • BLOCKED:使用synchronized的时候,如果锁被其他线程占用,就会阻塞等待,从而进入该状态。
  • WAITING:调用wait方法会进入该状态。
  • TIMED_WAITING:调用sleep方法或者wait(超时时间)会进入该状态。
  • TERMINATED:工作完成了。当线程run方法执行完毕后,会处于这个状态。

5.在多线程下,如果对一个数进行叠加,该怎么做?

  • 使用synchronized/ReentrantLock加锁。
  • 使用AtomicInteger原子操作

6.Servlet是否是线程安全的?

Servlet本身是工作在多线程环境下。

如果在Servlet中创建了某个成员变量,此时如果有多个请求到达服务器,服务器就会多线程进行操作,是可能出现线程不安全的情况的。

7.Thread和Runnable的区别和联系?

Thread类描述了一个线程。

Runnable描述了一个任务。

在创建线程的时候需要指定线程完成的任务,可以直接重写Thread的run方法,也可以使用Runnable来描述这个任务。

8.多次start一个线程会怎么样?

第一次调用start可以成功调用。

后续在调用start会抛出java.lang.IllegalThreadStateException 异常。

9.有synchronized方法,两个线程分别同时用这个方法,请问会发生什么?

synchronized加在非静态方法上,相当于针对当前对象加锁。

如果这两个方法属于同一个实例:

线程1能够获取到锁,并执行方法。线程2会阻塞等待,直到线程1执行完毕,释放锁,线程2获取到锁之后才能执行方法内容。

如果者两个方法属于不同实例:

两者能并发执行,互不干扰。

10.进程和线程的区别?

  • 进程是包含线程的。每个进程至少有一个线程存在,即主线程。
  • 进程和进程之间不共享内存空间。同一个进程的线程之间共享同一个内存空间。
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。

总结

本文围绕 Java 并发编程展开,重点讲解了 JUC 工具包的核心组件、线程安全相关技术、死锁问题及常见面试考点,具体内容如下:

一、JUC 核心组件及用法

1. 任务相关接口与工具

  • Callable 接口:用于描述带返回值的任务,需搭配 FutureTask 使用,FutureTask 负责存储任务返回结果并处理线程阻塞等待逻辑,相比传统线程同步(synchronized+wait/notify),代码更简洁高效。
  • ReentrantLock:可重入互斥锁,与 synchronized 功能类似但更灵活,支持手动释放锁、trylock 超时等待、公平锁模式,且能通过 Condition 类实现精准线程唤醒,锁竞争激烈或需公平锁时更适用。

2. 原子类

基于 CAS 机制实现,避免加锁开销,性能更优,常见类包括 AtomicBoolean、AtomicInteger、AtomicLong 等,支持自增、自减、累加等原子操作,适用于多线程环境下的变量更新。

3. 线程池

  • 核心作用:复用线程,减少线程创建销毁的开销,提高并发处理效率。
  • 创建方式
    • 通过 Executors 工厂类(newFixedThreadPool、newCachedThreadPool 等),简单便捷但定制性弱;
    • 通过 ThreadPoolExecutor 手动创建,可灵活配置核心参数,定制化能力强。
  • 关键参数:核心线程数、最大线程数、非核心线程存活时间、任务队列、线程工厂、拒绝策略(AbortPolicy、CallerRunsPolicy 等)。
  • 工作流程:优先使用核心线程,核心线程满则入队,队列满则创建非核心线程,非核心线程超时回收,超出最大线程数则执行拒绝策略。

4. 其他同步工具

  • Semaphore(信号量):本质是资源计数器,通过 acquire ()(P 操作,计数器减 1)和 release ()(V 操作,计数器加 1)控制资源访问,支持共享锁场景(如限制并发访问数)。
  • CountDownLatch:用于等待多个子任务完成,构造方法指定任务数,子任务执行完毕调用 countDown (),主线程通过 await () 阻塞等待所有任务完成。

二、线程安全的集合类

1. 列表(List)

  • 非线程安全的 ArrayList 可通过 synchronized 手动同步、Collections.synchronizedList 包装或使用 CopyOnWriteArrayList(写时复制,读多写少场景性能优,但占用内存多、写后读存在延迟)实现线程安全。

2. 队列(Queue)

提供多种阻塞队列,如 ArrayBlockingQueue(数组实现)、LinkedBlockingQueue(链表实现)、PriorityBlockingQueue(带优先级)、TransferQueue(单元素队列),适用于多线程任务传递场景。

3. 哈希表(Map)

  • Hashtable:线程安全但效率低,通过 synchronized 锁整个对象,锁冲突概率高,扩容和 size 计算性能差。
  • ConcurrentHashMap:优化后的线程安全哈希表(Java 1.8),读操作无锁(volatile 保证内存可见性),写操作锁单个哈希桶(链表头节点),锁冲突概率低,支持 CAS 更新,扩容采用 "化整为零" 模式,效率更高。

三、死锁问题

1. 死锁定义

多个线程因相互等待对方持有的资源而无限阻塞,程序无法正常终止。

2. 死锁产生条件

需同时满足互斥使用、不可抢占、请求和保持、循环等待四个条件。

3. 避免方案

最易实现的是打破 "循环等待",采用锁排序策略:对多线程需获取的锁进行编号,所有线程按固定顺序获取锁,避免环路等待。

四、核心面试考点

  1. 线程同步方式:synchronized、ReentrantLock、Semaphore 等;
  2. synchronized 与 ReentrantLock 的区别及选型场景;
  3. AtomicInteger 的 CAS 实现原理;
  4. 信号量的作用及应用场景;
  5. ThreadPoolExecutor 参数含义及工作流程;
  6. ConcurrentHashMap 的线程安全机制及 JDK 1.8 优化点;
  7. 死锁的定义、产生条件及避免方法;
  8. volatile 关键字(内存可见性)、线程状态及切换、进程与线程的区别等基础知识点。

本文通过代码示例结合原理讲解,覆盖了 Java 并发编程的核心技术和高频面试点,为多线程开发和面试准备提供了全面参考。

相关推荐
内存不泄露2 小时前
基于Django和Vue3的文件分享平台设计与实现
后端·python·django
有追求的开发者2 小时前
别再搞混了!127.0.0.1 和 localhost 背后的秘密
后端
6***A6632 小时前
SpringSecurity+jwt实现权限认证功能
java
野生技术架构师2 小时前
Spring Boot 4.0 预览版深度解析
java·spring boot·后端
CCPC不拿奖不改名2 小时前
计算机网络:电脑访问网站的完整流程详解+面试习题
开发语言·python·学习·计算机网络·面试·职场和发展
左绍骏2 小时前
01.学习预备
android·java·学习
wanderist.2 小时前
C++输入输出的一些问题
开发语言·c++·图论
PXM的算法星球2 小时前
用 semaphore 限制 Go 项目单机并发数的一次流量控制优化实践
开发语言·后端·golang
W001hhh2 小时前
260111
java·数据库