多线程编程基础(一)-- 线程的使用

前言

在面试过程当中,多线程编程也是一个必考的知识点。就像是面试时问你,sleep()wait()两个函数有什么区别同样。java

思考

  1. 什么是多线程编程?
  2. 为何要多线程编程?
  3. 如何多线程编程?

基础知识

阅读本文的读者,应该都上过操做系统的课程。而进程和线程就是操做系统的知识点之一。就如同现代的pc通常,从18年开始基础款笔记本基本上都已经使用上了四核八线程的芯片了。可是和这个实际意义上的芯片不一样,咱们讲的编程中使用到的线程是须要相似映射的过程慢慢的转换而后交给芯片处理的。面试

先上一张图,让读者清晰的了解什么叫作进程,什么叫作线程。 编程

活动监视器

这是Mac的活动监视器,Windows要访问的话打开任务管理器便可。从图就很清晰的看出来什么是进程了。而线程包含在进程中。设计模式

用概念型的语言描述。多线程

  1. 进程:程序在一个数据集合上的运行过程,是线程的容器。
  2. 线程:轻量级进程。

从定义中也算很明显的可以感知了,为何咱们的编程要叫多线程,而不叫作多进程。由于它轻啊,朋友们。app

下面再送读者们两张图片。 异步

进程状态转化图

进程转化图中标示了4个值用一句话来记忆比较有效。 进程因建立而产生,因调度而执行,因得不到资源而阻塞,因得不到资源而阻塞,因撤销而消亡。 图中表明的4个值: (1) 获得CPU的时间片 / 调度。 (2) 时间片用完,等待下一个时间片。 (3) 等待 I/O 操做 / 等待事件发生。 (4) I/O操做结束 / 事件完成。ide

虽然图示和须要记忆的句子其实相同,只是省去了建立和消亡的状态。函数

线程状态转化图

这张图其实对应的是Java线程运行时声明的6种状态,状态转化以及调用到的函数,也都清晰的呈如今图中了。post

那为何要多线程编程? 其实他包揽了不少事情,就拿知乎这个app举例子,咱们以前讲过UI线程,也就是通常咱们写程序时的main()函数。他若是只请求一个列表信息,那咱们的UI线程可能还忙的过来,可是他若是要同时请求用户信息,多个列表信息呢?他要忙到猴年马月,这就是线程的做用,每一个异步处理一个任务,而后返回结果,就比较成功的完成了这个任务。

如何多进程编程

先写两种很是简单的线程使用方法。

// 1
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("this is a Runnable");
    }
}
// 2
public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        System.out.println("this is thread");
    }
}

// 具体使用
public class Main {
    public static void main(String[] args) {
        // 第一种
        Thread thread1 = new Thread(new MyRunnable());
        thread1.start();
        // 第二种
        MyThread thread2 = new MyThread();
        thread2.start();
    }
}
复制代码

两种方法效果相同,不过通常来讲推荐的是使用第一种,也就是重写Runnable

多线程当然好,可是若是出现下面的状况,你还会愿意继续多线程编程吗?

public class Main {
    public int i = 0;
    public void increase(){
        I++;
    }

    public static void main(String[] args) {
        final Main main = new Main();
        for(int i=0; i< 10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j=0; j<1000; j++){
                        main.increase();
                    }
                }
            }).start();
        }
        while(Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(main.i);
    }
}
复制代码

上述结果只是经过一个自加操做得出的结果,由于两个线程互不干扰,可是当他们同时对一个公共数据进行操做的时候,就会出现读脏数据等现象,这个时候咱们就须要引入同步机制。

同步

通常状况下,咱们会经过两种方式来实现。

  1. synchronized
  2. Lock

在操做系统中,有这么一个概念,叫作临界区。其实就是同一时间只能容许存在一个任务访问的代码区间。代码模版以下:

Lock lock = new ReentrantLock();
public void lockModel(){
    lock.lock();
    // 用于书写共同代码,好比说卖同一辆动车的车票等等。

    lock.unlock();
}

// 上述模版等价于下面的函数
public synchronized void lockModel(){}
复制代码

其实这就是你们常说的锁机制,经过加锁和解锁的方法,来保证数据的正确性。

可是锁的开销仍是咱们须要考虑的范畴,在不太必要时,咱们更长会使用是volatile关键词来修饰变量,来保证数据的准确性。

Java内存模型

对上述的共享变量内存而言,若是线程A和B之间要通讯,则必须先更新主内存中的共享变量,而后由另一个线程去主内存中去读取。可是普通变量通常是不可见的。而volatile关键词就将这件事情变成了可能。 打个比方,共享变量若是使用了volatile关键词,这个时候线程B改变了共享变量副本,线程A就可以感知到,而后经历上述的通讯步骤。 这个时候就保障了可见性。 可是另外两种特性,也就是有序性和原子性中,原子性是没法保障的。 拿最开始的Main的类作例子,就只改变一个变量。

public volatile int i = 0;
复制代码

和上面的代码同样,每次运行的结果都不会相同。

因此通常关于volatile有如下两种用法。

  1. 状态标志 由于只会使用truefalse,天然也就知足了原子操做。
volatile boolean flag = false;
void doSomething(){
    flag = true;
}

void check(){
    if(flag){
        // ···········
    }
}
复制代码
  1. 双重检查模式 / DCL

详见设计模式(二)

以上就是个人学习成果,若是有什么我没有思考到的地方或是文章内存在错误,欢迎与我分享。


相关文章推荐:

多线程编程基础(二)-- 线程池的使用

JVM必备基础知识(一) -- 类的加载机制

聊一聊设计模式(一)-- 六大原则

相关文章
相关标签/搜索