day6-day7多线程和并发

进程

线程与进程的区别
线程是操作系统能够进行运算调度最小的单元,是进程的执行单元。线程是进程内部创建和调度的,共享进程的资源;进程具有独立的地址空间,而线程共享进程的地址空间。进程之间通信需要特殊的机制,如管道、消息队列,而线程之间通信可以通过共享内存直接访问。进程切换开销较大,涉及到地址空间的切换,而线程切换开销较小,因为线程共享地址空间和其他资源。

线程

线程(Thread)是计算机程序中的执行单元,是进程中的一个实体。

线程的创建

创建线程通过
1.继承 Thread 类:创建一个类并继承 Thread 类,重写 run() 方法作为线程的执行逻辑,并通过调用 start() 方法来启动线程。
2.实现 Runnable 接口:创建一个类实现 Runnable 接口,实现 run() 方法,并通过创建 Thread 对象,将 Runnable 对象作为参数传递给 Thread 构造函数,并调用 start() 方法启动线程。
3.实现Callable接口:创建一个类实现Callable接口,重写call()方法

线程的状态和生命周期

  1. 新建状态(New):

    • 当一个线程对象被创建时,它处于新建状态。
    • 在新建状态下,线程对象被分配了内存,但尚未开始执行。
  2. 就绪状态(Runnable):

    • 在就绪状态下,线程已经准备好执行,等待获取 CPU 资源。
    • 处于就绪状态的线程处于线程调度器的控制之下,可能会被选择执行。
  3. 运行状态(Running):

    • 当线程获得 CPU 资源并开始执行时,它进入运行状态。
    • 处于运行状态的线程正在执行其任务逻辑。
  4. 阻塞状态(Blocked):

    • 当线程等待某个条件,例如等待锁的释放,或者等待某个 I/O 操作完成时,它进入阻塞状态。
    • 处于阻塞状态的线程不会占用 CPU 资源,直到条件满足后才能进入就绪状态。
  5. 终止状态(Terminated):

    • 线程进入终止状态表示线程执行结束或者发生了异常。
    • 处于终止状态的线程不会再执行,并且释放其占用的资源。

线程的状态之间存在转换:

  • 线程从新建状态转换到就绪状态,可以通过调用线程对象的 start() 方法启动线程。
  • 线程从就绪状态转换到运行状态,由线程调度器选择执行。
  • 线程从运行状态转换到阻塞状态,可能是因为等待获取锁资源、等待 I/O 完成等。
  • 线程从阻塞状态转换到就绪状态,可能是因为获取到了锁资源、I/O 操作完成等。
  • 线程从运行状态转换到终止状态,可以是线程执行结束或者抛出了未捕获的异常。

线程的常用方法:

sleep():使当前线程休眠指定时间。
join():等待该线程执行完毕。
yield():暂停当前线程的执行,让出 CPU 资源。
interrupt():中断线程的执行。
isAlive():判断线程是否存活。

Java并发编程的基本原理和常用的并发工具类

线程池

线程池可以降低线程生命周期的系统开销问题,加快响应速度;统筹内存和CPU的使用,避免资源使用不当;可以统一管理资源,创建线程的方式有很多种,我了解的有:1、固定大小的线程池,可控制并发的线程数,超出的线程会在工作队列中等待。2、带有缓存的线程池。3、可以执行延迟任务的线程池。

线程池的创建有七个参数分别是:核心线程数,最大线程数,工作队列,线程工厂,存活时间,存活时间单位,拒绝策略
线程池的工作原理:
接收到任务,首先判断一下核心线程是否已满,如果未满则创建一个新的线程执行任务,如果核心线程已满,工作队列未满,将线程存储到工作队列当中,等待核心线程获取执行;如果工作队列已满且线程数小于最大线程数,则创建一个新的线程去处理任务;如果线程数超过了最大线程数,按照四种拒绝策略处理任务,四种拒绝策略分别是:1.提交最早的线程自己去执行该任务,2.默认拒绝策略,会抛出异常,3.直接丢弃任务,没有任何异常抛出,4,丢弃最老任务,其实就是把最早进入的工作丢掉,然后把新任务加入到工作队列当中。

线程安全

