三、多线程知识详解

1.线程和进程有什么区别

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

②一个线程从属于一个进程,一个进程可以包含多个线程

③一个线程挂掉,对应的进程挂掉;一个进程挂掉,不会影响其他进程。

④进程在执行时拥有独立的内存单元,多个线程共享进程的内存。

⑤进程的系统开销大于线程的开销,线程需要的系统资源较少

⑥进程和线程的通信方式不一样。

2. 进程之间的通信方式

进程间通信主要有以下7种方式:匿名管道、有名管道、信号、消息队列、共享内存、信号量、Socket

①管道/匿名管道(Pipes):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系.⽤于具有亲缘关系的⽗⼦进程之间的通信。

②有名管道(Names Pipes): (半双工)匿名管道由于没有名字,只能⽤于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据 ,有名管道以磁盘⽂件的⽅式存在,可以实现本机任意两个进程通信。有名管道的名字存在于文件系统中,内容存放在内存中。

③信号(Signal):信号是进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。如果该进程当前未处于执行状态,则该信号就有内核保存起来,直到该进程恢复执行并传递给它为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

④消息队列(Message Queuing):消息队列是消息的链表,具有写权限的进程可以按照一定的规则向消息队列中添加新数据,堆消息队列有读权限的进程则可以从消息队列中读取数据。

⑤信号量(Semaphores) :信号量是⼀个计数器,⽤于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信⽅式主要⽤于解决与同步相关的问题并避免竞争条件。 (信号量也用来解决互斥和同步问题)

对信号量的操作分为P操作和V操作,P操作是将信号量的值减1,V操作是将信号量的值加1。当信号量的值小于等于0之后,再进行P操作,当前进程或线程会被阻塞,直到另一个进程或线程执行了V操作将信号量的值增加到大于0之时,锁也是用的这种原理实现的。

⑥共享内存(Shared memory):进程间本身的内存是相互隔离的,而共享内存机制相当于给两个进程开辟了一块二者均可以访问的内存空间,这时两个进程便可以共享一些数据了,但是多进程同时占用资源会带来一些意料之外的情况,这时,往往需要用互斥锁和信号量来控制对内存空间的访问。可以说这是最有⽤的进程间通信⽅式。

⑦套接字(Sockets) : 此⽅法主要⽤于在客户端和服务器之间通过⽹络进⾏通信。用于网络中不同机器之间进程间的通信。

套接字是⽀持TCP/IP 的⽹络通信的基本操作单元,可以看做是不同主机之间的进程进⾏双向通信的端点,简单的说就是通信的两⽅的⼀种约定,⽤套接字中的相关函数来完成通信过程。

3. 线程之间的通信方式

Java中线程通信主要用以下3种方式:

①wait()、notify()、notifyAll()

如果线程之间采用synchronized来保证线程安全,则可以利用wait()、notify()、notifyAll()来实现线程通信。

wait()方法可以让当前线程释放对象锁并进入阻塞状态。notify()方法用于唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。notifyAll()用于唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。

每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列。就绪队列存储了已就绪(将要竞争锁)的线程,阻塞队列存储了被阻塞的线程。当一个阻塞线程被唤醒后,才会进入就绪队列,进而等待CPU的调度。反之,当一个线程被wait后,就会进入阻塞队列,等待被唤醒。

②await()、signal()、signalAll()

如果线程之间采用Lock来保证线程安全,则可以利用await()、signal()、signalAll()来实现线程通信。

这三个方法都是Condition接口中的方法,该接口是在Java 1.5中出现的,它用来替代传统的wait+notify实现线程间的协作,它的使用依赖于 Lock。相比使用wait+notify,使用Condition的await+signal这种方式能够更加安全和高效地实现线程间协作。

Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 。 必须要注意的是,Condition 的 await()/signal()/signalAll() 使用都必须在lock保护之内,也就是说,必须在lock.lock()和lock.unlock之间才可以使用。事实上,await()/signal()/signalAll() 与wait()/notify()/notifyAll()有着天然的对应关系。即:Conditon中的await()对应Object的wait(),Condition中的signal()对应Object的notify(),Condition中的signalAll()对应Object的notifyAll()。

③BlockingQueue

Java 5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但它的主要用途并不是作为容器,而是作为线程通信的工具。BlockingQueue具有一个特征:当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该线程被阻塞;当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该线程被阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。线程之间需要通信,最经典的场景就是生产者与消费者模型,而BlockingQueue就是针对该模型提供的解决方案。

4. 创建线程的四种方式

1.继承Thread类

2.实现Runnable接口

3.实现Callable接口

4.使用线程池创建

几种线程创建方式的优缺点

img

5. 线程中start和run的区别

start() :

它的作用是启动一个新线程。

