Java并发编程知识概览(一)

1,314 阅读11分钟

原文博客地址:pjmike的博客

进程与线程

下面比较简单介绍下进程与线程的概念:

进程

关于进程的定义,其实有很多:

  • 一个正在执行的程序
  • 计算机中正在运行的程序的一个实例
  • 可以分配给处理器并由处理器执行的一个实例。

个人觉得比较好的定义是:

进程是具有一定独立功能的程序关于某个数据集合上的一次运行过程

说白了,进程就是CPU执行的一次任务,在单个CPU中一次只能运行一次任务,即CPU总是运行一个进程,其他进程处于非运行状态。但是现在的操作系统都是多核CPU,所以可以同时运行多个进程,执行多个任务。

线程

线程实际上是一个进程中的"子任务",在一个进程中可以创建多个线程,打个比方,打开QQ就是执行了一个进程,而在QQ里与他人聊天,同时下载文件等操作就是线程。

为什么要使用多线程

多线程是指操作系统在单个进程内支持多个并发执行路径的能力。每个进程中只有一个线程在执行的传统方法称为单线程方法。但是单线程存在很多弊端,它并不能充分利用CPU资源,而且单线程在应对复杂业务时响应时间也是较差的,所以我们需要使用多线程来帮助我们。使用多线程有如下好处(原因):

  • 更多的处理器核心:多线程可以使用多个处理器资源,提升程序的执行效率
  • 更快的响应时间 :在复杂业务中,使用多线程可以缩短响应时间,提升用户体验
  • 更好的编程模型 :在Java开发中,多线程编程提供了良好,考究并且一致的编程模型。

那我们为什么去使用 多线程而不是使用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远小于 进程

线程的状态

首先有进程,其次才是线程,其实线程的生命周期及各种状态转换和进程类似,下面看一张 进程的状态转换图(图片摘自网络):

progress

进程一般有三种基本的状态:

  • 运行态:程序正在运行
  • 就绪态:进程做好了准备,有机会就开始执行
  • 阻塞态:进程等待某一事件而停止运行

进程被创建后,加入就绪队列等待被调度执行,CPU调度器根据一定的调度算法调度就绪队列中的进程在处理机上执行。

上面简述了进程的基本状态及变化过程,线程也是类似的:

java_thread

该图表示一个线程的生命周期状态流转图,很清楚的描绘了一个线程从创建到终止的一个过程。线程的所有状态在 Thread类中的State枚举定义,如下:

public enum State {

    NEW,
    
    RUNNABLE,

    BLOCKED,

    WAITING,

    TIMED_WAITING,

    TERMINATED;
}
  • New: 表示刚刚创建的线程,这种线程还没执行
  • Running: 表示线程已经调用 start()方法,线程只在执行
  • Blocked: 表示线程阻塞,等待获取锁,如碰到 synchronized同步块,一旦获取锁就进入 Running 状态继续执行
  • Waiting: 表示线程进入一个无时间限制的等待,等待一些特殊的事件来唤醒,比如通过 wait() 方法等待的线程在等待notify()方法,而通过 join() 方法等待的线程则会等待目标线程的终止。一旦等到了期望了事件,线程会再次执行,进入Running状态
  • Timed Waiting: 表示进行一个有时限的等待,如sleep(3000)等待(睡眠)3秒后,重新进入Running状态继续执行
  • Terminated: 表示线程执行结束,进入终止状态

注意:从New 状态出发后,线程不能再回到 New状态,同理,处于 Terminated 的线程也不能回到 Running状态

下面分别对线程的各种状态进行相关说明

线程的相关操作

新建线程

新建线程有两种方式:

  • 继承Thread类
  • 实现Runnable接口

Thread类是在java.lang包中定义的,本来我们可以直接继承Thread,重载 run() 方法来自定义线程,但是Java 是单继承的,如果一个类已经继承了其他类,就无法再继承Thread,所以我们这时就可以实现 Runnable接口来实现

// 1. 继承Thread类
public class Thread1 extends Thread{
    @Override
    public void run() {
        System.out.println("hello world");
    }

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        //调用start()运行线程,执行内部的run()方法
        thread1.start();
    }
}

//2. 实现Runnable接口
public class Thread2 implements Runnable{

