zoukankan      html  css  js  c++  java
  • 多线程初启编(一)

    一. 线程概念    

          我们可以在计算机上运行各种计算机软件程序。每一个运行的程序可能包括多个独立运行的线程(Thread)。 线程(Thread)是一份独立运行的程序,有自己专用的运行栈。线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。

          当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。

          同步这个词是从英文synchronize(使同时发生)翻译过来的。我也不明白为什么要用这个很容易引起误解的词。既然大家都这么用,咱们也就只好这么将就。

          线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。因此,关于线程同步,需要牢牢记住的第一点是:线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。关于线程同步,需要牢牢记住的第二点是 “共享”这两个字。只有共享资源的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要。关于线程同步,需要牢牢记住的第三点是,只有“变量”才需要同步访问。如果共享的资源是固定不变的,那么就相当于“常量”,线程同时读取常量也不需要同步。至少一个线程修改共享资源,这样的情况下,线程之间就需要同步。关于线程同步,需要牢牢记住的第四点是:多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。

    二. 线程的创建

           创建线程有四种方式,下面我们进行一一讲解。

          1.继承Thread类实现多线程

            run()为线程类的核心方法,相当于主线程的main方法,是每个线程的入口
            a.一个线程调用 两次start()方法将会抛出线程状态异常,也就是的start()只可以被调用一次 
            b. run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)

        

    public class test  extends Thread{
            @Override
            public void run() {
                //线程会执行的指令
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("输出");
            }
    
            public static void main(String[] args) {
                test test=new test();
                test.start();
    
                //test.stop(); //不建议 强制终止这个线程。
    
                //发送终止通知
    
    
            }
        }
    

      2.覆写Runnable()接口实现多线程,而后同样覆写run().推荐此方式

            a.覆写Runnable接口实现多线程可以避免单继承局限
            b.当子类实现Runnable接口,此时子类和Thread的代理模式(子类负责真是业务的操作,thread负责资源调度与线程创建辅助真实业务。

           

    //这段代码是有问题的,至于有啥问题后面会讲解
    public class MyThread implements Runnable{
        public static int count=20;
        public void run() {
            while(count>0) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"-当前剩余票数:"+count--);
            }
        }
        public static void main(String[] args) {
            MyThread Thread1=new MyThread();
            Thread mThread1=new Thread(Thread1,"线程1");
            Thread mThread2=new Thread(Thread1,"线程2");
            Thread mThread3=new Thread(Thread1,"线程3");
            mThread1.start();
            mThread2.start();
            mThread3.start();
        }
    
    }
    

      3.覆写Callable/Future接口实现多线程(JDK1.5)

               a.核心方法叫call()方法,有返回值
               b.有返回值

           

    public class CallableDemo implements Callable<String> {
    
        @Override
        public String call() throws Exception {
            System.out.println("come in");
            Thread.sleep(10000);
            return "SUCCESS";
        }
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            ExecutorService executorService=Executors.newFixedThreadPool(1);
            CallableDemo callableDemo=new CallableDemo();
            Future<String> future=executorService.submit(callableDemo);
            System.out.println(future.get()); //阻塞
        }
    }
    

      4.通过线程池启动多线程

             

    public class Test {
    	public static void main(String[] args) {
    		ExecutorService ex=Executors.newFixedThreadPool(5);
    		
    		for(int i=0;i<5;i++) {
    			ex.submit(new Runnable() {
    				
    				@Override
    				public void run() {
    					for(int j=0;j<10;j++) {
    						System.out.println(Thread.currentThread().getName()+j);
    					}
    					
    				}
    			});
    		}
    		ex.shutdown();
    	}	
    }
    

    三. 线程的基础

           当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直"霸占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换

    • 新建状态,当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值

    • 就绪状态,当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行

    • 运行状态,如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态

    • 阻塞状态,当处于运行状态的线程失去所占用资源之后,便进入阻塞状态

    • 死亡状态,线程在run()方法执行结束后进入死亡状态。此外,如果线程执行了interrupt()或stop()方法,那么它也会以异常退出的方式进入死亡状态。

     1.新建和就绪状态

           当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样,仅仅由Java虚拟机为其分配内存,并初始化其成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。

      当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。

      注意:启动线程使用start()方法,而不是run()方法。永远不要调用线程对象的run()方法。调用start0方法来启动线程,系统会把该run()方法当成线程执行体来处理;但如果直按调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是线程执行体。需要指出的是,调用了线程的run()方法之后,该线程已经不再处于新建状态,不要再次调用线程对象的start()方法。只能对处于新建状态的线程调用start()方法,否则将引发IllegaIThreadStateExccption异常。

      调用线程对象的start()方法之后,该线程立即进入就绪状态——就绪状态相当于"等待执行",但该线程并未真正进入运行状态。如果希望调用子线程的start()方法后子线程立即开始执行,程序可以使用Thread.sleep(1) 来让当前运行的线程(主线程)睡眠1毫秒,1毫秒就够了,因为在这1毫秒内CPU不会空闲,它会去执行另一个处于就绪状态的线程,这样就可以让子线程立即开始执行。

     2.线程调度

           如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU。那么在任何时刻只有一个线程处于运行状态,当然在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU上轮换的现象。

      当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了)。线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务;当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。

      所有现代的桌面和服务器操作系统都采用抢占式调度策略,但一些小型设备如手机则可能采用协作式调度策略,在这样的系统中,只有当一个线程调用了它的sleep()或yield()方法后才会放弃所占用的资源——也就是必须由该线程主动放弃所占用的资源。

        3.线程阻塞

          当发生如下情况时,线程将会进入阻塞状态

      ① 线程调用sleep()方法主动放弃所占用的处理器资源

      ② 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞

      ③ 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。关于同步监视器的知识、后面将有深入的介绍

      ④ 线程在等待某个通知(notify)

      ⑤ 程序调用了线程的suspend()方法将该线程挂起。但这个方法容易导致死锁,所以应该尽量避免使用该方法

      当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是说,被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它。

         4.解除阻塞

           针对上面几种情况,当发生如下特定的情况时可以解除上面的阻塞,让该线程重新进入就绪状态:

      ① 调用sleep()方法的线程经过了指定时间。

      ② 线程调用的阻塞式IO方法已经返回。

      ③ 线程成功地获得了试图取得的同步监视器。

      ④ 线程正在等待某个通知时,其他线程发出了个通知。

      ⑤ 处于挂起状态的线程被调甩了resdme()恢复方法。

         5.线程死亡

          线程会以如下3种方式结束,结束后就处于死亡状态:

      ① run()或call()方法执行完成,线程正常结束。

      ② 线程抛出一个未捕获的Exception或Error。

      ③ 直接调用该线程stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。

    四.线程的六大状态

           线程的状态类型在很多面试中会有问到,对于这个问题我们没必要死记,学习是有规律可找的,如果死记一个人的脑子能记住多少,如果不知道线程的状态有多少可以通过看码源找到答案,由下图可知道线程一共有六大状态,每种状态代表什么意思也有注解说明;

     这六种状态分别是:

            1. New:初始状态,线程被创建,没有调用start()

             2. Runnable:运行状态,Java线程把操作系统中的就绪和运行两种状态统一称为“运行中” 

             3. Blocked:阻塞,线程进入等待状态,线程因为某种原因,放弃了CPU的使用权 

             阻塞的几种情况: 
               A. 等待阻塞:运行的线程执行了wait(),JVM会把当前线程放入等待队列 
              B. 同步阻塞:运行的线程在获取对象的同步锁时,如果该同步锁被其他线程占用了,JVM会把当前线程放入锁池中 
              C. 其他阻塞:运行的线程执行sleep(),join()或者发出IO请求时,JVM会把当前线程设置为阻塞状态,当sleep()执行完,join()线程终止,IO处理完毕线程再次恢复 

            4. Waiting:等待状态 

            5. timed_waiting:超时等待状态,超时以后自动返回 

            6. terminated:终止状态,当前线程执行完毕

    五. 线程的六种状态之间的转换 

           当实例化一个线程之后,首先进入初始状态,即New状态,此时线程在启动的时候并不是立刻就运行,而是要等到操作系统调度之后才运行,然后调用start()进入运行状态,即runnable,其中运行状态中包括运行(running)和就绪(ready)两种状态,这两种状态在操作系统的调度下可以互相转换,如果运行中的线程时间片被CPU抢占的话就会变成就绪状态;运行中的线程通过调用synchronized方法或synchronized块进入阻塞状态,即blocked,当线程获取到锁之后进入运行状态;如果线程在执行过程中调用了sleep(),wait().join(),Locksupported.parkUtil()等方法时,会进入等待状态(waiting)或超时等待状态,即timed_waiting,再次调用notify(),notifyAll(),Locksupported.unpark()等方法时,又会重新进入运行时状态,当线程执行完成时,就进入了终止状态,即terminated状态。 
    注:Locksupported是JDK 1.6提供的一个工具类,在java.util.concurrent包中,它所提供的park,unpark方法比wait,notify方法的灵活性更高。

    下图是六种状态图:

       

     

    线程状态演示

    //演示线程状态
    public class ThreadState {
    
        public static void main(String[] args) {
            new Thread(() -> {
                while (true) {
                    try {
                        TimeUnit.SECONDS.sleep(100);//这个是对Thread.sleep方法的包装,实现是一样的,只是多了时间单位转换和验证
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "STATUS_01").start();  //阻塞状态
    
            new Thread(()->{
                while(true){
                    synchronized (ThreadState.class){
                        try {
                            ThreadState.class.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            },"STATUS_02").start(); //阻塞状态
            new Thread(new BlockedDemo(),"BLOCKED-DEMO-01").start();
            new Thread(new BlockedDemo(),"BLOCKED-DEMO-02").start();
        }
        static class BlockedDemo extends  Thread{
            @Override
            public void run() {
                synchronized (BlockedDemo.class){
                    while(true){
                        try {
                            TimeUnit.SECONDS.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
    

      我们可以用jps查看线程

     

     然后可以看到我们这个DEMO的ip是21812,然后用命令jstack 【线程号】 ,来查看堆栈信息,由下图我们可以很清楚的知道我们的线程是哪一类型

     六. 线程的创建终止

            前面聊了这么多相信大家对线程也有了一定的了解,对于线程的分类也有了一定的感悟;下面我们来聊下,一个线程的终点:线程的终止,对于一个正常的线程来说我们是不需要对他进行终止的,他线程运行结束后就可以正常的结束,但对于一些还在运行时的线程我们如果想要终止可以用.stop()方法进行终止,但是这种方式不建议用,因为这种方式是强终止,如果有些线程正在进行数据交互的话,我们采用这种终止方式会导致数据的不一致性出来;那么我们应该怎么友好的终止线程呢

        Thread.currentThread().isInterrupted()

         Thread.currentThread().isInterrupted()会判断线程的中断标记,我们可以通过中断标记来进行中断线程;我们可以设置线程的thread.interrupt()来改变中断标志

    public class Interrupt  implements Runnable{
    
        private int i=1;
        @Override
        public void run() {
    
    //        表示一个中断的标记  interrupted=fasle
            while(!Thread.currentThread().isInterrupted()){
                //
                System.out.println("Test:"+i++);
            }
            //
        }
    
        public static void main(String[] args) {
            Thread thread=new Thread(new Interrupt());
            thread.start();
            thread.interrupt(); //设置 interrupted=true;
        }
    }
    

     上面我们讲了用Thread.currentThread().isInterrupted()来终止一个运行的线程,下面我们来聊下,线程的复位;

    //线程的复位
    public class Interrupt1 implements Runnable{
        @Override
        public void run() {
            while(!Thread.currentThread().isInterrupted()){ //false
                try {
    //线程的睡眠导致线程无法正常的终止,看下面打印就可以知道,打印语句没有打印出来 TimeUnit.SECONDS.sleep(200); } catch (InterruptedException e) { //触发了线程的复位 e.printStackTrace(); } } System.out.println("processor End"); } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(new Interrupt1()); t1.start(); Thread.sleep(1000); t1.interrupt(); //有作用 true
        }
    }
    

      我们运行代码:我们会看到当线程方法还没有sleep的时候线程就被中断了,这时就会打印出如下 异常,我们通过下图可以看出虽然方法体运行完了但是进程还是存在的,我们的interrupt并没有强中断;原因就是我们的InterruptedException e触发了线程的复位;如果我们想要叫醒中断线程或者完全中断可以在catch中处理;

    //线程的复位
    public class Interrupt1 implements Runnable{
        @Override
        public void run() {
            while(!Thread.currentThread().isInterrupted()){ //false
                try {
    TimeUnit.SECONDS.sleep(200); } catch (InterruptedException e) { //触发了线程的复位 e.printStackTrace(); Thread.currentThread().interrupt(); //再次中断 } } System.out.println("processor End"); } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(new Interrupt1()); t1.start(); Thread.sleep(1000); t1.interrupt(); //有作用 true } }

      

  • 相关阅读:
    使用JMeter进行RESTful API测试
    Jmeter中Websocket协议支持包的使用
    JMeterPlugins插件监听器学习-监听器
    Jmeter实现WebSocket协议的接口和性能测试方法
    使用JMeter创建数据库(Mysql)测试
    jmeter --JVM调优设置
    Android 开发者不得不面对的六个问题
    年底盘点之十大开源安全工具
    作为一个程序员怎么通过android开发赚钱
    新手做2D手游该用哪些工具?
  • 原文地址:https://www.cnblogs.com/xing1/p/13759128.html
Copyright © 2011-2022 走看看