今天来分析下Java中的线程。

大纲如下:

  • 1. 线程的概念。
  • 2. Java创建线程的方式。
  • 3. 线程的常用方法。
  • 4. 线程的状态切换。
  • 5. 如何终止一个线程?
  • 6. 线程的优先级。
  • 7. 线程间的协作。

 

1. 线程的概念

进程是操作系统中进行保护和资源分配的基本单位,操作系统分配资源以进程为基本单位。而线程是进程的组成部分,线程共享着所属进程的内存地址,所以线程间互相通信就简单的多,通过共享进程级全局变量即可实现。一个进程最少有一个线程,线程是处理器调度的基本单位。在Java中,单核cpu场景下,多个线程不是同时执行的,而是通过cpu切换着执行。

 

2. Java创建线程的方式

共有三种方式,继承Thread类,实现Runnable接口,通过Future和Callable。

 

package io.kzw.advance.csdn_blog;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThreadTest {

    public static void main(String[] args) {
        // 创建方式 - 继承Thread类
        MyThread thread0 = new MyThread();
        thread0.start();

        // 创建方式 - 实现Runnable接口
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " start");
            }
        };
        Thread thread1 = new Thread(runnable, "Thread1");
        thread1.start();

        // 创建方式 - 通过FutureTask和Callable
        FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println(Thread.currentThread().getName() + " start");
                return 1;
            }
        });
        Thread thread2 = new Thread(futureTask, "Thread2");
        thread2.start();
        try {
            // 阻塞地去拿结果,也可以传入超时时间
            System.out.println("线程2的返回值:" + futureTask.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static class MyThread extends Thread {

        // 可以给线程设置name
        public MyThread() {
            super("MyThread");
        }

        @Override
        public void run() {
            super.run();
            System.out.println(Thread.currentThread().getName() + " start");
        }
    }
}

执行输出:

MyThread start
Thread1 start
Thread2 start
线程2的返回值:1

 

三种方式的对比

(1) 使用Runnable和Future的方式。

优势:

由于Java只支持单继承,使用Runnable和Future的方式,可以让线程类继承其他类。在这种方式下,多个线程可以共享一个target对象(Runnable or FutureTask),非常适合多个相同线程来处理同一份资源的情况。达到比较好的解耦效果,将cpu,代码和数据分开,比较符合面向对象的设计。

劣势:

如果要使用当前线程,只能通过Thread.currentThread()的方式。

 

(2) 使用继承Thread的方式。

优势:

编写简单,如果要使用当前线程,直接使用this即可获取。

劣势:

不能再继承其他类。

 

(3) 使用Runnable和Future方式的对比。

* Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。

* Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。

* call方法可以抛出异常,run方法不可以。

* 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

 

3. 线程的常用方法

 

start

调用该方法,使线程从新建状态进入就绪状态,到线程队列里排队,等待cpu分配资源执行

run

定义线程对象被调度之后执行的操作,是系统调用而不是用户调用

sleep

优先级高的线程可以在它的run()中调用sleep方法,使自己放弃cpu资源,睡眠一段时间,但是不会释放持有的锁

isAlive

线程处于新建状态时,调用该方法会返回false,而在线程还没执行完任务,即没有进入死亡状态之前,线程调用isAlive()方法返回true

currentThread

Thread类中的类方法,可以用类名调用,该方法返回当前正在使用CPU资源的线程

interrupt

当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一个标志,表示它已经被中断,并立即返回。这里需要注意的是,如果只是单纯的调用interrupt()方法,线程并没有实际被中断,会继续往下执行,通常配合isInterrupted()使用,来达到中断任务执行的效果。

join

等待某个线程执行完毕,再执行下面的逻辑代码

yield

暂停正在执行的线程对象,把正在占用的cpu资源让出来,自身重新进入就绪状态,然后各就绪线程开始抢占cpu执行

setPriority

更改线程的优先级。系统提供了三个优先级常量:NORM_PRIORITY = 5; MAX_PRIORITY = 10; MIN_PRIORITY = 1;

setDaemon

java中线程分为两种类型:用户线程和守护线程。通过Thread.setDaemon(false)设置为用户线程;通过Thread.setDaemon(true)设置为守护线程。如果不设置次属性,默认为用户线程。

1. 主线程结束后用户线程还会继续运行,JVM存活;主线程结束后守护线程和JVM的状态由下面第2条确定。

2. 如果没有用户线程,都是守护线程,那么JVM结束(随之而来的是所有的一切烟消云散,包括所有的守护线程)

wait

属于Object的方法,但是放在这边,因为经常在线程中使用。必须在synchronized方法或者代码块中使用,当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁

notify

notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现

notifyAll

notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行

 

 

4. 线程的状态切换

线程的状态分为以下5种:

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

(2) 就绪状态(Runnable): 线程创建后,其他线程调用该线程对象的start()方法。该状态的线程将会放入到可运行线程池中,等待获取cpu的使用权。

(3) 运行状态(Running): 就绪状态的线程获取到cpu,执行程序代码。

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

     4.1 等待阻塞:运行的线程执行wait()方法,JVM会将该线程放入等待池中。

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

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

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

画个草图理解下:

Java线程进阶 java线程知识_Java线程进阶

 

5. 如何终止一个线程?

停止一个线程意味着在任务处理完任务之前停掉正在做的操作,也就是放弃当前的操作。停止一个线程可以用Thread.stop()方法,但最好不要用它。虽然它确实可以停止一个正在运行的线程,但是这个方法是不安全的,而且是已被废弃的方法。
在java中有以下3种方法可以终止正在运行的线程:

(1) 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。

(2) 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。

     * stop方法是过时的。

     * stop方法会导致代码逻辑不完整,stop方法是一种"恶意" 的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这是非常危险的。比如直接调用stop函数,因为不能在run()里面收到停止回调,会导致IO资源没有得到释放,代码逻辑出现问题(如线程终止则把下载标记设置为false)等。

    * 在多线程同步里,stop方法却会带来更大的麻烦,它会丢弃所有的锁,导致原子逻辑受损。

(3) 使用interrupt方法中断线程。

// 使用标记的方式中断
 private static boolean isRunning = true;

    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (isRunning) {
                    try {
                        Thread.sleep(100L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("running...");
                }
            }
        });
        thread.start();
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            isRunning = false;
        }
    }

