0202年了你还不会java线程?

一、线程基本方法

wait、notify、notifyAll、sleep、join、yield

二、线程状态

2.1 线程等待(wait)

​ 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要主要的是调用wait()方法后,会释放对象的锁。因此wait方法一般用在同步方法或同步代码块中

2.2 线程睡眠(sleep)

​ sleep导致当前线程休眠,与wait方法不同的是sleep不会释放当前占有的锁,sleep(long)会导致线程进入TIMED-WATING状态,而wait()方法会导致当前线程进入WATING状态

2.3 线程让步(yield)

​ yield会使当前线程让出CPU执行时间片,与其他线程一起重新竞争CPU时间片。一般情况下优先级高的线程有更大的可能性成功竞争到CPU时间片,但这不是绝对的,有的操作系统对线程优先级并不敏感。

2.4 线程中断(interrupt)

​ 中断一个线程就是给这个线程一个通知信号,会影响这个线程内部的一个中断标志位。这个线程本身不会立刻改变状态(如阻塞、终止)

  • 调用interrupt()不会中断一个正在运行的线程,仅仅改变了内部维护的中断标志位
  • 若调用sleep()而使线程处于TIMED-WATING状态,此时调用interrupt()方法,会抛出InterruptedException从而是线程提前结束TIMED-WATING状态
  • 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异
    常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。
  • 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止
    一个线程thread的时候,可以调用thread.interrupt()方法,在线程的run方法内部可以
    根据 thread.isInterrupted()的值来优雅的终止线程。

2.5 Join等待其他线程终止

​ join()方法,等待其他线程终止,在当前线程中调用一个线程的join()方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待CPU的宠幸

2.5.1 为啥使用join()方法

​ 很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结构,也就是主线程需要在子进程结束后再结束,这个时候就要用到join()方法

System.out.println(Thread.currentThread().getName() + "线程运行开始!");
Thread6 thread1 = new Thread6(); 
thread1.setName("线程 B");
thread1.join();
System.out.println("这时 thread1 执行完毕之后才能执行主线程");

2.6 线程唤醒(notify)

​ Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会选择唤醒其中的一个线程,选择时任意的,并在对实现做出决定时发生,线程通过调用其中一个wait()方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,被唤醒的线程将以常规方式在该对象上主动同步的其他所有线程进行竞争。类似的还有notifyAll()会唤醒监视器上等待的所有线程

三、线程上下文切换

  • 上下文切换:利用时间片轮转的方式,CPU给每个人物都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务后,继续服务下一任务,任务的状态保存及再加载。

3.1 进程

  • 是值一个程序运行的实例。
  • 在Linux系统中,线程就是能并行运行并且与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级进程

3.2 上下文

  • 是值某一时间点CPU寄存器和程序计数器的内容

3.2 寄存器

  • 是CPU内部的数量较少但是速度很快内存(与之相对的是RAM主内存)
  • 通常对常用值(运算的中间值)的快速访问来提高计算机程序运行的速度

3.3 程序计数器

  • 是一个专用的寄存器,用于表明指令程序中CPU正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被指向的指令的位置

3.4 PCB—“切换桢”

  • 上下文切换可以认为是内核在CPU上对进程(线程)进行切换,上下文切换过程中的信息是保存在进程控制块(PCB)中的。PCB还经常被称为切换桢。信息会一直保存到CPU的内存中,知道他们被再次使用

3.5 上下文切换的活动

  • 挂起一个进程,将这个进程在CPU中的状态(上下文)存储于内存中的某处
  • 在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复
  • 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序中

3.6 引起上下文切换的原因

  • 当前执行任务的时间片用完以后,系统CPU正常调度到下一个任务
  • 当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一个任务
  • 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一个任务
  • 用户代码挂起当前任务,让出CPU时间
  • 硬件终端

四、同步锁与死锁

4.1 同步锁

  • 为了保证多个线程访问同一资源时不出错,我们需要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。
  • java中可以使用Synchronized来获取一个对象的同步锁

4.2 死锁

  • 就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放

五、线程池原理

  • 主要工作就是控制运行的线程的数据,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数据,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行
  • 特点:线程复用;控制最大并发量;管理线程

5.1 线程复用

  • 每一个Thread的类都有一个start方法。当调用start启动线程时java虚拟机会调用该类的run方法
  • 那么该类的run()方法中就是调用了Runnable对象的run()方法
  • 我们可以继承重写Thread类,在其中start方法中添加不断循环调用传递过来的Runnable对象。
  • 循环方法中不断获取Runnable是用Queue实现的,在获取下一个Runnable之前可以是阻塞的

5.2 线程池的组成

  • 线程池管理器:用于创建并管理线程池
  • 工作线程:线程池中的线程
  • 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
  • 任务队列:用于存放待处理的任务,提供一种缓冲机制

java中的线程池是通过Executor框架实现的,该框架中用到了 Executor,Executors,
ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。

ThreadPoolExecutor的构造方法如下

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable>workQueue){
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
	Executors.defaultThreadFactory(), defaultHandler);
}

其中:

  1. corePoolSize:指定了线程池中的线程数量。
  2. maximumPoolSize:指定了线程池中的最大线程数量。
  3. keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多
    次时间内会被销毁。
  4. unit:keepAliveTime 的单位。
  5. workQueue:任务队列,被提交但尚未被执行的任务。
  6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。
  7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务

