多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题

目录

一、Callable 接口

1. Callable 的用法

2.相关面试题

二、JUC(java.util.concurrent) 的常见类

1. ReentrantLock

2.原子类

三、线程池

1.ThreadPoolExecutor

2.信号量 Semaphore

3.CountDownLatch

⚾4.相关面试题

四、线程安全的集合类

1.ArrayList

2.多线程环境使用队列

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

3.1Hashtable

3.2ConcurrentHashMap

4.相关面试题

五、死锁

1.死锁是什么

2.如何避免死锁

3.相关面试题

六、其他常见面试题


                                                       哥几个来学多线程啦~~

                                                        多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第1张图片

一、Callable 接口

1. Callable 的用法

Callable和Runnable类似,它也是一个接口,与Runnable不同的是,它将线程封装了一个“返回值”,方便程序员使用线程计算结果。Callable 描述的是带有返回值的任务,Runnable 描述的是不带返回值的任务。

代码示栗:

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

public class Demo22 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //使用匿名内部类
        Callable callable = new Callable() {//1
            @Override//重写call方法
            public Integer call() throws Exception {//2
                int ret = 0;
                for (int i = 0;i <= 1000; i++) {
                    ret += i;
                }
                return ret;
            }
        };
        //FutureTask类是用来接收callable的结果的
        FutureTask futureTask = new FutureTask<>(callable);//3
        //创建一个线程,传入futureTask对象
        Thread thread = new Thread(futureTask);
        //要启动了才能有结果
        thread.start();
        //结果是存在futureTask中的,要用get()方法获取
        System.out.println(futureTask.get());
    }
}

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第2张图片

1.由于Callable可以将线程封装一个“返回值”,那么我们就需要知道它的返回值是什么类型,因此实现的匿名内部类Callable需要是一个泛型类,尖括号<>里需要填入返回值类型

2.Callable接口里的call()方法需要返回一个值,那么它的方法名前也要有返回类型

3.FutureTask类是用来接收callable的返回结果的,因此它也需要在尖括号<>内填入返回值类型

Callable 通常需要搭配 FutureTask 来使用。 FutureTask 用来保存 Callable 的返回结果。因为 Callable 往往是在另一个线程中执行的,啥时候执行完并不确定。FutureTask 就可以负责这个等待结果出来的工作~~

2.相关面试题

介绍下 Callable 是什么?

参考上文

二、JUC(java.util.concurrent) 的常见类

1. ReentrantLock

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

ReentrantLock的用法

  • lock():加锁,获取不到锁就死等
  • trylock(超时时间):加锁,如果在一定时间内获取不到锁就放弃获取锁
  • unlock():释放锁
        ReentrantLock reentrantLock = new ReentrantLock();
        
        try {
            reentrantLock.lock();
        } finally {
            reentrantLock.unlock();
        }

ReentrantLock和synchronized的区别:

1.synchronized是一个关键字,是JVM内部实现的(大概率是基于C++实现的)。ReentrantLock 是标准库的一个类,在JVM外实现的(基于Java实现)。

2.synchronized 使用的时候不需要手动释放锁ReentrantLock 使用时需要手动释放,使用起来更加灵活,但是也更容易遗漏 unlock。

3.synchronized 在申请失败时,会死等。 ReentrantLock可以通过 trylock的方式等待一段时间就放弃

4.synchronized 是非公平锁。ReentrantLock 默认是非公平锁,可以通过构造函数传入一个true开启公平锁模式

ReentrantLock reentrantLock = new ReentrantLock(true);

构造函数:

5.更加强大的唤醒机制。synchronized 是通过 Object 的wait / notify 实现等待-唤醒,每次唤醒的是一个随机等待的线程。ReentrantLock 搭配 Condition 类实现等待-唤醒,可以更精准控制某个指定的线程。

具体示例大家可以康康大佬的这篇文章:(10条消息) Java :ReentrantLock类和Condition类_AlgebraFly的博客-CSDN博客

如何选择锁?

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

2.原子类

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

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

以 AtomicInteger 举例,常用方法有:

        AtomicInteger atomicInteger = new AtomicInteger(0);
        atomicInteger.incrementAndGet();//++atomicInteger
        atomicInteger.getAndIncrement();//atomicInteger++
        atomicInteger.decrementAndGet();//--atomicInteger
        atomicInteger.getAndDecrement();//atomicInteger--
        atomicInteger.addAndGet(3);//atomicInteger += 3

三、线程池

在多线程初阶我们学习到,线程池可以用来解决线程频繁地创建销毁而导致资源开销大的问题。

1.ThreadPoolExecutor

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

ThreadPoolExecutor的构造函数:

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第3张图片

理解 ThreadPoolExecutor 构造函数里的参数:

