java的线程、建立线程的 3 种方式、静态代理模式、Lambda表达式简化线程


0、介绍


线程多个任务同时进行,看似多任务同时进行,但实际上一个时间点上咱们大脑仍是只在作一件事情。程序也是如此,除非多核cpu,否则一个cpu里,在一个时间点里仍是只在作一件事,不过速度很快的切换,形成同时进行的错觉。java

多线程编程

方法间调用:普通方法调用,从哪里来到哪里去,是一条闭合的路径;
使用多线程:开辟了多条路径。设计模式

进程和线程安全

也就是 Process 和 Thread ,本质来讲,进程做为资源分配的单位,线程是调度和执行的单位。具体来讲:网络

  • 每一个进程都有独立的代码和数据空间(进程上下文),进程间切换会有较大开销,操做系统中同时运行多个任务就是进程;
  • 线程能够当作轻量级的线程,同一类线程共享代码和数据空间,每一个线程有独立的运行栈和程序计数器(PC),线程切换的开销较小,同一个应用程序里多个顺序流在执行,他们就是线程,除了CPU外,不会为线程分配内存,它本身使用的是所属进程的资源,线程组只能共享资源。

其余概念多线程

  • 线程能够理解为一个独立的执行路径;
  • 在程序运行的时候,即便没有本身建立线程,后台也会存在gc线程、主线程等,而main() 就是主线程,是程序的入口点;
  • 一个进程里若是开辟了多个线程,线程一旦开始运行,是由调度器安排的,和操做系统紧密相关,他们的安排人为无法干预;
  • 对于同一份资源操做,会涉及资源抢夺问题,须要加入并发控制;
  • 线程会带来cpu调度时间、并发控制等额外的开销;
  • 每一个线程只在本身的工做内存交互,若是加载和存储主内存控制不当,就会形成数据不一致,也就是线程不安全。

建立线程并发

在 java 中,建立线程有 3 种方式:ide

  1. 继承Thread类(重写run方法)
  2. 实现Runnable接口(重写run方法)
  3. 实现Callable接口(重写call方法,这个是在j.u.c包下的)

根据设计原则,不论是里氏替换原则,仍是在工厂设计模式种,都提到过,尽可能多用实现,少用继承,因此通常状况下尽可能使用第二种方法建立线程。函数式编程


1、建立方法1:继承Thread类


先直接看下面一个 demo函数

/*
    建立方式1:继承Thread + 重写run
    启动方式:建立子类对象 + start
*/
public class StartThread extends Thread {
    //线程入口点
    @Override
    public void run() {
        for (int i=0; i<50; i++){
            System.out.print("睡觉ing ");
        }
    }

    public static void main(String[] args) {
        //建立子类对象
        StartThread startThread = new StartThread();
        //启动,主意是start
        startThread.start();
        for (int i=0; i<50; i++){
            System.out.print("吃饭ing ");
        }
    }
}

咱们把上面的run方法成为线程的入口点,里面是线程执行的代码,当程序运行以后,能够发现,每次的运行结果都是不同的。

能够看到这种随机穿插执行的结果,这是由cpu去安排时间片,调度决定的

到这里咱们总结使用第一种方法建立线程的步骤就是:

  1. 建立子类对象,这个子类是继承了Thread类的;
  2. 启动,调用start方法,而不是run方法,start方法是把这个线程丢给cpu的调度器,让他适时运行而不是当即运行。若是使用run方法,那么就是单纯的执行,并无开启多线程,会先执行完上面的内容,再往下走。

2、建立方法2:实现Runnable接口


这种方法是推荐的方式,和上一种写法相比较,很简单,只须要把 extends Thread 改为 implements Runnable ,其余的地方几乎没有变化。

区别在于,调用的时候,不能直接 start(),只能借助一个 Thread 对象做为代理。