如何保证线程安全的
在多线程环境下,确保数据的一致性和避免竞态条件是非常重要的。以下是几种常用的方法和技术来实现这一目标:

  1. 使用同步机制:

    • 使用 synchronized 关键字或锁机制来保证多个线程对共享资源的互斥访问。
    • 同一时间只允许一个线程访问被 synchronized 保护的代码块或方法,从而避免竞态条件。
  2. 使用原子操作类:

    • Java 提供了一些原子操作类(如 AtomicBoolean、AtomicInteger、AtomicReference 等)来实现原子性的读取和修改操作。
    • 原子操作类提供了线程安全的操作方法,可以避免竞态条件。
  3. 使用锁机制:

    • 使用显式锁(如 ReentrantLock)来实现线程之间的互斥访问。
    • 通过显式地加锁和解锁来控制对共享资源的访问,确保同一时间只有一个线程可以访问共享资源。
  4. 使用并发容器:

    • Java 提供了一些线程安全的并发容器(如 ConcurrentHashMap、ConcurrentLinkedQueue 等),可以避免竞态条件。
    • 这些并发容器提供了原子性的操作,并采用了内部的线程安全机制,可以安全地在多线程环境下使用。
  5. 使用线程安全的类:

    • Java 提供了一些线程安全的类(如 StringBuffer、Vector 等),可以避免在多线程环境下的竞态条件问题。
    • 这些类在设计上考虑了线程安全性,并提供了相应的同步机制。
  6. 使用线程间的通信:

    • 使用等待/通知机制(如 wait()、notify()、notifyAll())来实现线程之间的协调和同步。
    • 通过等待和通知的方式,可以确保线程在特定条件下等待或唤醒,避免竞态条件的发生。

1. 锁的类型
锁分为乐观锁、悲观锁、synchronized
乐观锁是每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。
悲观锁每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻止,直到这个锁被释放
2.lock和synchronize的区别synchronized
都是解决线程安全的工具,synchronize是java中的同步关键字;而lock是J.U.C包中提供的接口
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。

2.lock和synchronize的区别

这个问题我从四个方面来回答
第一个,从功能角度来看,lock和synchronize都是java中用来解决线程安全问题的一个工具,
第二个,从特性来看,首先synchronize是java中的同步关键字;而lock是J.U.C包中提供的接口而这个接口它有很多的实现类其中就包括reentrantLock这样一个重入锁的实现,其次synchronize可以通过两种方式去控制锁的力度,一种是把synchronize关键字修饰在方法层面,另一张种是修饰在代码块上,并且我们可以通过synchronize加锁对象的生命周期来控制锁的作用范围,比如锁对象是静态对象或者类对象那么这个锁就属于全局锁;如果锁对象是普通实例对象,那么这个锁的范围取决于这个实例的生命周期。lock中锁的力度是通过它里面提供的lock( )方法和unlock( )方法来决定的,包裹在两个方法之间的代码是可以保证线程安全的,而锁的作用域取决于lock实例的生命周期。
lock比synchronize的灵活性更高,lock可以自主的去决定什么时候加锁,什么时候释放锁,只需要调用lock()和unlock()方法就可以了。同时lock还提供了非阻塞的竞争锁的方法,叫trylock(),这个方法可以通过返回true/false来告诉当前线程是否已经有其他线程正在使用锁,而synchronize由于是关键字所以他无法去实现非阻塞竞争锁的方法,另外synchronize锁的释放是被动的,就是当synchronize同步代码块执行结束以后或者代码出现异常的时候才会被释放。
最后lock提供了公平锁和非公平锁的机制,公平锁是指线程竞争锁资源时候如果已经有其他线程正在排队或者等待锁释放那么当前竞争锁的线程是无法插队的;而非公平锁就是不管是否有线程在排队等待锁他都会去尝试竞争一次锁,synchronize只提供了一种非公平锁的实现。
第三个,从性能方面来看,synchronize和lock在性能方面相差不大。在实现上会有一定的区别,synchronize引入了偏向锁,轻量级锁,重量级锁,以及锁升级的机制去实现锁的优化,而lock中用到了自旋锁的方式,去实现性能优化,以上就是我对这个问题的理解。

3.说一下死锁
当线程 A 持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
产生死锁的原因(1) 因为系统资源不足。(2) 进程运行推进的顺序不合适。(3) 资源分配不当等。
怎样防止死锁
1、尽量使用try lock( )防范,设置超时时间,超时则关闭,防止死锁
2、使用安全类concurrent 代替自己手写锁
3、减少锁的使用粒度,避免几个功能共用一把锁
4、减少同步代码块

如何证明Hashmap不是线程安全的?

要证明 HashMap 不是线程安全的,可以通过以下步骤进行测试:

  1. 创建多个线程:创建多个并发线程,每个线程都尝试在同一个 HashMap 上执行并发读写操作。

  2. 并发写入操作:在每个线程中,进行并发写入操作,即向 HashMap 中添加新的键值对。

  3. 并发读取操作:同时在其他线程中进行并发读取操作,即从 HashMap 中获取键对应的值。

  4. 观察结果:观察在多线程环境下,是否出现以下情况:

    • 线程安全性问题:是否出现数据不一致、丢失、重复等问题,即写入的数据没有被正确地读取。
    • 非原子性操作:是否出现读取到不完整或不一致的数据,即读取操作没有在写入操作完成后进行。

如果在测试中出现了以上情况,即可得出结论:HashMap 不是线程安全的。

示例代码如下所示:

import java.util.HashMap;

public class HashMapThreadSafetyTest {
    private static final int NUM_THREADS = 10;
    private static final int NUM_OPERATIONS = 10000;

    private static HashMap<Integer, Integer> hashMap = new HashMap<>();