输出:

running...
running...
running...
running...
running...
running...
running...
running...
running...
running...

 

public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                // 使用interrupt()和isInterrupted()的方式
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("running...");
                }
            }
        });
        thread.start();
        try {
            Thread.sleep(100L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            thread.interrupt();
        }
    }

running...
running...
running...
running...
running...
...............
running...

Process finished with exit code 0

最后再来解释下这三个方法:

public static boolean interrupted()

public boolean isInterrupted() 

public void interrupt() 

(1) public static boolean interrupted():

测试当前线程是否已经中断。返回的是上一次的中断状态,并且会清除该状态,所以连续调用两次,第一次返回true,第二次返回false。

(2) public boolean isInterrupted() :

测试线程当前是否已经中断,但是不能清除状态标识。

(3) public void interrupt() :

只是改变中断状态,不会中断一个正在运行的线程,需要用户自己去监视线程的状态并做处理。

 

 

6. 线程的优先级

线程的优先级用数字来表示,默认范围是1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORTY。

一个线程的默认优先级是5,即Thread.NORM_PRIORTY

对优先级操作的方法:

int getPriority(): 得到线程的优先级。

void setPriority(int newPriority): 当线程被创建后,可通过此方法改变线程的优先级。

必须指出的是:线程的优先级无法保障线程的执行次序,只不过,优先级高的线程获取CPU资源的概率较大。

private static final class MyThread extends Thread {

        private final String message;

        MyThread(String message) {
            this.message = message;
        }

        public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println(message + " " + getPriority());
            }
        }
    }

    public static void main(String args[]) {
        Thread t1 = new MyThread("T1");
        t1.setPriority(Thread.MIN_PRIORITY);
        t1.start();

        Thread t2 = new MyThread("T2");
        t2.start();

        Thread t3 = new MyThread("T3");
        t3.setPriority(Thread.MAX_PRIORITY);
        t3.start();
    }

执行输出:

T2 5
T3 10
T3 10
T3 10
T1 1
T1 1
T1 1
T2 5
T2 5

 

 

7. 线程间的协作

线程间协作的两种方式:wait、notify、notifyAll和Condition。

最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。

(1) wait()、notify()和notifyAll()

wait()、notify()和notifyAll()是Object类中的方法,都是native修饰的。

public static void main(String[] args) {
        Object lock = new Object();
        Thread1 thread1 = new Thread1(lock);
        Thread2 thread2 = new Thread2(lock);

        thread1.start();
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        thread2.start();
    }

    private static class Thread1 extends Thread {

        private final Object lock;

        Thread1(Object lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println("线程1获取到了锁");
                try {
                    System.out.println("线程1 wait");
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程1" + "获取到了锁");
            }
        }
    }

    static class Thread2 extends Thread {

        private final Object lock;

        Thread2(Object lock) {
            this.lock = lock;
        }

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println("线程2获取到了锁");
                lock.notify();
                // 这边调用notify后,必须等线程2释放锁,线程1才能重新获取到锁
                System.out.println("调用了object.notify()");
            }
            System.out.println("线程2释放了锁");
        }
    }

执行输出:

线程1获取到了锁
线程1 wait
线程2获取到了锁
调用了object.notify()
线程2释放了锁
线程1获取到了锁

 

(2) Condition

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。

Condition是个接口,基本的方法就是await()和signal()方法。

Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 。

调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用。

Conditon中的await()对应Object的wait()。

Condition中的signal()对应Object的notify()。

Condition中的signalAll()对应Object的notifyAll()。

private int queueSize = 10;
    private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
    private Lock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();

    public static void main(String[] args) {
        ThreadTest test = new ThreadTest();
        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();

        producer.start();
        consumer.start();
    }

    private class Consumer extends Thread {

        @Override
        public void run() {
            consume();
        }

        private void consume() {
            while (true) {
                lock.lock();
                try {
                    while (queue.size() == 0) {
                        try {
                            System.out.println("队列空,等待数据");
                            notEmpty.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 每次移走队首元素
                    queue.poll();                
                    notFull.signal();
                    System.out.println("从队列取走一个元素,队列剩余" + queue.size() + "个元素");
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    private class Producer extends Thread {

        @Override
        public void run() {
            produce();
        }

        private void produce() {
            while (true) {
                lock.lock();
                try {
                    while (queue.size() == queueSize) {
                        try {
                            System.out.println("队列满,等待有空余空间");
                            notFull.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 每次插入一个元素
                    queue.offer(1);        
                    notEmpty.signal();
                    System.out.println("向队列取中插入一个元素,队列剩余空间:" + (queueSize - queue.size()));
                } finally {
                    lock.unlock();
                }
            }
        }
    }

 

如果对于锁比较陌生的同学,可以先跳过线程协作的内容。我后面会单开一篇文章介绍线程间的锁问题,包括synchronized,volatile,lock,读写锁,公平锁与非公平锁,ThreadLocal,死锁等。

今天平安夜,祝大家平安夜快乐!