我们可以把线程池理解为一个公司,而线程就是一个个员工

  • corePoolSize:正式员工数量。(一旦录用,永不辞退)
  • maximunPoolSize:正式员工 + 临时员工的数量。(临时员工:公司在一段时间里会比较空闲,那么临时员工就会被辞退)
  • keepAliveTime:临时工允许的空闲时间。
  • unit:keepAliveTime的时间单位是秒、分钟,还是其他值。
  • workQueue:传递任务的阻塞队列
  • threadFactory:创建线程的工厂,参与具体的创建线程工作。
  • RejectedExecutionHandler:拒绝策略,如果任务超出公司的负荷了接下来怎么处理。
    • AbortPolicy():超过负荷,直接抛出异常
    • CallerRunsPolicy():调用者负责处理
    • DiscardOldestPolicy():丢弃队列中最老的任务
    • DiscardPolicy():丢弃新来的任务  

 使用示栗:

import java.util.concurrent.*;

public class Demo24 {
    public static void main(String[] args) {
        ExecutorService pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,
                new SynchronousQueue(), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 2; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
    }
}

执行结果

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第4张图片

 但是如果将循环体内的 i < 2 改为 i < 3,那么就会报错:

 原因就是 maximunPoolSize(正式员工的数量 + 临时员工的数量)为2,但是循环体执行了3次,也就是添加了3次线程,那么会出现错误。

线程池工作流程

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第5张图片

2.信号量 Semaphore

信号量,用来表示“可用资源的个数”,实质上是一个计数器。

理解信号量:

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

如果有车开进停车场,那么展示牌的数字就 - 1,也就是可用资源 - 1。(称为信号量的P操作)

如果有车开出停车场,那么展示牌的数字就 + 1,也就是可用资源 + 1。(称为信号量的V操作)

如果展示牌为0了,也就是可用资源为0了,那么如果再申请资源,就会报错。

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

import java.util.concurrent.Semaphore;

public class Demo25 {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(4);//创建一个有四个资源的信号量

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(Thread.currentThread().getName() + "申请资源");
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "获取到资源啦~~先睡1000毫秒");
                    Thread.sleep(1000);
                    System.out.println("释放资源");
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(runnable, "Thread - " + i);
            thread.start();
        }
    }
}

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第6张图片

 我们发现每个线程都获取到了资源,并且都释放过资源。

那么如果我们将释放资源这两行代码注释掉,那么会发生什么事呢?

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第7张图片

 结果我们发现,只有四个线程获取到了资源,而且是先申请资源的那四个,原因就是我们的资源只设置了四个,如果四个申请到资源的线程都没有释放资源,那么其他线程就不会获得资源,就会阻塞等待~~

3.CountDownLatch

CountDownLatch 就是等待多个任务全部执行结束。

就像我们跑步比赛,要等到最后一名冲过终点,才公布成绩。

import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class Demo26 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        System.out.println("10人进行跑步比赛");
        Random random = new Random();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                try {
                    int time = 1000 * random.nextInt(10);
                    Thread.sleep(time);
                    System.out.println(Thread.currentThread().getName() + "跑了" + time + "毫秒");
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(runnable);
            thread.start();
        }
        countDownLatch.await();
        System.out.println("比赛结束");
    }
}

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第8张图片

 我们在主线程使用方法来记录

 被调用了多少次,当这10次都执行完后,主线程就停止阻塞,开始执行后续代码。

⚾4.相关面试题

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

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

2) 为什么有了 synchronized 还需要 juc 下的 lock?

以 juc 的 ReentrantLock 为例:

  • synchronized 使用时不需要手动释放锁。ReentrantLock 使用时需要手动释放,使用起来更灵活。
  • synchronized 在申请锁失败时,会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃。
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁。可以通过构造方法传入一个 true 开启公平锁模式。
  • synchronized 是通过 Object 的 wait / notify 实现等待-唤醒。每次唤醒的是一个随机等待的线程。 ReentrantLock 搭配 Condition 类实现等待-唤醒,可以更精确控制唤醒某个指定的线程。

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

基于CAS实现,伪代码如下:

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
        }
        return oldValue;
    }
}

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

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

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

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

参考上文

四、线程安全的集合类

1.ArrayList

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

2)Collections.synchronizedList(new ArrayList);

synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List。

synchronizedList 的关键操作上都带有 synchronized

3) 使用 CopyOnWriteArrayList

CopyOnWrite容器即写入数据时复制容器

  • 它会在线程写入数据的时候复制一个新的容器,写入数据的时候不写入当前容器,而是写入新的容器。
  • 写入完数据之后,再将原容器的引用指向新容器。

这样做的好处就是可以并发地执行读操作,而不用加锁,因为不会向原容器写入数据。

所以CopyOnWrite容器用的一种经典的读写分离的思想,读和写使用不同容器。

优点:

