java多线程(下)

目录

⭐常见的锁策略

悲观锁vs乐观锁

读写锁

重量级锁vs轻量级锁

自旋锁

公平锁vs非公平锁

可重入锁vs不可重入锁

独占锁vs共享锁

⭐相关面试题

⭐CAS

什么是CAS

CAS实现原理

CAS存在的问题(ABA问题)

如何解决

相关面试题

自旋锁

⭐Synchronized原理

基本原理

锁升级

偏向锁

轻量级锁

重量级锁

优化

锁消除

锁粗化

相关面试题

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

⭐原子性的并发包

ReentrantLock

⭐ReentrantLock 和 synchronized 的区别

如何选择使用哪个锁

信号量

方法

代码示例

CountDownLatch

方法

代码示例

相关面试题

线程安全的集合类

多线程环境使用ArrayList

多线程环境使用队列

多线程环境使用哈希表

相关面试题

死锁

什么是死锁

如何避免死锁

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

破坏循环等待

其他常见面试问题


👉  多线程(上)👈

⭐常见的锁策略

悲观锁vs乐观锁

悲观锁

以悲观的心态看待线程冲突,总是假设最坏的情况,每次拿数据都会认为别人会修改,所以每次操作数据的时候都会加锁

乐观锁

以乐观的心态看待线程冲突,假设数据一般情况下不会产生并发冲突,所以每次都不加锁,在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做

Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略.

读写锁

读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读锁.

ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReadLock readlock = readWriteLock.readLock();
WriteLock writeLock = readWriteLock.writeLock();
//写操作
writeLock.lock();
writeLock.unlock();
//读操作
readLock.lock();
readLock.unlock();

满足,读读并发,读写、写写都是互斥,适用于读多写少

Synchronized 不是读写锁

重量级锁vs轻量级锁

锁的核心特性 "原子性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的.

1.CPU 提供了 "原子操作指令".

2.操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.

3.JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.

重量级锁

加锁机制重度依赖了 OS 提供了 mutex

大量的内核态用户态切换很容易引发线程的调度

轻量级锁

加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成. 实在搞不定了, 再使用 mutex.

少量的内核态用户态切换.不太容易引发线程调度.

synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁.

自旋锁

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来

一般搭配乐观锁一起实现

优点:

一旦锁被释放, 就能第一时间获取到锁.

缺点:

如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源.

synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的

do{
    获取变量的值
}while(!compareAndSwap(变量的值))

公平锁vs非公平锁

公平锁:

遵守 "先来后到"

优点:效率高

缺点:可能出线程饥饿

非公平锁:

不遵守 "先来后到"

可重入锁vs不可重入锁

允许同一个线程多次获取同一把锁。也叫递归锁。

JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的

而 Linux 系统提供的 mutex 是不可重入锁.

独占锁vs共享锁

独占锁

1.synchronized加锁

2.lock加锁

共享锁

多个线程使用共享的锁,满足一定条件,就可以并发并行的执行,不满足条件,就需要等待

读锁:共享锁

写锁:独占锁

⭐相关面试题

1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?

悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁.

乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.

悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.

乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突.

2. 介绍下读写锁?

读写锁就是把读操作和写操作分别进行加锁.

读锁和读锁之间不互斥.

写锁和写锁之间互斥

.写锁和读锁之间互斥.

读写锁最主要用在 "频繁读, 不频繁写" 的场景中

3.什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.

相比于挂起等待锁,

优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.

缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源

4.synchronized 是可重入锁么?

是可重入锁.

可重入锁指的就是连续两次加锁不会导致死锁.

实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增.

⭐CAS

什么是CAS

Compare and Swap:比较并交换

1.比较读和写两个时间点,主存中变量的值是否相等

        读:从主存读到工作内存

        写:从工作内存写回主存

2.如果相等,交换把值写回主存

   如果不相等:不做修改

3.发挥操作的结果,如果修改成功则返回true,失败则返回false

CAS实现原理

1.java中cas利用的是unsafe这个类提供的cas方法

2.这个类的方法依赖了针对不同系统提供的汇编层面上的方法Atomic

3.这个汇编层面的方法,利用的是cpu硬件提供的lock机制保证原子性

简单来说,硬件给予了支持,软件层面才能做得到

CAS存在的问题(ABA问题)

在 t1 执行CAS操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A

如何解决

简单来说就是引入一个版本号字段进行解决

jdk提供了一个类叫AtomucStampedReference类,可以通过这个对象的方法保存我们需要修改的变量,这个对象本身也包含版本号字段

相关面试题

1. 讲解下你自己理解的 CAS 机制

全称 Compare and swap, 即 "比较并交换". 相当于通过一个原子的操作, 同时完成 "读取内存, 比较是否相等, 修改内存" 这三个步骤. 本质上需要 CPU 指令的支撑.

