一、基本概念

1. 进程

  • 进程是指正在运行的程序的实例。它是操作系统分配资源和执行任务的基本单位。

  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。

  • 例如打开网易云音乐就是开启了一个进程。

2. 线程

  • 线程是进程中的一个执行单元,它被包含在进程之中,是进程中的实际运作单位。

  • 一个进程中可以并发多个线程,每条线程并行执行不同的任务。

二者对比

进程基本上相互独立的,而线程存在于进程内,是进程的一个子集

进程拥有共享的资源,如内存空间等,供其内部的线程共享

进程间通信较为复杂,同一台计算机的进程通信称为 IPC(Inter-process communication),不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP。

线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量。

线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

3. 并发

并发是同一个时间段内,几个作业都在同一个CPU上运行,但任意一个时刻点上只有一个作业在处理机上运行。即多个任务在同一时刻是交替进行的。

4. 并行

并行是同一个时间段内,几个作业在几个CPU上运行,任意一个时刻点上,有多个作业在同时运行,并且多个作业之间互不干扰。即多个任务在同一时刻是同时进行的。

二、 java中线程的创建

1. 继承Thread类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class ThreadTest {
    public static void main(String[] args) {  
        MyThread mt1= new MyThread("一号窗口");  
        MyThread mt2= new MyThread("二号窗口");
        mt1.start();
        mt2.start();
    }  
}
class MyThread extends Thread{
    private int ticket = 10;  
    private String name;  
    public MyThread(String name){  
        this.name =name;  
    }  

    public void run(){
        while (this.ticket>0) {
            System.out.println(this.name+"卖票---->"+(this.ticket--));
        }
    }  
} 

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
一号窗口卖票---->10
一号窗口卖票---->9
一号窗口卖票---->8
一号窗口卖票---->7
一号窗口卖票---->6
一号窗口卖票---->5
一号窗口卖票---->4
一号窗口卖票---->3
一号窗口卖票---->2
二号窗口卖票---->10
二号窗口卖票---->9
二号窗口卖票---->8
二号窗口卖票---->7
二号窗口卖票---->6
二号窗口卖票---->5
二号窗口卖票---->4
二号窗口卖票---->3
二号窗口卖票---->2
二号窗口卖票---->1
一号窗口卖票---->1

2. 实现Runnable接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class RunnableTest{
     public static void main(String[] args) {
         MyRunnable mt = new MyRunnable();
         Thread t1 = new Thread(mt,"一号窗口");  
         Thread t2 = new Thread(mt,"二号窗口");
         t1.start();  
         t2.start();
    }  
}
class MyRunnable implements Runnable{  
    private int ticket =10;  
    public void run(){
        while (this.ticket>0) {
            System.out.println(Thread.currentThread().getName()+"卖票---->"+(this.ticket--));
        }
    }  
}

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
一号窗口卖票---->10
一号窗口卖票---->8
一号窗口卖票---->7
一号窗口卖票---->6
一号窗口卖票---->5
一号窗口卖票---->4
一号窗口卖票---->3
一号窗口卖票---->2
一号窗口卖票---->1
二号窗口卖票---->9

3. Callable

使用Callable和Future,Callable接口类似于Runnable接口,都是通过Thread类的有参构造方法传入Runnable接口类型的参数来实现多线程,不同的是,这里传入的是Runnable接口的子类FutureTask对象作为参数,并且它可以返回一个Future对象来获取任务的结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class CallableTest{
    public static void main(String[] args) throws Exception {
        MyCallable mc = new MyCallable();

        FutureTask<Object> ft1 = new FutureTask<>(mc);
        FutureTask<Object> ft2 = new FutureTask<>(mc);

        Thread t1 = new Thread(ft1,"一号窗口");
        Thread t2 = new Thread(ft2,"二号窗口");
        t1.start();
        t2.start();

        System.out.println("thread1返回结果:" + ft1.get());
        System.out.println("thread2返回结果:" + ft2.get());
    }
}
class MyCallable implements Callable {
    private int ticket =10;

    @Override
    public Object call(){
        while (this.ticket>0) {
            System.out.println(Thread.currentThread().getName()+"卖票---->"+(this.ticket--));
        }
        return Thread.currentThread().getName()+"剩余票"+ticket+"张";
    }
}

运行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
一号窗口卖票---->8
一号窗口卖票---->7
一号窗口卖票---->6
一号窗口卖票---->5
一号窗口卖票---->4
一号窗口卖票---->3
一号窗口卖票---->2
一号窗口卖票---->1
二号窗口卖票---->9
thread1返回结果:一号窗口剩余票0张
thread2返回结果:二号窗口剩余票0张

1

从上图可以看出,FutureTask本质是Runnable接口和Future接口的实现类,而Future则是JDK 5提供的用来管理线程执行返回结果的。其中Future接口中共有5个方法,用来对线程结果进行管理

方法声明 功能描述
boolean cancel(boolean mayInterruptIfRunning) 用于取消任务,参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行的任务
boolean isCancelled() 判断任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true
boolean isDone() 判断任务是否已经完成,若任务完成,则返回true
V get() 用于获取执行结果,这个方法会发生阻塞,一直等到任务执行完毕才返回执行结果
V get(long timeout, TimeUnit unit) 用于在指定时间内获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null

从上面运行结果来看,实现Runnable、Callable接口可以实现资源共享,因为实现Runnable、Callable接口每次都是同一个对象运行run()方法,而继承Thread类每次都是不同的对象来运行run()方法

注意:实现 Callable 接口,并结合 FutureTask 或 使用JDK自带的Executors创建线程池来创建线程也是可以的,但是其本质还是使用的Thread、Runnable。

FutureTask 实现的 RunnableFuture继承了Runnable接口

