Java并发体系知识点大总结

实现多线程的方法

  • 根据Oracle官方文档,实现多线程的方法只有两种
    一、实现Runnable接口,重写run,运行start()
    二、继承Thread类,重写run,运行start()

准确地讲,实现多线程只有一种方式,构建一个Thread类。而实现线程执行单元有种方式,实现Runnable接口和继承Thread类。

两种方式的本质:

实现Runnable接口:底层调用了target.run()
继承Thread类:重写了整个run方法

Runnable和Thread的区别

Java不允许多继承,实现runnable接口可以再继承其他类,而Thread不行
Runnable可以实现多个相同的程序代码的线程去共享一个资源,Thread也可以,但从Thread的源码可以看到,当Thread方式去实现资源共享时,实际上源码内部是将thread向下转型为了runnable,其内部依然是一runnable形式去实现资源的共享的。

线程池,collable创建线程的本质,也是新建了一个thread类




启动线程的正确方式

使用start()方法

start()和run()区别
start():

由主线程创建子线程,告诉JVM,让JVM在合适的时候启动。
方法含义:
启动新线程、顺序由JVM来分配调度
分配资源
运行线程
通过源码可知,启动新线程时,
1.先检查线程的状态,如果不为0,才运行,否则报IllegalThreadStateException
2.加入线程组
3.调用native本地方法 start0

run():

通过阅读源码,run方法并没有真正的启动线程,而是由一个main的主线程去调用run方法,这个方法和普通的方法没有区别,run方法的执行线程是主线程,并没有创建新线程。
runnable其实相对于一个task,并不具有线程的概念,如果你直接去调用runnable的run,其实就是相当于直接在主线程中执行了一个函数而已,并未开启线程去执行




如何正确停止线程

通过interrupt来通知线程停止,而不是强制停止

具体场景

通常情况下,代码执行完,线程就会停止运行。
正确方法:
使用interrupt来请求停止线程 [好处是能保证数据安全,把主动权交给被中断的线程]。

要正确停止线程,还需要请求方、被请求方、子方法被调用方互相配合。具体如何配合?在这里插入图片描述

Q:为什么volatile设置boolean是错的?
A:因为它无法长时间处理线程阻塞的情况

Q:如何处理不可中断的阻塞?
A:没有通用的解决方案,具体情况具体分析,编写代码过程中尽量选择可以中断的方法。

可以为了响应中断而抛出InterruptedException的常见方法列表
  • Object. wait()/ wait( long)/ wait( long, int)
  • Thread. sleep( long) /sleep( long, int)
  • Thread. join()/ join( long)/ join( long, int)
  • java. util. concurrent. BlockingQueue. take() /put( E)
  • java. util. concurrent. locks. Lock. lockInterruptibly()
  • java. util. concurrent. CountDownLatch. await()
  • java. util. concurrent. CyclicBarrier. await()
  • java. util. concurrent. Exchanger. exchange(V)
  • java.nio.channels.InterruptibleChannel相关方法
  • java.nio.channels.Selector的相关方法

线程的生命周期

线程的六种状态

New 新建 [start尚未执行]
Runnable 运行 [就绪和运行状态(ready)和(running)]
Blocked 阻塞 [被Synchronized所修饰的代码块/方法]
Waiting 等待 [不带参,有锁会释放]
TimedWaiting 超时等待 [计时等待 带time参数]
Terminated 终止

一般习惯而言,把Blocked(被阻塞)、Waiting(等待)、Timed_waiting(计时等待)都称为阻塞状态。

lock不会主动释放锁,要手动释放
线程sleep的时候不会释放Synchronized的monitor,sleep时间到了,正常结束或抛异常,才会释放锁


wait/notify/notifyAll的特点、性质

用必须先拥有monitor
只能唤醒其中一个
属于Object类
类似功能的Condition
同时持有多个锁的情况





生产者消费者模式

Q:为什么要使用这种模式?
A:使生产者和消费者解耦,生产者只需把生产好的数据往缓冲区放,消费者只管从缓冲区取。两者更好的配合[平衡生产快,消费慢的矛盾]

在这里插入图片描述

手写生产者消费者模式

package threadcoreknowledge.threadobjectclasscommonmethods;

import java.util.Date;
import java.util.LinkedList;

/**
 * 描述:     用wait/notify来实现生产者消费者模式
 */
public class ProducerConsumerModel {
    public static void main(String[] args) {
        EventStorage eventStorage = new EventStorage();
        Producer producer = new Producer(eventStorage);
        Consumer consumer = new Consumer(eventStorage);
        new Thread(producer).start();
        new Thread(consumer).start();
    }
}