通过Thread类中的start()方法来启动的新线程,处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行相应线程的run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run方法运行结束,此线程随即终止。start()不能被重复调用。用start方法来启动线程,真正实现了多线程运行,即无需等待某个线程的run方法体代码执行完毕就直接继续执行下面的代码。这里无需等待run方法执行完毕,即可继续执行下面的代码,即进行了线程切换。

run() :

run()就和普通的成员方法一样,可以被重复调用。

如果直接调用run方法,并不会启动新线程!程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到多线程的目的。

总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里。

6. 线程的生命周期和状态

①新建:线程对象已经创建,但是还没有调用start()方法

②就绪:线程对象调用start()方法,但是还未开始运行就是就绪状态。一旦获取CPU时间片,就开始运行。

③运行:当线程对象调用了start()方法,并且获取了CPU时间片,就是运行状态。

③阻塞:等待获取一个排它锁,如果其线程释放了锁就会结束此状态。如调用wait()方进入阻塞状态,调用sleep方进入睡眠状态。

⑥死亡:可以是线程结束任务后自己结束,或者产生了异常而结束。

7. 什么是线程死锁?如何避免死锁?

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

**死锁:**是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,这些线程都陷入无限的等待中。

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

①互斥条件:该资源在任意⼀个时刻只由⼀个线程占⽤。

②请求与保持条件:⼀个线程因请求资源⽽阻塞时,对已获得的资源保持不放。

③不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕后才释放资源。

④循环等待条件:若⼲线程之间形成⼀种头尾相接的循环等待资源关系。

如何避免线程死锁?

产⽣死锁的四个必要条件,为了避免死锁,我们只要破坏产⽣死锁的四个条件中的其中⼀个就可以了。

①破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。

②破坏请求与保持条件 :⼀次性申请所有的资源。

③破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

④破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。

避免死锁最简单的方法就是 阻止循环等待条件 ,将系统中所有的资源设置标志位、排序规定所有的进程申请资源必须以一定的顺序(升序或降序) 做操作来避免死锁

8. sleep和wait的区别

sleep():sleep 方法是属于Thread 类中的方法。它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,但是在此期间线程不会释放锁,只会阻塞线程,当指定的时间到了又会自动恢复运行状态,可中断,sleep 给其他线程运行机会时不考虑线程的优先级,高优先级和低优先级的线程都有机会执行。

wait():wait 方法是属于Object 类中的方法,wait 过程中线程会释放对象锁,只有当其他线程调用 notify()或notifyAll() 才能唤醒此线程。wait使用时必须先获取对象锁,即必须在 synchronized修饰的方法或代码块中使用,那么相应的notify方法同样必须在 synchronized 修饰的代码块中使用,如果没有在synchronized 修饰的代码块中使用时运行时会抛出IllegalMonitorStateException的异常。

9. wait()方法为什么要在while()循环中使用

wait()方法之所以要用while而不是if是因为: 当多个线程并发访问同一个资源的时候, 若消费者同时被唤醒,但是只有一个资源可用, 那么if会导致资源被用完后直接去获取资源(发生越界异常等),而while则会让每个消费者获取之前再去判断一下资源是否可用.可用则获取,不可用则继续wait住。

10. 线程池

img

11. 为什么要使用线程池

img

12. 线程池有哪些参数?每个参数的作用是什么?

一共有7个参数

img

7.时间的单位

keepAliveTime的时间单位

13. 线程池的拒绝策略和阻塞队列

拒绝策略

默认的拒绝策略:AbortPolicy

img

阻塞队列

①第一种阻塞队列是 **LinkedBlockingQueue。**对应的线程池:newSingleThreadExecutor( )和newFixedThreadPool(int n)

LinkedBlockingQueue,它的容量是 Integer.MAX_VALUE,为 231 -1 ,是一个非常大的值,可以认为是无界队列。

FixedThreadPool 和 SingleThreadExecutor 线程池的线程数是固定的,所以没有办法增加特别多的线程来处理任务,这时就需要 LinkedBlockingQueue 这样一个没有容量限制的阻塞队列来存放任务。

②第二种阻塞队列是 **SynchronousQueue。**对应的线程池是 newCachedThreadPool( )

线程池 CachedThreadPool 的最大线程数是 Integer.MAX_VALUE,可以理解为线程数是可以无限扩展的。

CachedThreadPool 和上一种线程池 FixedThreadPool 的情况恰恰相反,FixedThreadPool 的情况是阻塞队列的容量是无限的,而这里 CachedThreadPool 是线程数可以无限扩展,所以 CachedThreadPool 线程池并不需要一个任务队列来存储任务,因为一旦有任务被提交就直接转发给线程或者创建新线程来执行,而不需要另外保存它们。

我们自己创建使用 SynchronousQueue 的线程池时,如果不希望任务被拒绝,那么就需要注意设置最大线程数要尽可能大一些,以免发生任务数大于最大线程数时,没办法把任务放到队列中也没有足够线程来执行任务的情况。

