java 多线程详细笔记(原理理解到全部使用)

   鸽了好久以后终于又更新了,看同学去实习都是先学源码然后修改之类,才发觉只是知道语法怎么用还远远不够,必须要深入理解以后不管是学习还是工作,才能举一反三,快速掌握。

基础知识

  既然说到了多线程,那自然就要了解一下线程到底是什么,计算机又是怎么实现对应功能的呢。

进程与线程

进程: 是代码在数据集合的一次运行活动,是系统进行资源分配和调度的基本单位。我们常用的程序就是一个进程,像qq音乐、word之类,进程就是一个程序的动态执行过程。
线程: 线程是进程的一个组成部分,一个进程可以有多个线程,而一个线程只能属于一个进程。线程可以拥有自己的堆栈、自己的程序计数器还有自己的局部变量,但不能拥有系统资源。它与父进程的其他线程共享该进程的所有资源。就好比在使用word的时候,可以一遍打字,word自动统计字数、提示语法错误等,这些功能都属于一个线程。

   但其实我们电脑中存在的线程数,要远远大于电脑cpu的个数,如果是单cpu的电脑的话,每一时刻就只有一个线程可以执行,但我们仍然可以实现很多程序的同时使用,其实我们所看到的多线程的运行,只是电脑在极短时间里交替执行多个线程所表现出来的效果,比如我在一边使用word,还一边听歌,那电脑可以每隔1ms播放一下音乐,再接受一下输入,再检测一下字数等,这样以极快的速度处理每个线程的部分代码,从而使得多个任务看起来可以"同时"进行。
   多线程的确可以为我们处理任务提供很大的便利,提高效率,但是因为多线程的机制使得很多在单线程中正常执行的程序出现问题,其中包括:1、原子性问题 2、线程共享内存不可见的问题。

线程原子性问题

   此问题经常出现在多个线程对同一组共享变量的访问中,假设有三个线程,每个线程都会使共享变量x减少1并打印,那么应该有如下示意图:
   
但因为执行的操作 x -= 1 实际上并不是一个操作,执行过程是获得现有x,创建一个新的变量x',然后另x'的数值变为x - 1,之后再将公共变量中x的指针指向新的变量x',从而实现对应操作。这样的非原子性的操作就会出现这样的情况:当A线程在获得了x,并且执行x-1以后,还没有将公共变量指针指向新的变量x'的时候,线程B进入并且也获得了x,此时线程B获得的x就还是没有更新的x,从而造成了数据读写不一致的问题。

线程共享内存不可见问题

   出现不可见性问题的原因与线程访问共享数据方式有关,每一个线程都有单独的堆栈、程序计数器等信息,当线程要读取公共变量的时候,也是先读取到自己的栈中,然后进行操作,最后写回到进程的公共变量里去,但若在线程A将变量加入自己的栈后,线程B将修改后的结果写回公共变量,但此时线程A不知道公共变量已经修改,从而造成不一致问题。

   基于这些问题,我们可以使用synchronized 以及volatile 关键字来进行避免,这些将在下一篇单独讲解。

线程状态介绍

   线程共有五种状态,分别是新建状态(NEW),可运行状态(RUNNABLE), 运行状态(RUNNING),阻塞状态(BLOCKED),死亡状态(DEAD).现对其一一介绍:

新建状态

   新建状态(NEW):指新创建了一个线程对象。

可运行状态

   可运行状态(RUNNABLE):指有资格可以运行,但是没有获得cpu,所以要等待直到获得cpu后变成运行状态(RUNNING)。
   有很多方法可以使线程变成可运行状态,譬如:
   1、对新建立的线程执行start()方法。
   2、对线程调用sleep()方法使之变为阻塞状态,当sleep时间过后,会恢复成可运行状态。
   3、在本线程使用join()方法,当指定线程执行完毕后,本线程恢复为可运行状态。
   4、使用yield()方法,进入可运行状态。

运行状态

   运行状态就是线程获得了cpu,并执行对应代码。

阻塞状态

   阻塞状态是程序让出cpu控制权,并且暂时停止运行。直到指定条件达成以后,有资格运行,变成可运行状态。
   使程序进入阻塞状态的方法有:
   1、等待阻塞:使用wait()方法,直到被notify()方法唤醒
   2、同步阻塞:申请的资源上锁并被其他资源占用,此时阻塞直到对象锁被释放
   3、其他阻塞:比如使用join()方法,则当前线程会阻塞直到指定线程完成。使用sleep()等待进入阻塞状态,直到等待时间后恢复。等待io处理等。

死亡状态

   一般是因异常退出了run(),或者程序执行完成

多线程使用方法

   如果只是单纯地使用多线程而不是深究其同步问题的话,java提供的Thread类是已经足够使用的,而对于同步问题的解决,将会在下一篇中进行详细分析与介绍。

线程的创建

   可以使用两种方法来创建线程,分别是继承Thread并重写run()方法,或者实现Runable接口的run()方法,因为java不支持多继承,因此推荐使用实现接口的方式.
可以采用接口实现:

    //使用接口实现
    public class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println("run!");
    }
}

也可以使用继承的方式实现:

public class MyThread02 extends Thread {
    @Override
    public void run() {
        System.out.println("i am running!");
    }
}

线程的运行

   调用start()方法即可运行。虽然我们写的是run()方法,但不要直接调用run()方法,那样就起不到多线程的作用了,应该调用start()方法,然后会将该线程变为可运行状态,根据启动条件和cpu情况,适时地执行程序。

线程的礼让

   使用yield()方法,使线程让出cpu,重新回到可运行状态。这时候会继续争夺cpu,也就是有可能线程A使用yield()方法让出cpu以后,又重新获得cpu的使用资格。也就是如果有A、B两个线程,并不一定A线程礼让的时候,B线程一定先于A线程执行。

线程的睡眠

   使用sleep()方法,使线程阻塞一定时间。

线程的join

   使用threadx.join()方法,使得当前线程堵塞,直到threadx线程执行完成。

线程的等待与唤醒

   一般都与锁一起使用。

posted @ 2020-05-12 20:09  小新而已  阅读(528)  评论(0编辑  收藏  举报