class Producer implements Runnable {

    private EventStorage storage;

    public Producer(
            EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.put();
        }
    }
}

class Consumer implements Runnable {

    private EventStorage storage;

    public Consumer(
            EventStorage storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            storage.take();
        }
    }
}

class EventStorage {

    private int maxSize;
    private LinkedList<Date> storage;

    public EventStorage() {
        maxSize = 10;
        storage = new LinkedList<>();
    }

    public synchronized void put() {
        while (storage.size() == maxSize) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        storage.add(new Date());
        System.out.println("仓库里有了" + storage.size() + "个产品。");
        notify();
    }

    public synchronized void take() {
        while (storage.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("拿到了" + storage.poll() + ",现在仓库还剩下" + storage.size());
        notify();
    }
}



线程的各属性

优先级、各属性[ID,Name,IsDaemon,Priority]

守护线程特性[1.线程继承于父线程
2.被谁启动
3.不影响JVM的退出]

Q:守护线程和普通线程的区别?
A: ◆ 整体上没有区别
◆ 唯一区别在于是否影响JVM退出

守护线程主要用于给用户线程提供服务

线程优先级
默认为5,,10个级别

◆ 程序设计不应该依赖于优先级
◇ 不同操作系统的优先级映射和调度都不同
◇ 优先级会被操作系统改变





线程的未捕获异常怎么处理?

Q:为什么会捕获不到异常?

A:对于主线程来说,可以轻松发现异常,但子线程不行,如果子线程发生异常,无法用传统的方法捕获

解决方案
◆ 在每个run方法加try catch[不推荐]
◆ 利用UncaughtExceptionHandler全局捕获异常[推荐]

UncaughtExceptionHandler
package threadcoreknowledge.uncaughtexception;

import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @author Jaychan
 * @date 2020/5/8
 * @description 自己的UncaughtExceptionHandler
 */
public class  MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

    private String name;

    public MyUncaughtExceptionHandler(String name) {
        this.name = name;
    }

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        Logger logger = Logger.getAnonymousLogger();
        logger.log(Level.WARNING,"线程异常,终止了"+t.getName(),e);
        System.out.println(name+"捕获了异常"+t.getName()+"异常"+e);
    }
}

UseUncaughtExceptionHandler
package threadcoreknowledge.uncaughtexception;

/**
 * @author Jaychan
 * @date 2020/5/8
 * @description 使用自己写的UncaughtExceptionHandler
 */
public class UseOwnUncaughtExceptionHandler implements Runnable{
    public static void main(String[] args) throws InterruptedException {
        try{
            Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("捕获器1 "));

            new Thread(new UseOwnUncaughtExceptionHandler(),"My Thread -1").start();
            Thread.sleep(300);
            new Thread(new UseOwnUncaughtExceptionHandler(),"My Thread -2").start();
            Thread.sleep(300);
            new Thread(new UseOwnUncaughtExceptionHandler(),"My Thread -3").start();
            Thread.sleep(300);
            new Thread(new UseOwnUncaughtExceptionHandler(),"My Thread -4").start();
            Thread.sleep(300);
        }catch (RuntimeException e){
            System.out.println("Caught Exception.");
        }
    }


    @Override
    public void run() {
        throw new RuntimeException();
    }
}

线程性能和安全问题

Q:◆ 什么是线程安全?

不管业务中遇到怎样的多个线程访问某对象或者某方法的情况,在编写这个业务逻辑的时候,都不需要额外的做任何处理,线程也可以正常运行(得到正确的结果),就可以成为线程安全。

<当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的>



Q:◆ 什么情况下会出现线程安全问题

◇ 运行结果错误(i++)
◇ 活跃性问题:死锁、活锁、饥饿
◇ 对象发布和初始化的安全问题

Q:◆ 什么是对象发布

指的是“使对象能够在当前作用域之外的代码使用”。主要集中在public方法

◇ return 一个private对象

Q:◆什么是对象溢出

“对象逸出”指的是某不应该发布的对象被发布的情况,对象发布、创建最容易犯的错误就是对象逸出。

 ◇ 方法返回一个private对象
 ◇ 还未完成初始化(构造函数还没完全执行完毕),就把对象提供给外界
 ◇ ↑{
 1.构造函数中未初始化完毕就this赋值
 2.注册监听事件
 3.构造函数中运行线程
  }

  如何解决?

 ◇ 返回副本,而不是对象的引用
 ◇ 用工厂模式生产对象


