多线程整体理解
一、关于多线程整体的理解
多线程复杂的地方在 对象及变量的并发访问。指多个线程并发的访问同一个对象或者变量。为了保证线程安全,有两个思路。都有自己的使用场景。线程同步和ThreadLocal两种方式。其中线程同步的思路是用“时间换空间”。访问串行化。确保对共享数据的访问同一时刻只有一个线程进行访问。整体串行操作。而ThreadLocal采用“空间换时间”,访问并行化,对象独享化
前者仅有一份数据,线程排队进行访问。后者为每个线程提供一份数据,同时访问互不影响。
(1) 线程同步
主要的方式是互斥。即通过互斥达到同步的目的。实现互斥的有三种方式。Synchronized、volatile和lock。
1) Synchronized
2) Volatile
3) Lock
(2) ThreadLocal
Thread Local解决思路,也称之为线程局部变量。它不是一个线程,而是保存线程本地化对象的容器。当运行在多线程环境下,某个对象使用Thread Local进行维护的时候,Thread Local为每个使用该变量的线程分配一个独立的变量副本。每个线程都可以修改自己的变量副本,从而真正的并发访问,相互不影响。
1) Thread Local 的接口方法
Thread Local 本质是一个线程安全的Map。其中key为当前线程,即currentThread类型为Thread。所以,不论set、个体、或者remove操作map的key都是当前线程。
1.Set
设置当前线程的私有变量副本。即map的put方法。map . put( current Thread,value)
2.Get
获取当前线程的私有副本。即map的get方法。map.get( current Thread )
3.Remove
移除当前线程的变量。
4.Initiative
对私有变量的初始化。即在第一次set之前,变量的初始化。
2) Spring 使用 Thread Local解决线程安全问题
Spring中大多数的bean是有状态的,比如:connection。其中有很多设置,比如字符集、自动提交或手动提交等。有状态的bean在多线程的环境下有线程安全问题。而spring的解决思路就是通过ThreadLocal来解决的。
一般情况下,从接收访问到返回响应都在同一个线程中。而将非线程安全的变量保存在ThreadLocal中。Bean就可以是无状态的。
二、线程的特征
线程的三个特征。可以对比事务的4个特征(原子性、一致性、隔离性和持久性)对比记忆。
(1) 原子性
多个操作,要么都执行,要么都不执行。
(2) 可见性
多个线程共同访问同一个变量。一个线程修改了这个变量,其它线程立即就能观测到改变。
可见性主要牵扯到JAVA的内存模型。多个线程共享的对象存储在堆或者方法区中。这部分称之为主内存。而各个线程都有自己的线程内存。及方法区或者线程内存。线程在执行的过程中会从主内存备份数据到线程内部,运行过程中对线程内部变量进行修改。如果需要被其它线程观测到。需要及时的写入主内存。
这部分主要的知识点在JVM的java内存模型部分。
而volatile主要是通过确保可见性,来完成轻量级的线程安全。
(3) 有序性
了解有序性,需要了解背景知识。Java字节码的执行是通过编译器或者执行器来对字节码执行进行执行的,将字节码转成机器码,调用执行执行引擎。在此过程中,会发生 指令重排序。
1) 指令重排序
允许编译器和处理器对执行进行重排序。重排序的过程不会影响单线程程序的执行,但是会影响到多线程并发执行的正确性。
2) 指令重排序的原则
严格遵循指令数据之间的依赖关系。从单线程的角度来看,不影响执行的正确性。
3) 多线程的有序性
遵循先行发生生原则。即第一个线程优先于第二个线程发生。则第二个线程能观测到第一个线程的修改结果。这个称之为先行发生原则。
4) JMM(JAVA内存模型)怎样指定规则满足先行发生原则
要说明这个问题,需要说明JVM的JAVA内存模型。
1.Java内存模型
Java内存模型分为主内存和工作内存两种。此处的内存划分和JVM内存【JVM内存的划分为5部分,依次是:堆,方法区、java虚拟机栈、本地方法栈和程序计数器】的划分是在不同层次上的划分。如果需要将两者对应起来。主内存对应java堆的实例对象部分。工作内存对应栈中的部分区域(局部变量表)。
JVM内存执行流程如下图:
JVM在设计的时候考虑到,如果工作内存每次修改都去修改主内存,会对性能影响较大。所以,每个线程拥有自己的工作内存。在执行的过程中,修改工作内存,而不是直接修改主内存。
这样造成了一个线程对变量进行修改,只修改了工作内存中的变量,而没有及时的修改主内存变量的值。即这次操作对其余线程不可见。会导致线程不安全的问题。因为JMM(JAVA内存模型)制定了一套标准,确保在多线程的情况下,能够控制什么时候内存会被同步给其它线程。
2.内存交互操作
内存模型的操作有8种。JVM虚拟机保证每个操作都是原子性的(double和long在某些平台除外)。
内存交互操作的执行流程个人理解
为了方便理解内存的操作。其实可以理解为三个层面。主内存层面数据,工作内存层面的数据和JVM执行子系统层面的数据。根据数据的流向来分析这8中操作。
准备从主内存读取数据。首先使用lock对主内存的变量进行加锁。即lock操作。其次,使用read将主内存数据加载到工作内存。同时使用load将read的变量值赋值给工作内存的变量。以上,就完成了数据从主内存层面到工作内存层面。同时,read和load必须配置使用。不可拆开。
工作内存的数据在执行指令的时候,执行子系统会调用操作系统API,将工作内存数据传递给执行子系统数据。即将工作内存变量赋值给执行层面使用。这个使用使用use。
执行子系统执行完成后得到的变量需要传递给工作内存。即将执行子系统层面的数据传递给工作内存。这个时候使用assign。
工作内存的变量想同步给主内存。通过store将工作内存变量值传递给主内存,之后用write将store的数值赋值给主内存变量。这样完成了工作内存变量赋值给主内存变量的操作。同时,store和write必须配置合适,不能拆开。
以上的描述过程可见下图
对主内存对象取消加锁操作。即unlock。
(1)Lock 锁定
作用于主内存的变量。把一个变量标识成线程独占状态。
(2)Read
作用于主内存变量,将一个主内存变量的值赋值并传递给工作内存中,供load使用。
(3)Load
作用于工作内存变量。对于load过来的变量,赋值给工作内存变量。
(4)Use
作用于工作内存变量。将工作内存中的变量传递给执行引擎。
(5)Assign
作用于工作内存变量。将执行引擎传递回来的数据赋值给工作内存变量。
(6)Store
作用于工作内存变量。将工作内存的变量值传递给主内存模型中,供后续的write使用。
(7)Write
作用于主内存变量。将store来的变量赋值给主内存变量。
(8)Unlock
作用于主内存变量。把一个锁定状态的变量释放出来。
3.JMM提供了三种保证有序性的方法
JMM通过指定了8给规则,让操作满足有序性。比如,read和load必须配合使用。Store和write必须配合使用。
- 使用volatile保证有序性
- 使用synchronized保证有序性
- 使用显示锁lock来保证有序性。
三、线程的创建
线程的创建按照定义由两种,继承thread类或者实现runnable接口。实际使用中还有使用线程池的方式。
(1) 继承Thread
通过继承Thread线程类来实现线程。完成线程体代码。Run方法。
Class MyThread extends Thread{
Run(){
}
}
启动线程 MyThread.start();
(2) 实现runnable接口
MyThread implement runnable{
Run(){
}
}
启动线程 MyThread.start()
(3) 使用Executor框架创建线程池
Executors.newXXX ;newFixedThreadPool(int) 、newCacheThreadPool()、newScheduleThreadPool(int)、 newSingleThreadExecutor
通过Executors的四个静态放法获取ExecutorService实例。然后执行runnable任务或者callable任务。
1) Executor执行runnable任务
通过Executor的newXXX方法获取ExecutorService实例。然后调用该实例的executor(Runnable command)方法即可。一旦runnable方法传递到execute方法上,则该方法将会加入到任务队列中。等待线程调用。
- public class TestCachedThreadPool{
- public static void main(String[] args){
- ExecutorService executorService = Executors.newCachedThreadPool();
- for (int i = 0; i < 5; i++){
- executorService.execute(new TestRunnable());
- System.out.println("************* a" + i + " *************");
- }
- executorService.shutdown();
- }
- }
- class TestRunnable implements Runnable{ //重写run方法
- public void run(){
- System.out.println(Thread.currentThread().getName() + "线程被调用了。");
- }
2) Executor执行callable任务
将callable方法传递给ExecutorService的submit方法。则call方法将自动提交到任务队列中。根据线程池线程的使用情况。分配线程给该任务。
Submit会返回一个Feature对象。
- public class CallableDemo{
- public static void main(String[] args){
- ExecutorService executorService = Executors.newCachedThreadPool();
- List<Future<String>> resultList = new ArrayList<Future<String>>();
- //创建10个任务并执行
- for (int i = 0; i < 10; i++){
- //使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中
- Future<String> future = executorService.submit(new TaskWithResult(i));
- //将任务执行结果存储到List中
- resultList.add(future);
- }
- //遍历任务的结果
- for (Future<String> fs : resultList){
- try{
- while(!fs.isDone);//Future返回如果没有完成,则一直循环等待,直到Future返回完成
- System.out.println(fs.get()); //打印各个线程(任务)执行的结果
- }catch(InterruptedException e){
- e.printStackTrace();
- }catch(ExecutionException e){
- e.printStackTrace();
- }finally{
- //启动一次顺序关闭,执行以前提交的任务,但不接受新任务
- executorService.shutdown();
- }
- }
- }
- }
- class TaskWithResult implements Callable<String>{
- private int id;
- public TaskWithResult(int id){
- this.id = id;
- }
- // 重写call()方法
- public String call() throws Exception {
- System.out.println("call()方法被自动调用!!! " + Thread.currentThread().getName());
- //该返回结果将被Future的get方法得到
- return "call()方法被自动调用,任务返回的结果是:" + id + " " + Thread.currentThread().getName();
- }
- }
四、Synchronized
Synchronized 是java关键字。能保证被他修饰的方法或者代码块的线程同步。即任意时刻只能被一个线程访问。它是多线程中重要的线程同步方式。
(1) Synchronized使用方式
Synchronized主要的三种使用方式。修饰实例方法,修饰静态方法、修饰代码块
1) 修饰实例方法
锁是当前实例对象。
2) 修饰静态方法
锁是当前类的class对象。【由JVM可知,是class在JVM的java.lang.class 类型的内存对象】,也称之为全局锁。
3) 修饰代码块
锁是synchronized()括号中的对象。
当一个线程试图访问同步代码的时候,必须先获得锁。退出或者抛出异常时,必须释放锁。
(2) Synchronized 锁对象存在哪里
synchronized用到的锁是存在Java对象头里的。synchronized关键字实现的锁是依赖于JVM的,底层调用的是操作系统的指令集实现。
对比LOCK接口。Lock接口实现的锁不一样,例如ReentrantLock锁是基于JDK实现的,有Java原生代码来实现的。
即synchronized的锁对象是哪个对象。则在哪个对象的对象头中保存占用当前锁的线程信息。
(3) Synchronized在JVM中的实现
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因。
五、Volatile
Volatile主要的作用是使变量在多个线程间可见。Volatile是多线程的轻量级实现。
(1) Volatile的可见性
Volatile修饰的变量,在线程内存中使用时候,必须先从主内存进行同步过来,然后在使用,确保只要主内存数据被修改,每次使用的时候必须拿到的是最新的主内存变量值。
其次,线程每次对线程内存变量修改的使用,主动地同步修改主内存的变量数据。确保线程内变量修改,及时的同步到主内存。确保线程修改,立马对其它线程可见。
由以上两个操作,确保了volatile修饰的变量对其它线程可见。
(2) Volatile 的有序性
JMM(Java 内存模型)有三个方案保证多线程的有序性。Volatile、synchronized和lock。能保证多线程的有序性。即先行发生原则。则后执行的线程能观测到先执行线程的修改。
如果一个变量被声明volatile的话,那么这个变量不会被进行重排序。
(3) Volatile不能保证原子性
Volatile 只能保证修饰的变量的可见性和有序性。不能保证原子性。所以, 在变量修改和自身数据无关的情况下,相当于原子操作的。因为不存在多个线程同时对volatile修饰变量的同时访问。这种情况下,是线程安全的。具体情况如下。
1) 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
2) 变量不需要与其他状态变量共同参与不变约束。
在以上两种情况下,volatile修饰的变量能保证原子性。因为其本身能保证可见性和有序性。所以,以上两种情况下能保证线程安全。
六、Lock
Lock是接口。具体的实现类有两种。ReentrantLock和ReentrantReadWriteLock两个子类。和synchronized相比较。Synchronized是通过在编译的过程中,在字节码中添加monitorrenter和monitorexit字节码命令。在执行字节码的过程中,调用系统API,确保线程同步的。而lock是jdk中的包提供的功能,实现的线程同步。
(1) Lock实现同步
根据synchronized关键字可以了解。它实现线程同步是使用一个锁对象。然后在需要线程同步的地方添加获取锁。在使用完成之后释放锁。其实,lock的实现思路也是一样的。Lock的子类就是一个锁对象而已。
在使用之前先创建一个锁对象。比如 Lock lock = new ReentrantLock();然后在使用的开始地方获取锁。即lock.lock();在使用完成的地方添加一个释放锁。即lock.unlock()操作。即通过lock完成线程的同步。
(2) 使用condition实现等待/通知
个人理解的线程之间的等待/通知模式和线程的并行操作操作模式其实是多线程使用的两个场景。
1) 多线程并行操作模式的理解
多线程并行操作模式,其实是多个线程针对共享数据的不同部分,做相同的流程的操作。
2) 等待通知模式的理解
等待/通知模式 其实是不同种类线程(有不同的操作流程)之间的配合协作使用用场景。
3) Lock的condition使用
关键字synchronized和wait/notify配合使用,可以实现等待通知模式。而lock中提供了condition,能提供多路通知的功能。即可以选择性通知。
1.使用方法
通过Condition condition = lock.newCondition()获取condition对象。在需要等待的地方调用condition.wait().则对当前线程处于等待状态。
2.使用需要注意
创建condition的时候,在此之前需要对lock加锁。即在之前需要调用lock.lock()。否则会报错。
七、线程池相关
(1) 线程池的好处
1) 降低资源消耗
通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
2) 提高响应速度
任务准备好就可以立即执行,不需要因为等待创建线程的时间。
3) 提高线程的可管理型
线程是稀缺资源,使用线程池可以统一分配,监控。
(2) 常见的线程池及使用的场景
常见的线程池ThreadPoolExecutor有FixedThreadPool、CacheThreadPool、SingleThreadExecutor。
1.FixedThreadPool 可重用固定线程数的线程池
适用场景:适用于负载比较重的服务器
- FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列。
- 该线程池的线程数始终保持不变。当一个新的任务提交时,线程池若有空闲线程,则立即执行。若没有空闲线程,则新任务被暂存在任务队列中。待有线程空闲时,从队列中取出需要处理的任务开始执行。
2.CacheThreadPool 根据需要调节线程数量的线程池
适用场景:大小无界,适用于执行需要短期异步的小程序。或者负载较轻的服务器。
- CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。
- 线程池的数量不确定,但是,如果有空闲线程,则优先复用空闲的线程。若所有线程都在工作,此时,有新任务提交,则创建新线程处理任务。
3.SingleThreadExecutor 只创建一个线程执行任务
适用场景:需要保持顺序执行任务。任意时间点,没有多线程活动的场景。
- SingleThreadExecutor使用无界队列LinkedBlockingQueue 作为线程池的工作队列。
- 若有新任务提交,则保存到任务队列。待线程空闲,按照队列的先后顺序,执行任务。
(3) 线程池有哪几种工作队列
1) ArrayBlockingQueue
基于数据结构的有界阻塞队列。使用元素遵循队列属性,FIFO先进先出。
2) LinkedBlockingQueue
基于链表的无界阻塞队列(链表结构确定了无界)。吞吐量高于ArrayBlockingQueue。静态方法Executor. newFixedThreadPool()使用这个队列。
3) SynchronousQueue
是一个不存储元素的队列。每个插入队列必须等待另一个线程调用移除操作。否则插入操作一直处于阻塞状态。
4) PriorityBlockingQueue
一个具有优先级的无阻塞队列。
(4) 线程池参数
1) CorePoolSize 线程池基本大小
即使线程池中没有任务,也会有corePoolSize个线程等待任务。
2) MaximumPoolSize 最大线程数
线程池最多的线程数量。
3) KeepAliveTime线程存活时间
如果线程池中的线程数大于CorePoolSize,且等待时间大于KeepAliveTime,仍然没有任务执行,则线程退出。
4) Unit 线程存活时间的单位
比如:TimeUnit.SECONDS。
5) WorkQueue 工作队列
用于保持执行任务的阻塞队列。
6) ThreadFactory 线程工厂
主要是为了给线程起名字。
7) Handler 拒绝策略
当线程和队列已经满的时候,应该采用什么样的策略才处理新提交的任务。比如 报错,丢弃等
(5) 线程池执行流程
任务提交到线程池,判断当前线程数和基本线程数和最大线程数之间的关系。
1) 当前线程数小于基本线程数
创建线程来提交任务。
2) 当前线程数大于基本线程数小于最大线程数
1.工作队列未满
放入任务队列中,等待线程池安排线程执行队列中任务。
2.工作队列已满
调用拒绝策略,进行拒绝任务。
八、synchronized和volatile区别
所以,一定情况下,synchronized和volatile都能解决线程数据同步的问题。但是,各有特点。
- Synchronized 修饰的是方法,代码块。Volatile修饰的是共享变量。
- Synchronized 是通过同步阻塞的方式完成变量的同步,体现的是原子性。Volatile是非阻塞的,能保证可见性,不能保证原子性。第一时间回去修改数据到主内存。
- 什么时候下可以使用volatile?任何时候都可以使用synchronized,都能起到数据同步的作用。如果写入的变量值不依赖当前的变量值的情况下可以使用。