zoukankan      html  css  js  c++  java
  • 深入理解java:2. 多线程机制

    引言

    很多人都对其中的一些概念不够明确,如同步、并发等等,让我们先理清一些概念,以免产生误会。

    • 多线程:指的是这个程序(一个进程)运行时,产生了不止一个线程。
    • 并行与并发:
      • 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
      • 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
    • 线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码:
      void transferMoney(User from, User to, float amount){
        to.setMoney(to.getBalance() + amount);
        from.setMoney(from.getBalance() - amount);
      }
    • 同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能

    线程的状态

    线程状态

    线程状态转换


    各种状态一目了然,值得一提的是"blocked"这个状态:
    线程在Running的过程中可能会遇到阻塞(Blocked)情况

    1. 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
    2. 调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)
    3. 对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。

    此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。

    线程的内存

    在本节,我们需要将Thread类和Java中线程的概念分开来讲了:
    • 一个Thread类实例只是一个对象,像Java中的任何其他对象一样,具有变量和方法,生死于JVM堆上。
    • Java中,每个线程都有一个调用栈,即使你不在Java程序中创建任何新的线程,线程也在后台运行着(如:main线程)。一个Java应用总是从main()方法开始运行,mian()方法运行在一个线程内,它被称为主线程。一旦创建一个新的线程,就产生一个新的调用栈。
     
    Java的虚拟机内存分为几个部分,其中线程共享的是堆区和方法区,线程私有的是虚拟机栈,本地方法栈和程序计数器。在本文,我们只关注Java线程内存模型中最突出的两个模块:堆内存 和 栈内存
     
         在这里,堆内存,就是前面说的堆区,由各个线程共享,存储的是对象的实例;
         而栈内存,指的是虚拟机栈(在HotSpot虚拟机中,本地方法栈是与虚拟机栈放在一起实现的,所以这里不再专门区分),存储的是局部变量表、动态链接、方法出口。
     
     
    把上面所说的堆内存中的对象和基本数据类型,称为主内存(main memory)
    把上面所说的栈内存中用于存储变量的副本,称为本地内存(local memory)(或叫工作内存),这就组成了Java内存模型(JMM)
    1,Java线程对于变量的所有操作(读取、赋值),都是在自己的工作内存中进行的,线程不直接读写主内存中的变量。
    2,不同线程无法直接访问对方工作内存中的变量;
    3,线程间变量值的传递,需要主内存来完成。
     
     

    线程间通信

    上节所讲到的Java内存模型(JMM)控制了线程间的通信,Java线程间通信,使用的是共享内存模型,而非消息传递模型。
    也就是说,线程间是通过write-read内存中的公共状态来进行隐式通信的,多个线程之间不能直接传递数据交互,它们之间的交互只能通过共享变量来实现。
     
    线程间通信的步骤:
    • 线程A把本地内存A中更新过的共享变量刷新到主内存中去。
    • 线程B到主内存中去读取线程A之前已更新过的共享变量。
     
     

    每个对象都有的方法(机制)

    synchronized, wait, notify 是任何对象都具有的同步工具。

    讲其本质,首先就要明确monitor的概念,Java中的每个对象都有一个监视器,来监测并发代码的重入。

    在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用。

    wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。
     

    synchronized, wait, notify结合:典型场景生产者消费者问题

    复制代码
    /**
       * 生产者生产出来的产品交给店员
       */
      public synchronized void produce()
      {
          if(this.product >= MAX_PRODUCT)
          {
              try
              {
                  wait();  
                  System.out.println("产品已满,请稍候再生产");
              }
              catch(InterruptedException e)
              {
                  e.printStackTrace();
              }
              return;
          }
    
          this.product++;
          System.out.println("生产者生产第" + this.product + "个产品.");
          notifyAll();   //通知等待区的消费者可以取出产品了
      }
    
      /**
       * 消费者从店员取产品
       */
      public synchronized void consume()
      {
          if(this.product <= MIN_PRODUCT)
          {
              try 
              {
                  wait(); 
                  System.out.println("缺货,稍候再取");
              } 
              catch (InterruptedException e) 
              {
                  e.printStackTrace();
              }
              return;
          }
    
          System.out.println("消费者取走了第" + this.product + "个产品.");
          this.product--;
          notifyAll();   //通知等待去的生产者可以生产产品了
      }
    复制代码
     
    volatile

    多线程的内存模型:main memory(主存)、working memory(线程栈),在处理数据时,线程会把值从主存load到本地栈,完成操作后再save回去。

    (volatile关键词的作用:每次针对该变量的操作都激发一次load and save)。

    本质上,volatile就是不去缓存,直接取值。在线程安全的情况下加volatile会牺牲性能。
     
     
     
    基本的线程类

    基本的线程类指的是

    Thread类,

    Runnable接口,

    Callable接口。

    通过继承Thread类,重写Thread的run()方法,将线程运行的逻辑放在其中

    1. class MyThread extends Thread{  
    2.       
    3.     private int ticket = 10;  
    4.     private String name;  
    5.     public MyThread(String name){  
    6.         this.name =name;  
    7.     }  
    8.       
    9.     public void run(){  
    10.         for(int i =0;i<500;i++){  
    11.             if(this.ticket>0){  
    12.                 System.out.println(this.name+"卖票---->"+(this.ticket--));  
    13.             }  
    14.         }  
    15.     }  
    16. }


    Thread 类实现了Runnable接口,启动一个线程的方法:

     MyThread my = new MyThread();
      my.start();

    Thread类相关方法:

    复制代码
    //当前线程可转让cpu控制权,让别的就绪状态线程运行(切换)
    public static Thread.yield() //让渡
    //暂停一段时间
    public static Thread.sleep()  //睡眠
    //在一个线程中调用other.join(),将等待other执行完后才继续本线程。    
    public join()//中断
    //后两个函数皆可以被打断
    public interrupte()//打断
    复制代码

    关于打断:它并不像stop方法那样会中断一个正在运行的线程。

    线程会不时地检测 断标识位,以判断线程是否应该被断(断标识值是否为true)。

    终端只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。
    Thread.interrupted()检查当前线程是否发生中断,返回boolean
    synchronized在获锁的过程中是不能被中断的。

    中断是一个状态!interrupt()方法只是将这个状态置为true而已。

    所以说正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。如果在正常运行的程序中添加while(!Thread.interrupted()) ,则同样可以在中断后离开代码。

    Thread类最佳实践
    写的时候最好要设置线程名称 Thread.name,并设置线程组 ThreadGroup,目的是方便管理。

    在出现问题的时候,打印线程栈 (jstack -pid) 一眼就可以看出是哪个线程出的问题,这个线程是干什么的。

    ThreadGroup tg = new ThreadGroup("My Group");
    MyThread thrd = new MyThread(tg, "MyThreadName");
    ThreadGroup比ExecutorService多以下几个优势  

    1.ThreadGroup可以遍历线程,知道那些线程已经运行完毕,那些还在运行  

    2.可以通过ThreadGroup.activeCount知道有多少线程从而可以控制插入的线程数

    参考:http://www.cnblogs.com/yy2011/archive/2011/05/05/2037564.html


    Runnable

    1. class MyThread1 implements Runnable{  
    2.     private int ticket =10;  
    3.     private String name;  
    4.     public void run(){  
    5.         for(int i =0;i<500;i++){  
    6.             if(this.ticket>0){  
    7.                 System.out.println(Thread.currentThread().getName()+"卖票---->"+(this.ticket--));  
    8.             }  
    9.         }  
    10.     }  

    Callable

    future模式:并发模式的一种,可以有两种形式,即无阻塞和阻塞,分别是isDone和get。其中Future对象用来存放该线程的返回值以及状态

    ExecutorService e = Executors.newFixedThreadPool(3);
     //submit方法有多重参数版本,及支持callable也能够支持runnable接口类型.
    Future future = e.submit(new myCallable());
    future.isDone() //return true,false 无阻塞
    future.get() // return 返回值,阻塞直到该线程运行结束

     

    高级的多线程控制类

    Java1.5提供了一个非常高效实用的多线程包:java.util.concurrent, 提供了大量高级工具,可以帮助开发者编写高效、易维护、结构清晰的Java多线程程序。

    1.ThreadLocal类

    用处:保存线程的独立变量。对一个线程类(继承自Thread)
    当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

    常用于用户登录控制,如记录session信息。

    实现:每个Thread都持有一个TreadLocalMap类型的变量

    (该类是一个轻量级的Map,功能与map一样,区别是桶里放的是entry而不是entry的链表。功能还是一个map。)以本身(ThreadLocal对象)为key,以目标为value。
    主要方法是get()和set(T a),set之后在map里维护一个threadLocal -> a,  get时将a返回。

    ThreadLocal是一个特殊的容器。

     

    2.原子类(AtomicInteger、AtomicBoolean……)

    如果使用atomic wrapper class如atomicInteger,或者使用自己保证原子的操作,则等同于synchronized

    //返回值为boolean
    AtomicInteger.compareAndSet(int expect,int update)

    该方法可用于实现乐观锁,CAS算法。

    if(b.value.compareAndSet(old, value)){
       return ;
    }else{
       //try again
       // if that fails, rollback and log
    }

    AtomicReference
    对于AtomicReference 来讲,也许对象会出现,属性丢失的情况,

    即oldObject == current,但是oldObject.getPropertyA != current.getPropertyA。
    这时候,AtomicStampedReference就派上用场了。这也是一个很常用的思路,即加上版本号

    3.Lock类 

    lock: 在java.util.concurrent包内。共有三个实现:

    ReentrantLock
    ReentrantReadWriteLock.ReadLock
    ReentrantReadWriteLock.WriteLock

    主要目的是和synchronized一样, 两者都是为了解决同步问题,处理资源争端而产生的技术。功能类似但有一些区别。

    区别如下:

     
    lock更灵活,可以自由定义多把锁的枷锁解锁顺序(synchronized要按照先加的后解顺序)
    提供多种加锁方案,lock 阻塞式, trylock 无阻塞式, lockInterruptily 可打断式, 还有trylock的带超时时间版本。
    本质上和监视器锁(即synchronized是一样的)
    能力越大,责任越大,必须控制好加锁和解锁,否则会导致灾难。
    和Condition类的结合。
    

     

    ReentrantLock    
    可重入的意义在于持有锁的线程可以继续持有,并且要释放对等的次数后才真正释放该锁。
    使用方法是:

    1.先new一个实例

    static ReentrantLock r=new ReentrantLock();
    2.加锁      
    r.lock()或r.lockInterruptibly();

    此处也是个不同,后者可被打断。当a线程lock后,b线程阻塞,此时如果是lockInterruptibly,那么在调用b.interrupt()之后,b线程退出阻塞,并放弃对资源的争抢,进入catch块。(如果使用后者,必须throw interruptable exception 或catch)    

    3.释放锁   

    r.unlock()

    必须做!何为必须做呢,要放在finally里面。以防止异常跳出了正常流程,导致灾难。这里补充一个小知识点,finally是可以信任的:经过测试,哪怕是发生了OutofMemoryError,finally块中的语句执行也能够得到保证。

    ReentrantReadWriteLock

    可重入读写锁(读写锁的一个实现) 

     ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
      ReadLock r = lock.readLock();
      WriteLock w = lock.writeLock();

    两者都有lock,unlock方法。写写,写读互斥;读读不互斥。可以实现并发读的高效线程安全代码

    4.容器类

    这里就讨论比较常用的两个:

    BlockingQueue
    ConcurrentHashMap

    BlockingQueue
    阻塞队列。该类是java.util.concurrent包下的重要类,通过对Queue的学习可以得知,这个queue是单向队列,可以在队列头添加元素和在队尾删除或取出元素。类似于一个管  道,特别适用于先进先出策略的一些应用场景。普通的queue接口主要实现有PriorityQueue(优先队列),有兴趣可以研究

    BlockingQueue在队列的基础上添加了多线程协作的功能:


    BlockingQueue


    除了传统的queue功能(表格左边的两列)之外,还提供了阻塞接口put和take,带超时功能的阻塞接口offer和poll。put会在队列满的时候阻塞,直到有空间时被唤醒;take在队 列空的时候阻塞,直到有东西拿的时候才被唤醒。用于生产者-消费者模型尤其好用,堪称神器。

    常见的阻塞队列有:

    ArrayListBlockingQueue
    LinkedListBlockingQueue
    DelayQueue
    SynchronousQueue

    ConcurrentHashMap
    高效的线程安全哈希map。请对比hashTable , concurrentHashMap, HashMap

    5.管理类

    管理类的概念比较泛,用于管理线程,本身不是多线程的,但提供了一些机制来利用上述的工具做一些封装。
    了解到的值得一提的管理类:ThreadPoolExecutor和 JMX框架下的系统级管理类 ThreadMXBean
    ThreadPoolExecutor
    如果不了解这个类,应该了解前面提到的ExecutorService,开一个自己的线程池非常方便:

    复制代码
    ExecutorService e = Executors.newCachedThreadPool();
        ExecutorService e = Executors.newSingleThreadExecutor();
        ExecutorService e = Executors.newFixedThreadPool(3);
        // 第一种是可变大小线程池,按照任务数来分配线程,
        // 第二种是单线程池,相当于FixedThreadPool(1)
        // 第三种是固定大小线程池。
        // 然后运行
        e.execute(new MyRunnableImpl());
    复制代码

    该类内部是通过ThreadPoolExecutor实现的,掌握该类有助于理解线程池的管理,本质上,他们都是ThreadPoolExecutor类的各种实现版本。请参见javadoc:

    corePoolSize:池内线程初始值与最小值,就算是空闲状态,也会保持该数量线程。
    maximumPoolSize:线程最大值,线程的增长始终不会超过该值。
    keepAliveTime:当池内线程数高于corePoolSize时,经过多少时间多余的空闲线程才会被回收。回收前处于wait状态
    unit:
    时间单位,可以使用TimeUnit的实例,如TimeUnit.MILLISECONDS 
    workQueue:待入任务(Runnable)的等待场所,该参数主要影响调度策略,如公平与否,是否产生饿死(starving)
    threadFactory:线程工厂类,有默认实现,如果有自定义的需要则需要自己实现ThreadFactory接口并作为参数传入。
     
     
     
     
     
    参考资料

    《Java编程思想》
    线程执行者:http://ifeve.com/thread-executors-1/
    线程原理:http://www.open-open.com/bbs/view/1318332540655
    Java线程详解:http://www.cnblogs.com/riskyer/p/3263032.html
    计算机操作系统之进程和线程:http://www.cnblogs.com/loveyakamoz/archive/2012/11/14/2770811.html
    用户级线程和内核级线程的区别:http://blog.csdn.net/debugingstudy/article/details/12668389
    Java多线程编程总结:http://lavasoft.blog.51cto.com/62575/27069
    Java多线程小结,及解决应用挂死的问题:http://sis*huo*k.com/forum/posts/list/1280.html(这个网页居然是违禁词。。无语了)
    Java编程体验:线程的7种状态及相互转换(图) http://blog.csdn.net/jiafu1115/article/details/6804386
    Java内存模型总结:http://blog.csdn.net/vking_wang/article/details/8574376
    java内存模型与多线程:http://www.cnblogs.com/fguozhu/articles/2657904.html
    java虚拟机内存模式。栈和堆:http://rainyear.iteye.com/blog/1735121
    Java内存模型:http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
    Java 多线程编程之八:多线程的调度 :http://blog.csdn.net/defonds/article/details/8664948
    Java 线程的Interrupt:http://blog.csdn.net/hudashi/article/details/6958550
    用Interrupt中断Java线程:http://hapinwater.iteye.com/blog/310558
    Runnable、Callable、Executor、Future、FutureTask关系解读 : http://blog.csdn.net/zhangzhaokun/article/details/6615454
    Java并发编程:Callable、Future和FutureTask : http://www.cnblogs.com/dolphin0520/p/3949310.html
    Java并发编程:线程池的使用:http://www.cnblogs.com/dolphin0520/p/3932921.html
     java中Executor、ExecutorService、ThreadPoolExecutor介绍 :http://blog.csdn.net/linghu_java/article/details/17123057
    ScheduledThreadPoolExecutor  :http://hubingforever.blog.163.com/blog/static/17104057920109643632988/
    ThreadPoolExecutor  : http://hubingforever.blog.163.com/blog/static/1710405792010964339151/
     
  • 相关阅读:
    微信 token ticket jsapi_ticket access_token 获取 getAccessToken get_jsapi_ticket方法
    PHP 日志 记录 函数 支持 数组 对象 新浪 sae 环境 去掉 空格 换行 格式化 输出 数组转字符串
    原生 原始 PHP连接MySQL 代码 参考mysqli pdo
    PHP 数字金额转换成中文大写金额的函数 数字转中文
    使用PHPMailer发送带附件并支持HTML内容的邮件
    设置输出编码格式 header 重定向 执行时间 set_time_limit 错误 报告 级别 error_reporting
    html5 bootstrap pannel table 协议 公告 声明 文书 模板
    指向指针的指针
    二级指针
    c语言:当指针成为参数后
  • 原文地址:https://www.cnblogs.com/my376908915/p/6756895.html
Copyright © 2011-2022 走看看