zoukankan      html  css  js  c++  java
  • 快速认识线程

     本文参考自Java高并发编程详解

     

    1、创建并启动一个线程

    下面是不添加线程的程序代码。

    package concurrent.chapter01;
    
    import java.util.concurrent.TimeUnit;
    public class TryConcurrency {
        public static void main(String[] args) {
            browseNews();
            enjoyMusic();
        }
        private static void browseNews() {
            while(true) {
                System.out.println("Uh-huh,the good news.");
                sleep(1);
            }
        }
        private static void enjoyMusic() {
            while(true) {
                System.out.println("Uh-huh,the nice music");
                sleep(1);
            }
        }
        private static void sleep(int i) {
            try {
                TimeUnit.SECONDS.sleep(i);
            }catch (Exception e) {
                
            }
        }
    }

    运行结果如下:

    程序永远不会执行第二个方法。因此我们需要使用线程。

    这里通过匿名内部类的方式创建线程,并且重写其中的run方法,使程序交互运行。

    package concurrent.chapter01;
    
    import java.util.concurrent.TimeUnit;
    public class TryConcurrency {
        public static void main(String[] args) {
            new Thread() {
                @Override
                public void run() {
                    enjoyMusic();
                }
            }.start();
            browseNews();
        }
        private static void browseNews() {
            while(true) {
                System.out.println("Uh-huh,the good news.");
                sleep(1);
            }
        }
        private static void enjoyMusic() {
            while(true) {
                System.out.println("Uh-huh,the nice music");
                sleep(1);
            }
        }
        private static void sleep(int i) {
            try {
                TimeUnit.SECONDS.sleep(i);
            }catch (Exception e) {
                
            }
        }
    }

    运行结果如下:

    注意:

    1、创建一个线程,需要重写Thread中的run方法,Override的注解是重写的标识,然后将enjoyMusic交给他执行。

    2、启动新的线程需要重写Thread的start方法,才代表派生了一个新的线程,否则Thread和其他普通的Java对象并无区别,start放法是一个立即返回方法,并不会让程序陷入阻塞。

    如果使用Lambda表达式改造上面的代码,那么代码会变得更简洁。

    public static void main(String[] args) {
            new Thread(TryConcurrency::enjoyMusic).start();
            browseNews();
        }

    2、线程的创建与结束生命周期。

    1、线程的new状态。

    当我们用关键字new创建一个Thread对象时,此时他并不处于执行状态,因为没用start启动该线程,那么线程的状态为NEW状态,准确的说,它只是Thread对象的状态,因为在没用start之前,该线程根本不存在,与你用new创建一个普通的Java对象没什么区别。

    2、线程的RUNNABLE状态

    线程对象进入RUNNABLE状态必须调用start方法,那么此时才是真正地在JVM中创建了一个线程,线程一经启动就可以立即执行吗?答案是否定的,线程的运行与否和进程一样都要听令于CPU的调度,那么我们把这个中间状态成为可执行状态,也就是说它具备执行的资格,但是并没有真正地执行起来,而是等待CPU的调度。

    3、线程的RUNNING状态

    一旦CPU通过轮询或者其他方式从任务可执行队列中选中了线程,那么此时它才能真正的执行自己的逻辑代码,需要说明一点是一个正在RUNNING状态的线程事实上也是RUNNABLE的,但是反过来则不成立。

    在该状态中,线程的状态可以发生如下的状态转换。

    1. 直接进入TERMINATED状态,比如调用JDK已经不推荐使用的stop方法或者判断某个逻辑标识。
    2. 进入BLOCKED状态,比如调用了sleep,或者wait方法而加入了waitSet中。
    3. 进行某个阻塞的IO操作,比如因网络数据的读写而进入了BLOCKED状态。
    4. 获取某个锁资源,从而加入到该锁的阻塞队列中而进入了BLOCKED状态。
    5. 由于CPU的调度器轮询使该线程放弃执行,进入RUNNABLE状态。
    6. 线程主动调用yield方法,放弃CPU执行权,进入RUNNABLE状态。

     4、线程的BLOCKED状态

    BLOCKED为线程阻塞时的状态,它能进入以下几个状态:

    1. 直接进入TERMINATED状态,比如调用JDK已经不推荐使用的stop方法或者JVMCrash
    2. 线程阻塞的操作结束,比如读取了想要的数据字节进入到RUNNABLE状态。
    3. 线程完成了指定时间的休眠,进入到了RUNNABLE状态
    4. Wait中的线程被其他线程notify/notifyall唤醒,进入RUNNABLE状态。
    5. 线程获取到了某个锁资源,进入RUNNABLE状态。
    6. 线程在阻塞过程中被打断,比如其他线程调用了interrupt方法,进入RUNNABLE状态。

     5、线程的TERMINATED状态

    TERMINATED是一个线程的最终状态,在该状态中,线程将不会切换到其他任何状态,线程进入Terminated状态意味着整个线程的生命周期结束了,下列情况将会使线程进入Terminated状态。

    1. 线程运行正常结束,结束生命周期。
    2. 线程运行出错意外结束
    3. JVM崩溃,导致所有的线程都结束。

    3、线程的start方法是什么?

    首先:Thread start源码如下:

    public synchronized void start() {
            /**
             * This method is not invoked for the main method thread or "system"
             * group threads created/set up by the VM. Any new functionality added
             * to this method in the future may have to also be added to the VM.
             *
             * A zero status value corresponds to state "NEW".
             */
            if (threadStatus != 0)
                throw new IllegalThreadStateException();
    
            /* Notify the group that this thread is about to be started
             * so that it can be added to the group's list of threads
             * and the group's unstarted count can be decremented. */
            group.add(this);
    
            boolean started = false;
            try {
                start0();
                started = true;
            } finally {
                try {
                    if (!started) {
                        group.threadStartFailed(this);
                    }
                } catch (Throwable ignore) {
                    /* do nothing. If start0 threw a Throwable then
                      it will be passed up the call stack */
                }
            }
        }
    
        private native void start0();

    start方法的源码足够简单,其实最核心的部分是start0这个本地方法,也就是JNI方法;

    也就是说在start方法中会调用start0方法,那么重写的那个run方法何时被调用了呢?

    实际上在开始执行这个线程的时候,JVM将会调用该线程的run方法,换言之,run方法是被JNI方法start0调用的,仔细阅读start的源码将会总结出如下几个知识要点。

    1. Thread被构造后的NEW状态,实际上threadStatus这个内部属性为0.
    2. 不能俩次启动Thread,否则就会出现IllegalThreadStateException异常。
    3. 线程启动后会被加入到一个ThreadGroup中。
    4. 一个线程生命周期的结束也就是到了terminated再次调用start方法是不允许的,也就是说Terminated状态是没有办法回到runnable状态的。

    如执行以下代码:

    import java.util.concurrent.TimeUnit;
    
    public class A {
        public static void main(String[] args) {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    try {
                        TimeUnit.SECONDS.sleep(10);
                    }catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            };
            thread.start();
            thread.start();
        }
    }

    此时程序就会抛出

    当我们改下代码,也就是生命周期结束后,再重新调用时。

    import java.util.concurrent.TimeUnit;
    
    public class A {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    }catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            };
            thread.start();
            TimeUnit.SECONDS.sleep(5);
            thread.start();
        }
    }

    我们会发现程序同样会抛出illegalThread异常。

    注意:程序虽然同样会抛出异常,但是这俩个异常是有本质区别的。

    1. 第一个是因为重复启动,只是第二次启动时不允许的,但是此时线程是处于运行状态的。
    2. 第二次企图重新激活也抛出了非法状态的异常,但是此时没有线程,因为该线程的生命周期已经被终结。

    通过以上分析我们不难看出,线程真正的执行逻辑是在run方法中,通常我们会把run方法成为线程的执行单元。

    如果我们没有重写run,那run就是个空方法。

    Thread的run和start是一个比较经典的模板设计模式,父类编写算法结构代码,子类实现逻辑细节,下面是一个简单的模板设计模式。

    package concurrent.chapter01;
    
    public class TemplateMethod {
        public final void print(String message) {
            System.out.println("###");
            wrapPrint(message);
            System.out.println("###");
        }
        protected void wrapPrint(String message) {
            
        }
        public static void main(String[] args) {
            TemplateMethod t1 = new TemplateMethod() {
                @Override
                protected void wrapPrint(String message) {
                    System.out.println("*"+message+"*");
                }
            };
            t1.print("Hello Thread");
            TemplateMethod t2 = new TemplateMethod() {
                @Override
                protected void wrapPrint(String message) {
                    System.out.println("+"+message+"+");
                }
            };
            t2.print("Hello Thread");
        }
    }

    运行结果如下:

    4、下面是一个模拟营业大厅叫号机的程序

    假设共有4台出号机,这就意味着有4个线程在工作,下面我们用程序模拟一下叫号的过程,约定当天最多受理50笔业务,也就是说号码最多可以出到50

    代码如下:

    package concurrent.chapter01;
    
    public class TicketWindow extends Thread{
        private final String name;
        private static final int MAX = 50;
        private int index = 1;
        public TicketWindow(String name) {
            this.name=name;
        }
        @Override
        public void run() {
            while(index<=MAX) {
                System.out.println("柜台:"+name+" 当前号码是:"+(index++));
            }
        }
        public static void main(String[] args) {
            TicketWindow t1 = new TicketWindow("一号初号机");
            t1.start();
            TicketWindow t2 = new TicketWindow("二号初号机");
            t2.start();
            TicketWindow t3 = new TicketWindow("三号初号机");
            t3.start();
            TicketWindow t4 = new TicketWindow("四号初号机");
            t4.start();
        }
    }

    运行结果如下:

    显然这不是我们想看到的。如何改进呢?

    这里我将index设置为staic变量

    貌似有了改善。但是会出现线程安全问题。

    所以Java提供了一个接口:Runnable专门用于解决该问题,将线程和业务逻辑的运行彻底分离开。

    5、Runnable接口的引入及策略模式

    Runnalbe接口非常简单,只是定义了一个无参数无返回值的run方法,具体代码如下:

    public interface Runnable{
      void run();    
    }

    在很多书中,都会说,创建线程有俩种方式,第一种是构造一个Thread,第二种是实现Runnable接口,这种说法是错误的,最起码是不严谨的,在JDK中,代表线程的就只有Thread这个类,我们在前面分析过,线程的执行单元就是run方法,你可以通过继承Thread然后重写run方法实现自己的业务逻辑,也可以实现Runnable接口实现自己的业务逻辑,代码如下:

    @override
    public void run(){
       if(target!=null){
       target.run(); 
       }    
    }

    上面的代码段是Thread run方法的源码,我们从中可以去理解,创建线程只有一种方式,那就是构造Thread类,而实现线程的执行单元则有俩种方式,第一种是重写Thread的run方法,第二种是实现Runnable接口的run方法,并将Runnable实例用作构造Thread的参数。

    策略模式

    其实无论是Runnable的run方法还是,Thread本身的run方法都说想将线程的控制本身和业务逻辑的运行分离开,达到职责分明,功能单一的原则,这一点与GoF设计模式中的策略设计模式很相近。

    以JDBC来举例子:

    package concurrent.chapter01;
    
    import java.sql.ResultSet;
    
    public interface RowHandler <T>{
        T handle(ResultSet set);
    }

    rowhandler接口只负责对从数据库中查询出来的结果集进行操作,至于最终返回成什么样的数据结构,需要自己去实现,类似于Runnable接口。

    package concurrent.chapter01;
    
    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    
    public class RecordQuery {
        private final Connection connection;
        
        public RecordQuery(Connection connection) {
            this.connection = connection;
        }
        public<T> T query(RowHandler<T> handler,String sql,Object... params) throws SQLException{
            try(PreparedStatement stmt = connection.prepareStatement(sql)){
                int index = 1;
                for(Object param:params) {
                    stmt.setObject(index++,param);
                }
                ResultSet resultSet = stmt.executeQuery();
                return handler.handle(resultSet);
            }
        }
    }

    上面的代码的好处就是可以用Query方法应对任何数据库的查询,返回结果的不同只会因为你传入RowHandler的不同而不同,同样RecodeQuery只负责数据的获取,而RowHanlder则只负责数据的加工,职责分明,每个类均功能单一。

    重写Thread类的run方法和实现Runnable接口的run方法是不能共享的,也就是说A线程不能把B线程的run方法当作自己的执行单元,而使用Runnable接口则很容易就能实现这一点即使用同一个Runnable的实例构造不同的实例。

    如果不明白的话,看下面的代码。

    package concurrent.chapter01;
    
    public class TicketWindowRunnable implements Runnable{
        private int index = -1;
        private final static int MAX = 50;
        @Override
        public void run() {
            while(index<=MAX) {
                System.out.println(Thread.currentThread()+" 的号码是:"+(index++));
                try {
                    Thread.sleep(100);
                }catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        public static void main(String[] args) {
            final TicketWindowRunnable task = new TicketWindowRunnable();
            Thread WindowThread1 = new Thread(task,"一号窗口");
            Thread WindowThread2 = new Thread(task,"二号窗口");
            Thread WindowThread3 = new Thread(task,"三号窗口");
            Thread WindowThread4 = new Thread(task,"四号窗口");
            WindowThread1.start();
            WindowThread2.start();
            WindowThread3.start();
            WindowThread4.start();
        }
    }

    运行结果如下:

     惊不惊喜?

    上面并没有对index进行static进行修饰,但是和上面被static修饰的是一个效果。原因是我们每次操作的都是同一个对象即task。

  • 相关阅读:
    基于 OAI 部署私有的 4G EPS
    Ubuntu Snap 简述
    OAI SDR LTE 基站部署
    企业文化二三谈
    OpenStack 的 SR-IOV 虚拟机热迁移
    在 ThinkPad E470 上安装 Ubuntu 16.04 无线网卡驱动
    读写可编程 SIM/USIM 卡
    4G LTE/EPC UE 的附着与去附着
    4G EPS 的网络协议栈
    Java- 类型转换
  • 原文地址:https://www.cnblogs.com/godoforange/p/11008865.html
Copyright © 2011-2022 走看看