多线程系列(一)多线程基础

前言

从今天开始,我将写下一系列关于多线程的文章,包括多线程基础、线程间通信、阻塞队列、线程池。今天写第一章《多线程基础》,如果你对线程还不是很了解,读完本篇文章你将会对线程有一个初步的认识,如果你对线程很熟悉了也希望你能够仔细的看一遍,回顾一下基础知识没啥坏处的吧。另外,内容中部分实例借鉴于毕向东老师的javaSE教程,在此呢也给大家安利一波,如果java基础不太扎实的同学可以去听一遍这套课程,讲的特别好,至今我看了四遍有余,每一遍都有不同的收获。

1 概述

1.1 进程

了解线程之前我们先来了解一下进程这个概念。

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

以上是进程官方的叙述,其实进程很好理解,比如我们windows下打开任务管理器,里面就有启动的各种进程,像里面的QQ、360之类的都是一个进程,它们运行后都会在内存中开辟一块空间,其实说的通俗一点进程就是:正在进行的程序。

1.2 多线程

说完进程我们来说一下线程

线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。

以上是线程的官方解释,下面我用通俗一点的语言给大家解释一遍,进程负责开辟一块内存空间,而具体的内容也就是我们写的代码是由线程去执行的,线程是CPU执行的基本单位,所以一个进程中应该至少有一个线程,而一个进程可以有多个线程存在。

1.3 多线程的好处与弊端

好处:首先跟大家举一个例子,比如你们手中的智能手机,如果只有一个线程的话,那么你聊天的时候不能听歌,听歌的时候不能浏览网页,更苦逼的是如果你在下载东西而且网速还很慢,那这段时间手机就成板砖了,只能看着等下载结束,这种体验是非常差的。多线程就解决了这个问题,将多个任务放到多个线程中去执行,而CPU会随机的切换到线程中去执行任务,这个切换的频率是非常快的,这样就会达到一个同时运行的效果。

弊端:多线程技术看起来很完美,那是不是以后每个任务都可以用多线程去执行?答案是否定的,因为线程过多的话CPU切换的频率也会增加,大大的降低了CPU执行任务的效率,所以一般我们都只在耗时的任务中开启一个线程去执行,比如网络请求、本地文件读写之类的。

1.4 JVM中的多线程

在Java语言当中也是支持多线程的

public class Demo {
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
         System.out.print("hello world");
    }

}

上面这个hello world是在哪个线程中执行的呢?答案是主线程,主线程是JVM启动的时候开启的一条线程,往往也是执行我们开发者代码的一个入口。JVM启动后会开启多个线程,除了主线程外还有垃圾回收线程等等。

2 多线程的创建

2.1 通过Thread创建

通过继承Thread重写其run()进行创建

class MyThread extends Thread{
        public void run(){
            
        }
    }

而我们的任务中就可以放在这个run()中进行执行。开启一个线程也非常简单,调用其start()方法即可。

new MyThread().start();

2.2 通过Runnable创建

通过实现Runnable接口进行创建

 new Thread(new Runnable() {
            @Override
            public void run() {
                
            }
        }).start();

也很简单,实现Runnable接口重写其run()方法,创建一个实体传入Thread(),然后调用start()开启。
通过Runnable创建的好处:

public static void main(String[] args) {
        // TODO Auto-generated method stub
        ThreadDemo demo = new ThreadDemo();
        new Thread(demo).start();
        new Thread(demo).start();
    }

static class ThreadDemo implements Runnable{
        public void run() {
            for(int i=0;i<100;i++){
                //Thread.currentThread().getName()为当前线程名称
                System.out.println(Thread.currentThread().getName()+"---"+i);
            }
        }
    }

该段代码的执行结果:

Thread-1---64
Thread-0---65
Thread-1---66
Thread-0---67
Thread-1---68
Thread-0---69

可以看出两个线程共同把任务执行完毕,所以在需要多个线程执行同一任务时可以使用Runnable创建线程。我们开发的过程中也是应该首选Runnable方式创建线程。

