月泉的博客

并发编程基础知识(上)

月泉 并发编程

并发编程基础知识(上)

线程是什么?

进程是系统调度及分配资源的基本单位,在现今每个进程中都至少拥有一个线程在执行,而线程就是CPU调度执行的基本单位。

为什么需要线程

从操作系统的角度出发,从最开始的单进程阻塞执行再到实现了多进程,使得整个操作系统可以运行多个应用程序,如果没有线程,那么系统本身是可以运行多个应用程序,但这些应用程序其本身是阻塞的,比如我点击一个查询按钮,需要耗费30秒, 那么其应用程序就得无响应30秒,为了使进程本身不是阻塞执行且更大化的利用进程的资源和CPU及用户体验从而在进程中在建立了线程的概念,每个线程都拥有一个栈和程序计数器(此句话大多通用,这里限定为Java)

理解线程上下文切换

现在如今主流系统对CPU的调度执行大多采用时间片轮转,即每个进程中的线程的执行时间都以时间片为单位,消耗完时间片的时间就挂起,那么再次轮转到该线程执行时?如何知道我上次执行到那条指令?以前线程中的值等,这就需要一个快照,还记得上面提的每个线程都有一个栈和程序计数器吗?程序计数器存储了当前线程所要跳转的指令,栈中存储了当前线程的值

死锁

死锁是什么?

死锁的概念就是说双方手中都持有对方所需要的资源,但其自身并没有将其释放掉,然后去请求所需要的资源,造成了互相等待。

形成死锁有4个必要条件,分别为:

  • 互斥条件
  • 请求并持有条件
  • 不可剥夺条件
  • 环路等待条件

互斥条件

指资源同时只能被一个线程使用,其它线程如果要使用必须得等待资源的锁被释放掉才可以使用,否则只能等待

请求并持有条件

指手中持有互斥的资源没有释放掉,然后又去请求新的资源

不可剥夺条件

指这个资源被一个线程占用时,就只能等到它自己使用完然后释放掉

环路等待条件

要使用的资源被其它线程所占用了,占用方所需要的资源同时也是被请求方或者它方占用了,形成了一个环路逻辑。

如何避免死锁?

破坏4个条件中的至少一个,目前可行的只有:请求并持有条件、环路等待条件,破坏其构成就行

Java中使用线程

有三种方式来创建使用线程

  • 继承Thread类
  • 实现Runnable接口
  • 使用FutureTask

继承Thread

class ThreadDemo extends Thread{
    @Override
    public void run() {
        System.out.println("I am a thread");
    }
}

class Test {
    public static void main(String[] args){
        new ThreadDemo().start();
    }
}

实现Runnable接口

class RunnableDemo implements Runnable{
    public void run() {
    	System.out.println("I am a child thread");
    }
}

class Test{
    public static void main(String[] args){
 		new Thread(new RunnableDemo()).start();
    }
}

使用FutureTask

class CallableDemo implements Callable<String>{
    public String call(){
        return "call";
    }
}

public static void main(String[] args){
    FutureTask<CallableDemo> task = new FutureTask(new CallableDemo());
    new Thread(task).start();
    try{
    	String result = task.get();
    }catch(Exception e){
        e.printStackTrace();
    } 
}

其实使用FutureTask就类似于使用RunnableFutureTask类本身就是实现了RunnableFuture接口而RunnableFuture接口则是继承了RunnableFuture接口,使其本身拥有Runnable的特性和Future接口的特性。

三种创建方式的优缺点

使用继承

优点

简单易用,使用this关键字就能引用到当前线程的信息,传递参数方便,可通过构造函数或者set*来传递参数

缺点

Java本身是单继承的,如果使用了Thread用作父类,那么就不能继承其它父类了,线程和任务没有进行隔离。

实现Runnable接口

优点

还可以继承父类进行扩展,任务和线程对象进行了隔离

缺点