    @Override
    public void run() {
        System.out.println("hello world");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new Thread2());
        thread.start();
    }
}

Thread有一个重要的构造方法:

public Thread(Runnable target)

它传入一个Runnable 接口的实例,在start()方法调用时,新的线程就会执行 Runnable.run()方法,实际上,默认的Thread.run()就是这么做的:

public void run() {
    if (target != null) {
        target.run();
    }
}

默认的Thread.run()就是直接调用 内部的 Runnable接口

终止线程

一般来说,线程在执行完毕后就会结束,无须手动关闭,但是还是有些后台常驻线程可能不会自动关闭。

Thread提供了一个 stop()方法,该方法可以立即将一个线程终止,但是目前stop()已经被废弃,不推荐使用,原因是 stop()方法太过于简单粗暴,强行把执行到一半的线程终止,可能会引起一些数据不一致的问题。

线程中断

线程中断是一种重要的线程协作机制,它并不会使线程立即退出,而是给线程发送一个通知,告知目标线程,有人希望你退出。至于目标线程接到通知后如何处理,则完全由目标线程自行决定。

与线程中断有关的,有三个方法:

public void Thread.interrupt()  //中断线程
public boolean Thread.isInterrupted()  //判断是否被中断
public static boolean Thread.interrupted()  //判断是否被中断,并清除当前中断状态
  • Thread.interrupt()方法是一个实例方法,通知目标线程中断,设置中断标志位,该标志位表明当前线程已经被中断了
  • Thread.isInterrupted()方法也是实例方法,判断当前线程是否有被中断(通过检查中断标志位)
  • 静态方法Thread.interrupted也是用来判断当前线程的中断状态的,但同时会清楚当前线程的中断标志位状态

举例说明:

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                //通过中断标志位判断是否被中断
                if (Thread.currentThread().isInterrupted()) {
                    //中断逻辑处理
                    System.out.println("Interrupted!");
                    // break退出循环
                    break;
                }
            }
        });
        //开启线程
        thread.start();
        Thread.sleep(2000);
        //设置中断标志位
        thread.interrupt();
    }

Thread有个 sleep方法,它会让当前线程休眠若干时间,它会抛出一个 InterruptedException中断异常。InterruptedException不是运行时异常,也就是说程序必须捕获并且处理它,当线程在 sleep()休眠时,如果被中断,这个异常就产生了。

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread() {
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    System.out.println("Interrupted When Sleep");
                    //中断异常会清楚中断标记,重新设置中断状态
                    Thread.currentThread().interrupt();
                }
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("Interrupted!");
                    break;
                }
            }
        }
    };
    thread.start();
    Thread.sleep(2000);
    thread.interrupt();
}

//output:

Interrupted When Sleep
Interrupted!

Thread.sleep() 方法由于中断而抛出异常,此时,它会清楚中断标记,如果不加处理,那么在下一次循环开始时,就无法捕获这个中断,故在异常处理中,再次设置中断标志位

等待(wait) 和通知(notify)

为了支持多线程之间的协作,JDK 提供了两个非常重要的接口线程等待 wait() 方法和通知 notify() 方法,这两个方法定义在 Object类中。

public final void wait(long timeout) throws InterruptedException;

public final void notify();

当在一个对象实例上调用了object.wait() 方法,那么它就会进入object对象的等待队列进行等待,这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某一个对象。当object.notify()被调用时,它就会从 这个等待队列中,随机选择一个线程,并将其唤醒

除了notify()方法外,Object 对象还有一个类似的 notifyAll()方法 ,它和notify()的功能一样,不同的是,它会唤醒这个等待队列中所有等待的线程

下面看一个代码示例:

