为什么要使用多线程
使用多线程,本质上是提升为了提升程序的性能,也就是降低延迟,提高吞吐量。延迟是指发出请求到收到响应这个过程的时间;吞吐量是指单位时间内能够处理请求的数量。所以在要在并发编程的角度实现这两点,就是要提升I/O的利用率和CPU的利用率从而降低延迟提高吞吐量。而I/O设备和CPU都需要单独的线程执行,所以我们可以使用多个线程来提高它们的利用率。
线程间通信(处理线程安全的方式)
① Java支持多个线程同时访问一个对象,对于每个线程都会拥有该变量的拷贝,所以在程序的执行过程中线程访问到的变量就不一定是新的,所以就需要volatile和synchronized关键字来实现线程间的通信。
volatile~
synchronized~
②等待、通知机制:生产线程修改了一个对象的值,那消费线程就感知到了变化,实现线程间的通信。 java中的等待通知机制可以通过一个对象的Object.wait()和Object.notify()实现。A线程获取对象的锁,执行obj.wait()方法进入该对象的等待队列中。B线程又获取了A线程释放的锁,执行obj.notify()会将该对象等待队列中的一个线程A放入同步队列中阻塞。等B线程释放锁之后,A线程就又可以获取锁了。
③管道输入、输出流
管道流用于线程之间的数据传输,而传输的媒介为内存。(补充:PipedOutputStream、PipedInputStream面向字节,PipedReader、PipedWriter面向字符。需要out.connect(in),out.write(),in.read())
④Thread.join():A线程中执行B.join()表示线程A需要等待线程B终止后才能从B.join()返回。当B线程终止时,会调用它自身的notifyAll()方法,通知所有等待在该线程对象上的线程唤醒。
⑤ThreadLocal:
原理:它是线程本地变量,访问ThreadLocal的线程都会得到一个ThreadLocal副本,线程操作ThreadLocal时不会影响其他线程。它底层是有一个静态内部类ThreadLocalMap,ThreadLocalMap中有个Entry,Entry的key存放ThreadLocal对象(key是弱引用),value存放我们要放入的数据对象。
key为什么要使用弱引用:若key是强引用指向线程的ThreadLocal对象,则ThreadLocal对象就会因为与这个Entry(ThreadLocalMap)对象强关联而无法被GC回收,造成内存泄露(除非线程结束),所以key为弱引用可以解决一部分内存泄露。但正是因为key是弱引用,key会被GC,那它的value就无法被访问了。 所以Thread使用完成后要remove(),删除,否则会内存泄露。
remove():防止内存溢出和内存泄露。底层是获取当前线程的ThreadLocalMap,通过hash找到当前key位置,然后得到value对象并调用value.clear()清除引用。同时也会去清除其他的key为null的弱引用。
我们在业务开发时,可以使用ThreadLocal作为缓存,减少获取信息比较昂贵的操作(查数据库)。但是我们的应用程序都是跑在Tomcat上的,Tomcat是用的线程池啊,所以在有些情况下可能会出现线程重用,就得到了旧数据 / 其他线程的数据。所以我们在使用时,一定要在最后用finally语句块进行threadlocal.remove()。 使用InheritableThreadLocal时,子线程可以直接获取父线程存入Map的值。原理:InheritableThreadLocal在线程初始化时会把父线程的Map值拷贝到子线程中,且get()方法被重写成获取子线程中的这个拷贝值,所以就可以得到了父类的Map值。但我们的应用场景是线程池时,线程是反复使用的,并没有子线程的初始化过程,InheritableThreadLocal就没有意义了。
TransmittableThreadLocal解决了线程池场景下的线程变量继承问题。它把保存父线程的Map对象过程放在了提交任务前,所以他封装了一个TtlRunnable,在set方法时通过变量保存父线程中的线程变量,在任务执行时取出。而且它通过一层缓存holder以便获取父线程中的变量,避免包权限。
Java内存模型
Java对多线程的并发机制采用共享内存模型的方案。JMM通过决定一个线程对共享变量的可见性来控制Java线程间的通信,而线程之间的共享变量是存在主内存中的,也就是说JMM定义了线程和主存之间的关系。每个线程都有一个私有的抽象本地内存存储了共享变量的副本,所以Java线程间的通信是通过把本地内存中的数据刷入主存中实现的,那JMM就控制了主存与每个线程的本地内存的交互顺序等,为Java程序提供内存可见性的保证。
指令重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,重排序有三种:
①编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以对语句的执行顺序进行重排。
②指令级并行的重排序:现代处理器采用了指令级并行技术,可以将多条指令重叠执行。如果这些指令不存在数据依赖性(其中一个操作对共享变量由写操作时),那处理器就可以改变机器指令的执行顺序。当然了,数据依赖性只是针对单线程的结果需要保持不变,对于不同线程之间的数据依赖性,处理器不会考虑,仍然会进行指令重排序。
③内存系统的重排序:处理器使用了读写缓冲区,所以对于多线程的加载和存储可能是乱序的。 这些指令重排序就可能会引发内存可见性问题。①属于编译器重排序,JMM的编译器重排序规则会禁止特定类型的编译器重排序;②③属于处理器重排序,JMM的处理器重排序规则要求Java编译器在生成指令序列时,要插入特定的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。
Happens-Before:
happens-before阐述了操作之间的内存可见性,即一个操作结果要对另一个操作可见,那它必须happens-before于另一个操作。它定义了:如果一个操作happens-before另一个操作,那么第一个操作的结果对第二个操作可见。happens-before规定JMM要遵循这么一个原则:只有不改变程序的执行结果,编译器和处理器怎么优化都行。(as-if-serial语义奥正办学从内程序的执行结果不被改变;happens-before保证多线程程序执行结果不被改变)
happens-before规则:
①程序顺序规则:一个线程中的每个操作,happens-before与该线程中的后续操作
②对于一个锁的解锁,happens-before于随后对这个锁的加锁。
③对于volatile域的写,happens-before与后续对volatile域的读
④传递性:A→B,B→C,则A→C
⑤线程A执行ThreadB.start(),则此操作happens-before与线程B中的任意操作
volatile
volatile用于修饰共享变量,在happens-before原则中,被volatile修饰的变量的写操作happens-before与后续对volatile的读操作,即线程对volatile变量的写操作立即可见。站在内存的角度。当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中;当读一个volatile变量时,JMM会把读线程对应的本地内存置为无效,然后去主内存中读取共享变量。
而且JMM为了实现volatile的内存语义,会在volatile变量读写操作的前后插入内存屏障来限制指令重排序。它保证了volatile写之前的操作不会被重排序到volatile写之后,volatile读之后的操作不会被重排序到volatile读之前,volatile写→读不乱序。