多线程入门
什么是进程和线程?
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
为什么使用多线程?
使用多线程可以编写高效率的程序来达到充分利用 CPU,可以大大提高系统整体的并发能力以及性能.
线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
下图显示了一个线程完整的生命周期。
-
新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程. -
就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。 -
运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。 -
阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
-
死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
创建线程的方式
java 提供了三种创建线程的方法:实现 Runnable 接口;继承 Thread 类本身;通过 Callable 和 Future 创建线程。
1.实现 Runnable 接口
创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类,同时重写 run()方法.
然后创建Runnable实现类的实例,并以此实例作为Thread的target对象,即该Thread对象才是真正的线程对象。
新线程创建之后,你调用它的 start() 方法它才会运行。
package com.example.test;
/**
* @author ydx
*/
public class RunnableDemo implements Runnable{
/**
* 线程名称
*/
private String threadName;
/**
* 构造方法
* @param threadName 线程名称
*/
public RunnableDemo(String threadName) {
this.threadName = threadName;
}
@Override
public void run() {
System.out.println(threadName + " is running");
//业务
for (int i = 0; i < 5; i++) {
System.out.println(threadName + " 执行 " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(threadName + " is exiting");
}
public static void main(String[] args) {
RunnableDemo runnable1 = new RunnableDemo("thread-1");
Thread thread1 = new Thread(runnable1);
thread1.start();
RunnableDemo runnable2 = new RunnableDemo("thread-2");
Thread thread2 = new Thread(runnable2);
thread2.start();
}
}
第一次运行结果如下:
thread-1 is running
thread-1 执行 0
thread-2 is running
thread-2 执行 0
thread-2 执行 1
thread-1 执行 1
thread-2 执行 2
thread-1 执行 2
thread-1 执行 3
thread-2 执行 3
thread-1 执行 4
thread-2 执行 4
thread-2 is exiting
thread-1 is exiting
第二次运行结果如下:
thread-1 is running
thread-1 执行 0
thread-2 is running
thread-2 执行 0
thread-1 执行 1
thread-2 执行 1
thread-1 执行 2
thread-2 执行 2
thread-1 执行 3
thread-2 执行 3
thread-2 执行 4
thread-1 执行 4
thread-1 is exiting
thread-2 is exiting
可以看出两次运行结果是不一样的,每次两个线程的执行顺序是随机的.
2.继承Thread类
创建一个线程的第二种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。
继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
package com.example.test;
/**
* @author ydx
*/
public class ThreadDemo extends Thread {
/**
* 线程名称
*/
private String threadName;
public ThreadDemo(String threadName) {
this.threadName = threadName;
}
@Override
public synchronized void start() {
System.out.println(threadName+ " is starting......");
super.start();
}
@Override
public void run() {
System.out.println(threadName + " is running");
//业务
for (int i = 0; i < 3; i++) {
System.out.println(threadName + " 执行 " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(threadName + " is exiting");
}
public static void main(String[] args) {
ThreadDemo thread1 = new ThreadDemo("thread-1");
thread1.start();
ThreadDemo thread2 = new ThreadDemo("thread-2");
thread2.start();
}
}
运行结果如下:
thread-1 is starting......
thread-2 is starting......
thread-1 is running
thread-1 执行 0
thread-2 is running
thread-2 执行 0
thread-1 执行 1
thread-2 执行 1
thread-2 执行 2
thread-1 执行 2
thread-2 is exiting
thread-1 is exiting
3.通过 Callable 和 Future 创建线程
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
- 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
-
调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
package com.example.test; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * @author ydx */ public class CallableTest implements Callable<Integer> { @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i < 5; i++) { sum += i; System.out.println(i); //sleep 200ms Thread.sleep(200); } return sum; } public static void main(String[] args) { long start = System.currentTimeMillis(); CallableTest callableTest = new CallableTest(); FutureTask<Integer> futureTask = new FutureTask<>(callableTest); new Thread(futureTask, "thread-1").start(); CallableTest callableTest2 = new CallableTest(); FutureTask<Integer> futureTask2 = new FutureTask<>(callableTest2); new Thread(futureTask2, "thread-2").start(); try { System.out.println("thread-1的结果: " + futureTask.get()); System.out.println("thread-2的结果: " + futureTask.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("耗时: " + (end - start) + "ms"); } }
运行结果:
0 0 1 1 2 2 3 3 4 4 thread-1的结果: 10 thread-2的结果: 10 耗时: 1004ms
我们创建了两个线程, 每个线程计算0~4的和,单个线程耗时200ms * 5 = 1000ms,而最终两个线程的总耗时约1000ms,由此可见两个线程是并发进行.
4.创建线程的三种方式的对比
- 使用继承 Thread 类的方式创建多线程时,编写简单,但是不够灵活
- 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类,创建线程比较灵活.
线程管理
Java提供了一些方法用于线程状态的控制。具体如下:
1.sleep(线程睡眠)
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep方法。
Thread.sleep(long millis)方法,millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。
2.wait(线程等待)
Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法.
3.yield(线程让步)
Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。
yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。
4.join(线程加入)
join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
5.notify(线程唤醒)
Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争
线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MINPRIORITY ) – 10 (Thread.MAXPRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
线程池
线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗.
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
Java通过Executors提供四种线程池,分别为:
- newCachedThreadPoo
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。 - newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。 - newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。 - newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。