- 进程:程序的一次执行 线程:一个进程在执行的过程可以产生多个线程 多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈、本地方法栈 其中程序计数器是为了线程切换后恢复到正确的执行位置;虚拟机栈和本地方法栈是为了保证线程中的局部变量不被别的线程访问到
- 一个 Java 程序的运行是 main 线程和多个其他线程同时运行
- 创建线程
Java
创建线程有很多种方式啊,像实现Runnable、Callable
接口、继承Thread
类、创建线程池等等,不过这些方式并没有真正创建出线程,严格来说,Java
就只有一种方式可以创建线程,那就是通过new Thread().start()
创建。
而所谓的Runnable、Callable……
对象,这仅仅只是线程体,也就是提供给meme务,并不属于真正的Java
线程,它们的执行,最终还是需要依赖于new Thread()
…… - 线程的生命周期 ready-running-wait-timed_waiting-blocked-terminated
- 线程上下文切换
- thread.sleep()和object.wait()方法区别 一个是thread类的静态方法一个是object的本地方法 sleep没有释放锁,是让当前线程暂停执行,不实际到对象类,也不需要获得对象锁 wait释放了锁 ,自动释放当前线程占有的对象所 被调用后不会自动苏醒,需要noitfy()方法唤醒
- yield()让当前正在运行的线程回到可运行状态,以允许具有相同或者更高优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同或者更高优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。
- 调用
start()
方法方可启动线程并使线程进入就绪状态,直接执行run()
方法的话不会以多线程的方式执行 - 并发:多个作业在同一时间段执行 并行 :多个作业在同一时刻执行
- JVM 本身不负责线程的调度,而是将线程的调度委托给操作系统。操作系统通常会基于线程优先级和时间片来调度线程的执行,高优先级的线程通常获得 CPU 时间片的机会更多
- 单核 CPU 同时运行多个线程的效率是否会高,取决于线程的类型和任务的性质。对于单核 CPU 来说,如果任务是 CPU 密集型的,那么开很多线程会影响效率;如果任务是 IO 密集型的,那么开很多线程会提高效率。
- 线程安全:指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
- 线程死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。检测可以通过JDK 的jconcole工具进行检测
- volatile保证数据的可见性,指示JVM这个变量是共享且不稳定的,每次使用它都需要到主存中进行读取 同时,防止 JVM 的指令重排序。但是不保证原子性,因为它没有加锁
- 悲观锁:共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。乐观锁:通常多用于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量
- 如何实现乐观锁:版本号控制 CAS
- CAS:一个预期值和要更新的变量值进行比较,两值相等才会进行更新
- CAS问题:ABA问题,循环时间开销大
- synchronized 主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 可以修饰实例方法(需要获取当前对象实例的锁)、静态方法(需要获得当前类的锁)和代码块
-
synchronized
同步语句块的实现使用的是monitorenter
和monitorexit
指令,其中monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。synchronized
修饰的方法并没有monitorenter
指令和monitorexit
指令,取得代之的确实是ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。 -
synchronized
关键字和volatile
关键字是两个互补的存在volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块 。volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性 -
ReentrantLock ReentrantReadWriteLock StampedLock
-
ThreadLocal
类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal
类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal
变量名的由来。他们可以使用get()
和set()
方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。 - 每个
Thread
中都具备一个ThreadLocalMap
,而ThreadLocalMap
可以存储以ThreadLocal
为 key ,Object 对象为 value 的键值对。ThreadLocalMap
中使用的 key 为ThreadLocal
的弱引用,而 value 是强引用。所以,如果ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。使用完ThreadLocal
方法后最好手动调用remove()
方法 - 线程池:降低资源消耗、提高响应速度、提高管理性
- 用
ThreadPoolExecutor
构造函数 - 线程池的核心线程数不会被回收,
这样才会被回收
- 线程池的回收策略 ThreadPoolExecutor.AbortPolicy(默认,抛出异常拒绝新任务的处理)ThreadPoolExecutor.CallerRunsPolicy
CallerRunsPolicy
和其他的几个策略不同,它既不会抛弃任务,也不会抛出异常,而是将任务回退给调用者,使用调用者的线程来执行任务。这样可能会导致线程池阻塞,进而导致后续任务无法及时执行,严重情况下会OOM 任务持久化 ,Netty它的拒绝策略则是直接创建一个线程池以外的线程处理这些任务 - 简单来说:使用
execute()
时,该异常会导致当前线程终止,并且异常会被打印到控制台或日志文件中。未捕获异常导致线程终止,线程池创建新线程替代;使用submit()
时,异常被封装在Future
中,线程继续复用 - 命名
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(threadNamePrefix + "-%d").setDaemon(true).build(); ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);
大小:多线程会增加上下文切换成本,任务从保存到再加载就是一次上下文切换
-
如何设计一个根据任务优先级来执行的线程池 可以考虑使用
PriorityBlockingQueue
提交到线程池的任务实现Comparable
接口,并重写compareTo
方法来指定任务之间的优先级比较规则。创建PriorityBlockingQueue
时传入一个Comparator
对象来指定任务之间的排序规则(推荐)。 -
为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题
-
JMM Java内存模型 因为并发编程下,像 CPU 多级缓存和指令重排这类设计可能会导致程序运行出现一些问题 JMM 说白了就是定义了一些规范来解决这些问题,开发者可以利用这些规范更方便地开发多线程程序。
-
并发编程的三个重要特征:原子性、可见性、有序性
-
几个常用的内置线程池的作用 FixedThreadPool和SingleThreadExecutor 任务队列容量为Integer.MAX_VALUE;CachedThreadPool
CachedThreadPool
使用的是同步队列SynchronousQueue
, 允许创建的线程数量为Integer.MAX_VALUE
,可能会创建大量线程 所以都不推荐使用,线程池必须手动通过ThreadPoolExecutor
的构造函数来声明,避免使用Executors
类创建线程池,会有 OOM 风险。说白了就是:使用有界队列,控制线程创建数量。 -
用完线程池要记得关闭 其中
shutdown()
:关闭线程池,线程池的状态变为SHUTDOWN
。线程池不再接受新任务了,但是队列里的任务得执行完毕 它只是异步的通知线程池进行关闭处理awaitTermination要同步等待线程池彻底关闭后才继续往下执行