多线程学习(二)--整体简介

多线程整体理解

一、关于多线程整体的理解

  多线程复杂的地方在 对象及变量的并发访问。指多个线程并发的访问同一个对象或者变量。为了保证线程安全,有两个思路。都有自己的使用场景。线程同步ThreadLocal两种方式。其中线程同步的思路是用“时间换空间”。访问串行化。确保对共享数据的访问同一时刻只有一个线程进行访问。整体串行操作。而ThreadLocal采用“空间换时间”,访问并行化,对象独享化

前者仅有一份数据,线程排队进行访问。后者为每个线程提供一份数据,同时访问互不影响。

(1) 线程同步

  主要的方式是互斥。即通过互斥达到同步的目的。实现互斥的有三种方式。Synchronizedvolatilelock

  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操作mapkey都是当前线程。

1.Set

  设置当前线程的私有变量副本。即mapput方法。map . put( current Threadvalue)

2.Get

  获取当前线程的私有副本。即mapget方法。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的内存模型。多个线程共享的对象存储在堆或者方法区中。这部分称之为主内存。而各个线程都有自己的线程内存。及方法区或者线程内存。线程在执行的过程中会从主内存备份数据到线程内部,运行过程中对线程内部变量进行修改。如果需要被其它线程观测到。需要及时的写入主内存。

这部分主要的知识点在JVMjava内存模型部分。

volatile主要是通过确保可见性,来完成轻量级的线程安全。

(3) 有序性

  了解有序性,需要了解背景知识。Java字节码的执行是通过编译器或者执行器来对字节码执行进行执行的,将字节码转成机器码,调用执行执行引擎。在此过程中,会发生 指令重排序。

1) 指令重排序

  允许编译器和处理器对执行进行重排序。重排序的过程不会影响单线程程序的执行,但是会影响到多线程并发执行的正确性

2) 指令重排序的原则

  严格遵循指令数据之间的依赖关系。从单线程的角度来看,不影响执行的正确性。

3) 多线程的有序性

  遵循先行发生生原则。即第一个线程优先于第二个线程发生。则第二个线程能观测到第一个线程的修改结果。这个称之为先行发生原则。

4) JMMJAVA内存模型)怎样指定规则满足先行发生原则

  要说明这个问题,需要说明JVMJAVA内存模型。

1.Java内存模型

  Java内存模型分为主内存工作内存两种。此处的内存划分和JVM内存【JVM内存的划分为5部分,依次是:方法区java虚拟机栈本地方法栈程序计数器】的划分是在不同层次上的划分。如果需要将两者对应起来。主内存对应java堆的实例对象部分。工作内存对应栈中的部分区域(局部变量表)

  JVM内存执行流程如下图:

 

  JVM在设计的时候考虑到,如果工作内存每次修改都去修改主内存,会对性能影响较大。所以,每个线程拥有自己的工作内存。在执行的过程中,修改工作内存,而不是直接修改主内存。

  这样造成了一个线程对变量进行修改,只修改了工作内存中的变量,而没有及时的修改主内存变量的值。即这次操作对其余线程不可见。会导致线程不安全的问题。因为JMMJAVA内存模型)制定了一套标准,确保在多线程的情况下,能够控制什么时候内存会被同步给其它线程。

2.内存交互操作

  内存模型的操作有8种。JVM虚拟机保证每个操作都是原子性的(doublelong在某些平台除外)。

  内存交互操作的执行流程个人理解

  为了方便理解内存的操作。其实可以理解为三个层面。主内存层面数据,工作内存层面的数据和JVM执行子系统层面的数据。根据数据的流向来分析这8中操作。

准备从主内存读取数据。首先使用lock对主内存的变量进行加锁。即lock操作。其次,使用read将主内存数据加载到工作内存。同时使用loadread的变量值赋值给工作内存的变量。以上,就完成了数据从主内存层面到工作内存层面。同时,readload必须配置使用。不可拆开。

