3.多线程(进阶)(三)

简介: 3.多线程(进阶)(三)

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

5.1ReentrantLock

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

ReentrantLock 也是可重入锁. “Reentrant” 这个单词的原意就是 “可重入”

ReentrantLock [ri:'entrəntlɒk] 的用法:

  • lock(): 加锁, 如果获取不到锁就死等.
  • trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
  • unlock(): 解锁
ReentrantLock lock = new ReentrantLock(); 
-----------------------------------------
lock.lock();   
try {    
 // working    
} finally {    
 lock.unlock()    

ReentrantLock 和 synchronized 的区别:

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

如何选择使用哪个锁?

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

5.2原子类

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

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

以 AtomicInteger 举例,常见方法有

addAndGet(int delta);   i += delta;
decrementAndGet(); --i;
getAndDecrement(); i--;
incrementAndGet(); ++i;
getAndIncrement(); i++;

5.3线程池

虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效.

线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到一个 “池子” 中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了.

5.3.1ExecutorService 和 Executors

代码示例:

  • ExecutorService 表示一个线程池实例.
  • Executors 是一个工厂类, 能够创建出几种不同风格的线程池.
  • ExecutorService 的 submit 方法能够向线程池中提交若干个任务.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello");
   }
});

Executors 创建线程池的几种方式

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

Executors 本质上是 ThreadPoolExecutor 类的封装.

5.3.2ThreadPoolExecutor

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

ThreadPoolExecutor 的构造方法

理解 ThreadPoolExecutor 构造方法的参数

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

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

代码示例:

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
        void run() {
            System.out.println("hello");
       }
   });
}

5.3.3线程池的工作流程

5.4信号量 Semaphore[ˈseməfɔː®]

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

此处的信号量是 Java 把操作系统原生的信号量封装了一下

理解信号量

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

当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)

当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)

如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.

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

代码示例一

  • 创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源.
  • acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
  • 创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果.
Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable() {
    @Override
    public void run() {
        try {
            System.out.println("申请资源");
            semaphore.acquire();
            System.out.println("我获取到资源了");
            Thread.sleep(1000);
            System.out.println("我释放资源了");
            semaphore.release();
       } catch (InterruptedException e) {
            e.printStackTrace();
       }
   }
};
for (int i = 0; i < 20; i++) {
    Thread t = new Thread(runnable);
    t.start();
}

代码示例二:

import java.util.concurrent.Semaphore;
public class ThreadDemo31 {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(3);
        semaphore.acquire();
        System.out.println("执行一次 P 操作");
        semaphore.acquire();
        System.out.println("执行一次 P 操作");
        semaphore.acquire();
        System.out.println("执行一次 P 操作");
        semaphore.acquire();
        System.out.println("执行一次 2P 操作");
    }
}

因为semaphore只申请了3个资源,因此在我们第四次semaphore.acquire();申请资源时,我们线程就回处于阻塞状态。

 

在我们修改代码,在第四次semaphore.acquire();前加上semaphore.release();来释放我们的一个资源后,此时我们就相当于从3-3=0个资源到0+1=1个资源,那么我们再次调用semaphore.acquire();来申请资源打印执行一次 2P 操作即可打印成功。

5.5CountDownLatch

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

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

  • 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
  • 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
  • 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了.
public class Demo {
    public static void main(String[] args) throws Exception {
        CountDownLatch latch = new CountDownLatch(10);
        Runnable r = new Runable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(Math.random() * 10000);
                    latch.countDown();
               } catch (Exception e) {
                    e.printStackTrace();
               }
           }
       };
        for (int i = 0; i < 10; i++) {
            new Thread(r).start();
       }
   // 必须等到 10 人全部回来
        latch.await();
        System.out.println("比赛结束");
        }
}

 

异步和同步是相对的。

同步:发送请求方,自己主动的等待响应结果。

异步:发送方发完请求就不管了。等有了结果之后,对方把结果主动推送过来。