③第三种阻塞队列是**DelayedWorkQueue。**对应的线程池分别是 newScheduledThreadPool (int n)和 newSingleThreadScheduledExecutor( )

这两种线程池的最大特点就是可以延迟执行任务,比如说一定时间后执行任务或是每隔一定的时间执行一次任务。

DelayedWorkQueue 的特点是内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构(堆的应用之一就是 优先级队列)。之所以线程池 ScheduledThreadPool 和 SingleThreadScheduledExecutor 选择 DelayedWorkQueue,是因为它们本身正是基于时间执行任务的,而延迟队列正好可以把任务按时间进行排序,方便任务的执行。

14. 线程池类型

线程池类型用途说明适用场景
Executors.newFixedThreadPool创建固定线程数的线程池,使用的是LinkedBlockingQueue无界队列,线程池中实际线程数永远不会变化适用于可以预测线程数量的业务中,或者服务器负载较重,对线程数有严格限制的场景
Executors.newSingleThreadExecutor创建只有一个线程的线程池,使用的是LinkedBlockingQueue无界队列,线程池中实际线程数只有一个适用于需要保证顺序执行各个任务,并且在任意时间点,不会同时有多个线程的场景
Executors.newCachedThreadPool创建可供缓存的线程池,该线程池中的线程空闲时间超过60s会自动销毁,使用的是SynchronousQueue特殊无界队列适用于创建一个可无限扩大的线程池,服务器负载压力较轻,执行时间较短,任务多的场景
Executors.newScheduledThreadPool创建可供调度使用的线程池(可延时启动,定时启动),使用的是DelayWorkQueue无界延时队列适用于需要多个后台线程执行周期任务的场景
Executors.newWorkStealingPooljdk1.8提供的线程池,底层使用的是ForkJoinPool实现,创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu核数的线程来并行执行任务适用于大耗时,可并行执行的场景

15. 线程池的状态有哪些?

RUNNING

  • 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
  • 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态;
  • 调用线程池的shutdownNow()方法,可以切换到STOP状态;

SHUTDOWN

  • 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
  • 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;

STOP

  • 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
  • 线程池中执行的任务为空,进入TIDYING状态;

TIDYING

  • 该状态表明所有的任务已经运行终止,记录的任务数量为0。
  • terminated()执行完毕,进入TERMINATED状态

TERMINATED

  • 该状态表示线程池彻底终止

16. JUC

JUC 中提供了三种常用的辅助类,通过这些辅助类可以很好的解决线程数量过多时 Lock 锁的频繁操作。这三种辅助类为:

• CountDownLatch: 减少计数

• CyclicBarrier: 循环栅栏

• Semaphore: 信号灯

下面我们分别进行详细的介绍和学习

14.1 减少计数 CountDownLatch

CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句。

• CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞

• 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程不会阻塞)

• 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行

场景: 6 个同学陆续离开教室后值班同学才可以关门。

//演示 CountDownLatch
public class CountDownLatchDemo {
//6个同学陆续离开教室之后,班长锁门
public static void main(String[] args) throws InterruptedException {

    //创建CountDownLatch对象,设置初始值
    CountDownLatch countDownLatch = new CountDownLatch(6);
    //6个同学陆续离开教室之后
    for (int i = 1; i <=6; i++) {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+" 号同学离开了教室");
            //计数  -1
            countDownLatch.countDown();
        },String.valueOf(i)).start();
    }
    //等待
    countDownLatch.await();
    System.out.println(Thread.currentThread().getName()+" 班长锁门走人了");
}

}

14.2 循环栅栏 CyclicBarrier

CyclicBarrier 看英文单词可以看出大概就是循环阻塞的意思,在使用中CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。可以将 CyclicBarrier 理解为加 1 操作

场景: 集齐 7 颗龙珠就可以召唤神龙

//集齐7颗龙珠就可以召唤神龙
public class CyclicBarrierDemo {
    //创建固定值
    private static final int NUMBER = 7;
    public static void main(String[] args) {
        //创建CyclicBarrier
        CyclicBarrier cyclicBarrier =
                new CyclicBarrier(NUMBER,()->{
                    System.out.println("*集齐7颗龙珠就可以召唤神龙");
                });
        //集齐七颗龙珠过程
        for (int i = 1; i <=7; i++) {
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+" 星龙被收集到了");
                    //等待
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

14.3 信号灯 Semaphore

Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方法获得许可证,release 方法释放许可场景: 抢车位, 6 部汽车 3 个停车位

SemaphoreDemo

//6辆汽车,停3个车位
public class SemaphoreDemo {
    public static void main(String[] args) {
        //创建Semaphore,设置许可数量
        Semaphore semaphore = new Semaphore(3);
        //模拟6辆汽车
        for (int i = 1; i <=6; i++) {
            new Thread(()->{
                try {
                    //抢占
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+" 抢到了车位");
                    //设置随机停车时间
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                    System.out.println(Thread.currentThread().getName()+" ------离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值