工作内存的数据在执行指令的时候,执行子系统会调用操作系统API,将工作内存数据传递给执行子系统数据。即将工作内存变量赋值给执行层面使用。这个使用使用use

  执行子系统执行完成后得到的变量需要传递给工作内存。即将执行子系统层面的数据传递给工作内存。这个时候使用assign

  工作内存的变量想同步给主内存。通过store将工作内存变量值传递给主内存,之后用writestore的数值赋值给主内存变量。这样完成了工作内存变量赋值给主内存变量的操作。同时,storewrite必须配置合适,不能拆开。

  以上的描述过程可见下图

 

  对主内存对象取消加锁操作。即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给规则,让操作满足有序性。比如,readload必须配合使用。Storewrite必须配合使用。

  1. 使用volatile保证有序性
  2. 使用synchronized保证有序性
  3. 使用显示锁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任务

  通过ExecutornewXXX方法获取ExecutorService实例。然后调用该实例的executor(Runnable command)方法即可。一旦runnable方法传递到execute方法上,则该方法将会加入到任务队列中。等待线程调用。

  1. public class TestCachedThreadPool{     
  2. public static void main(String[] args){     
  3. ExecutorService executorService = Executors.newCachedThreadPool();      
  4. for (int i = 0; i < 5; i++){     
  5. executorService.execute(new TestRunnable());     
  6. System.out.println("************* a" + i + " *************");     
  7. }     
  8. executorService.shutdown();     
  9. }     
  10. }     
  11. class TestRunnable implements Runnable{      //重写run方法   
  12. public void run(){     
  13. System.out.println(Thread.currentThread().getName() + "线程被调用了。");     
  14. }  

2) Executor执行callable任务

  将callable方法传递给ExecutorServicesubmit方法。则call方法将自动提交到任务队列中。根据线程池线程的使用情况。分配线程给该任务。

  Submit会返回一个Feature对象。

  1. public class CallableDemo{     
  2. public static void main(String[] args){     
  3. ExecutorService executorService = Executors.newCachedThreadPool();     
  4. List<Future<String>> resultList = new ArrayList<Future<String>>();     
  5. //创建10个任务并执行     
  6. for (int i = 0; i < 10; i++){     
  7. //使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中     
  8. Future<String> future = executorService.submit(new TaskWithResult(i));     
  9. //将任务执行结果存储到List中     
  10. resultList.add(future);     
  11. }     
  12. //遍历任务的结果     
  13. for (Future<String> fs : resultList){     
  14. try{     
  15. while(!fs.isDone);//Future返回如果没有完成,则一直循环等待,直到Future返回完成    
  16. System.out.println(fs.get());     //打印各个线程(任务)执行的结果     
  17. }catch(InterruptedException e){     
  18. e.printStackTrace();     
  19. }catch(ExecutionException e){     
  20. e.printStackTrace();     
  21. }finally{     
  22. //启动一次顺序关闭,执行以前提交的任务,但不接受新任务    
  23. executorService.shutdown();     
  24. }     
  25. }     
  26. }     
  27. }     
  28. class TaskWithResult implements Callable<String>{     
  29. private int id;     
  30. public TaskWithResult(int id){     
  31. this.id = id;     
  32. }     
  33. // 重写call()方法  
  34. public String call() throws Exception {    
  35. System.out.println("call()方法被自动调用!!!    " + Thread.currentThread().getName());     
  36. //该返回结果将被Future的get方法得到    
  37. return "call()方法被自动调用,任务返回的结果是:" + id + "    " + Thread.currentThread().getName();     
  38. }     

四、Synchronized

  Synchronized java关键字。能保证被他修饰的方法或者代码块的线程同步。即任意时刻只能被一个线程访问。它是多线程中重要的线程同步方式。

(1) Synchronized使用方式

  Synchronized主要的三种使用方式。修饰实例方法,修饰静态方法、修饰代码块

1) 修饰实例方法

  锁是当前实例对象。

2) 修饰静态方法

  锁是当前类的class对象。【由JVM可知,是classJVMjava.lang.class 类型的内存对象】,也称之为全局锁。

3) 修饰代码块

  锁是synchronized()括号中的对象。

  当一个线程试图访问同步代码的时候,必须先获得锁。退出或者抛出异常时,必须释放锁。

(2) Synchronized 锁对象存在哪里

  synchronized用到的锁是存在Java对象头里的。synchronized关键字实现的锁是依赖于JVM的,底层调用的是操作系统的指令集实现。

  对比LOCK接口。Lock接口实现的锁不一样,例如ReentrantLock锁是基于JDK实现的,有Java原生代码来实现的。

  即synchronized的锁对象是哪个对象。则在哪个对象的对象头中保存占用当前锁的线程信息。

(3) SynchronizedJVM中的实现

  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 的有序性

  JMMJava 内存模型)有三个方案保证多线程的有序性。Volatilesynchronizedlock。能保证多线程的有序性。即先行发生原则。则后执行的线程能观测到先执行线程的修改。

  如果一个变量被声明volatile的话,那么这个变量不会被进行重排序