各种需要考虑线程安全的情况

 ◆ 访问共享的静态变量或资源,会有并发风险
 [如对象的属性、静态变量、共享缓存、数据库]

 ◆ 所有依赖时序的操作,即使线程是安全的
 [read-modify-write,check-then-act]

 ◆ 不同数据存在捆绑关系
 [ip和端口]

 ◆ 使用其他类时,对方没有声明自己线程是安全时
 [hashmap]

线程带来的性能问题

 ◆ 性能问题的体现
  ◇ 服务响应慢、吞吐量低、资源消耗(例如内存)过高等
  ◇ 虽然不是结果错误,但依然危害巨大(据统计)


为什么多线程会带来性能问题

  ◆ 调度:

      频繁的上下文切换

   Q:◇ 什么是上下文切换?

上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行以下的活动:
(1)挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处,
(2)在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复,(3)跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程。

Q:◇ 何时导致密集的上下文切换?

频繁的竞争锁、IO的读写操作等

同时还会带来额外的缓存开销

◆ 协调:内存同步
涉及到Java内存模型



Thread和Object类中和和线程相关的重要方法

Q:为什么wait()需要在同步代码块内使用,而sleep()不需要?

[防止wait之后没有notify来唤醒,防止线程安全问题的发生]

这是Java设计者为了避免使用者出现lost wake up问题而搞规定的

Lost Wake Up问题
假设有两个线程,一个消费者一个生产者
生产者任务简化成将Count+1,而后唤醒消费者
消费者任务简化成将Count-1,在减到0的时候陷入睡眠

在这里插入图片描述

初始化的时候,count为0,这是消费者判断到Count小于等于0,正准备睡眠
而在这瞬间,生产者已把步骤执行完了,发出了通知(notify),这时候消费者还醒着,正准备睡眠,notify毫无效果,通知就丢掉了。紧接着,消费者进入休眠状态。

那么怎么解决这个问题呢?

现在我们应该就能够看到,问题的根源在于,消费者在检查count到调用wait()之间,count就可能被改掉了。
这就是一种很常见的竞态条件。
很自然的想法是,让消费者和生产者竞争一把锁,竞争到了的,才能够修改count的值。
于是生产者的代码是:

trylock()
count+1
notify()
releaseLock()

消费者

tryLock()
while(count<=0){
    wait();
}
count-1;
releaseLock();
Q:3.为什么wait()需要在同步代码块内使用,而sleep()不需要

wait()需要在同步代码块内使用主要让通信变得可靠,防止线程死锁,如果不把wait/notify放在同步代码块中的话,很有可能在执行wait之前,线程很有可能已经切换到了另一个执行notify的线程,这样的话有可能另一个线程先把notify都执行完毕了,那wait永远没有被唤醒了,这就导致了永久等待或者死锁的发生,这就需要把两个方法都放到同步代码块中去。
sleep()只关心自己这个线程,和其他线程关系并不大,所以并不需要同步。

Q:4.为什么线程通信的方法wait(),notify()和notifyAll()被定义在Object类里?而sleep定义在Thread类里?

因为在java中,wait(),notify()和notifyAll()属于锁级别的操作,而锁是属于某个对象的。

Q:wait方法是属于Object对象的,那调用Thread.wait会怎么样?

Thread也是个对象,这样调用也没有问题,但是Thread是个特殊的对象,线程退出的时候会自动执行notify,这样会是我们设计的流程受到干扰,所以我们一般不这么用。

Q:6.如何选择用notify还是notifyAll?

唤醒多个线程和一个线程的区别。

Q:7.notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?

没抢到锁的线程处于等待状态,等待锁的释放

Q:8.用suspend和resume来阻塞线程可以吗?为什么

这两个方法被弃用了,推荐使用wait、notify。

Q:9.讲讲sleep方法的特点?

相同点

wait和sleep方法都可以使线程阻塞,对应线程状态是Waiting或Time_Waiting。
wait和sleep方法都可以响应中断Thread.interrupt()。
不同点

wait方法的执行必须在同步方法中进行,而sleep则不需要
在同步方法里执行sleep方法时,不会释放monitor锁,但是wait方法会释放monitor锁。
sleep方法短暂休眠之后会主动退出阻塞,而没有指定时间的wait方法则需要被其他线程中断后才能退出阻塞。
wait()、notify()和notifyAll()是Object类的方法, sleep()和yeild()是Thread类的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值