5.6相关面试题

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

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

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

以 juc 的 ReentrantLock 为例,

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

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

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

执行过程参考 2.3"CAS 有哪些应用" 章节.

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

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

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

  1. 解释一下 ThreadPoolExecutor 构造方法的参数的含义
  • corePoolSize 核心线程数,在线程池中要保证的最小线程数
  • mainumPoolSize 最大线程数,线程池中能运行的最大线程数
  • keepAliveTime 保证存活时间,当线程空闲时间,多久会回收线程。
  • unit 和 keepAliveTime配合使用,时间单位
  • workQueue 工作队列,用于存储任务在任务被执行之前
  • threadFactory 线程创建工厂,用于创建线程
  • RejectedExecutionHandler 当workQueue工作队列达到容量上限时,对任务进行的拒绝策略。

6.线程安全的集合类

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

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

6.1多线程环境使用 ArrayList

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

即自己加锁,自己使用 synchronized 或者 ReentrantLock 来是我们多线程从不安全变为安全

  1. Collections.synchronizedList(new ArrayList);

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

synchronizedList 的关键操作上都带有 synchronized

  1. 使用 CopyOnWriteArrayList

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

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

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

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

优点:

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

缺点:

  1. 占用内存较多.
  2. 新写的数据不能被第一时间读取到.

6.2多线程环境使用队列

  1. ArrayBlockingQueue

基于数组实现的阻塞队列

  1. LinkedBlockingQueue

基于链表实现的阻塞队列

  1. PriorityBlockingQueue

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

  1. TransferQueue

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

6.3多线程环境使用哈希表

HashMap 本身不是线程安全的.

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

  • Hashtable
  • ConcurrentHashMap

6.3.1Hashtable

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

 

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

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

6.3.2ConcurrentHashMap

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

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

简单来讲,我们ConcurrentHashMap不是像HashMap一样直接创建一个新的更大的数组,一次性全部搬运过去。我们ConcurrentHashMap是创建一个新的更大的数组每次只搬运一部分(不是全部!),这样做的目的就是为了我们用户的体验感,我们一点一点搬运的话程序不会等待很长时间,但是也完成了我们需要扩容的目的。

参考资料:https://blog.csdn.net/u010723709/article/details/48007881

6.3.3ConcurrentHashMap和HashMap区别

他们两个最大的区别就是ConcurrentHashMap是加一把大锁,把整个HashMap全部锁起来了,而ConcurrentHashMap是加一把一把的小锁,在不同线程上进行的操作不会相互的影响,不会导致因为只有一把锁使得整个程序效率低下。

eg:HashMap:

在对1和2这个情况下,是针对一把锁进行加锁,会有锁竞争,保证线程安全。(ConcurrentHashMap和HashMap是一样的 - 加一把大锁和加多把小锁作用相同都保证了线程安全)

但是在我们HashMap的场景下我们使用两个线程分别对3和4进行插入操作,但是因为只有一把大锁(锁的粒度太大了),所以我们只能一个一个插入,此时锁竞争就很激烈,导致我们插入效率低下。

ConcurrentHashMap:

在对1和2这个情况下,是针对一把锁进行加锁,会有锁竞争,保证线程安全。(ConcurrentHashMap和HashMap是一样的 - 加一把大锁和加多把小锁作用相同都保证了线程安全)

而在我们ConcurrentHashMap的场景下我们是给每一个链表下都加上了一把小锁(锁的粒度变小了),此时我们使用两个线程分别对3和4进行插入操作是不会有任何问题的,因为我们3和4所在的链表分别有各自对应的锁,就不会有锁竞争了,没有阻塞等待,程序就回更快。(快是相对的,不管咋样,不会比不加锁快)

上述谈到的情况,是针对 JDK1.8及其以后的情况,在1.7和之前,ConcurrentHashMap使用的是 “分段锁”