public class Example {
    public static void main(String[] args) {
        final Object object = new Object();
        new Thread(() -> {
            System.out.println("thread A is waiting to get lock");
            synchronized (object) {
                System.out.println("thread A already get lock");
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("thread A do wait method");
                    object.wait();
                    System.out.println("thread A wait end");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(() -> {
            System.out.println("thread B is waiting to get lock");
            synchronized (object) {
                System.out.println("thread B already get lock");
                try {
                    TimeUnit.SECONDS.sleep(5);
                    System.out.println("thread B do wait method");
                    object.notify();
                    System.out.println("thread B do notify method");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

执行结果:

thread A is waiting to get lock
thread A already get lock
thread B is waiting to get lock
thread A do wait method
thread B already get lock
thread B do wait method
thread B do notify method
thread A wait end

上述开启了两个线程A和B,A执行 wait()方法前,A先申请 object 的对象锁,在执行 object.wait()时,持有object的锁,执行后,A会进入等待,并释放 object的锁。

B在执行notify()之前也会先获得 object的对象锁,执行notify()之后,释放object的锁,然后A重新获得锁后继续执行

等待线程结束(join) 和 谦让(join)

join方法使当前线程等待调用 join方法的线程结束后才能继续往下执行,下面是代码示例:

public class ThreadExample {
    public volatile static int i = 0;
    public static class Thread_ extends Thread{
        @Override
        public void run() {
            for (i = 0; i < 1000000; i++) {

            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread_ A = new Thread_();
        A.start();
        //让当前的主线程等待thread_线程执行完毕后才往下执行
        A.join();
        System.out.println(i);
    }
}

join()的本质实际上是让调用线程wait()在当前线程对象实例上,当线程执行完毕后,被等待线程退出前调用 notifyAll()通知所有的等待线程继续执行

以上面的例子说明,线程A调用join()使main线程 wait()在A对象实例上,等线程A执行完毕,线程A调用notifyAll(),main线程等到通知继续往下执行。

而Thread.yield()方法的定义如下:

public static native void yield();

它是静态方法,一旦执行,它会使当前调用该方法的线程让出CPU,但是让出CPU不代表 当前线程不执行,当前线程让出CPU后,还会进行CPU资源的争夺,但是是否能够被分配到,就不一定了。

Thread.yield()的调用就好像在说:

我已经完成了一些最重要的工作了,我应该是可以休息一下了,可以给其他线程一些工作机会了

守护线程(Daemon)

守护线程是一种特殊的下线程,它是系统的守护者,在后台默默地完成一些系统性的服务,比如垃圾回收线程,JIT线程就可以理解为守护线程,与之对应的是用户线程,用户线程可以理解为系统的工作线程,它会完成这个程序应该要完成的业务操作。当一个Java应用内,只有守护线程时,Java虚拟机就会自然退出。

代码示例如下:

public class DaemonDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("I am alive");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        //将线程t设置为守护线程
        t.setDaemon(true);
        t.start();
        //主线程休眠10s
        Thread.sleep(10000);
    }
}

输出结果:

I am alive
I am alive
I am alive
I am alive
I am alive
I am alive
I am alive
I am alive
I am alive
I am alive

上面的例子,将t设置为守护线程,系统中只有主线程 main 为用户线程,在 main休眠10s后,守护线程退出,整个线程也退出。

线程优先级

Java 中的线程可以有自己的优先级,优先级高的线程在竞争资源时会更有优势,更可能抢占资源。线程的优先级调度和操作系统密切相关,当然高优先级可能也会有抢占失败的时候,但是大部分情况下,高优先级比低优先级对于抢占资源来说更有优势。

代码示例如下:

public class PriorityDemo {
    private static int count = 0;
    public static void main(String[] args) {
        Thread A = new Thread() {
            @Override
            public void run() {
                while (true) {
                    synchronized (PriorityDemo.class) {
                        count++;
                        if (count > 100000) {
                            System.out.println("HighPriority is complete");
                            break;
                        }
                    }
                }
            }
        };
        Thread B = new Thread() {
            @Override
            public void run() {
                while (true) {
                    synchronized (PriorityDemo.class) {
                        count++;
                        if (count > 100000) {
                            System.out.println("LowPriority is complete");
                            break;
                        }
                    }
                }
            }
        };
        //设置线程A为高优先级
        A.setPriority(Thread.MAX_PRIORITY);
        //设置线程B为低优先级
        B.setPriority(Thread.MIN_PRIORITY);
        //启动线程B
        B.start();
        //启动线程A
        A.start();
    }
}

运行结果:

HighPriority is complete
LowPriority is complete

由上可以初步得出,高优先级的线程在大部分情况下会首先完成任务。

总结

上面简单总结了一部分Java并发编程所涉及到知识点,算是入门Java并发的一个开端。

参考资料 & 鸣谢