5.3 拒绝策略

​ 线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也
塞不下新任务了。

  • JDK 内置的拒绝策略如下:

    • AbortPolicy : 直接抛出异常,阻止系统正常运行。

    • CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的
      任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。

    • DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再
      次提交当前任务。

    • DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢
      失,这是最好的一种方案。

      以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。

5.4 java线程池工作过程

  • 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面
    有任务,线程池也不会马上执行它们。
  • 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    • 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要
      创建非核心线程立刻运行这个任务;
    • 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
  • 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  • 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运
    行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它
    最终会收缩到 corePoolSize 的大小。

六、java阻塞队列原理

在阻塞队列中,线程阻塞有这样两种情况:

  • 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列
  • 当队列中填满数据的情况下,生产者对的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒

6.1 阻塞队列的主要方法

方法类型 抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time,unit)
检查 element() peek() 不可用 不可用
  • 抛出异常:抛出一个异常
  • 特殊值:返回一个特殊值(null或false,视情况而定)
  • 阻塞:在成功操作以前,一直阻塞线程
  • 超时:放弃前只在最大的时间内阻塞
6.1.1 插入操作
  • public abstract boolean add(E paramE):将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException。如果该元素是 NULL,则会抛出 NullPointerException 异常。

  • public abstract boolean offer(E paramE):将指定元素插入此队列中(如果立即可行且不会违反容量限制),成功时返回 true,如果当前没有可用的空间,则返回 false。

  • public abstract void put(E paramE) throws InterruptedException: 将指定元素插入此队列中,将等待可用的空间(如果有必要)

    public void put(E paramE) throws InterruptedException {
    	checkNotNull(paramE);
    	ReentrantLock localReentrantLock = this.lock;
    	localReentrantLock.lockInterruptibly();
    	try {
    		while (this.count == this.items.length)
    			this.notFull.await();//如果队列满了,则线程阻塞等待
    		enqueue(paramE);
    		localReentrantLock.unlock();
    	} finally {
    		localReentrantLock.unlock();
    	}
    }
    
  • offer(E o, long timeout, TimeUnit unit):可以设定等待的时间,如果在指定的时间
    内,还不能往队列中加入 BlockingQueue,则返回失败。

6.1.2 取数操作
  • poll(time):取走 BlockingQueue 里排在首位的对象,若不能立即取出,则可以等 time 参数规定的时间,取不到时返回 null;
  • poll(long timeout, TimeUnit unit):从 BlockingQueue 取出一个队首的对象,如果在指定时间内,队列一旦有数据可取,则立即返回队列中的数据。否则直到时间超时还没有数据可取,返回失败
  • take():取走 BlockingQueue 里排在首位的对象,若 BlockingQueue 为空,阻断进入等待状态直到 BlockingQueue 有新的数据被加入。
  • drainTo():一次性从 BlockingQueue 获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

6.2 java中的阻塞队列

  • ArrayBlockingQueue :由数组结构组成的有界阻塞队列。(公平、非公平)

    • 按照先进先出的原则对元素进行排序
    • 默认情况下不保证访问者公平的访问队列
    • 通常情况下为了保证公平性会降低吞吐量

    创建一个公平的阻塞队列:

    ArrayBlockingQueue fairQueue = new ArrayBlockQueue(1000,true);
    
  • LinkedBlockingQueue :由链表结构组成的有界阻塞队列。(两个独立锁提高并发)

    • 先见先出的原则
    • 对于生产者端和消费者端分别采用了独立的锁来控制数据同步,意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能
    • 会默认一个类似无限大小的容量(lnteger.MAX_VALUE)
  • PriorityBlockingQueue :支持优先级排序的无界阻塞队列。(compareTo排序实现优先)

    • 默认情况下元素采用自然顺序升序排列
    • 可以自定义实现compareTo()方法来指定元素进行排序规则,或者初始化 PriorityBlockingQueue 时,指定构造参数 Comparator 来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
  • DelayQueue:使用优先级队列实现的无界阻塞队列。(缓存失效、定时任务)

    • 队列使用PriorityQueue来实现。队列中的元素必须实现 Delayed 接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
    • 应用场景:
      • 缓存系统的设计:可以用 DelayQueue 保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从 DelayQueue 中获取元素时,表示缓存有效期到了。
      • 定时任务调度:使用 DelayQueue 保存当天将会执行的任务和执行时间,一旦从DelayQueue 中获取到任务就开始执行,从比如 TimerQueue 就是使用 DelayQueue 实现的
  • SynchronousQueue:不存储元素的阻塞队列。(不存储数据、可用于传递数据)

    • 每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列。

  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列

ueue 中获取元素时,表示缓存有效期到了。
- 定时任务调度:使用 DelayQueue 保存当天将会执行的任务和执行时间,一旦从DelayQueue 中获取到任务就开始执行,从比如 TimerQueue 就是使用 DelayQueue 实现的

  • SynchronousQueue:不存储元素的阻塞队列。(不存储数据、可用于传递数据)

    • 每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列。

  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列

猜你喜欢

转载自blog.csdn.net/issunmingzhi/article/details/105395375