zoukankan      html  css  js  c++  java
  • 多线程

    java有一些重点的技术,非初学者都必须掌握的。以下,就是我关于这些知识的交流和分享。

    • Spring的IOC(依赖注入和控制反转)

    依赖注入核心解决的问题:依赖注入的思想是不去主动获得你需要的东西,而是相反,你自己作为一种服务,让需要的东西来找你。该模式的核心是将一个服务与它所依赖的其他服务解耦,这样一来,那些依赖可以替换为测试用的mock对象,或者针对其他环境替换为恰当的变体。通过保持对于所依赖对象的不可知性,一个服务是高内聚的,功能专一的并且易于进化。

    依赖注入常用的三种方式。1.set添加依赖的对象。2.构造方法带入依赖的对象。3.实例工厂的方式注入对象(工厂模式中常见,依赖接口或者抽象类,具体对象根据工厂模式获取)。

    示例场景:客户端需要使用一个用户类,用户类中依赖用户信息类。分别从传统和依赖注入两种不同思想下程序的实现过程。

    传统场景下:

    clipboard

    以上可以看出,客户端类在程序的运行中占据主导地位。创建者(客户端类)控制着整个被创建的类生成和组织。

    依赖注入思想下spring容器的操作

    clipboard

    业务控制类(客户端类)将控制权交出给IoC容器,自己也被IoC容器控制(生命周期等),控制发生的反转。

    换个角度看待问题,传统是谁需要谁创造谁控制,现在变成有统一的控制容器控制,其实就是分工明细,擅长的人做擅长的事情(IoC容器管理控制),业务高度内聚。告别小作坊,本来就是一个先进的理念。

    IoC 不是一种技术,只是一种思想,一个重要的面向对象编程的法则。使用IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便 测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

    • 反射:

    有人说,如果把java中的各项技术比作一门武功的话,那么反射就是易筋经之类的武功。一旦学会就等于打通了任督二脉,对很多技术的了解更加的容易。学习其它技术也变得简单。我认可这样的理解。

    因为反射,让java从静态变成了动态语言。不再是提前写好代码,运行就是按照业务流程流动。反射改变整个处理过程。在运行的过程中动态的加载实体类。比如spring的动态加载bean,spring的AOP面向切面的动态代理。还有在实际工作中关于反射的使用,让代码变得很灵活。让人感到神来之笔。

    下面,就分别从spring动态加载bean的实例,springAOP动态代理的实例和实际工作(excel导入)的实例来详细分析反射机制。实例代码如下:

    1. spring动态加载bean:
    2. springAOP的动态代理:
    3. 数据导入的动态代理实现(万能excel导入/导出):

    获取class的三种方式

    1. // 第一种方式 Class  静态方法forName - 获取类对象

            try {

              demo = Class.forName("com.maop.rf.bean.Book");

            } catch (Exception e) {

              e.printStackTrace();

            }

            System.out.println(demo);

       

          2.  // 第二种 通过实例化对象来获取类对象

            Book bo = new Book();

            Object ob = bo;

            System.out.println("第二种 " + ob.getClass());

       

          3.  // 第三种 直接使用类名点class

            demo2 = Book.class;

            System.out.println("第三种:" + demo2);

       

            try {

              Book bo1 = (Book) demo2.newInstance();

              System.out.println(bo1);

            } catch (Exception e) {

              // TODO: handle exception

            }

    • 多线程:

    多线程是中级的开发人员都必须掌握的基本技术。基本上面试都会遇到这方面的问题。

    1. 概念和原理:

    进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。

    线程是指进程中的一个执行流程,一个进程中可以运行多个线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。

    使用java.lang.Thread类或者java.lang.Runnable接口编写代码来定义、实例化和启动新线程。

    线程总体分两类:用户线程和守候线程

    1. 创建与启动

    一、定义线程

    1、扩展java.lang.Thread类。

    此类中有个run()方法,应该注意其用法:

    public void run()

    如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。

    Thread 的子类应该重写该方法。

    2、实现java.lang.Runnable接口。

    void run()

    使用实现接口 Runnable 的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的 run 方法。

    方法 run 的常规协定是,它可能执行任何所需的操作。

    两种方式的区别:既然都能实现线程的功能,怎样区别使用呢。java规定只能单继承,如果自定义类需要继承其他类,只能选择实现Runnable接口。

    二、实例化线程

    1、如果是扩展java.lang.Thread类的线程,则直接new即可。

    2、如果是实现了java.lang.Runnable接口的类,则用Thread的构造方法:

    Thread(Runnable target)

    Thread(Runnable target, String name)

    三、启动线程

    在线程的Thread对象上调用start()方法,而不是run()或者别的方法。

    四、例子

    1、实现Runnable接口的多线程例子

    /**

    * 实现Runnable接口的类

    *

    * @author

    */

    public class DoSomething implements Runnable {

    private String name;

    public DoSomething(String name) {

    this.name = name;

        }

    public void run() {

    for (int i = 0; i < 5; i++) {

    for (long k = 0; k < 100000000; k++) ;

                System.out.println(name + ": " + i);

            }

        }

    }

    /**

    * 测试Runnable类实现的多线程程序

    *

    * @author leizhimin 2008-9-13 18:15:02

    */

    public class TestRunnable {

    public static void main(String[] args) {

            DoSomething ds1 = new DoSomething("阿三");

            DoSomething ds2 = new DoSomething("李四");

            Thread t1 = new Thread(ds1);

            Thread t2 = new Thread(ds2);

            t1.start();

            t2.start();

        }

    }

    2、扩展Thread类实现的多线程例子

    /**

    * 测试扩展Thread类实现的多线程程序

    *

    * @author leizhimin

    */

    public class TestThread extends Thread{

    public TestThread(String name) {

    super(name);

        }

    public void run() {

    for(int i = 0;i<5;i++){

    for(long k= 0; k <100000000;k++);

                System.out.println(this.getName()+" :"+i);

            }

        }

    public static void main(String[] args) {

            Thread t1 = new TestThread("阿三");

            Thread t2 = new TestThread("李四");

            t1.start();

            t2.start();

        }

    }

    1. 线程状态的转换

    线程的状态转换是线程控制的基础。线程状态总的可分为五大状态:分别是生、死、可运行、运行、等待/阻塞。用一个图来描述如下:

    clipboard

    新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

    就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

    运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就     绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

    阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

    1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

    2.同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

    3.其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

    死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    线程在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。

    clipboard

    221320062031

    1. 内功心法:每个对象都有的方法(机制)

    synchronized, wait, notify 是任何对象都具有的同步工具。让我们先来了解他们

    0771d68cb2ba

    monitor

    他们是应用于同步问题的人工线程调度工具。讲其本质,首先就要明确monitor的概念,Java中的每个对象都有一个监视器,来监测并发代码的重入。在非多线程编码时该监视器不发挥作用,反之如果在synchronized 范围内,监视器发挥作用。

    wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。这意味着wait之后,其他线程可以进入同步块执行。

    当某代码并不持有监视器的使用权时(如图中5的状态,即脱离同步块)去wait或notify,会抛出java.lang.IllegalMonitorStateException。也包括在synchronized块中去调用另一个对象的wait/notify,因为不同对象的监视器不同,同样会抛出此异常。

    再讲用法:

    • synchronized单独使用:
      • 代码块:如下,在多线程环境下,synchronized块中的方法获取了lock实例的monitor,如果实例相同,那么只有一个线程能执行该块内容

    public class Thread1 implements Runnable { Object lock; public void run() { synchronized(lock){ ..do something } } }

    直接用于方法: 相当于上面代码中用lock来锁定的效果,实际获取的是Thread1类的monitor。更进一步,如果修饰的是static方法,则锁定该类所有实例。

    public class Thread1 implements Runnable { public synchronized void run() { ..do something } }

    • 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)。

    6cfda7042c67

    volatile

    针对多线程使用的变量如果不是volatile或者final修饰的,很有可能产生不可预知的结果(另一个线程修改了这个值,但是之后在某线程看到的是修改之前的值)。其实道理上讲同一实例的同一属性本身只有一个副本。但是多线程是会缓存值的,本质上,volatile就是不去缓存,直接取值。在线程安全的情况下加volatile会牺牲性能。

    1. 太祖长拳:基本线程类

    基本线程类指的是Thread类,Runnable接口,Callable接口

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

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

    Thread类相关方法:

    copycode

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

    copycode

    关于中断:它并不像stop方法那样会中断一个正在运行的线程。线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为true)。终端只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。

    Thread.interrupted()检查当前线程是否发生中断,返回boolean

    synchronized在获锁的过程中是不能被中断的。

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

    Thread类最佳实践:

    写的时候最好要设置线程名称 Thread.name,并设置线程组 ThreadGroup,目的是方便管理。在出现问题的时候,打印线程栈 (jstack -pid) 一眼就可以看出是哪个线程出的问题,这个线程是干什么的。

    如何获取线程中的异常

    5b7f8df6e8d3

    不能用try,catch来获取线程中的异常

    Runnable

    与Thread类似

    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 返回值,阻塞直到该线程运行结束

    1. 九阴真经:高级多线程控制类

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

    6.1.ThreadLocal类

    用处:保存线程的独立变量。对一个线程类(继承自Thread)

    当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。

    实现:每个Thread都持有一个TreadLocalMap类型的变量(该类是一个轻量级的Map,功能与map一样,区别是桶里放的是entry而不是entry的链表。功能还是一个map。)以本身为key,以目标为value。

    主要方法是get()和set(T a),set之后在map里维护一个threadLocal -> a,get时将a返回。ThreadLocal是一个特殊的容器。

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

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

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

    该方法可用于实现乐观锁,考虑文中最初提到的如下场景:a给b付款10元,a扣了10元,b要加10元。此时c给b2元,但是b的加十元代码约为:

    copycode

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

    copycode

    AtomicReference

    对于AtomicReference 来讲,也许对象会出现,属性丢失的情况,即oldObject == current,但是oldObject.getPropertyA != current.getPropertyA。

    这时候,AtomicStampedReference就派上用场了。这也是一个很常用的思路,即加上版本号

    6.3.Lock类 

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

    ReentrantLock ReentrantReadWriteLock.ReadLock ReentrantReadWriteLock.WriteLock

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

    区别如下:

    copycode

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

    copycode

    93b31bfed934

    synchronized和Lock性能对比

    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方法。写写,写读互斥;读读不互斥。可以实现并发读的高效线程安全代码

    6.4.容器类

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

    BlockingQueue ConcurrentHashMap

    BlockingQueue

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

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

    7b06b8d86db8

    BlockingQueue

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

    常见的阻塞队列有:

    ArrayListBlockingQueue LinkedListBlockingQueue DelayQueue SynchronousQueue

    ConcurrentHashMap

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

    6.5.管理类

    管理类的概念比较泛,用于管理线程,本身不是多线程的,但提供了一些机制来利用上述的工具做一些封装。

    了解到的值得一提的管理类:ThreadPoolExecutor和 JMX框架下的系统级管理类 ThreadMXBean

    ThreadPoolExecutor

    如果不了解这个类,应该了解前面提到的ExecutorService,开一个自己的线程池非常方便:

    copycode

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

    copycode

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

    8a70ba646843

    ThreadPoolExecutor参数解释

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

    copycode

    1. 知识点
    • 线程使用run方法调用不会创建新的线程 ,使用start调用会产生新的线程
    • sleep和wait的区别:
    • sleep是Thread类的方法,wait是Object类中定义的方法.
    • Thread.sleep不会导致锁行为的改变, 如果当前线程是拥有锁的, 那么Thread.sleep不会让线程释放锁.
    • Thread.sleep和Object.wait都会暂停当前的线程. OS会将执行时间分配给其它线程. 区别是, 调用wait后, 需要别的线程执行notify/notifyAll才能够重新获得CPU执行时间.
    • 线程同步以及线程调度相关的方法
      • wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
      • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
      • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
      • notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
    • 线程的sleep()方法和yield()方法有什么区别?

    ① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行 的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;

    ② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;

    • 如何在Java中实现线程?

    创建线程有两种方式:

    一、继承 Thread 类,扩展线程。

    二、实现 Runnable 接口。

    • 线程和进程有什么区别?

    一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。

    1. 线程简单介绍

    线程的特征:轻量,独立,共享(共享进程中的资源)。

    线程的实现:1.继承thread类,重新run方法。2.实现runnable接口,实现run方法。

    线程的生命周期:新建,就绪,运行,阻塞,死亡。

    常见API:

    静态方法:currentThread():获取当前运行线程引用。yield:自动进入就绪状态,放弃时间片。sleep休眠多长时间后进入就绪状态。

    实例方法:start(启动) setname(线程名)

    1. 线程的同步(协同,有序)

    1.对象锁,同步块。(同步区域--粒度更小)

    2.同步方法。

    // 第一种方式 Class 的 静态方法forName - 获取类对象try {demo = Class.forName("com.maop.rf.bean.Book");} catch (Exception e) {e.printStackTrace();}System.out.println(demo);
    // 第二种 通过实例化对象来获取类对象Book bo = new Book();Object ob = bo;System.out.println("第二种 :" + ob.getClass());
    // 第三种 直接使用类名点classdemo2 = Book.class;System.out.println("第三种:" + demo2);
    try {Book bo1 = (Book) demo2.newInstance();System.out.println(bo1);} catch (Exception e) {// TODO: handle exception}

  • 相关阅读:
    Nginx URL后面不加斜杠301重定向
    Jenkins 配置 Node.js 项目
    在 Linux 上搭建IntelliJ IDEA license server服务器
    Vue 使用细节收集
    sinopia 搭建记录
    cli 开发记录
    Cgroup(一)简介
    Kubernetes (一)POD驱逐
    RabbitMQ(五)镜像队列
    RabbitMQ(四)队列结构
  • 原文地址:https://www.cnblogs.com/maopneo/p/8478636.html
Copyright © 2011-2022 走看看