zoukankan      html  css  js  c++  java
  • Java 线程详解(一)线程的基础

    为了9月份的秋招呢,现在开始进行多线程以及并发相关知识的学习。。加油加油

    一、一些基本的概念

    1.线程和进程

      进程:正在运行的程序,是系统进行资源分配的独立单位。

      线程:进程的执行路径,调度和执行的单位,单个路径单线程,多个路径多线程。

    以上的解释太“官方”了,在《Java多线程编程核心技术》里,把进程理解成我们打开的每个程序,而线程则是程序里每一个子任务。 

    比如,我们打开WeChat.exe运行,此时WeChat.exe就可以理解成一个进程,而你用微信和别人视频,拿来传输文件,发送信息等等就有很多子任务,其中每一个任务就可以理解成线程

    其中多线程就是指,一个进程里面有多个线程,其优点在于解决了多部分同时运行的问题,提高效率

    而缺点是1.当线程太多会导致效率的降低,因为线程的执行依靠的是CPU的来回切换,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势(通常减少上下文切换可以采用无锁并发编程,CAS算法,使用最少的线程和使用协程)

           2.临界区线程安全问题,解决方法:(https://www.jianshu.com/p/959cf355b574

    2.同步和异步(Asynchronized和Synchronised)

    多线程同步:就是在发出一个功能请求时,在没有得到结果之前,就不能发送下一个请求。好比所有跑道只剩一条的时候,运动员在一条跑道上跑完一个到一个

    好处:解决了线程的安全问题。(线程的安全问题和非线程安全问题有区别吗??还没解决)

    弊端:每次都有判断锁,浪费时间降低了效率。但是在安全与效率之间,首先考虑的是安全。

    多线程异步:则是在发出一个功能请求时,不需要等待返回,随时可以再发送下一个请求。好比训练的时候,运动员在各自的跑道上跑自己的步。

    好处:并发好,cpu利用率高。

    弊端:要考虑线程间同步互斥问题等。

    3.并发和并行

    并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。

    实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。

    4.阻塞和非阻塞

    阻塞和非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。

    5.临界区

    又可以称之为互斥区,用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。

    如synchronize可以在任意对象及方法上加锁,而加锁的这段代码就被称为临界区。

    二、如何创建一个线程

    在Java中实现多线程有两种方法,1.继承Thread类,2.实现Runnable接口。(至于还有别的线程池或者是callable接口是这个基础上的增强版??之后会说到)

    1.Thread类和Runnable接口的结构

    public class Thread extends Object implements Runnable

    Thread类实现Runnable接口,它们具有多态关系

    可以传递Runnable接口的对象,而Runnable的设计是为了解决Java中单继承的限制。

    还可以看出可以传递Thread类的对象,说明可以将一个Thread对象中的run()方法交给别的线程调用。

    Runnable中只有一个run()方法

    2.继承Thread类

    1)创建一个新类继承Thread类。

    2)重写Thread类中的public void run()方法,将线程的代码封装到run()里。

    3)  创建该新类对象,创建线程。

    4)对象调用start(),启动线程(即调用run方法)。

    class MyThread extends Thread {  
        // 重载run函数  
        public void run() {  
            System.out.println("MyThread");
        }  
      
        public static void main(String argv[]) {  
            MyThread td = new MyThread(); // 创建,并初始化MyThread类型对象td  
            td.start(); // 调用start()方法执行一个新的线程  
            System.out.println("运行结束") 
        }  
    } 
    输出:运行结束
            MyThread
      因为“运行结束”是主线程main里的,所以是先执行

    3.实现Runnable接口

    1)定义一个新类,实现Runnable接口。

    2)重写接口中run()方法,将线程的代码封装到run()里。

    3)创建Runnable接口的子类对象。

    4)将Runnabl接口的子类对象作为参数传递给Thread类的构造函数,创建Thread类对象(原因:线程的任务都封装在Runnable接口子类对象的run方法中。所以要在线程对象创建时就必须明确要运行的任务)。

      反正就是Runnable没有start()方法,需要通过传对象参数形式用线程类去启用它的run方法。

    5)Thread类对象调用start(),启动线程(即调用run方法)。

    class MyRunnable implements Runnable {  
        // 重载run函数  
        public void run() {  
            System.out.println("运行中!");
        }  
      
        public static void main(String argv[]) {  
            MyRunnable rb = new MyRunnable(); // 创建,并初始化MyRunnable对象rb  
            Thread td = new Thread(rb); // 通过Thread创建线程  
            td.start(); // 启动线程td  
            System.out.pritnln("运行结束");
        }  
    }  
    输出:运行结束   
            运行中!

       注意:调用线程的 run()方法是通过启动线程的start()方法来实现的。 因为线程在调用start()方法之后,系统会自动调用 run()方法。与一般方法调用不同的地方在于一般方法调用另外一个方法后,必须等被调用的方法执行完毕才能返回,而线程的 start()方法被调用之后,系统会得知线程准备完毕并且可以执行run()方法,start()方法就返回了,start()方法不会等待run()方法执行完毕。  

    为什么要重写run()方法呢?

    因为不是类中的所有代码都需要被线程执行的。而这个时候,为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。

    4.两种区别

    两种创建线程性质都是一样的,但是通常建议创建线程是实现Runnable接口

    优势是:1.可以支持多继承

           2.适合多个相同的程序代码的线程去处理同一个资源

        3.线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

    三、线程的各个状态

    理解下面的图!

    1、新建状态(New):新创建了一个线程对象。

    2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

    3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码

    4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

    (一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)

    (二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。

    (三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

    5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

    四、Thread类的常用API方法(这里只是讲解方法基本,具体方法间比较后面会说到)

    1.currentThread()(静态)

    public static Thread currentThread()

     返回对当前正在执行的线程对象的引用。注意是静态

    通常用作,Thread.currentThread.getName()返回线程名。

    2.isAlive()

    public final boolean isAlive()

     测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。

     返回:如果该线程处于活动状态,则返回 true;否则返回 false

    3.sleep()(静态)

    public static void sleep(long millis) throws InterruptedException
    
    public static void sleep(long millis, int nanos) throws InterruptedException

    在指定的毫秒数(或者加指定的纳秒数)内让当前正在执行的线程休眠(暂停执行)。注意是个静态方法

    参数含义:millis - 以毫秒为单位的休眠时间。nanos - 要休眠的另外 0-999999 纳秒。

    为什么要用sleep,主要是为了暂停当前线程把cpu片段让出给其他线程减缓当前线程的执行。 

    (后面再说和wait()的区别)

    3.getId()

    public long getId()

    返回该线程的唯一标识。线程 ID 是一个正的 long 数,在创建该线程时生成。线程 ID 是唯一的,并终生不变。线程终止时,该线程 ID 可以被重新使用。

    4.interrupt(),interrupted()(静态),isInterrupted()

    public void interrupt()
    public static boolean interrupted(){
            return currentThread().isInterrupted(true);
    }
    public boolean isInterrupted(){
            return isInterrupted( false);//至于为什么是false请看下面链接
    }

    interrupt():对线程进行中断操作。

    isInterrupted():测试线程Thread对象是否已经是中断状态,但不清除状态标志,如果该线程已经中断,则返回 true;否则返回 false

    interrupted():对当前线程进行中断操作(由上面源代码可知,就算当前是main线程用别的对象线程调用该方法,也还是判断返回main是否被中断),该方法会清除中断标志位为false。需要注意的是,当抛出InterruptedException时候,会清除中断标志位,也就是说此时再调用isInterrupted会返回false。

    详细讲解这三个方法:https://www.cnblogs.com/w-wfy/p/6414801.html

    5.yield()(静态)

    public static void yield()

     放弃当前的CPU资源,让它让给当前线程相同优先级的线程去占用CPU,即CPU暂停当前正在执行的线程对象,并执行其他线程。

    但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。

    6.setDaemon() 守护线程Daemon

    public final void setDaemon(boolean on)

     将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。

    class ThreadDemo extends Thread {
        @Override
        public void run() {
            for (int x = 0; x < 100; x++) {
                System.out.println(getName() + ":" + x);
            }
        }
    }
    public class test {
        public static void main(String[] args) {
            ThreadDemo td1 = new ThreadDemo();
            ThreadDemo td2 = new ThreadDemo();
    
            td1.setName("A");
            td2.setName("B");
    
            // 设置守护线程
            td1.setDaemon(true);
            td2.setDaemon(true);
    
            td1.start();
            td2.start();
    
            Thread.currentThread().setName("C");
            for (int x = 0; x < 5; x++) {
                System.out.println(Thread.currentThread().getName() + ":" + x);
            }
        }
    }
    当C执行完第五次之后(即C:4时候),A和B也会执行完,并不会执行100次

     7.join()

    public final void join() throws InterruptedException

     等待该线程终止

    class ThreadDemo extends Thread {
         @Override
         public void run() {
                for (int x = 0; x < 100; x++) {
                    System.out.println(getName() + ":" + x);
                }
            }
        }
    public class test {
        public static void main(String[] args) {
            ThreadDemo ty1 = new ThreadDemo();
            ThreadDemo ty2 = new ThreadDemo();
            ThreadDemo ty3 = new ThreadDemo();
            ty1.setName("A");
            ty2.setName("B");
            ty3.setName("C");
            ty1.start();
            try {
                ty1.join();
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            ty2.start();
            ty3.start();
        }
    }
    运行程序可以发现,名字A的线程运行完之后才开始运行B和C

    五、Thread类和Object类一些相似的方法区别

    sleep()和wait():

    Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。

    两者的区别在于:

    1.sleep()方法是Thread的静态方法,而wait是Object实例方法

    2.wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁;而sleep()方法没有这个限制可以在任何地方种使用

    3.wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源;而sleep()方法只是会让出CPU并不会释放掉对象锁

    例子:https://www.cnblogs.com/DreamSea/archive/2012/01/16/2263844.html

    4.sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行;而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。

    sleep()和yield():

    sleep()方法和yield()方法都是Thread类的静态方法(注意两个方法都是作用于当前线程,都会使当前处于运行状态的线程放弃CPU,把运行机会让给别的线程。

    两者的区别在于:

    1.sleep()方法会给其他线程运行的机会,不考虑其他线程的优先级,因此会给较低优先级线程一个运行的机会;yield()方法只会给相同优先级或者更高优先级的线程一个运行的机会。

    2.当线程执行了sleep(long millis)方法,将转到阻塞状态,参数millis指定睡眠时间;当线程执行了yield()方法,将转到就绪状态

    3.sleep()方法声明抛出InterruptedException异常,而yield()方法没有声明抛出任何异常。

    4.sleep()方法比yield()方法具有更好的可移植性,不能依靠yield()方法来提高程序的并发性能。对于大多数程序员来说,yield()方法的唯一用途是在测试期间人为地提高程序的并发性能,以帮助发现一些隐藏的错误。

    runnble和callable

  • 相关阅读:
    【web前端面试题整理03】来看一点CSS相关的吧
    【web前端面试题整理02】前端面试题第二弹袭来,接招!
    【web前端优化之图片模糊到清晰】看我QQ空间如何显示相片
    【web前端面试题整理01】各位加班累了吧,来做点前端面试题吧
    【javascript激增的思考03】MVVM与Knockout
    【javascript激增的思考02】模块化与MVC
    【javascript激增的思考01】模块化编程
    【position也可以很复杂】当弹出层遇上了鼠标定位(下)
    【position也可以很复杂】当弹出层遇上了鼠标定位(上)
    iOS开发拓展篇—音乐的播放
  • 原文地址:https://www.cnblogs.com/furaywww/p/8859140.html
Copyright © 2011-2022 走看看