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

posted @ 2018-04-25 22:53  Fuu  阅读(910)  评论(0编辑  收藏  举报