一. 线程
Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程包含以下内容:
- 一个指向当前被执行指令的指令指针;
- 一个栈;
- 一个寄存器值的集合,定义了一部分描述正在执行线程的处理器状态的值
- 一个私有的数据区。
多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
二. 线程的生命周期
- 新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
- 就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
- 运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
- 阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
-
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
-
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
-
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
-
- 死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
线程在Running的过程中可能会遇到阻塞(Blocked)情况
- 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
- 调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool ),释放同步锁使线程回到可运行状态(Runnable)
- 对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool ),同步锁被释放进入可运行状态(Runnable)。
此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。
四. 创建线程
Java 提供了三种创建线程的方法:
- 通过实现 Runnable 接口;
- 通过继承 Thread 类本身;
- 通过 Callable 和 Future 创建线程。
1. 通过实现 Runnable 接口来创建线程
为了实现 Runnable,一个类只需要执行一个方法调用 run(),声明如下:
public void run()
在创建一个实现 Runnable 接口的类之后,你可以在类中实例化一个线程对象。
Thread 定义了几个构造方法,下面的这个是我们经常使用的:
Thread(Runnable threadOb,String threadName);
其中,threadOb 是一个实现 Runnable 接口的类的实例,并且 threadName 指定新线程的名字。
新线程创建之后,你调用它的 start() 方法它才会运行。
void start();
package com.zth;
class A implements Runnable{
private String name;
public A(String name){
this.name = name;
}
@Override
public void run() {
for(int i = 0;i<10000;i++){
System.out.println(name+":"+i);
}
}
}
public class Demo0{
public static void main(String[] args){
A a1 = new A("zth");
A a2 = new A("haha");
Thread t1 = new Thread(a1);
Thread t2 = new Thread(a2);
t1.start();
t2.start();
}
}
2. 通过继承Thread来创建线程
创建一个线程的第二种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。
继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。
该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。
package com.zth;
class ThreadDemo extends Thread{
private Thread t;
private String threadName;
ThreadDemo (String name){
threadName = name;
System.out.println("Creating "+ threadName);
}
@Override
public void run(){
System.out.println("Running "+ threadName);
for(int i = 4; i>0; i--){
System.out.println("Thread: "+threadName +","+i);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
System.out.println("Thread: "+threadName +"interrupted");
}
}
System.out.println("Thread "+ threadName+"exiting.");
}
public void start(){
System.out.println("Starting "+threadName);
if(t == null){
t = new Thread(this,threadName);
t.start();
}
}
}
public class Demo0{
public static void main(String[] args){
ThreadDemo t1 = new ThreadDemo("线程1");
ThreadDemo t2 = new ThreadDemo("线程2");
t1.start();
t2.start();
}
}
执行结果:
Creating 线程1
Creating 线程2
Starting 线程1
Starting 线程2
Running 线程1
Thread: 线程1,4
Running 线程2
Thread: 线程2,4
Thread: 线程1,3
Thread: 线程2,3
Thread: 线程1,2
Thread: 线程2,2
Thread: 线程1,1
Thread: 线程2,1
Thread 线程1exiting.
Thread 线程2exiting.
3. 通过 Callable 和 Future 创建线程
-
1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
-
2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
-
3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
-
4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
package com.zth;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class Demo0 implements Callable<Integer>{
public static void main(String[] args) throws Exception {
Demo0 d1 = new Demo0();
FutureTask<Integer> ft = new FutureTask<>(d1);
for(int i= 0;i<100;i++){
System.out.println(Thread.currentThread().getName()+ " i 的值"+i);
if(i == 20){
new Thread(ft,"有返回值的线程:").start();
}
}
}
@Override
public Integer call() throws Exception {
int i = 0;
for(; i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+ i);
}
return i;
}
}
4. 通过线程池创建线程
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。
java.util.concurrent.Executors 类提供大量创建连接池的静态方法,是必须掌握的。
Java5的线程池分好多种:固定尺寸的线程池、单任务线程池、可变尺寸连接池、延迟连接池、单任务延迟连接池、自定义线程池。
4.1 固定大小的线程池
package com.zth;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo0{
public static void main(String[] args){
// 创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
//创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
Thread t1 =new MyThread();
Thread t2 =new MyThread();
Thread t3 =new MyThread();
Thread t4 =new MyThread();
Thread t5 =new MyThread();
// 将线程放入线程池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
// 关闭线程池
pool.shutdown();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在执行。。。");
}
}
执行结果:
pool-1-thread-1正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-2正在执行。。。
4.2 单任务线程池
在上例的基础上改一行创建pool对象的代码为:
//创建一个使用单个 worker线程的 Executor,以无界队列方式来运行该线程。
ExecutorService pool = Executors.newSingleThreadExecutor();
执行结果:
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
对于以上两种连接池,大小都是固定的,当要加入的池的线程(或者任务)超过池最大尺寸时候,则入此线程池需要排队等待。
一旦池中有线程完毕,则排队等待的某个线程会入池执行。
4.3 可变尺寸的线程池
改动 pool 的创建方式:
//创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
ExecutorService pool = Executors.newCachedThreadPool();
执行结果:
pool-1-thread-1正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-3正在执行。。。
pool-1-thread-5正在执行。。。
pool-1-thread-4正在执行。。。
4.4 延迟连接池
newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
参数:
corePoolSize
- 池中所保存的线程数,即使线程是空闲的也包括在内
threadFactory
- 执行程序创建新线程时使用的工厂
返回:
新创建的安排线程池
package com.zth;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Demo0{
public static void main(String[] args){
//创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
//创建实现了Runnable接口对象,Thread对象当然也实现了Runnable接口
Thread t1 =new MyThread();
Thread t2 =new MyThread();
Thread t3 =new MyThread();
Thread t4 =new MyThread();
Thread t5 =new MyThread();
// 将线程放入线程池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
// 使用延迟执行风格的方法
pool.schedule(t4,10, TimeUnit.MILLISECONDS);
pool.schedule(t5,10,TimeUnit.MILLISECONDS);
// 关闭线程池
pool.shutdown();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在执行。。。");
}
}
执行结果:
pool-1-thread-1正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-1正在执行。。。
4.5 单任务延迟连接池
在4.4代码基础上,做改动
//创建一个单任务执行线程池,它可安排在给定延迟后运行命令或者定期地执行。
ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();
执行结果:
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
4.6 创建线程不同方式的对比
-
1. 采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
-
2. 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。
五. Thread 方法
下表列出了Thread类的一些重要方法:
序号 | 方法描述 |
---|---|
1 | public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。 |
2 | public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。 |
3 | public final void setName(String name) 改变线程名称,使之与参数 name 相同。 |
4 | public final void setPriority(int priority) 更改线程的优先级。 |
5 | public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。 |
6 | public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。 |
7 | public void interrupt() 中断线程。 |
8 | public final boolean isAlive() 测试线程是否处于活动状态。 |
上述方法是被Thread对象调用的。下面的方法是Thread类的静态方法。
序号 | 方法描述 |
---|---|
1 | public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。 |
2 | public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 |
3 | public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。 |
4 | public static Thread currentThread() 返回对当前正在执行的线程对象的引用。 |
5 | public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。 |
六. 阻止线程执行
对于线程的阻止,考虑一下三个方面,不考虑IO阻塞的情况:
- 睡眠;
- 等待;
- 因为需要一个对象的锁定而被阻塞。
1. 睡眠
Thread.sleep(longmillis)和Thread.sleep(long millis, int nanos)(毫秒加纳秒)静态方法强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。当线程睡眠时,它入睡在某个地方,在苏醒之前不会返回到可运行状态。当睡眠时间到期,则返回到可运行状态。
线程睡眠的原因:线程执行太快,或者需要强制进入下一轮,因为Java规范不保证合理的轮换。
睡眠的实现:调用静态方法。
try {
Thread.sleep(123);
} catch (InterruptedException e) {
e.printStackTrace();
}
睡眠的位置:为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程中会睡眠。
注意:
1、线程睡眠是帮助所有线程获得运行机会的最好方法。
2、线程睡眠到期自动苏醒,并返回到可运行状态,不是运行状态。sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就开始执行。
3、sleep()是静态方法,只能控制当前正在运行的线程。
package com.zth;
/**
* @author 时光·漫步
* 一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串
*/
public class Demo0 extends Thread{
@Override
public void run() {
for(int i = 0;i<100;i++){
if(i%10 == 0){
System.out.println("*******"+i + "*******");
}
System.out.print(i);
try {
Thread.sleep(1);
System.out.println(" 线程睡眠1毫秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
new Demo0().start();
}
}
2. 线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
当线程池中线程都具有相同的优先级,调度程序的JVM实现自由选择它喜欢的线程。这时候调度程序的操作有两种可能:一是选择一个线程运行,直到它阻塞或者运行完成为止。二是时间分片,为池内的每个线程提供均等的运行机会。
设置线程的优先级:线程默认的优先级是创建它的执行线程的优先级。可以通过setPriority(int newPriority)更改线程的优先级。
package com.zth;
class A extends Thread{
@Override
public void run() {
while (true){
System.out.println("AAAAAAAA");
}
}
}
class S extends Thread{
@Override
public void run() {
while (true){
System.out.println("111111111");
}
}
}
class N extends Thread{
@Override
public void run() {
while (true){
System.out.println("********");
}
}
}
public class Demo0{
public static void main(String[] args){
Thread t1 = new A();
Thread t2 = new S();
Thread t3 = new N();
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t3.setPriority(Thread.NORM_PRIORITY);
t1.start();
t2.start();
t3.start();
}
}
3. Thread.yield()方法
线程的让步是通过Thread.yield()来实现的。yield()方法的作用是:暂停当前正在执行的线程对象,并执行其他线程。
Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。
yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
更改以上代码:
class A extends Thread{
@Override
public void run() {
while (true){
System.out.println("AAAAAAAA");
Thread.yield();
}
}
}
class N extends Thread{
@Override
public void run() {
while (true){
Thread.yield();
System.out.println("********");
}
}
}
4、join()方法
Thread的非静态方法 join() 等待该线程终止。
join() 方法还有带超时限制的重载版本。例如 t.join(5000);则让线程等待5000毫秒,如果超过这个时间,则停止等待,变为可运行状态。
线程的加入join()对线程栈导致的结果是线程栈发生了变化,当然这些变化都是瞬时的。
package com.zth;
class S implements Runnable{
@Override
public void run() {
for(int i = 1;i<= 10; i++){
Thread.yield();
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
class T implements Runnable{
private Thread s;
public T(Thread s) {
this.s = s;
}
@Override
public void run() {
for(int i =1;i<=10;i++){
if(i == 5){
try {
s.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+ " "+i);
}
}
}
public class Demo0{
public static void main(String[] args){
S s = new S();
Thread t1 = new Thread(s,"线程1");
T t = new T(t1);
Thread t2 = new Thread(t,"线程2");
t1.start();
t2.start();
}
}
5. interrupt()
中断线程
Thread.interrupted()默认情况下返回的是 false
package com.zth;
import java.io.IOException;
class S implements Runnable {
@Override
public void run() {
while (!Thread.interrupted()){
System.out.println(111111);
}
}
}
public class Demo0{
public static void main(String[] args) throws IOException {
S s = new S();
Thread t = new Thread(s);
t.start();
System.in.read();
t.interrupt();
}
}
【小结】:
除了以上方式外,还有下面几种特殊情况可能使线程离开运行状态:
1、线程的run()方法完成。
2、在对象上调用wait()方法(不是在线程上调用)。
3、线程不能在对象上获得锁定,它正试图运行该对象的方法代码。
4、线程调度程序可以决定将当前运行状态移动到可运行状态,以便让另一个线程获得运行机会,而不需要任何理由。