多线程这块一直是面试的重点,也是开发当中的重点,于是下决定把这一块的内容吃透它。整个系列的基础文章最少就在60篇以上,因为这块的知识看一两篇确实感觉没什么作用,需要有一个体系的才好。这也是第一篇文章。点到为止。

一、认识线程

1、概念

什么是线程呢?

线程是进程划分成的更小的运行单位。就好比电脑QQ是一个进程,里面还有各种子模块,比如QQ空间,个性皮肤等子功能。

这里出现了另外一个名词进程。

进程是系统运行程序的基本单位。就好比是一个个应用程序QQ、微信等等。

看概念确实是一脸懵逼,举个例子就明白了。我们打开电脑的任务管理器,会发现上面就有进程,点击这个进程我们就能看到一个个应用程序,这就是进程。

图片

那什么是线程呢?不知道我们注意到了没有,每一个进程最左边都有一个小箭头>,我们打开来看看:

图片

这些小的模块就好比是一个个线程。有了这个印象我们重新来认识一下线程和进程就容易多了。

线程:

线程是一个比进程小的执行单位,也被称为轻量级进程。一个进程可以产生多个线程。多个线程共享同一块内存和系统资源,CPU在这多个线程间来回切换去执行。

进程:

进程是系统运行程序的基本单位,也就是一个程序。系统运行一个程序即是一个进程从创建,运行到消亡的过程。

有了对线程的基本认识接下来我们就可以去理解一下,这一系列文章常用到的一些概念了。

2、并行与并发

其实他们俩区分很容易区分。

并发是多个任务交替使用CPU,同一时刻还是只有一个任务在跑,并行是多个任务同时跑。举个例子就明白了。

桌子上有三个馒头,每一时刻,小明只能咬一个馒头。

桌子上有三个馒头,每一时刻,小明、小红、小华三个人同时咬三个馒头。

明白了吧。

3、同步和异步

这两个名词我们在学习的时候经常会遇到,举个例子去理解,

对于同步:我们去餐厅吃饭,只有一个客户的时候商家比较容易处理,但是当有两个人三个人的时候,这时候就需要排队了,也就是拥塞了,这就是同步。换个例子来说,就是我们请求服务器的时候,必须要等到服务器的反馈我们才能够去做其他的事。

对于异步:就好比微信聊天,我们只管把信息发送给对方,不管对方有没有回复我们,我们都可以去做其他的事,也就是说执行完函数之后,不必等到反馈就可以去做其他的事。

4、死锁

和操作系统里面的死锁意思一样,也就是说多个人同时竞争一个资源,

例子一:好比多个男孩追求同一个女孩,这时候女孩就不知道该嫁给谁了。

例子二:我们去图书馆借书,发现这本书被借走了,我们只能等到那个人把书还到图书馆才可以看。

5、原子变量与原子操作

所谓原子操作,就是“不可中断的一个或一系列操作”。就好比你高考考试的时候,就算天塌下来也要把卷子做完。再急的事也不能抢夺他的优先权。

原子变量其实是一个抽象的意思,因为本质上并没有严格意义上的原子变量,但是在这里,我们可以这样理解,原子变量提供原子操作,就好比变量a,多个线程对其操作改变时,每一次只能有一个线程拿到他的执行权进行操作。

在这里我们基本上列举了一些基本的概念,但其实还有很多,我们在遇到的时候再去分析和理解会比较容易。我们说了这么久的线程,下面我们来认识一下java中的线程。

二、基础案例

我们以一个生活中的案例来解释说明,比如我们敲代码的同时还想听音乐。

java中创建一个线程有两种方式,继承Thread类和实现runnable接口。我们两种都实现一下。

1、继承Thread类

在这里我们定义两个线程

public class ThreadTest01 extends  Thread{
    @Override
    public void run() {
        for (int i=0;i<10;i++)
            System.out.println("听音乐");
    }
}

还有一个线程可以敲代码

public class ThreadTest02 extends  Thread{
    @Override
    public void run() {
        for (int i=0;i<10;i++)
            System.out.println("敲代码");
    }
}

最后我们就可以一边敲代码一边听音乐了

public class ThreadTest {
    public static void main(String[] args) {
        ThreadTest01 thread1=new ThreadTest01();
        ThreadTest02 thread2=new ThreadTest02();
        thread1.start();
        thread2.start();
    }
}

我们运行一边会发现,敲代码和听音乐是交叉输出的。这就体现了多线程的含义,因为要是平时输出,肯定就是谁在前先输出谁,比如说先输出十个听音乐,在输出十个敲代码。不会交叉输出。

当然我们也可以使用一个线程类去演示,在这里,首先我们创建了两个类ThreadTest01和ThreadTest02,并且都继承了Thread,然后再测试类中,我们只需要调用相应的start方法即可。