Executors 创建线程池创建线程详见源码

三、java中线程的状态

1、简介

Java线程的状态有初始(NEW)、运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIMED_WAITING)、终止(TERMINATED) 这几种状态。这几种状态的流转过程如下:

stateDiagram [*] --> NEW: 创建线程 NEW--> RUNNABLE:Thread.start() state RUNNABLE { 运行中(RUNNING)--> 就绪(READY):yield()
系统调度 就绪(READY)--> 运行中(RUNNING):系统调度 } RUNNABLE --> WAITING:Object.wait()
Thread.join() WAITING--> RUNNABLE:Object.notify()
Object.notifyAll()
等待调用join方法的线程执行完成完成 RUNNABLE--> TIMED_WAITING:Object.wait(long)
Thread.join(long)
Thread.sleep(long) TIMED_WAITING--> RUNNABLE:Object.notify()
Object.notifyAll()
超过等待时间
等待调用join方法的线程执行完成完成 RUNNABLE--> BLOCKED:synchronized BLOCKED--> RUNNABLE:获得锁 RUNNABLE--> TERMINATED:执行完成,
Exception WAITING--> TERMINATED:Exception TIMED_WAITING--> TERMINATED:Exception BLOCKED--> TERMINATED:Exception TERMINATED--> [*]

2、线程状态说明

2.1 初始

实现Runnable、Callable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

2.2 可运行

可能正在运行,也可能正在等待 CPU 时间片。包含了操作系统线程状态中的 Running 和 Ready。

2.2.1 就绪

  • 就绪状态只是说你有资格运行,调度程序没有挑选到你,你就永远是就绪状态。

  • 调用线程的start()方法,此线程进入就绪状态。

  • 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。

  • 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。

  • 锁池里的线程拿到对象锁后,进入就绪状态。

2.2.2 运行中

线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式

2.3 阻塞

阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁失败)时的状态,如果其它线程释放了锁就会结束此状态。

2.4 等待

处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

进入方法 退出方法
没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕

2.5 超时等待

处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

进入方法 退出方法
Thread.sleep() 方法 时间结束
设置 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll()
设置 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕

2.6 终止(TERMINATED)

  • 当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。线程一旦终止了,就不能复生。

  • 可以是线程结束任务之后自己结束,或者产生了异常而结束。

  • 在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

3、wait()、join()、yield()

3.1 wait

wait()方法的作用是让当前线程进入等待状态,线程会释放锁,因为如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中。

wait()会与notify()和notifyAll()方法一起使用。notify()和notifyAll()方法的作用是唤醒等待中的线程,notify()方法:唤醒单个线程,notifyAll()方法:唤醒所有线程。

注意:使用wait()的前提是必须拿到锁,因为调用会执行释放锁的操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
    public static void main(String[] args) {
        Object object = new Object();
        new Thread(() -> {
            try {
                synchronized (object) {
                    object.wait();
                }
                System.out.println("sss");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            synchronized (object) {
                object.notify();
            }
        }).start();
    }
}

3.2 join

join()方法是等待这个线程结束,完成其执行。它的主要起同步作用,使线程之间的执行从“并行”变成“串行”。

也就是说,当我们在线程A中调用了线程B的join()方法时,线程执行过程发生改变:线程A,必须等待线程B执行完毕后,才可以继续执行下去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Test {
    public static void main(String[] args) {
        Thread t1 = new MyThread1(null,"线程一");
        Thread t2 = new MyThread1(t1,"线程二");

        t2.start();
        t1.start();
    }
}
class MyThread1 extends Thread{
    private Thread t = null;
    private String name;
    public MyThread1(Thread t,String name){
        this.t = t;
        this.name = name;
    }
    @Override
    public void run() {
        if (this.t != null) {
            try {
                System.out.println("我是:"+this.name+",让对方先执行");
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(this.name);
    }
}

3.3 wait()、join()相同之处

  • wait()和join()方法都用于暂停Java中的当前线程,进入等待状态。

  • 在Java中都可以调用interrupt()方法中断wait()和join()的线程状态。

  • wait()和join()都是非静态方法。

  • wait()和join()都在Java中重载。wait()和join()没有超时,但接受超时参数。

3.4 wait()、join()不同之处

  • wait()方法是Object类的方法,join()是Thread的方法。

  • wait()方法用于线程间通信;而join()方法用于在多个线程之间添加排序,第二个线程需要在第一个线程执行完成后才能开始执行。

  • 我们可以通过使用notify()和notifyAll()方法启动一个通过wait()方法进入等待状态的线程。但是我们不能打破join()方法所施加的等待,除非或者中断调用了连接的线程已执行完了。

  • wait()方法必须从同步(synchronized)的上下文调用,即同步块或方法,否则会抛出IllegalMonitorStateException异常。但在Java中有或没有同步的上下文,我们都可以调用join()方法。

3.5 wait()、sleep()不同之处

  • wait()方法是Object类的方法,sleep()是Thread的静态方法。

  • wait() 必须拿到锁才能使用,sleep() 没有此限制。

  • wait() 会释放锁,sleep() 不会。

3.6 yield()

这是个静态native方法,意思是让出当前正在执行的线程的cpu执行权并让其进入可运行状态以允许具有相同优先级的其他线程获得运行的机会,仅仅只有这个作用。但是该方法并不能保证cpu的执行权一定被让出了,因为cpu调度哪个线程来执行并不是确定的,所以也有可能当前线程刚刚把执行权让出来马上cpu又将执行权分配给它。

注意:yield()方法是不会释放锁的。虽然调用yield()方法让出了CPU资源,让当前线程进入就绪状态,但是因为yield()方法不会释放锁,所以即使其它线程获得CPU资源也还是会继续wait。