/*
    建立方式2:实现Runnable + 重写run
    启动方式:建立实现类对象 + 借助thread代理类 + start
*/
public class StartThreadwithR implements Runnable {
    @Override
    public void run() {
        for (int i=0; i<50; i++){
            System.out.print("睡觉ing ");
        }
    }

    public static void main(String[] args) {
        StartThreadwithR startThread = new StartThreadwithR();
        //建立代理类
        Thread t = new Thread(startThread);
        t.start();//启动
        for (int i=0; i<50; i++){
            System.out.print("吃饭ing ");
        }
    }
}

总结第二种建立线程的方法步骤是:

  1. 建立实现类对象,实现类实现的是Runnable接口;
  2. 建立代理类Thread
  3. 将实现类对象丢给代理类,而后用代理类start。

特殊的,若是咱们的一个对象只使用一次,那就彻底能够用匿名,上面的

StartThreadwithR startThread = new StartThreadwithR();
        Thread t = new Thread(startThread);
        t.start();

能够改为:

new Thread(new StartThreadwithR()).start();

两种方法相比,由于推荐优先实现接口,而不是继承类,因此第二种方法是推荐的。


3、可能出现的问题


3.1 黄牛订票

当多个线程同时进行修改资源的时候,可能出现线程不安全的问题,最上面咱们提到了,这里作一个简单模拟。

假如三个黄牛同时在抢票,服务端的票数--的过程,对于三个线程可能会出现哪些问题呢?

/*
    使用多线程修改资源带来的线程安全问题
*/
public class Tickets implements Runnable{
    private int ticketNum = 100;
    @Override
    public void run() {
        while(true){
            if (ticketNum<0){
                break;
            }
            System.out.println(Thread.currentThread().getName() + "正在抢票,余票" + ticketNum--);
        }
    }
    //客户端
    public static void main(String[] args) {
        Tickets tickets = new Tickets();
        //多个Thread代理
        new Thread(tickets,"黄牛1").start();
        new Thread(tickets,"黄牛2").start();
        new Thread(tickets,"黄牛3").start();
    }
}

这里面用了简单的模拟服务端和客户端行为,请求票的时候,分别对票数进行 -- 操做,执行以后咱们来看:

显然出现了逻辑上的错误,由于多个线程的执行带来的问题。

从运行结果的最后两行入手,背后的缘由是:

  1. 黄牛 2 先进入run;
  2. 但是到将票数-1以前,因为cpu的调度,黄牛 3 线程也开始执行,而且比黄牛 2 更快一步,直接进行了 -- 操做,票数变成了 0 ;
  3. 此时黄牛 2 输出告终果,余票0;
  4. 随后黄牛 3 线程才执行完输出语句,票数反却是 1 ?

若是咱们再模拟一个网络延迟,在 run 方法里加入:

//加入线程阻塞,模拟网络延迟
try {
    Thread.sleep(200);
} catch (InterruptedException e) {
    e.printStackTrace();
}

多运行几遍,甚至可能票数变成负数。

显然,若是在实际开发中,票数的变化,应该是严格递减的过程,而且,余票到达 0 就应该 break,而不能还出现继续执行了--操做,从而出现这种错误(不考虑退票之类的业务)。

这就是 高并发 问题,主要就是多线程带来的安全问题。


3.2 龟兔赛跑

再来看一个例子,假若有乌龟和兔子进行赛跑,咱们模拟两个线程,分别对距离++。

/*
    龟兔赛跑,借助Runnable和Thread代理
*/
public class Racer implements Runnable{
    private String winner;
    @Override
    public void run() {
        for (int dis=1; dis<=100; dis++){
            System.out.println(Thread.currentThread().getName() + " 跑了 " + dis);
            //每走一步,判断是否比赛结束
            if (gameOver(dis))break;
        }
    }