因为隔离了线程对象,无法直接操作线程对象,必须要取到当前线程才可以操作线程对象

使用FutureTask

优点

拥有实现Runnable接口的优点,同时具有返回值的特性

缺点

同实现Runnable接口的缺点一致

三种方式的共同性

  • 都是为了创建一个线程任务使用,而继承Thread更倾向于创建一个线程的整体,而实现Runable接口更注重线程对象和线程执行的任务隔离性,使用FutureTask是能够希望在线程任务执行完后能够获得一个值
  • 使用继承和实现Runnable接口执行完任务都没有返回值,只有FutureTask才有
  • 实现Runnable接口和使用FutureTask都需要获取当前线程来操作线程对象,而Thread不用

join等待

Thread中有一个实例方法叫做join使用join会阻塞当前线程,直到被join的线程执行完毕才会继续往下执行

Thread threadA = new Thread(() -> {
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
threadA.start();
try {
    //睡一秒,保证threadA线程已经启动
    Thread.sleep(1000);
    threadA.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("线程A执行完毕");

join还有2个重载函数,分别为join(long millis)join(long millis, int nanos),看join()代码可以看见其本身是调用的join(0)

public final void join() throws InterruptedException {
	join(0);
}

使用join(long millis)实际上就是限制等待线程的超时时间,如果在限定的时间内还没有处理完线程就会放弃等待往下执行,如果传入的是0,显而易见就是没有超时时间一定会等待线程执行完毕,假如传入的是一个负数则会抛出异常IllegalArgumentException:timeout value is negative

`join(long millis, int nanos) 这个就很故名思议了吧nanos表示纳秒

当然join的时候被join的线程对象是有可能发生中断操作的,从而抛出中断异常InterruptedException

wait/notify/notifyAll 等待与唤醒

Object是所有类的根父类,在Object类上有3个native方法

public final native void notify();
public final native void notifyAll();
public final void wait() throws InterruptedException {
    wait(0);
}
public final native void wait(long timeout) throws InterruptedException;

首先来说一下wait是干什么用的,wait方法调用后,会使得当前线程挂起和释放当前监视器锁,直到超时(如果设为0为永不超时)或者被唤醒,当前线程才会又恢复到就绪状态供调度执行,当然在使用wait方法时必须获取其监视器锁,否则会抛出IllegalMonitorStateException

new Thread(() -> {
            try {
                synchronized (resource){
                    resource.wait();
                    System.out.println("wait1 end");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

new Thread(() -> {
    try {
        synchronized (resource){
            resource.wait();
            System.out.println("wait2 end");
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}).start();

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
new Thread(() -> {
    synchronized (resource){
        resource.notify();
    }
}).start();

使用notify将其唤醒,注意的是notify只唤醒挂起线程队列中的其中一个,而notifyAll则是唤起挂起线程队列中的所有线程,当然只是唤醒,并不能保证其唤起后就马上执行。

当然wait时也有可能发生InterruptedException

yield方法

yield方法是Thread类中的一个静态的native方法,它的意义是说本轮剩下的时间片我不想使用了放弃掉,让其它的线程尽早的轮转,当然这个只是个提示

sleep方法

挂起睡眠指定描述然后自动唤醒,当然sleep的过程中也会发生InterruptedException

interrupt/interrupted/isInterrupted

Thread类中有3个实例函数,分别为interruptinterruptedisInterrupted,其目的是以一种优雅的方式来中止当前线程,其中止逻辑由实现者自己实现,但是同时会触发其它可能的地方会抛出InterruputedException

thread.interrupt();	

会给当前线程打上中断标志,可通过isInterruptedinterrupted来获取该状态,这2种方式的不同点在于isInterrupted可以在线程中获取中断标志状态或者使用线程对象来获取中断标志而interrupted必须在线程中使用,因为它会直接获得当前调用线程来获取中断标志,同时将中断标志复位成false

月泉
伪文艺中二青年,热爱技术,热爱生活。