-
Java 的内存模型和线程调度机制
java内存模型(Java Memory Model,JMM)是java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异。
每一个运行在Java虚拟机里的线程都拥有自己的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问自己的线程栈。一个线程创建的本地变量对其它线程不可见,仅自己可见。即使两个线程执行同样的代码,这两个线程任然在在自己的线程栈中的代码来创建本地变量。因此,每个线程拥有每个本地变量的独有版本
-
线程的类别(daemon)分为守护线程和用户线程
- 守护线程通常用于执行不重要的任务,比如监控其他线程的运行情况,GC 线程就是一个守护线程
- setDaemon() 要在线程启动前设置,否则 JVM 会抛出非法线程状态异常(IllegalThreadStateException)
-
线程的优先级(Priority)
Java 中线程优先级的取值范围为 1~10,默认值是 5,
-
线程的六个方法
三个非静态方法 start()、run()、join() 和三个静态方法 currentThread()、yield()、sleep()
1.join() 方法用于等待其他线程执行结束。如果线程 A 调用了线程 B 的 join() 方法,那线程 A 会进入等待状态,直到线程 B 运行结束
2.yield() 方法是一个静态方法,用于使当前线程放弃对处理器的占用,相当于是降低线程优先级
-
线程的状态
线程的状态有新建、可运行、阻塞、等待、限时等待和终止 6 种
1.阻塞状态
- 发起阻塞式 I/O 操作
- 申请其他线程持有的锁
- 进入一个 synchronized 方法或代码块失败
-
Java内存模型
Java 内存模型(Java Memory Model,JMM)规定了所有变量都存储在主内存中,每条线程都有自己的工作内存。
-
高速缓存
现代处理器的处理能力要远胜于主内存(DRAM)的访问速率,主内存执行一次内存读/写操作需要的时间,如果给处理器使用,处理器可以执行上百条指令。为了弥补处理器与主内存之间的差距,硬件设计者在主内存与处理器之间加入了高速缓存(Cache)
高速缓存相当于是一个由硬件实现的容量极小的散列表,这个散列表的 key 是一个对象的内存地址,value 可以是内存数据的副本,也可以是准备写入内存的数据
高速缓存相当于是一个链式散列表(Chained Hash Table),它包含若干个桶,每个桶包含若干个缓存条目(Cache Entry)
-
线程调度和安全问题
JVM 采用的是抢占式调度模型,也就是先让优先级高的线程占用 CPU,如果线程的优先级都一样,那就随机选择一个线程,并让该线程占用 CPU
线程安全相关的竞态和实现线程安全要保证的三个点:
原子性、可见性和有序性原子性:原子性就是指该操作不可再分,不论是多核还是单核,具有原子性的量同一时刻只能有一个线程对他进行操作。也就是说在整个操作过程中不会被线程调度器中断操作,都可以认为是原子性,例如a++不具有原子性,a=1具有原子性。可见性:一个线程对共享变量值得修改,能够及时被其他线程所看到重排序:处理器和编译器是对代码做的一种优化,它可以在不影响单线程程序正确性的情况下提升程序的性能,但是它会对多线程程序的正确性产生影响,导致线程安全问题。 -
线程安全
常见的实现线程安全的办法是使用锁和原子类型,而锁可分为内部锁、显式锁、读写锁、轻量级锁(volatile)四种
-
内部锁
1.因为使用 synchronized 实现的线程同步是通过监视器(monitor)来实现的,所以内部锁也叫监视器锁
2.内部锁是使用的是非公平策略,是非公平锁,也就是不会增加上下文切换开销。
-
显示锁
显式锁(Explict Lock)是 Lock 接口的实例,Lock 接口对显式锁进行了抽象,ReentrantLock 是它的实现类
- 可重入,显示锁是可重入锁,也就是一个线程持有了锁后,能再次成功申请这个锁
- 手动获取/释放,显示锁与内部锁区别在于,使用显示锁,我们要自己释放和获取锁,为了避免锁泄露,我们要在finally块中释放锁。
- 临界区,lock()和unLock()方法之间的代码就是显示锁的临界区
- 公平/非公平锁,显示锁允许我们自己选择锁调度策略。ReentrantLock有一个构造函数,允许传入一个fair值,当这个值为true时,说明创建的是公平锁,由于公平锁比非公平锁开销大,所以ReentrantLock默认调度策略是非公平策略。
-
读写锁
读写锁 ReadWriteLock 接口的实现类是 ReentrantReadWriteLock,它包含两个静态内部类ReadLock和WriteLock。只读取共享变量的线程叫读线程,只更新共享变量的线程叫写线程
-
可重入锁和不可重入锁
可重入锁指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取对象上的锁,而其他线程时不可以的。synchronized和ReentrantLock都是可重入锁,可重入锁的意义防止死锁
可重入锁的实现原理是: 通过为每个锁关联一个
请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的,线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将计数器置为1,如果同一个线程再次请求这个锁时,计数器将递增,每次占用线程退出同步块,计数器值将递减,直到计数器为0时锁被释放。不可重入锁,一但一个线程获取这个锁只有等到锁被释放才能再次进入这个方法
-
悲观锁与乐观锁
锁的一种宏观分类方式是
悲观锁和乐观锁。悲观锁和乐观锁并不是特指某个锁,而是在并发情况下的两种不同策略。悲观锁,就是很悲观,每次拿取数据时候都会认为别人会修改。所以每次拿数据的时候都会上锁,这样别人拿数据的时候都会上锁。这样别人想拿数据的时候就被挡住,直到悲观锁被是释放。
乐观锁,就是很乐观,每次去拿数据的时候都会认为别人不会修改。所以不会上锁,不会上锁,但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功。
** 悲观锁阻塞事务,乐观锁回滚重试**
-
CAS
CAS(Compare-and-Swap),即比较替换,允许多个线程同时读取(因为根本没有加锁),但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试,CAS利用CPU指令,从硬件层面保证了操作的原子性。
-
偏向锁→轻量级锁→重量级锁(synchronized锁)
初次执行到
synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里就不需要重新加锁。如果自始至终使用锁的线程只有一个,偏向锁几乎没有额外开销,性能极高。一但有第二个线程加入锁竞争,偏向锁就升级为轻量级锁将自旋。在轻量级锁状态下继续锁竞争,没有抢到的线程将自旋,即不停地循环判断锁是否能够成功获取。但是长时间自旋是非常消耗资源的,一个线程持有锁,其他线程只能原地空耗CPU,执行不了任何有效任务,这种想象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间忙等现象。短时间的忙等,换取线程在用户态和内核态之间的切换开销。但是此忙等是有次数限制的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是修改CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用锁时重量级锁,则直接挂起(而不是忙等),等待将来被唤醒。
一个锁只能按照偏向锁、轻量级锁、重量级锁的循序升级**(也叫锁膨胀)**,不允许降级。
-
Volatile关键字
volatile 用于修饰容易发生变化的变量,不稳定指的是对这种变量的读写操作要从高速缓存或主内存中读取,而不会分配到寄存器中
volatile的开销比锁低,所以也叫轻量级锁
volatile变量读操作开销比普通变量要高,这是因为volatile变量的值每次都要从高速缓存或主存中读取,无法暂存到寄存器中
volatile变量的读写操作会加载和获取存储屏障
保证变量的有序性、可见性和原子性
volatile变量禁止指令重排序优化
-
原子类型
JUC 下有一个 atomic 包,这个包里面有一组原子类,使用原子类的方法,不需要加锁也能保证线程安全,而原子类是通过 Unsafe 类中的 CAS 指令从硬件层面来实现线程安全的,这个包里面有如 AtomicInteger、AtomicBoolean、AtomicReference、AtomicReferenceFIeldUpdater 等
-
线程阻塞唤醒
Object的wait/notify/notifyAll和Condition接口中的await()/signal()/signalAll()进行线程的等待唤醒。
Condition通过Lock.newCondition()可以获得Condition的一个实例。
CountDownLatch可以实现一个或多个线程完成一组特定的操作后才继续运行,
CycliBarrier,有时候多个线程需要互相等待对方代码中的某个地方(集合点),这些线程才能继续执行,这时候可以使用CyclicBarrier(栅栏)
使用 CyclicBarrier.await() 实现等待的线程叫参与方(Party),除了最后一个执行 CyclicBarrier.await() 方法的线程外,其他执行该方法的线程都会被暂停
和 CountDownLatch 不同,CyclicBarrier 是可以重复使用的,也就是等待结束后,可以再次进行一轮等待
final int parties = 3; final Runnable barrierAction = new Runnable() { @Override public void run() { System.out.println("人来齐了,开始爬山"); } }; final CyclicBarrier barrier = new CyclicBarrier(parties, barrierAction); public void tryCyclicBarrier() { firstDayClimb(); secondDayClimb(); } private void firstDayClimb() { new PartyThread("第一天爬山,老李先来").start(); new PartyThread("老王到了,小张还没到").start(); new PartyThread("小张到了").start(); } private void secondDayClimb() { new PartyThread("第二天爬山,老王先来").start(); new PartyThread("小张到了,老李还没到").start(); new PartyThread("老李到了").start(); } public class PartyThread extends Thread { private final String content; public PartyThread(String content) { this.content = content; } @Override public void run() { System.out.println(content); try { barrier.await(); } catch (BrokenBarrierException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } } //运行结果 第一天爬山,老李先来 老王到了,小张还没到 小张到了 人来齐了,开始爬山 第二天爬山,老王先到 小张到了,老李还没到 老李到了 人来齐了,开始爬山 -
ConcurrentHashMap
特定: 分离读写锁,volatile读CAS写(JDK 7-8) 读失败在加锁(JDK 5-7),弱一致性,添加元素不一定马上能读到,清空后可能仍有元素。
-
使用线程有哪些准则
1.严禁直接创建线程
2.提供基础线程池供各个业务线使用
3.选择合适的异步方式
4.线程必须命名
5.重视优先级设置
-
HandlerThread
HandlerThread 内部是以串行的方式执行任务,它比较适合需要长时间执行,不断从队列中取出任务执行的场景
-
线程池
1.线程池存在的意义
减少创建和销毁线程的次数,可重复利用线程,避免创建线程的开销,可以并发执行任务。可以根据系统承受能力,调整线程池工作线程的数目。
2.Java里面的线程池的定价接口
Executor。实例类 说明 ExecutorService 真正的线程池接口 ScheduledExecutorService 和Timer/TimerTask类似,解决需要重复执行的问题 ThreadPoolExecutor ExecutorService的默认实现 ScheduledThreadPoolExecutor 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度类实现。 3.java提供几种默认线程池创建实例
-
1.NewSingleThreadExecutor
创建一个单线程线程池,线程池只有一个线程在工作,所有的任务需要等待进行串行任务执行。
-
2.newFixedThreadPool
创建一个固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大。线程池的大小一但达到最大值就会保持不变。
-
3.newCachedThreadPool
创建一个可缓存的线程池。如果线程池大小超过了处理任务所需要的线程,那么会回收部分空闲的线程,当任务数增加时,此时线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程数,
-
4.newScheduledThreadPool
创建一个大小无限的线程池,此线程池支持定时及周期性执行任务的需求
4.ThreadPoolExecutor的参数详解
方法签名:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,RejectedExecutionHandler handler){ }corePoolSize-线程池中的线程数包含空闲线程maximumPoolSize-线程池中最大线程数keepAliveTime-当线程数大于核心线程数是,此为线程空闲的最大时长TimeUnit-keepAliveTime的时间单位workQueue-执行当前用于保存任务的队列。此队列仅保持由execute方法提交的Runnable任务threadFactory-执行创建线程时的工厂类handler-用于超出线程范围和队列容量时而执行被阻塞时所使用的处理程序5.线程池关闭操作
shoutDown :尝试关闭没有执行的线程,shoutDownNow:尝试关闭不管有没有执行的线程。
-
-
创建线程的几种方式
1.继承Thread,
2.实现Runnable接口
3.通过Calladble和Future创建线程
(1) 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程的执行体,并且有返回值。
(2) 使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3) 使用FutureTask对象作为Thread对象的target创建并启动新线程
(4) 调用FutureTask对象的get()方法获得子线程执行结束的返回值
针对多线程知识点的总结,帮助自己记录,如果有理解错误的欢迎指正,谢谢大家,一起成长,一起冲