    public boolean gameOver(int dis){
        if (winner != null){
            return true;
        } else if (dis == 100){
            winner = Thread.currentThread().getName();
            System.out.println("获胜者是 "+winner);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        Racer racer = new Racer();//1.建立实现类
        new Thread(racer,"兔子").start();//2.建立代理类并start
        new Thread(racer,"乌龟").start();
    }
}

这样运行起来,总会有一我的赢,可是赢的每次不必定是哪个。


4、建立方法3:实现Callable


面对高并发的状况,须要用到线程池。

来看从新实现的龟兔赛跑:

/*
    建立方法3:Callable,是java.util.concurrent包里的内容
*/
public class RacerwithCal implements Callable<Integer> {
    private String winner;

    //须要实现的是call方法
    @Override
    public Integer call() throws Exception {
        for (int dis=1; dis<=100; dis++){
            System.out.println(Thread.currentThread().getName() + " 跑了 " + dis);
            //每走一步,判断是否比赛结束,而且结束能够有返回值
            if (gameOver(dis))return dis;
        }
        return null;
    }

    public boolean gameOver(int dis){
        if (winner != null){
            return true;
        } else if (dis == 100){
            winner = Thread.currentThread().getName();
            if (winner.equals("pool-1-thread-1"))System.out.println("获胜者是 乌龟");
            else System.out.println("获胜者是 兔子");
            return true;
        }
        return false;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1.建立目标对象
        RacerwithCal race = new RacerwithCal();
        //2.建立执行服务,含有2个线程的线程池
        ExecutorService service = Executors.newFixedThreadPool(2);
        //3.提交执行
        Future<Integer> result1 = service.submit(race);
        Future<Integer> result2 = service.submit(race);
        //4.获取结果:pool-1-thread-1也就是第一个线程是乌龟,第二个兔子
        Integer i = result1.get();
        Integer j = result2.get();
        System.out.println("比分是: "+ i + " : " + j);
        //5.关闭服务
        service.shutdownNow();
    }
}

来看执行结果:

总结一下,步骤通常分为 5 步:

  1. 建立目标对象;
  2. 建立执行服务;
  3. 提交执行;
  4. 获取结果;
  5. 关闭服务。

能够看到,这种方法的特殊之处在于:

  • 目标类继实现Callable接口的 call 方法,能够有返回值(前面的run是没有返回值的);
  • 不用处理异常,能够直接 throw;
  • 使用的过程相比前两种方法,变得复杂。

5、静态代理模式


注意到在前面使用第二种方法建立多线程的时候,提到了 new Thread(tickets,"黄牛1").start(); 是使用了 Thread 做为代理。代理模式自己也是设计模式种的一种,分为动态代理和静态代理,代理模式在开发中记录日志等等很经常使用。

静态代理的代理类是直接写好的,拿过来用,动态代理则是在程序执行过程当中临时建立的。

在这里简单介绍静态代理。

实现一个婚庆公司,做为你的婚礼的代理,而后进行婚礼举办。

/*
    静态代理模式demo
    1.真实角色
    2.代理角色
    3.1和2都实现同一个接口
*/
public class StaticProxy {
    public static void main(String[] args) {
        //彻底相似于 new Thread(new XXX()).start();
        new WeddingCompany(new You()).wedding();
    }
}

//接口
interface Marry{
    void wedding();
}

//真实角色
class You implements Marry{
    @Override
    public void wedding() {
        System.out.println("结婚路上ing");
    }
}

//代理角色
class WeddingCompany implements Marry{
    //要代理的真实角色
    private Marry target;
    public WeddingCompany(Marry target) {
        this.target = target;
    }