    public static void main(String[] args) throws InterruptedException {
        // 创建多个并发线程
        Thread[] threads = new Thread[NUM_THREADS];
        for (int i = 0; i < NUM_THREADS; i++) {
            threads[i] = new Thread(new HashMapWriter());
            threads[i].start();
        }

        // 等待所有线程执行完毕
        for (int i = 0; i < NUM_THREADS; i++) {
            threads[i].join();
        }

        // 输出 HashMap 的大小
        System.out.println("HashMap size: " + hashMap.size());
    }

    static class HashMapWriter implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < NUM_OPERATIONS; i++) {
                // 并发写入操作
                hashMap.put(i, i);

                // 并发读取操作
                for (int j = 0; j < NUM_OPERATIONS; j++) {
                    Integer value = hashMap.get(j);
                    if (value == null || !value.equals(j)) {
                        System.out.println("HashMap is not thread-safe");
                        return;
                    }
                }
            }
        }
    }
}

在上述示例中,我们创建了多个并发线程,在每个线程中进行并发写入和读取操作。如果出现了数据不一致的情况,即可得出结论:HashMap 不是线程安全的。

如果创建线程池指定了核心数和最大线程数,这时来了并发请求,线程池具体是如何运作的呢?

当线程池指定了核心线程数和最大线程数,并且有并发请求到达时,线程池的运作如下:

  1. 核心线程处理请求:如果当前线程池中的线程数量小于核心线程数,线程池会创建新的线程来处理请求,直到达到核心线程数。

  2. 任务队列存储请求:如果当前线程池中的线程数量已达到核心线程数,而且任务队列还有剩余容量,线程池会将新的请求任务放入任务队列中等待执行。

  3. 创建新的线程处理请求:如果当前线程池中的线程数量已达到核心线程数,并且任务队列已满,而且当前线程池中的线程数量还未达到最大线程数,线程池会创建新的线程来处理请求。

  4. 拒绝策略处理请求:如果当前线程池中的线程数量已达到最大线程数,并且任务队列已满,而且没有空闲线程可用,线程池会根据指定的拒绝策略来处理新的请求。常见的拒绝策略包括抛出异常、直接丢弃任务、丢弃队列中最早的任务、或者在调用者线程中执行任务等。

  5. 空闲线程回收:如果线程池中的线程数量超过核心线程数,并且有空闲线程,那么超过空闲线程存活时间的空闲线程将被终止,以减少资源消耗。

总结起来,线程池在接收到并发请求时,首先会创建核心线程来处理请求,然后将多余的请求放入任务队列中。如果任务队列已满,线程池会创建新的线程来处理请求,直到达到最大线程数。如果线程池中的线程数量已达到最大线程数并且没有空闲线程可用,根据指定的拒绝策略来处理新的请求。同时,空闲线程会根据存活时间进行回收,以减少资源消耗。这样可以在并发请求的情况下,合理利用线程池中的线程资源,并根据负载情况自动调整线程数量,以提高系统的并发处理能力。

线程池创建的参数?.这些参数在线程创建的时候是如何生效的?

线程池的创建参数可以控制线程池的大小、线程的存活时间、任务队列的容量等。常见的线程池创建参数包括以下几个:

  1. 核心线程数(corePoolSize):线程池中保留的核心线程数,即初始创建的线程数量。这些线程会一直存活,即使没有任务需要执行。

  2. 最大线程数(maximumPoolSize):线程池中允许创建的最大线程数。当任务数量超过核心线程数且任务队列已满时,线程池会创建新的线程,直到达到最大线程数。超过最大线程数的任务将按照线程池的拒绝策略进行处理。

  3. 空闲线程存活时间(keepAliveTime):当线程池中的线程数量超过核心线程数,并且这些线程处于空闲状态时,空闲线程的存活时间。超过存活时间的空闲线程将被终止,以减少资源消耗。

  4. 任务队列(workQueue):用于存放等待执行的任务的队列。线程池中的线程会从任务队列中取出任务并执行。

  5. 线程工厂(threadFactory):用于创建线程的工厂类。可以自定义线程工厂来创建具有自定义属性的线程。

这些参数在线程池创建时通过构造函数或者对应的设置方法进行配置。在线程创建时,这些参数会影响线程池的行为:

  • 核心线程数决定了初始创建的线程数量,这些线程会一直存活,即使没有任务需要执行。
  • 最大线程数限制了线程池能够创建的最大线程数量,避免无限制地创建线程。
  • 空闲线程存活时间决定了空闲线程的存活时间,超过存活时间的空闲线程会被终止以减少资源消耗。
  • 任务队列用于存放等待执行的任务,当线程池中的线程空闲时,它们会从任务队列中获取任务并执行。
  • 线程工厂用于创建线程,可以自定义线程工厂来创建具有自定义属性的线程。

这些参数的设置会影响线程池的行为,例如线程的数量、任务的排队等,以满足应用程序的需求。在运行时,线程池会根据这些参数动态调整线程的创建和销毁,以及任务的执行。

你可能感兴趣的:(java,jvm,服务器)