多线程快速入门(三)
一. 线程三大特性:
1、原子性:
即单个线程中是一个整体,要么全部执行且执行过程中不被外界因素打断,要么全部不执行;
2、可见性:
当多个线程访问同一变量时,一个线程修改了变量的值,其他线程都能立即知道;
3、有序性:
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。因此可通过 Volatile 关键字以及 synchronized 和 lock 保证其有序性。
二. Java内存模型(JMM)
Java内存模型(JMM)决定一个线程对共享变量写入时,其他线程都可见,根据JMM的设计,系统存在一个主内存,所有线程共享的变量都存储在主内存中,且对所有内存都是共享的。每个线程都有自己的工作内存,工作内存中的数据是从主内存中拷贝过来的,线程对所有内存的操作都是在工作内存中进行,线程之间是无法直接访问,变量的传递主要由主内存完成。本地内存是JMM的一个抽象概念,他涵盖了缓存,写缓冲区,寄存器以及其他硬件和编译器优化。
当线程A需要同线程B通信时,必须要经过两个步骤:
1、线程A将修改后的共享变量由本地内存A同步到主内存中;
2、线程B从主内存中拷贝共享变量到本地内存B中。
三. Volatile 关键字
一旦一个共享变量被 Volatile 关键字修饰后,那么:
- 该变量在对于多线程而已都具有可见性,当某一线程操作了该变量时,该变量的新值对于其他线程都是可见的。
- 禁止进行指令重排序。
//线程Aboolean type = true;while(type){ doSomeThing();}//线程Btype = false;
当线程B用于中断线程 A 的执行时,大多数情况下,线程都能被正常中断。但是,由于在多线程环境下,线程都是从主内存中拷贝变量到工作内存中,当 B 线程修改了 type 的值后没来得及将工作内存中的 type 值更新到主内存中,就去执行其他指令了。那么对于 A 线程来说,并不知道全局 type 变量的改变,将出现死循环。
当 type 被 Volatile 关键字修饰时:
1、每当 type 被修改后, volatile 关键字会强制将其更新到主内存中;
2、当 B 线程对 type 进行修改时,A 线程中的 type 值将失效;
3、当 A 线程执行到失效的 type 值时,就会重新到主内存中获取新的 type 值。
因此,当变量被 volatile 关键字修饰时,就可避免线程中断时死循环问题。
然而,volatile 关键字并不具备原子性,且不能保证线程安全。
原因很简单,在执行内存屏障之前,不同 CPU 依旧可以对同一个缓存行持有,一个 CPU 对同一个缓存行的修改不能让另一个 CPU 及时感知,因此出现并发冲突。因此 volatile 只能保证内存可见,而不能保证线程安全和原子性。
四. ThreadLocal
1.ThreadLocal 介绍:
ThreadLocal 是指 ThreadLocal 中填充的变量属于当前线程,该变量对于其他线程而言都是隔离的。
当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用他的线程都创建了一个变量副本,每个线程都独立使用自己的副本,而不会影响其他线程的变量。
2. ThreadLocal 使用场景:
- 在进行对象跨层传输时,使用ThreadLocal能避免多次传递,打破层次之间的间隔;
- 线程间数据隔离,保证每个线程使用的数据不被影响;
- 进行事务操作,用于存储线程事务信息;
- Session会话管理。
3. ThreadLocal 常用方法:
1、void set(T value):
设置当前线程的线程局部变量的值。
2、T get():
该方法返回当前线程所对应的线程局部变量。
3、void remove():
将当前线程局部变量的值删除,目的是为了减少内存的占用,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
4、protected T initialValue():
返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用 get() 或 set(Object) 时才执行,并且仅执行1次。ThreadLocal 中的缺省实现直接返回一个null。
4. 案例:
创建 3 条线程,并生成独立序列号。
public class ThreadLocalNum { //创建全局变量 threadLocal private static ThreadLocal threadLocal = new ThreadLocal() { //调用 initialValue() 初始化 @Override public Integer initialValue() { return 0; } }; //获取下一个序列值 public int getNum() { int number = threadLocal.get() + 1; threadLocal.set(number); return number; }}
public class ThreadLocalDemo extends Thread { private ThreadLocalNum threadLocalNum; public ThreadLocalDemo(ThreadLocalNum localNum) { this.threadLocalNum = localNum; } //继承Thread类,实现run()方法 @Override public void run() { for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + "的序列号:" + threadLocalNum.getNum()); } } public static void main(String[] args) { ThreadLocalNum localNum = new ThreadLocalNum(); //3个线程共享localNum,各自产生序列号 ThreadLocalDemo threadLocalDemo1 = new ThreadLocalDemo(localNum); ThreadLocalDemo threadLocalDemo2 = new ThreadLocalDemo(localNum); ThreadLocalDemo threadLocalDemo3 = new ThreadLocalDemo(localNum); threadLocalDemo1.start(); threadLocalDemo2.start(); threadLocalDemo3.start(); }}
结果:
5. ThreadLocal 实现原理:
当我们创建一个全局的 ThreadLocal 变量后,并对它做了赋值操作,由前面 set()方法源码可以看出:
1、获取当前线程;
2、获取线程的私有ThreadLocalMap;
3、getMap(Thread t) 操作会返回当前线程的 threadLocals ,即:每个线程都有自己独立的ThreadLocalMap。
4、当map为空时,创建map对象,当map不为空时:
5、该 set 方法中的操作是通过 ThreadLocal 的哈希值计算出存储位置,获得该位置的value,整个过程即为map的存储过程。
五.线程池:
线程池是指在初始化一个多线程应用时,会创建一个线程集合,每当需要执行新的任务时,并非重新创建一条线程,而是之间从线程池中获取,当任务执行完成后,线程回到池子中等待下一次任务分配。线程池的大小应由可用内存数量及应用程序的需求而定。
1. 线程池的作用:
- 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务;
- 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,导致服务器瘫痪。
2.常见线程池的创建方式:
1、newSingleThreadExecutor :创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
/** * 线程池 */public class ThreadPoolDemo { public static void main(String[] args) { ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { final int index = i; cachedThreadPool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + index); } }); } }}
运行结果:
由结果可知:线程池为无限大,当执行第二个任务时,第一个任务已完成,会复用上一个任务的线程,而不用每次新建线程。
2、newFixedThreadPool : 创建一个定长线程池,可控制线程最大并发数,超过线程池数量的在队列中等待。
public class NewFixedThreadPoolDemo { public static void main(String[] args) { //创建定长线程池,数量为5 final ExecutorService nftp = Executors.newFixedThreadPool(5); for (int i = 0; i < 100; i++) { final int index = i; nftp.execute(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("i:" + index); } }); } }}
运行结果:
由结果可知:因为线程池大小为5,每次任务输出 index 后 seleep 1s ,因此,每1s输出5个值。该定长最好根据系统资源进行设定,如:java.lang.Runtime.availableProcessors() //处理器的 Java 虚拟机数量。
3. newScheduledThreadPool : 创建一个定长线程池,支持定时和周期性任务执行。
public class NewScheduledThreadPoolDemo { public static void main(String[] args) { ScheduledExecutorService stp = Executors.newScheduledThreadPool(5); System.out.println("开始执行!"); stp.schedule(new Runnable() { @Override public void run() { System.out.println("延时3s!"); } }, 3, TimeUnit.SECONDS); }}
执行结果:
由结果可知:newScheduleThreadPool 可做延时及周期性任务。
4. newSingleThreadExecutor : 创建一个单线程化的线程池,它只会用唯一的线程来执行任务,保证所有任务按照指定顺序(FIFO//先进先出,LIFO//后进先出,优先级)执行。
public class NewSingleThreadExecutorDemo { public static void main(String[] args) { ExecutorService ste = Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) { final int index = i; ste.execute(new Runnable() { @Override public void run() { System.out.println("index:" + index); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } }}
执行结果:
由结果可知:newSingleThreadExecutor 创建的线程池每次只有一条线程执行,且整体执行顺序按顺序执行。
按照阿里巴巴代码规范,创建线程池是最好避免使用以上4种方式,手动创建线程,能最大的利用资源
需要《阿里巴巴 Java 开发手册 》1.40版的小伙伴可留下联系方式。
手动创建方式:
public class MutillThreadPoolDemo { public static void main(String[] args) { ThreadPoolExecutor tpe = new ThreadPoolExecutor( 2, 5, 1L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy()); tpe.execute(new Runnable() { @Override public void run() { //doSomething } }); }}
参数说明:
corePoolSize:线程池的核心线程数;maximumPoolSize:能容纳的最大线程数;keepAliveTime:空闲线程存活时间;unit:存活的时间单位;workQueue:存放提交但未执行任务的队列;threadFactory:创建线程的工厂类;handler:等待队列满后的拒绝策略。