使用一个线程类和使用多个线程类的区别你可以这样理解,一个是多个不同的线程分别完成自己的任务,一个是多个相同的线程共同完成一个任务。

2、实现Runnable接口

我们同样拿上面的例子进行说明

public class MyRunnable01 implements Runnable{
    @Override
    public void run() {
        for (int i=0;i<10;i++)
            System.out.println("听音乐");
    }
}

然后还可以敲代码

public class MyRunnable02 implements Runnable{
    @Override
    public void run() {
        for (int i=0;i<10;i++)
            System.out.println("敲代码");
    }
}

最后我们可以测试一下了

public class ThreadTest {
    public static void main(String[] args) {
        MyRunnable01 runnable01=new MyRunnable01();
        MyRunnable02 runnable02=new MyRunnable02();
        Thread thread1=new Thread(runnable01);
        Thread thread2=new Thread(runnable02);
        thread1.start();
        thread2.start();
    }
}

每次运行的时候,在控制台你都会看到不一样的效果。不过多运行几次依然能够发现交叉运行的效果。

其实呢还有一种方式也可以实现线程,那就是实现Callable接口,不过很少用到,这种方式在以后的文章中再进行详细的介绍。毕竟这是第一篇文章。只是认识了解一下线程。

通过Runnable接口的方式,我们依然发现,创建了两个MyRunnable,然后直接赋给Thread即可,调用的时候同样是使用start方法来启动。这就是简单的使用一下线程。

不知道我们注意到没有,java其实为我们已经提供了Thread,我们可以直接进行实例化。有时候我们会经常使用匿名内部类的方式来创建一个线程,比如说下面这种

new Thread("th1") {
     @Override
     public void run() {
          System.out.println( "匿名内部类");
     }
}.start();

注意:上面的两种创建线程的方式中,明明都是重写的run方法,为什么要去调用start启动线程呢?而且这两种方式有什么区别呢?在这里先留一个悬念,下一篇文章将会介绍道。

三、分析多线程

1、使用线程有什么好处呢?

好处你已经能够看到了,就是我们可以同时做好几件事,在玩游戏的时候可以听歌,还可以看电影等等,多方便。

2、使用线程有什么坏处嘛?

坏处其实也很多,比如说对于单核 CPU,CPU 在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换,上下文切换是一个复杂的过程,比如要记录程序计数器、CPU寄存器的状态等信息,耗时又耗空间。

为了解决这个问题,才有了现在的多核CPU。我们经常会听到手机或者是电脑是八核的,就是减少上下文切换带来的时间空间损耗,提高程序运行的效率。

3、多线程带来一个问题

从上面的例子其实我们发现只是各干各的事,相互之间互不干扰。还有一种情况是一个资源被多个线程所用到了,这就带来了线程安全问题。我们使用例子来演示一下这个问题,

public class ThreadTest {
    private int value=0;
    public int getValue() {
        return value++;
    }
    public static void main(String[] args) throws InterruptedException {
        final ThreadTest test = new ThreadTest();
        //我们的本意可能是th1执行后value变为1
        new Thread("th1") {
            @Override
            public void run() {
                System.out.println( test.getValue()+" "+super.getName());
            }
        }.start();
        //然后th2执行后value变为2
        new Thread("th2") {
            @Override
            public void run() {
                System.out.println(test.getValue()+" "+super.getName());
            }
        }.start();
    }
}

在上面,我们直接创建了两个线程,在第一个线程执行完之后value,在第二个线程执行完之后value变为2。但是结果与我们想的往往不一样,我测试了N多次,结果总是0 th2和1 th1。或者是反过来,再或者是00、11等等。这就是线程不安全的例子。如何去确保线程安全呢?方式其实有很多种,我们一点一点深入之后再去解决。

下一篇文章,我们将对线程的生命周期,常用API、以及源码(构造函数等)进行一个讲解,正式开始我们的多线程之旅。感谢支持。


更多相关文章

  1. 68.查看子进程脚本
  2. 多线程环境下生成随机数
  3. 线程池调整真的很重要
  4. Java线程之线程的调度-休眠
  5. Java线程之线程的调度-优先级
  6. Java线程之线程的交互
  7. Java线程之线程的同步与锁
  8. Java线程之线程状态的转换
  9. Java线程之创建与启动

随机推荐

  1. PHP魔术方法之__clone详解(代码实例)
  2. PHP面向对象之多态详解(代码实例)
  3. PHP 有趣的经典算法
  4. PHP魔术方法之__iset,__unset详解(代码实
  5. php中session时间设置浅析
  6. PHP 与 Go 的语法区别
  7. PHP面向对象之接口详解(代码实例)
  8. PHP中正则表达式详解(代码实例)
  9. 面向对象中什么是封装
  10. php中的接口与抽象类及接口与抽象类的区