2.3 通过Callbale创建

当我们要获取线程的执行结果时,一般的方法是用接口的回调,代码如下。

 new Thread(new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                for(int i =0;i<100;i++){
                    sum += i;
                }
                if(callback!=null){
                    callback.call(sum);
                }
            }
        }).start();

这种方式获取执行结果有几个缺点

  • 需要写一个回调接口
  • 任务执行的过程中不能取消
  • 想要重复的获取执行结果需要再次执行任务

说完了传统线程的创建方式我们就来引入Callbale的概念
Callable:它是一个接口,内部由一个call()方法,相当于Runnabl的run(),耗时任务写在call()方法内部即可。

public interface Callable {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

Future : Future也是一个接口,内部有cancel()、get(),用来取消任务和获取执行结果。

public interface Future {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

FutureTask:它内部实现了Runnable和Future接口,也就是说它可以当做一个线程任务,并且能够对执行的任务操作。FutureTask内部原理我会在以后的文章中进行详细叙述,本章只介绍FutureTask的使用。使用步骤如下:

private void callable(){

        final FutureTask task = new FutureTask<>(new Callable() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                Thread.sleep(2000);
                for(int i =0;i<100;i++){
                    sum += i;
                }
                return sum;
            }
        });
        new Thread(task).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Log.i("zs","sum="+task.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    }

重写FutureTask中的call()方法,将耗时任务写在call()方法内,把FutureTask对象传入到Thread中并开启,可以通过调用FutureTask的cancel()方法和get()方法进行取消和获取执行结果操作。需要注意的是get()方法为阻塞式的,如果获取不到结果就会一直等待,所以说我们不要在主线程中进行调用。

3 线程的状态

在描述状态之前我要先讲一下几个方法和概念:
sleep(long time):通过调用该方法可以使当前线程处于休眠状态,睡眠时间为time。
wait():通过调用该方法,可以将当前线程处于等待状态,属于Object的方法。
notify:可以将一个线程唤醒,属于Object的方法。
CPU执行资格: 正在排队等待CPU执行。
CPU执行权:正在被CPU执行。

说完了这几个方法我将结合一张图为大家描述线程的状态(纯手工,略微粗糙)。


线程状态图
  • 线程创建后通过start()进行开启
  • run()方法结束后线程会消亡
  • sleep(int time)可以让一个线程释放执行资格并释放执行权,处于冻结 状态,知道经过time时间后会被唤醒,被唤醒后可能立即运行也可能处于 临时阻塞状态。
  • wait()可以让一个线程释放执行资格并释放执行权,处于冻结状态,知道被notify()后才会被唤醒,被唤醒后可能立即运行也可能处于临时阻塞状态。

4 线程同步概念

在讲同步之前我先给大家讲述一个案例:

class Demo02 implements Runnable{
        int num= 10;
        @Override
        public void run() {
             while (true){
                if(num>0) {
                    System.out.println(Thread.currentThread().getName() + "   " + --num);
                }
            }
        }
}
...
//创建Demo02 实例
Demo02 demo02 = new Demo02();
//开启两个线程执行demo02中的任务
new Thread(demo02).start();
new Thread(demo02).start();

执行结果为:

Thread-7 9
Thread-7 8
Thread-7 7
Thread-7 6
Thread-7 5
Thread-7 4
Thread-8 3
Thread-7 2
Thread-7 1
Thread-8 0

我们看没什么问题的,两个线程共同把任务执行完毕。我在循环中加一个sleep再来看看结果

static class Demo02 implements Runnable{
        int num= 10;
        @Override
        public void run() {
            while (true){
                if(num>0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "-- " + --num);
                }
            }
        }
    }

运行结果

Thread-0-- 9
Thread-1-- 8
Thread-1-- 7
Thread-0-- 6
Thread-1-- 5
Thread-0-- 4
Thread-1-- 3
Thread-0-- 2
Thread-1-- 1
Thread-0-- 0
Thread-1-- -1