在读多写少的情况下性能很高,因为不涉及到加锁。

缺点:

1.占用内存多。

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

2.多线程环境使用队列

1) ArrayBlockingQueue

基于数组实现的阻塞队列

2) LinkedBlockingQueue

基于链表实现的阻塞队列

3) PriorityBlockingQueue

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

4) TransferQueue

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

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

HashMap在多线程状态下是不安全的

要保证线程安全可以使用 Hashtable 或 ConcurrentHashMap

3.1Hashtable

Hashtable只是简单地把关键从操作加上 synchronized 关键字而已

 

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

  • 如果多线程访问同一个Hashtable就会直接造成锁冲突
  • size属性也是通过 synchronized 来控制同步,也是比较慢的
  • 一旦触发扩容,就由该线程完成整个扩容过程,这个过程涉及到大量的元素拷贝,效率会非常低。

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第9张图片

一个Hashtable只有一把锁,两个线程访问这个Hashtable任意数据都会触发锁竞争。 

3.2ConcurrentHashMap

相比于 Hash Table 做出了一系列的改进和优化,以 Java1.8为例:

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

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第10张图片

 每个ConcurrentHashMap里的桶都有有一把锁,两个线程访问这个ConcurrentHashMap同一个桶里的数据才会触发锁竞争。

4.相关面试题

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

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

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

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

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

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

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

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

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

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

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

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

五、死锁

1.死锁是什么

1)一个线程一把锁:

一个线程对一把锁加锁多次,如果这把锁不是可重入锁,那么就会死锁。

2)两个线程两把锁:

线程1获得锁A

线程2获得锁B

线程1尝试获得锁B

线程2尝试获得锁A

这样,线程1无法获得线程2未释放的锁B,线程2无法获得线程1未释放的锁A,那么就会死锁。

3)M个线程获取N把锁:

这里不得不提到经典的哲学家问题

一共有5名哲学家,每两名哲学家之间有一根筷子。哲学家只会做两件事:思考人生(思考人生的时候会放下筷子),吃饺子(吃饺子的时候需要用到两根筷子,先拿左边的筷子,再拿右边的筷子)

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第11张图片

 如果哲学家发现筷子拿不起来了,就会阻塞等待

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第12张图片

 关键在此处:如果哲学家们同时拿起左手边的筷子,再尝试拿起右边的筷子,发现右边的筷子都被占用了,且五个哲学家互不相让,那么就会导致死锁。

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第13张图片

2.如何避免死锁

死锁是一种非常严重的BUG,会使程序的线程“卡死”,无法正常运行。

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

1.互斥使用,即当资源被一个线程使用(占有时),别的线程不能使用。

2.不可抢占,资源请求者不能强制从资源占有者中夺取资源,资源只能由资源占有者主动释放。

3.请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

4.循环等待,即存在一个等待队列:P1占有P2的资源,P2占用P3的资源,P3占用P1的资源。这样就形成了一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让 死锁消失。其中最容易破坏的就是循环等待。

如何破坏循环等待:

我们给每双筷子编号,并且规定哲学家只能先拿编号小的筷子,再拿编号大的筷子:

每个哲学家都拿了一根筷子

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第14张图片

 拿第二根的时候只能拿比第一根筷子编号大的

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第15张图片

 那么此时我们发现左上角的哲学家可以拿到两根筷子:

多线程进阶(下篇)---Callable 接口、JUC(java.util.concurrent) 的常见类、线程池、线程安全的集合类、死锁、其他常见面试题_第16张图片

代码体现:

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

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

可能产生环路等待的代码:两个线程对于加锁的顺序没有约定,就容易产生环路等待。

import java.util.Hashtable;

public class Main1 {
    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread() {
            @Override
            public void run() {
                synchronized (lock1) {
                    synchronized (lock2) {
                        // do something...
                    }
                }
            }
        };
        t1.start();
        Thread t2 = new Thread() {
            @Override
            public void run() {
                synchronized (lock2) {
                    synchronized (lock1) {
                        // do something...
                    }
                }
            }
        };
        t2.start();

    }
}

不会产生环路等待的代码:约定好先获取 lock1,再获取 lock2,就不会环路等待。

import java.util.Hashtable;

public class Main1 {
    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread() {
            @Override
            public void run() {
                synchronized (lock1) {
                    synchronized (lock2) {
                        // do something...
                    }
                }
            }
        };
        t1.start();
        Thread t2 = new Thread() {
            @Override
            public void run() {
                synchronized (lock1) {//约定好先获取编号小的锁
                    synchronized (lock2) {
                        // do something...
                    }
                }
            }
        };
        t2.start();

    }
}

3.相关面试题

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

参考上文

六、其他常见面试题

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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) 进程和线程的区别?

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

你可能感兴趣的:(EE初阶,java,jvm,面试,java-ee)