    @Override
    public void wedding() {
        ready();//准备
        this.target.wedding();
        after();//善后
    }
    private void after() {
        System.out.println("结束ing");
    }
    private void ready() {
        System.out.println("布置ing");
    }
}

能够看到,最后的调用方法就至关因而写线程的时候用到的 new Thread(new XXX()).start();

小小区别就在于,咱们写的线程类是实现的 run 方法,没有实现start方法,可是不重要。

重要的是,代理类 可能作了不少的事,而中间须要 真实类 实现的一个方法必须实现,其余的方法,真实类不须要关心,也就是交给代理类去办了。


6、Lambda表达式简化线程


jdk1.8 后可使用 lambda 表达式来简化代码,通常用在 只使用一次的、简单的线程 里面。

简化的写法有不少,下面是逐渐简化的过程。


6.1 静态内部类

若是某个类只但愿使用一次,能够用静态内部类来实现,调用的时候同样。

public class StartThreadLambda {
    //静态内部类
    static class Inner implements Runnable{
        @Override
        public void run() {
            for (int i=0; i<50; i++){
                System.out.print("睡觉ing ");
            }
        }
    }
    //静态内部类
    static class Inner2 implements Runnable{
        @Override
        public void run() {
            for (int i=0; i<50; i++){
                System.out.print("吃饭ing ");
            }
        }
    }
    public static void main(String[] args) {
        new Thread(new Inner()).start();
        new Thread(new Inner2()).start();
    }
}

使用静态内部类的好处是,不使用的时候这个内部类是不会编译的,这其实就是一个单例模式。


6.2 方法内部类

还能够直接写到 main 方法内部,由于main 方法就是static,只启动一次。

public class StartThreadLambda {
    public static void main(String[] args) {
        //方法内部类(局部内部类)
        class Inner implements Runnable{
                    //。。。。。。
        }
        class Inner2 implements Runnable{
                    //。。。。。。
        }
        new Thread(new Inner()).start();
        new Thread(new Inner2()).start();
    }
}

6.3 匿名内部类

更进一步,能够直接利用匿名内部类,不用声明出类的名称来。

public class StartThreadLambda {
    public static void main(String[] args) {
        //匿名内部类,必须借助接口或者父类,由于没有名字
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=0; i<50; i++){
                    System.out.print("吃饭ing ");
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i=0; i<50; i++){
                    System.out.print("睡觉ing ");
                }
            }
        }).start();
    }
}

这里面必须带上实现体了就,由于没有名字,那么就要借助父类或者接口,而父类或者接口的run方法是须要重写/实现的。


6.4 Lambda表达式

jdk 8 对匿名内部类写法再进行简化,只用关注线程体,也就是只关注 run 方法里面的内容。

public class StartThreadLambda {
    public static void main(String[] args) {
        //使用Lambda表达式
        new Thread(()-> {
            for (int i=0; i<50; i++){
                System.out.print("吃饭ing ");
            }
        }).start();

        new Thread(()->{
            for (int i=0; i<50; i++){
                System.out.print("睡觉ing ");
            }
        }).start();
    }
}

() - > 这个符号,编译器就默认你是在实现 Runnable,而且默认是在实现 run 方法。


6.5 扩展

显然,若是不是线程,是其余的咱们本身写的接口+实现类,Lambda表达式也是可用的,并且能够进行参数和返回值的扩展。

public class LambdaTest {
    public static void main(String[] args) {
        //直接使用lambda表达式实现接口
        Origin o = (int a, int b)-> {
            return a+b;
        };
        System.out.println(o.sum(100,100));
    }
}

//自定义接口,至关于Runnable
interface Origin{
    int sum(int a, int b);
}

更有甚者,参数的类型也能够省略,他会本身去匹配:

//省略参数类型
Origin o1 = (a, b) -> {
    return a+b;
};

若是实现接口的方法,只有一行代码,甚至花括号也能够省略:

Origin o2 = (a, b) -> a+b;

有关返回值和参数的个数仍是有一些细微差异的。

Lambda表达式也在 Sort 方法里有应用,要想对引用类型里面统一按照某个属性进行排序,须要实现Comparator接口里面的compare方法,可使用简化写法。

  • Lambda 表达式的支持,主要是为了避免匿名内部类定义过多,实质上是属于函数式编程的概念
  • 须要注意的是,Lambda表达式只支持实现一个方法。
相关文章
相关标签/搜索