2. ABA问题怎么解决?

给要修改的数据引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当前版本号比之前读到的版本号大, 就认为操作失败

自旋锁

通常结合CAS一起来保证线程安全的修改变量的操作

CAS虽然是线程安全的无锁操作,但是可能修改失败,引入自旋操作,满足不停的尝试修改

对比CAS+自旋实现线程安全的无锁的++ 和 synchronized加锁保证线程安全的++

⭐Synchronized原理

基本原理

1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.

2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.

3. 实现轻量级锁的时候大概率用到的自旋锁策略

4. 是一种不公平锁

5. 是一种可重入锁

6. 不是读写锁

锁升级

偏向锁

偏向锁不是真的 "加锁", 只是给对象头中做一个 "偏向锁的标记", 记录这个锁属于哪个线程.

1.如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)

2.如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态

轻量级锁

随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).

此处的轻量级锁就是通过 CAS 来实现.

1.通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)

2.如果更新成功, 则认为加锁成功

3.如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU)

重量级锁

如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁此处的重量级锁

就是指用到内核提供的 mutex .

1.执行加锁操作, 先进入内核态.

2.在内核态判定当前锁是否已经被占用

3.如果该锁没有占用, 则加锁成功, 并切换回用户态.

4.如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.

5.经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁

优化

锁消除

一个对象,不可能有其他线程持有引用,就是线程安全的,可以不加锁

锁粗化

共享变量,可能被其他线程持有,但不停的加锁释放锁操作,jvm可以优化为第一次加锁,全部执行完再释放锁

for(int i=0;i<10000;++i){
    synchronized(synchronized线程安全.class){
        count++;    
    }
}
//优化为
synchronized(synchronized线程安全.class){
    for(int i=0;i<10000;++i){
        count++;    
    }
}
//例如
private static StringBuffer sb = new StringBuffer();
public static void main(String[] args){
    sb.append("a");
    sb.append("b");
    sb.append("c");
}

相关面试题

1. 什么是偏向锁?

偏向锁不是真的加锁, 而只是在锁的对象头中记录一个标记(记录该锁所属的线程). 如果没有其他线程参与竞争锁, 那么就不会真正执行加锁操作, 从而降低程序开销. 一旦真的涉及到其他的线程竞争,再取消偏向锁状态, 进入轻量级锁状态.

2. synchronized 实现原理 是什么?

参考上面的 synchronized 原理 章节全部内容.

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

java并发包:提供了很多多线程编程需要的线程安全且效率很高的api

⭐原子性的并发包

AtomicInteger:提供线程安全的n++,n--,++n,--n,n+=等

相关的也有AtomicLong和AtomicBoolean

package juc练习;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo {

    private static AtomicInteger count = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    count.getAndIncrement();
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    count.getAndIncrement();
                }
            }
        });
        t1.start();
        t2.start();
        //main线程等待t1,t2都执行完
        t1.join();
        t2.join();
        //看看count的值
        System.out.println(count);
    }
}

原理是自旋+CAS

ReentrantLock

是一种可重入锁,也是独占锁(synchronized也是)

读写锁也是Lock体系中的一员

ReentrantLock 的用法:

lock(): 加锁, 如果获取不到锁就死等.

trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.

unlock(): 解锁

ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    // working
} finally {
    lock.unlock()
}

⭐ReentrantLock 和 synchronized 的区别

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

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

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

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

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

如何选择使用哪个锁

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

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

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

信号量

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

方法

代码示例

有限资源的并发执行

package juc练习;

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {

    private static Semaphore S = new Semaphore(5);

    public static void main(String[] args) {
        //模拟停车场同时停车,但不能超出有限的车位数量
        for (int i = 0; i < 20; i++) {
            final int j = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //获取一个车位
                        S.acquire();
                        //停车以后做其他事情
                        System.out.println(j);
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        //出停车场,归还车位
                        S.release();
                    }
                }
            }).start();
        }
    }
}

多个线程并发执行,需要全部执行完,某个线程再执行后续的代码

package juc练习;

import java.util.concurrent.Semaphore;

public class SemaphoreDemo2 {

    private static Semaphore S = new Semaphore(0);

    public static void main(String[] args) throws InterruptedException {
        //模拟停车场同时停车,但不能超出有限的车位数量
        for (int i = 0; i < 5; i++) {
            final int j = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(j);
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        S.release();
                    }
                }
            }).start();
        }
        S.acquire(5);
        System.out.println("main");
    }
}

CountDownLatch

同时等待 N 个任务执行结束.

方法

代码示例

package juc练习;

import java.util.concurrent.CountDownLatch;

public class LatchDemo {

    private static CountDownLatch C = new CountDownLatch(6);