(3) Volatile不能保证原子性

  Volatile 只能保证修饰的变量的可见性和有序性。不能保证原子性。所以, 在变量修改和自身数据无关的情况下,相当于原子操作的。因为不存在多个线程同时对volatile修饰变量的同时访问。这种情况下,是线程安全的。具体情况如下。

  1) 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值

  2) 变量不需要与其他状态变量共同参与不变约束

  在以上两种情况下,volatile修饰的变量能保证原子性。因为其本身能保证可见性和有序性。所以,以上两种情况下能保证线程安全

六、Lock

  Lock是接口。具体的实现类有两种。ReentrantLockReentrantReadWriteLock两个子类。和synchronized相比较。Synchronized是通过在编译的过程中,在字节码中添加monitorrentermonitorexit字节码命令。在执行字节码的过程中,调用系统API,确保线程同步的。而lockjdk中的包提供的功能,实现的线程同步。

(1) Lock实现同步

  根据synchronized关键字可以了解。它实现线程同步是使用一个锁对象。然后在需要线程同步的地方添加获取锁。在使用完成之后释放锁。其实,lock的实现思路也是一样的。Lock的子类就是一个锁对象而已。

  在使用之前先创建一个锁对象。比如 Lock lock = new ReentrantLock();然后在使用的开始地方获取锁。即lock.lock();在使用完成的地方添加一个释放锁。即lock.unlock()操作。即通过lock完成线程的同步。

(2) 使用condition实现等待/通知

  个人理解的线程之间的等待/通知模式和线程的并行操作操作模式其实是多线程使用的两个场景。

1) 多线程并行操作模式的理解

  多线程并行操作模式,其实是多个线程针对共享数据的不同部分,做相同的流程的操作

2) 等待通知模式的理解

  等待/通知模式 其实是不同种类线程(有不同的操作流程)之间的配合协作使用用场景

3) Lockcondition使用

 

  关键字synchronizedwait/notify配合使用,可以实现等待通知模式。而lock中提供了condition,能提供多路通知的功能。即可以选择性通知。

1.使用方法

  通过Condition condition = lock.newCondition()获取condition对象。在需要等待的地方调用condition.wait().则对当前线程处于等待状态。

2.使用需要注意

  创建condition的时候,在此之前需要对lock加锁。即在之前需要调用lock.lock()。否则会报错。

七、线程池相关

(1) 线程池的好处

1) 降低资源消耗

  通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。

2) 提高响应速度

  任务准备好就可以立即执行,不需要因为等待创建线程的时间。

3) 提高线程的可管理型

线程是稀缺资源,使用线程池可以统一分配,监控。

(2) 常见的线程池及使用的场景

  常见的线程池ThreadPoolExecutorFixedThreadPoolCacheThreadPoolSingleThreadExecutor

1.FixedThreadPool 可重用固定线程数的线程池

  适用场景:适用于负载比较重的服务器

  1. FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列。
  2. 该线程池的线程数始终保持不变。当一个新的任务提交时,线程池若有空闲线程,则立即执行。若没有空闲线程,则新任务被暂存在任务队列中。待有线程空闲时,从队列中取出需要处理的任务开始执行。

2.CacheThreadPool 根据需要调节线程数量的线程池

  适用场景:大小无界,适用于执行需要短期异步的小程序。或者负载较轻的服务器

  1. CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPoolmaximumPool是无界的。
  2. 线程池的数量不确定,但是,如果有空闲线程,则优先复用空闲的线程。若所有线程都在工作,此时,有新任务提交,则创建新线程处理任务。

3.SingleThreadExecutor 只创建一个线程执行任务

  适用场景:需要保持顺序执行任务。任意时间点,没有多线程活动的场景

  1. SingleThreadExecutor使用无界队列LinkedBlockingQueue 作为线程池的工作队列。
  2. 若有新任务提交,则保存到任务队列。待线程空闲,按照队列的先后顺序,执行任务。

(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.工作队列已满

    调用拒绝策略,进行拒绝任务。

 

八、synchronizedvolatile区别

所以,一定情况下,synchronizedvolatile都能解决线程数据同步的问题。但是,各有特点。

  1. Synchronized 修饰的是方法,代码块。Volatile修饰的是共享变量。
  2. Synchronized 是通过同步阻塞的方式完成变量的同步,体现的是原子性Volatile是非阻塞的,能保证可见性,不能保证原子性。第一时间回去修改数据到主内存。
  3. 什么时候下可以使用volatile?任何时候都可以使用synchronized,都能起到数据同步的作用。如果写入的变量值不依赖当前的变量值的情况下可以使用
posted @ 2020-12-16 10:42  IT迷途小书童  阅读(99)  评论(0编辑  收藏  举报