分段锁,本质上也是缩小锁的范围,从而降低锁冲突的概率,但是这种做法不够彻底。一方面粒度切分的还不够细。另一方面代码实现也更繁琐。

小知识:

HashMap存的是键值对(key-value),通过key可以唯一获取value。

HashMap 的 key 允许为 null,ConcurrentHashMap 的 key 值不允许为null。

6.4相关面试题

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

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

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

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

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

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

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

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

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

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

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

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

7.死锁

7.1死锁是什么

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

举个栗子理解死锁

滑稽老哥和女神一起去饺子馆吃饺子. 吃饺子需要酱油和醋.

滑稽老哥抄起了酱油瓶, 女神抄起了醋瓶.

滑稽: 你先把醋瓶给我, 我用完了就把酱油瓶给你.

女神: 你先把酱油瓶给我, 我用完了就把醋瓶给你.

如果这俩人彼此之间互不相让, 就构成了死锁.

酱油和醋相当于是两把锁, 这两个人就是两个线程.

为了进一步阐述死锁的形成, 很多资料上也会谈论到 “哲学家就餐问题”.

  • 有个桌子, 围着一圈 哲 ♂ 家, 桌子中间放着一盘意大利面. 每个哲学家两两之间, 放着一根筷子.

  • 每个 哲 ♂ 家 只做两件事: 思考人生 或者 吃面条. 思考人生的时候就会放下筷子. 吃面条就会拿起左 右两边的筷子(先拿起左边, 再拿起右边).

  • 如果 哲 ♂ 家 发现筷子拿不起来了(被别人占用了), 就会阻塞等待.

  • [关键点在这] 假设同一时刻, 五个 哲 ♂ 家 同时拿起左手边的筷子, 然后再尝试拿右手的筷子, 就会 发现右手的筷子都被占用了. 由于 哲 ♂ 家 们互不相让, 这个时候就形成了 死锁

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

7.2如何避免死锁

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

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

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

其中最容易破坏的就是 “循环等待”.

破坏循环等待

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

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

可能产生环路等待的代码:

两个线程对于加锁的顺序没有约定, 就容易产生环路等待.

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 , 就不会环路等待.

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();

7.3相关面试题

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

使用可重入锁

8.其他常见问题

面试题:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. 在多线程下,如果对一个数进行叠加,该怎么做?
  • 使用 synchronized / ReentrantLock 加锁
  • 使用 AtomInteger 原子操作.
  1. Servlet是否是线程安全的?

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

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

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

Thread 类描述了一个线程.

Runnable 描述了一个任务.

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

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

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

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

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

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

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

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

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

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

  1. 进程和线程的区别?

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

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

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

感谢各位读者的阅读,本文章有任何错误都可以在评论区发表你们的意见,我会对文章进行改正的。如果本文章对你有帮助请动一动你们敏捷的小手点一点赞,你的每一次鼓励都是作者创作的动力哦!😘

目录
相关文章
|
6月前
|
关系型数据库 MySQL 编译器
C++进阶 多线程相关(下)
C++进阶 多线程相关(下)
33 0
|
6月前
|
Linux API C++
|
6月前
|
安全 Java 调度
多线程【进阶版】(下)
多线程【进阶版】
34 0
|
6月前
多线程【进阶版】(上)
多线程【进阶版】
46 0
|
6月前
|
安全
多线程【进阶版】(中)
多线程【进阶版】
31 0
|
2天前
|
安全 调度
多线程入门
多线程入门
|
25天前
|
安全 算法 Java
多线程知识点总结
多线程知识点总结
35 3
|
2月前
|
安全 Java
多线程(进阶三:JUC)
多线程(进阶三:JUC)
45 0
|
6月前
|
算法 Ubuntu C++
[总结] C++ 知识点 《四》多线程相关
[总结] C++ 知识点 《四》多线程相关
|
8月前
|
存储 安全 Java
3.多线程(进阶)(二)
3.多线程(进阶)(二)
32 0