    public static void main(String[] args) throws InterruptedException {
        //模拟停车场同时停车,但不能超出有限的车位数量
        for (int i = 0; i < 5; i++) {
            final int j = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(j);
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        C.countDown();
                    }
                }
            }).start();
        }
        C.await();
        System.out.println("main");
    }
}

相关面试题

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

synchronized, ReentrantLock, Semaphore 等都可以用于线程同步.

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

以juc 的 ReentrantLock 为例,

1.synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,

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

3.synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个true 开启公平锁模式.

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

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

基于CAS的机制

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

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

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

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

参考上面的 ThreadPoolExecutor 章节

线程安全的集合类

多线程环境使用ArrayList

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

2. Collections.synchronizedList(传入一个list对象)

        内部都是synchronized保证线程安全,但是效率不高

3.使用 CopyOnWriteArrayList

        写时复制的list

        写操作时,是复制一个新的容器,往新容器中写元素,旧的容器就还可以进行并发并行的读

        读读/读写并发,效率高,但是空间占用多,新的数据不一定被第一时间读取到

多线程环境使用队列

1. ArrayBlockingQueue

        基于数组实现的阻塞队列

2. LinkedBlockingQueue

        基于链表实现的阻塞队列

3. PriorityBlockingQueue

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

4. TransferQueue

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

多线程环境使用哈希表

HashMap 本身不是线程安全的.

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

Hashtable

ConcurrentHashMap

(1)Hashtable

底层数据结构:数组+链表

特性:所有方法都是synchronized加锁保证线程安全

缺点:效率低

⭐(2)ConcurrentHashMap:并发的hashmap

jdk1.8底层数据结构:数组+链表/红黑树

特性:

读操作:使用volatile

写操作:锁Node

(1)使用key对象的hashcode,基于hashmao中的哈希函数就可以计算出数组的索引位置

(2)如果这个位置没有元素,那就直接放进去

        CAS+自旋的方式(因为此时是数组位置没有元素,做插入操作,线程冲突几率小)

(3)如果这个位置有元素,遍历链表,通过equals判断是否相等

        synchronized加锁

(4)equals相等就替换,不相等就插入链表

(5)如果插入了元素,有可能会发生扩容

扩容:

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

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

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

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

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

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

jdk1.7底层数据结构:基于数组+链表

实现方式:分段锁

使用segment分段,加锁的方式是ReentrantLock加锁,加锁的对象是一个分段(几个元素)效率没有1.8高

相关面试题

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

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

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

这个是 Java1.7 中采取的技术. Java1.8 中已经不再使用了. 简单的说就是把若干个哈希桶分成一个"段" (Segment), 针对每个段分别加锁.目的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争.

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

取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象).将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于 8个元素)就转换成红黑树

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

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

死锁

什么是死锁

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

简单来说,就是大家都占有别人的锁,但是又都无限期的等待别人的锁释放

package juc练习;

public class 死锁 {
    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();

        Thread t1 = new Thread() {
            @Override
            public void run() {
                try {
                    synchronized (lock1) {
                        Thread.sleep(1000);
                        synchronized (lock2) {
                            // do something...
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                try {
                    synchronized (lock2) {
                        Thread.sleep(1000);
                        synchronized (lock1) {
                            // do something...
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t2.start();
    }
}

如何避免死锁

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

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

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

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

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

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

破坏循环等待

最常用的一种死锁阻止技术就是锁排序.

假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号(1, 2, 3...M).N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待.

简单来说就是大家根据一定的顺序来拥有锁

其他常见面试问题

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

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

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

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

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

其中堆区这个内存区域是多个线程之间共享的.

只要把某个数据放到堆内存中, 就可以让多个线程都能访问到.

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

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

1.Executors 工厂类创建. 创建方式比较简单, 但是定制能力有限.

2.通过 ThreadPoolExecutor 创建. 创建方式比较复杂, 但是定制能力强.

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

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

1.NEW: 安排了工作, 还未开始行动. 新创建的线程, 还没有调用 start 方法时处在这个状态.

2.RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作. 调用 start 方法之后, 并正在CPU 上运行/在即将准备运行 的状态.

3.BLOCKED: 使用 synchronized 的时候, 如果锁被其他线程占用, 就会阻塞等待, 从而进入该状态.

4.WAITING: 调用 wait 方法会进入该状态.

5.TIMED_WAITING: 调用 sleep 方法或者 wait(超时时间) 会进入该状态.

6.TERMINATED: 工作完成了. 当线程 run 方法执行完毕后, 会处于这个状态

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

1.使用 synchronized / ReentrantLock 加锁

2.使用 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. 进程和线程的区别?

进程是包含线程的. 每个进程至少有一个线程存在,即主线程。

进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.

进程是系统分配资源的最小单位,线程是系统调度的最小单位

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值