我们可以看到出现了-1,哎,怎么会出现-1呢?当时我刚学的时候也是一脸懵逼,其实理由很简单的,我来跟大家一步一步分析。

当num=1时cpu切换到了Thread-0,Thread-0进行判断,num>0,但就在此时,Thread-0还未对num进行输出,cpu不执行Thread-0了,转而去执行Thread-1。Thread-1得到执行权后判断num>0,并输出num=0,然后cpu又切换到了Thread-0,因为现在num=0,所以输出num=-1。那既然出现了安全问题,有没有办法解决呢?当然是有的啦,方法总比困哪多的吗(゜-゜)つ。

为了解决上述的安全问题,我们就引入一个概念,"同步",什么是同步呢?我们先来看一下同步官方的解释:

所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,同时其它线程也不能调用这个方法。

什么意思呢?我来用白话跟大家叙述一遍:当一个线程执行一个同步的任务时,在该任务执行完毕之前,其他线程是不得进入该任务的(好像描述的还是很官方(゜-゜)つ,哈哈,皮一波),我就结合上述例子来讲吧,如果例子中的任务是同步任务,当Thread-0执行完毕同步任务前其他线程是不准进来执行的,也就是说如果将判断和打印几句代码加上同步,当Thread-0在判断语句内的时候Thread-1是进不来的。所以加个同步就可以完美的解决例子中的安全问题。

java中的同步:在java中可以通过关键字synchronized对任务进行同步,具体的书写方式:

class Demo02 implements Runnable{
        int count = 10;
        @Override
        public void run() {
            while (true){
                synchronized(this) {
                    if(count>0) {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "-- " + --count);
                    }
                }
            }
        }
    }

把需要同步执行的任务用synchronized括起来,括号内参数为锁的意思,可传入任意对象,一般来说传入this即可,静态方法中传入类名.Class。好了,我们再来看一下运行结果。

Thread-0-- 9
Thread-0-- 8
Thread-0-- 7
Thread-0-- 6
Thread-0-- 5
Thread-0-- 4
Thread-0-- 3
Thread-0-- 2
Thread-0-- 1
Thread-0-- 0

加了同步代码块完美运行。这样就引出另外一个疑问,那是不是以后每个线程任务都可以加上同步代码块呢?答案是否定的,因为加上同步锁之后每次执行任务的时候都会判断一次锁,这样是很影响效率的。

拓展

单例设计模式我相信大家应该都很熟悉,那单例模式是否存在线程安全问题呢?我们先来看一下单例模式常规的写法:

public class Single {
    private static Single instance = null;
    private Single(){}
    public static Single getInstance() {
        if (instance == null){
            instance = new Single();
        }
        return instance;
    }
}

细心地同学可能已经发现,这种写法是存在线程安全问题的,假如Thread-0判断了instance,此时为空,所以Thread-0就进入了判断语句,就在此时Thread-1也调用了该方法,判断instance也为空,最后会创建出两个instance对象,这就违背了单例设计模式的概念,所以单例模式正确的写法应该是:

public class Single {
    private static Single instance = null;
    private Single(){}
    public static Single getInstance() {
        //多加一个判断是为了避免每次调用都判断锁
        if (instance == null){
            synchronized (Single.class) {
                if(instance==null) {
                    instance = new Single();
                }
            }
        }
        return instance;
    }
}

好了,本章关于同步的叙述到此为止,但Java中关于同步的内容远不止这些,在后面写线程间通信的时候我再为大家详细叙述。

5 总结

这篇文章讲述了线程的概念、线程的创建方式以及同步。为了解决多个任务同时进行和充分的利用CPU所以就有了线程这一概念。线程的创建方式有三种,这三种创建方式各有特点可以结合实际场景进行选择使用。多线程中可能会产生安全问题,可以结合同步去解决。好了,本篇文章对线程基础的描述到此为止,但并不代表线程就这点东西,如果详细的去写10篇文章都写不完,在这里只是起到一个抛砖引玉的作用。下一篇文章《多线程系列(二)线程间通信》。

你可能感兴趣的:(多线程系列(一)多线程基础)