【JAVA EE初阶】多线程(进阶)

目录

1.常见的锁策略

[1.1 悲观锁vs乐观锁](#1.1 悲观锁vs乐观锁)

[1.2 重量级锁vs轻量级锁](#1.2 重量级锁vs轻量级锁)

[1.3 挂起等待锁vs自旋锁](#1.3 挂起等待锁vs自旋锁)

[1.4 普通互斥锁vs读写锁](#1.4 普通互斥锁vs读写锁)

[1.5 可重入锁vs不可重入锁](#1.5 可重入锁vs不可重入锁)

[1.6 公平锁vs非公平锁](#1.6 公平锁vs非公平锁)

[1.7 synchronized的优化:](#1.7 synchronized的优化:)

[1.8 相关面试题](#1.8 相关面试题)

[2.CAS比较和交换(compare and swap)](#2.CAS比较和交换(compare and swap))

[2.1 相关面试题](#2.1 相关面试题)

3.JUC中的组件

[3.1 Callable接口](#3.1 Callable接口)

[3.2 ReentrantLock可重入](#3.2 ReentrantLock可重入)

[3.3 原子类](#3.3 原子类)

[3.4 线程池](#3.4 线程池)

[3.5 信号量Semaphore](#3.5 信号量Semaphore)

[3.6 CountDownLatch](#3.6 CountDownLatch)

[3.7 相关⾯试题](#3.7 相关⾯试题)

4.线程安全的集合类

[4.1 多线程环境使用ArrayList](#4.1 多线程环境使用ArrayList)

[4.2 多线程环境使用队列](#4.2 多线程环境使用队列)

[4.3 多线程环境使用哈希表](#4.3 多线程环境使用哈希表)

[5. 面试题](#5. 面试题)


1.常见的锁策略

1.1 悲观锁vs乐观锁

不是针对某一个具体的锁,而是某个具体锁具有"悲观"特性或者"乐观"特性。

悲观:加锁的时候,预测接下来的锁竞争的情况非常激烈,需要针对这样的激烈情况额外做一些工作。

乐观:加锁的时候,预测接下来的锁竞争的情况不激烈,不需要做额外工作。

1.2 重量级锁vs轻量级锁

重量级锁,当悲观的场景下,此时要付出更多的代价(更低效)。

轻量级锁,当乐观的场景下,此时要付出的代价更小(更高效)。

1.3 挂起等待锁vs自旋锁

挂起等待锁:重量级锁的典型实现。操作系统内核级别的,加锁的时候发现竞争,就会使该线程进入阻塞状态,后续就需要内核进行唤醒。

自旋锁:轻量级锁的典型实现。应用程序级别的,加锁的时候发现竞争,一般也不是进入阻塞,而是通过忙等的形式进行等待。(乐观锁的场景,本身遇到锁竞争的概率很小,真的遇到竞争,在短时间内就能拿到锁)

JVM内部会统计每个锁竞争的激烈程度,如果竞争不激烈,此时synchronized就会按照轻量级锁(自旋);如果竞争激烈,此时synchronized就会按照重量级锁(挂起等待)。

1.4 普通互斥锁vs读写锁

synchronized不是读写锁。

多个线程读取一个数据,是本身就线程安全的。多个线程读取,一个线程修改,肯定会涉及到线程安全问题。

读写锁,确保读锁和读锁之间不是互斥的(不会产生阻塞)。写锁和读锁之间产生互斥。写锁和写锁之间产生互斥。

保证线程安全的前提下,降低锁冲突的概率,提高效率。

读写锁适合读多写少的情况。

1.5 可重入锁vs不可重入锁

synchronized是"可重入锁",一个线程一把锁,连续加锁多次,是否会死锁。出现死锁,就是可重入,否则不可重入。

  1. 锁要记录当前是哪个线程拿到的这把锁
  2. 使用计数器,记录当前加锁了多少次,在合适的时候进行解锁。

1.6 公平锁vs非公平锁

synchronized是非公平锁。

结合上⾯的锁策略, 我们就可以总结出, synchronized 具有以下特性(只考虑 JDK 1.8):

  1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
  2. 开始是轻量级锁实现, 如果锁被持有的时间较⻓, 就转换成重量级锁.
  3. 实现轻量级锁的时候⼤概率⽤到的⾃旋锁策略
  4. 是⼀种不公平锁
  5. 是⼀种可重⼊锁
  6. 不是读写锁

1.7 synchronized的优化:

锁升级:

synchronized是自适应的。自适应的过程,锁升级:无锁=>偏向锁=>自旋锁=>重量级锁。

偏向锁:本质上是懒汉模式。进行synchronized一开始不真加锁,而是简单做个标记,这个标记非常轻量,相对于加锁解锁效率高很多。如果没有其他线程竞争这个锁,最终当前线程执行到解锁代码也就是简单清楚上述标记即可。如果有其他线程来竞争,就抢先一步,在另一个线程拿到锁之前抢先拿到锁,进行加锁,偏向锁就变成轻量级锁,其他线程只能阻塞等待。

当前JVM中,只提供了"锁升级"不能"锁降级"。

锁消除:也是编译器优化的一种体现。编译器会判定当前这个代码逻辑是否真的需要加锁,如果不需要加锁,但有synchronized,就会自动把synchronized去掉。
锁粗化

加锁和解锁之间包含的代码越多(不是代码行数,而是实际执行的指令/时间),就认为锁的粒度越粗。如果包含的代码越少,就认为锁的粒度越细。

一个代码中,反复针对细粒度的代码加锁,就可能被优化成更粗粒度的加锁。

1.8 相关面试题

  1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
    悲观锁认为多个线程访问同⼀个共享变量冲突的概率较⼤, 会在每次访问共享变量之前都去真正加锁.
    乐观锁认为多个线程访问同⼀个共享变量冲突的概率不⼤. 并不会真的加锁, ⽽是直接尝试访问数据.
    在访问的同时识别当前的数据是否出现访问冲突.
    悲观锁的实现就是先加锁(⽐如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
    乐观锁的实现可以引⼊⼀个版本号. 借助版本号识别出当前的数据访问是否冲突.
  2. 介绍下读写锁?
    读写锁就是把读操作和写操作分别进⾏加锁.
    读锁和读锁之间不互斥.
    写锁和写锁之间互斥.
    写锁和读锁之间互斥.
    读写锁最主要⽤在 "频繁读, 不频繁写" 的场景中.
  3. 什么是⾃旋锁,为什么要使⽤⾃旋锁策略呢,缺点是什么?
    如果获取锁失败, ⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为⽌. 第⼀次获取锁失败, 第⼆次的尝试会在极短的时间内到来. ⼀旦锁被其他线程释放, 就能第⼀时间获取到锁.
    相⽐于挂起等待锁, 优点: 没有放弃 CPU 资源, ⼀旦锁被释放就能第⼀时间获取到锁, 更⾼效. 在锁持有时间⽐较短的场景下⾮常有⽤.
    缺点: 如果锁的持有时间较⻓, 就会浪费 CPU 资源.
  4. synchronized 是可重⼊锁么?
    是可重⼊锁.
    可重⼊锁指的就是连续两次加锁不会导致死锁.
    实现的⽅式是在锁中记录该锁持有的线程⾝份, 以及⼀个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数⾃增.

2.CAS比较和交换(compare and swap)

java 复制代码
        boolean CAS(address,expectValue,swapValue){
            //(内存地址,寄存器的值,另一个寄存器的值)
            if(&address==expectValue){
                &address=swapValue;
                return true;
            }
            return false;
        }
        //判定内存中的值和寄存器1的值是否一致,如果一致就把内存中的值和寄存器2进行交换。
        //但是由于基本上只是关心交换后内存中的值,不关心寄存器2的值,此处也可以把这样的操作理解为"赋值"

CAS是CPU的一条指令。(原子)

CAS本质上是CPU的指令,操作系统就会把这个指令进行封装,提供一些API,就可以在C++中调用,JVM又是基于C++实现的,JVM也能够使用C++调用CAS操作。

CAS最主要的用途:实现原子类。使用原子类的目的就是为了避免加锁。通过CAS能够确保性能,同时保证线程安全。

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class Demo39 {
    //使用原子类代替int

    private static AtomicInteger count = new AtomicInteger(0);
    //原子类基于CAS实现
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            for(int i=0;i<50000;i++){
                count.incrementAndGet();
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<50000;i++){
                count.incrementAndGet();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count.get());
    }
}

原子类,专有名词,atomic这个包里的类,synchronized保证一个修改的原子性,和"原子类"无关。

基于CAS实现自旋锁

java 复制代码
    public class SpinLock{
        private Thread owner=null;//如果为null,锁是空闲的。如果非null,锁已经被某个线程占有了
        public void lock(){
            while(!CAS(this.owner,null,Thread.currentThread())){
                //加锁操作中,需要判定锁是否被人占用
                //如果未被人占用就把当前线程的引用设置到owner中
                //如果已经被人占用就等待

                //这里就开始自旋,发现锁已经被占用,CAS不会执行交换,返回false,进入循环,进入下一次判定
                //由于循环体是空着的,整个循环速度非常快(忙等)
                //但是一旦其他线程释放锁,此时该线程就能第一时间拿到这里的锁
            }
        }
        public void unlock(){
            this.owner=null;//引用赋值操作,本身就是原子指令
        }
    }

CAS的典型缺陷:ABA问题

使用CAS能够进行线程安全的编程,核心是先比较"相等",内存和寄存器是否相等。(这里本质上在判定是否有其他线程插入进来做了一些修改)

认为如果发现这里寄存器和内存的值一致,就可以认为是没有线程穿插过来修改,因此接下来的修改操作就是线程安全的。本来判定内存的值是否是A,发现果然是A,说明没有其他线程修改过。但是实际上,可能存在一种情况,另一个线程把内存从A修改成B,又从B修改为A。

2.1 相关面试题

  1. 讲解下你⾃⼰理解的 CAS 机制
    全称 Compare and swap, 即 "⽐较并交换". 相当于通过⼀个原⼦的操作, 同时完成 "读取内存, ⽐较是否相等, 修改内存" 这三个步骤. 本质上需要 CPU 指令的⽀撑.
  2. ABA问题怎么解决?
    给要修改的数据引⼊版本号. 在 CAS ⽐较数据当前值和旧值的同时, 也要⽐较版本号是否符合预期. 如果发现当前版本号和之前读到的版本号⼀致, 就真正执⾏修改操作, 并让版本号⾃增; 如果发现当前版本号⽐之前读到的版本号⼤, 就认为操作失败

3.JUC中的组件

3.1 Callable接口

和Runnable接口并列关系

java 复制代码
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Demo40 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //此处的Callable只是定义了一个"带有返回值"的任务
        //并没有真正在执行,执行还需要搭配Thread对象
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i <= 10; i++) {
                    sum += i;
                }
                return sum;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        //Thread本身不提供获取结果的方法,需要凭FutureTask对象来拿到结果
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
        //get操作就是获取到FutureTask的返回值,这个返回值来自于Callable的call方法
    }
}

创建线程的写法:

  1. 继承Thread(定义单独的类/匿名内部类)
  2. 实现Runnable(定义单独的类/匿名内部类)
  3. lambda
  4. 实现Callable(定义单独的类/匿名内部类)
  5. 线程池 ThreadFactory

3.2 ReentrantLock可重入

和synchronized是并列关系。

java 复制代码
import java.util.concurrent.locks.ReentrantLock;

public class Demo42 {
    private static int count=0;
    public static void main(String[] args) {
        ReentrantLock locker = new ReentrantLock();
        Thread t1=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();
                    }
                }
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

ReentrantLock和synchronized的区别:

  1. synchronized是关键字(内部实现是JVM,内部通过C++实现),ReentrantLock标准库的类(基于JAVA实现)

  2. synchronized通过代码块控制加锁解锁,不需要手动释放锁,ReentrantLock需要lock/unlock方法,需要注意unlock不被调用的问题,需要手动释放锁

  3. ReentrantLock除了lock,unlock之外,还提供了一个方法tryLock() ,不会阻塞。加锁成功后返回true,加锁失败返回false.调用者判定返回值决定接下来怎么做。可以设置超时时间,等待时间达到超时时间再返回true/false

  4. ReentrantLock提供了公平锁的实现,默认是非公平的

    java 复制代码
    ReentrantLock locker = new ReentrantLock(true);//公平锁
  5. ReentrantLock搭配的等待通知机制是Condition类,可以更精确控制唤醒某个指定的线程,相比wait notify来说功能更强大

如何选择使⽤哪个锁?

  1. 锁竞争不激烈的时候, 使⽤ synchronized, 效率更⾼, ⾃动释放更⽅便.
  2. 锁竞争激烈的时候, 使⽤ ReentrantLock, 搭配 trylock 更灵活控制加锁的⾏为, ⽽不是死等.
  3. 如果需要使⽤公平锁, 使⽤ ReentrantLock

3.3 原子类

原子类内部使用CAS实现,性能要比加锁实现i++高很多,原子类有AtomicInteger,AtomicLong

3.4 线程池

3.5 信号量Semaphore

能够协调多个线程之间的资源分配

信号量表示的是"可用资源的个数",申请一个资源(P操作),计数器-1,释放一个资源(V操作),计数器+1。计数器为0,继续申请就会阻塞等待。

信号量的一个特殊情况:初始值为1的信号量,取值要么是1要么是0(二元信号量),等价于"锁",普通的信号量相当于锁的广泛推广。

如果是普通的N个信号量,就可以限制同时有多少个线程来执行某个逻辑。

java 复制代码
import java.util.concurrent.Semaphore;

public class Demo44 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        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();
                        count++;
                        semaphore.release();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

3.6 CountDownLatch

使用多线程,经常把一个大的任务拆分成多个任务,使用多线程执行这些子任务提高程序的效率。

衡量子任务全部完成:

  1. 构造方法指定参数,描述拆分成多少个任务
  2. 每个任务执行完毕之后,都调用一次countDown方法
  3. 主线程中调用await方法,等待所有任务执行完毕。await就会返回/阻塞等待
java 复制代码
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo45 {
    public static void main(String[] args) throws InterruptedException {
        //把整个任务拆成10个部分,每个部分视为一个"子任务"
        //可以把10个子任务丢到线程池,也可以安排10个独立的线程执行

        CountDownLatch latch = new CountDownLatch(10);
        //构造方法中传入的10表示任务的个数,CountDownLatch用于阻塞主线程,直到所有的子任务都执行完毕
        ExecutorService exec = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            int id=i;
            exec.submit(()->{
                System.out.println("子任务开始执行"+id);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("子任务结束执行"+id);
                latch.countDown();
            });
        }
        latch.await();//阻塞等待所有的任务结束
        System.out.println("所有任务执行完毕");
        exec.shutdown();
    }
}

3.7 相关⾯试题

  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 机制. 伪代码如下:
java 复制代码
class AtomicInteger {
     private int value;
     public int getAndIncrement() {
         int oldValue = value;
         while ( CAS(value, oldValue, oldValue+1) != true) {
             oldValue = value;
         }
         return oldValue;
     }
}
  1. 信号量听说过么?之前都⽤在过哪些场景下?
    信号量, ⽤来表⽰ "可⽤资源的个数". 本质上就是⼀个计数器.
    使⽤信号量可以实现 "共享锁", ⽐如某个资源允许 3 个线程同时使⽤, 那么就可以使⽤ P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进⾏ P 操作就会阻塞等待, 直到前⾯的线程执⾏了 V 操作.

4.线程安全的集合类

4.1 多线程环境使用ArrayList

  1. 自己使用同步机制(synchronized或者ReentrantLock)自行加锁,分析清楚把哪些代码打包到一起成为一个"原子"操作
  2. Collections.synchronizedList(new ArrayList),返回的List的各种关键方法都是带有synchronized,类似于Vector,Hashtable,StringBuffer
  3. 使用CopyOnWriteArrayList,多线程读取,复制过程中如果其他线程在读,就直接读取旧版本的数据,虽然复制过程不是原子的(消耗一段时间),由于提供了旧版本的数据,不影响其他线程读取。新版本数组复制完毕之后,直接进行引用的修改,引用的赋值是"原子"。确保读取过程中,要么读到的是旧版数据,要么读到的是新版数据,不会读到"修改一半"的数据。
    1. 这个过程没有加锁,不会产生阻塞
    2. 有明显的缺点:数组很大,非常低效;如果多个线程同时修改容易出问题

4.2 多线程环境使用队列

  1. ArrayBlockingQueue基于数组实现的阻塞队列
  2. LinkedBlockingQueue基于链表实现的阻塞队列
  3. PriorityBlockingQueue基于堆实现的带优先级的阻塞队列
  4. TransferQueue最多只包含一个元素的阻塞队列

4.3 多线程环境使用哈希表

HashMap本身不是线程安全的。

Hashtable是线程安全的(给各种public 方法都加synchronized),此时任意两个线程,访问任意的两个不同元素都会产生锁竞争。

如果修改的两个元素在不同链表上,本身就不涉及线程安全问题(修改不同变量);

如果修改同一个链表上的两个元素,可能有线程安全问题,比如把这两个元素插入到同一个元素后面,就可能产生竞争。

给每一个链表加上不同的锁(针对不同的锁对象加锁),不会产生锁竞争(不会阻塞)。Java中任意一个对象都可以作为锁对象,在这个逻辑中不需要额外创建对象作为锁,直接使用每个链表的头结点作为synchronized的锁对象即可。

在多线程环境下通常使用ConcurrentHashMap替代Hashtable

ConcurrentHashMap核心优化点:

  1. 按照桶级别进行加锁,而不是给整个哈希加一个全局锁,把锁整个表优化成锁桶(用每个链表的头结点作为锁对象),大大降低了锁冲突的概率
  2. 充分利用CAS特性,使用原子类针对size进行维护,避免出现重量级锁的情况
  3. 优化了扩容方式:化整为零。针对哈希扩容(意味着需要创建更大的数组,把旧哈希表中的所有元素复制到新的哈希中,元素多耗时长),一次复制完所有的元素比较耗时,需要多次的put/get来完成

5. 面试题

  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.
  5. 谈谈 volatile关键字的⽤法?
    volatile 能够保证内存可⻅性. 强制从主内存中读取数据. 此时如果有其他线程修改被 volatile 修饰的变量, 可以第⼀时间读取到最新的值.
  6. Java多线程是如何实现数据共享的?
    JVM 把内存分成了这⼏个区域:
    ⽅法区, 堆区, 栈区, 程序计数器.
    其中堆区这个内存区域是多个线程之间共享的.
    只要把某个数据放到堆内存中, 就可以让多个线程都能访问到.
  7. Java创建线程池的接⼝是什么?参数 LinkedBlockingQueue 的作⽤是什么?
    创建线程池主要有两种⽅式:
    1.通过 Executors ⼯⼚类创建. 创建⽅式⽐较简单, 但是定制能⼒有限.
  8. 通过 ThreadPoolExecutor 创建. 创建⽅式⽐较复杂, 但是定制能⼒强.
    LinkedBlockingQueue 表⽰线程池的任务队列. ⽤⼾通过 submit / execute 向这个任务队列中
    添加任务, 再由线程池中的⼯作线程来执⾏任务.
  9. Java线程共有⼏种状态?状态之间怎么切换的?
    NEW: 安排了⼯作, 还未开始⾏动. 新创建的线程, 还没有调⽤ start ⽅法时处在这个状态.
    RUNNABLE: 可⼯作的. ⼜可以分成正在⼯作中和即将开始⼯作. 调⽤ start ⽅法之后, 并正在 CPU 上运⾏/在即将准备运⾏ 的状态.
    BLOCKED: 使⽤ synchronized 的时候, 如果锁被其他线程占⽤, 就会阻塞等待, 从⽽进⼊该状态.
    WAITING: 调⽤ wait ⽅法会进⼊该状态.
    TIMED_WAITING: 调⽤ sleep ⽅法或者 wait(超时时间) 会进⼊该状态.
    TERMINATED: ⼯作完成了. 当线程 run ⽅法执⾏完毕后, 会处于这个状态.
  10. 在多线程下,如果对⼀个数进⾏叠加,该怎么做?
    使⽤ synchronized / ReentrantLock 加锁
    使⽤ AtomInteger 原⼦操作.
  11. Servlet是否是线程安全的?
    Servlet 本⾝是⼯作在多线程环境下.
    如果在 Servlet 中创建了某个成员变量, 此时如果有多个请求到达服务器, 服务器就会多线程进⾏操
    作, 是可能出现线程不安全的情况的.
  12. Thread和Runnable的区别和联系?
    Thread 类描述了⼀个线程.
    Runnable 描述了⼀个任务.
    在创建线程的时候需要指定线程完成的任务, 可以直接重写 Thread 的 run ⽅法, 也可以使⽤Runnable 来描述这个任务.
  13. 多次start⼀个线程会怎么样
    第⼀次调⽤ start 可以成功调⽤.
    后续再调⽤ start 会抛出 java.lang.IllegalThreadStateException 异常
  14. 有synchronized两个⽅法,两个线程分别同时⽤这个⽅法,请问会发⽣什么?
    synchronized 加在⾮静态⽅法上, 相当于针对当前对象加锁.
    如果这两个⽅法属于同⼀个实例: 线程1 能够获取到锁, 并执⾏⽅法. 线程2 会阻塞等待, 直到线程1 执⾏完毕, 释放锁, 线程2 获取到锁之后才能执⾏⽅法内容.
    如果这两个⽅法属于不同实例: 两者能并发执⾏, 互不⼲扰.
  15. 进程和线程的区别?
    进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程。
    进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间.
    进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位。
相关推荐
yuezhilangniao10 分钟前
关于开发语言的一些效率 从堆栈角度理解一部分c java go python
java·c语言·开发语言
码luffyliu19 分钟前
Java NIO 核心原理与秋招高频面试题解析
java·nio
一只叫煤球的猫20 分钟前
⚠️ 不是危言耸听,SpringBoot正在毁掉Java工程师
java·spring boot·spring
vvilkim1 小时前
深入理解Java访问修饰符:封装的艺术
java·开发语言
張萠飛1 小时前
生产环境Tomcat运行一段时间后,如何测试其性能是否满足后续使用
java·tomcat
Hurry61 小时前
web应用服务器tomcat
java·前端·tomcat
hqxstudying2 小时前
java分布式定时任务
java·开发语言·分布式
小猪咪piggy2 小时前
【JavaEE】(10) JavaEE 简介
java·spring·java-ee
yangmf20403 小时前
Easysearch 冷热架构实战
java·大数据·elasticsearch·搜索引擎