欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

【原创】Java并发编程系列2:线程概念与基础操作

程序员文章站 2022-03-10 11:51:55
【原创】Java并发编程系列2:线程概念与基础操作 伟大的理想只有经过忘我的斗争和牺牲才能胜利实现。 本篇为【Dali王的技术博客】Java并发编程系列第二篇,讲讲有关线程的那些事儿。主要内容是如下这些: 线程概念 线程基础操作 线程概念 进程代表了运行中的程序,一个运行的Java程序就是一个进程。 ......

【原创】java并发编程系列2:线程概念与基础操作

伟大的理想只有经过忘我的斗争和牺牲才能胜利实现。

本篇为【dali王的技术博客】java并发编程系列第二篇,讲讲有关线程的那些事儿。主要内容是如下这些:

  • 线程概念
  • 线程基础操作

线程概念

进程代表了运行中的程序,一个运行的java程序就是一个进程。在java中,当我们启动main函数时就启动了一个jvm的进程,而main函数所在的线程就是这个进程中的一个线程,称为主线程。
进程和线程的关系如下图所示:
【原创】Java并发编程系列2:线程概念与基础操作

由上图可以看出来,一个进程中有多个线程,多个线程共享进程的堆的方法区资源,但是每个线程有自己的程序计数器和栈区域。

线程基础操作

线程创建与运行

java中有三种线程创建方式,分别为:继承thread类并重写run方法,实现runnable接口的run方法,使用futuretask方式。
先看继承thread方式的实现,代码示例如下:

public class threaddemo {
    public static class demothread extends thread {
        @override
        public void run() {
            system.out.println("this is a child thread.");
        }
    }
    public static void main(string[] args) {
        system.out.println("this is main thread.")
        demothread thread = new demothread();
        thread.start();
    }
}

上面代码中demothread类继承了thread类,并重写了run方法。在main函数里创建了一个demothread的实例,然后调用其start方法启动了线程。

tips:调用start方法后线程并没有马上执行,而是处于就绪状态,也就是这个线程已经获取了除cpu资源外的其他资源,等待获取cpu资源后才会真正处于运行状态。
使用继承方式,好处在于通过this就可以获取当前线程,缺点在于java不支持多继承,如果继承了thread类,那么就不能再继承其他类。而且任务与代码耦合严重,一个线程类只能执行一个任务,使用runnable则没有这个限制。
来看实现runnable接口的run方法的方式,代码示例如下:

public class runnabledemo {
    public static class demorunnable implements runnable {
        @override
        public void run() {
            system.out.println("this is a child thread.");
        }
    }
    public static void main(string[] args) {
        system.out.println("this is main thread.");
        demorunnable runnable = new demorunnable();
        new thread(runnable).start();
        new thread(runnable).start();
    }
}

上面代码两个线程共用一个runnable逻辑,如果需要,可以给runnabletask添加参数进行任务区分。在java8中,可以使用lambda表达式对上述代码进行简化:

 public static void main(string[] args) {
    system.out.println("this is main thread.");
    thread t = new thread(() -> system.out.println("this is child thread"));
    t.start();
}

上面两种方式都有一个缺点,就是任务没有返回值,下面看第三种,使用futuretask的方式。代码示例如下:

public class callabledemo implements callable<jsonobject> {
    @override
    public jsonobject call() throws exception {
        return new jsonobject();
    }
    public static void main(string[] args) {
        system.out.println("this is main thread.");
        futuretask<jsonobject> futuretask = new futuretask<>(new callabledemo());   // 1. 可复用的futuretask
        new thread(futuretask).start();
        try {
            jsonobject result = futuretask.get();
            system.out.println(result.tostring());
        } catch (interruptedexception | executionexception e) {
            e.printstacktrace();
        }

        // 2. 一次性的futuretask
        futuretask<jsonobject> innerfuturetask = new futuretask<>(() -> {
            jsonobject jsonobject = new jsonobject();
            jsonobject.addproperty("name", "dali");
            return jsonobject;
        });
        new thread(innerfuturetask).start();

        try {
            jsonobject innerresult = innerfuturetask.get();
            system.out.println(innerresult.tostring());
        } catch (interruptedexception | executionexception e) {
            e.printstacktrace();
        }
    }
}

如上代码,callabledemo实现了callable接口的call方法,在main函数中使用callabledemo的实例创建了一个futuretask,然后使用创建的futuretask对象作为任务创建了一个线程并启动它,最后通过futuretask等待任务执行完毕并返回结果。
同样的,上面的操作过程适合于需要复用的任务,如果对于一次性的任务,大可以通过lambda来简化代码,如注释2处。

等待线程终止

在项目中经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行。thread类中有一个join方法就可以用来处理这种场景。直接上代码示例:

    public static void main(string[] args) throws interruptedexception {
        system.out.println("main thread starts");
        thread t1 = new thread(() -> system.out.println("this is thread 1"));
        thread t2 = new thread(() -> system.out.println("this is thread 2"));
        t1.start();
        t2.start();
        system.out.println("main thread waits child threads to be over");
        t1.join();
        t2.join();
        system.out.println("child threads are over");
    }

上面代码在主线程里启动了两个线程,然后分别调用了它们的join方法,主线程会在调用t1.join()后被阻塞,等待其执行完毕后返回;然后主线程调用t2.join()后再次被阻塞,等待t2执行完毕后返回。上面代码的执行结果如下:

main thread starts
main thread waits child threads to be over
this is thread 1
this is thread 2
child threads are over

需要注意的是,线程1调用线程2的join方法后会被阻塞,当其他线程调用了线程1的interrupt方法中断了线程1时,线程1会抛出一个interruptedexception异常而返回。

让线程睡眠

thread类中有一个static的sleep方法,当一个执行中的线程调用了thread的sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与cpu的调度,但是该线程所拥有的监视器资源,比如锁还是不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后等待cpu的调度执行。

tips:面试当中wait和sleep经常会被用来比较,需要多加体会二者的区别。
调用某个对象的wait()方法,相当于让当前线程交出此对象的monitor,然后进入等待状态,等待后续再次获得此对象的锁;notify()方法能够唤醒一个正在等待该对象的monitor的线程,当有多个线程都在等待该对象的monitor的话,则只能唤醒其中一个线程,具体唤醒哪个线程则不得而知。
调用某个对象的wait()方法和notify()方法,当前线程必须拥有这个对象的monitor,因此调用wait()方法和notify()方法必须在同步块或者同步方法中进行(synchronized块或者synchronized方法)。

看一个线程睡眠的代码示例:

private static final lock lock = new reentrantlock();
public static void main(string[] args) {
    thread t1 = new thread(() -> {
       // 获取独占锁
       lock.lock();
       system.out.println("thread1 get to sleep");
        try {
            thread.sleep(1000);
            system.out.println("thread1 is awake");
        } catch (interruptedexception e) {
            e.printstacktrace();
        } finally {
            lock.unlock();
        }
    });
    thread t2 = new thread(() -> {
        // 获取独占锁
        lock.lock();
        system.out.println("thread2 get to sleep");
        try {
            thread.sleep(1000);
            system.out.println("thread2 is awake");
        } catch (interruptedexception e) {
            e.printstacktrace();
        } finally {
            lock.unlock();
        }
    });

    t1.start();
    t2.start();
}

上面的代码创建了一个独占锁,然后创建了两个线程,每个线程在内部先获取锁,然后睡眠,睡眠结束后会释放锁。执行结果如下:

thread1 get to sleep
thread1 is awake
thread2 get to sleep
thread2 is awake

从执行结果来看,线程1先获取锁,然后睡眠,再被唤醒,之后才轮到线程2获取到锁,也即在线程1sleep期间,线程1并没有释放锁。
需要注意的是,如果子线程在睡眠期间,主线程中断了它,子线程就会在调用sleep方法处抛出了interruptedexception异常。

线程让出cpu

thread类中有一个static的yield方法,当一个线程调用yield方法时,实际就是暗示线程调度器当前线程请求让出自己的cpu使用,如果该线程还有没用完的时间片也会放弃,这意味着线程调度器可以进行下一轮的线程调度了。
当一个线程调用yield方法时,当前线程会让出cpu使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出cpu的那个线程来获取cpu执行权。
请看代码示例:

public static void main(string[] args) {
    thread t1 = new thread(() -> {
        for (int i = 0; i < 10; i++) {
            if (i == 8) {
                system.out.println("current thread: " + thread.currentthread() + " yield cpu");
            }
            thread.yield(); // 2
        }
        system.out.println("current thread: " + thread.currentthread() + " is over");
    });

    thread t2 = new thread(() -> {
        for (int i = 0; i < 10; i++) {
            if (i == 8) {
                system.out.println("current thread: " + thread.currentthread() + " yield cpu");
            }
            thread.yield(); // 1
        }
        system.out.println("current thread: " + thread.currentthread() + " is over");
    });
    t1.start();
    t2.start();
}

在如上的代码中,两个线程的功能一样,运行多次,同一线程的两行输出是顺序的,但是整体顺序是不确定的,取决于线程调度器的调度情况。
当把上面代码中1和2处代码注释掉,会发现结果只有一个,如下:

current thread: thread[thread-1,5,main] yield cpu
current thread: thread[thread-0,5,main] yield cpu
current thread: thread[thread-1,5,main] is over
current thread: thread[thread-0,5,main] is over

从结果可知,thread.yiled方法生效使得两个线程分别在执行过程中放弃cpu,然后在调度另一个线程,这里的两个线程有点互相谦让的感觉,最终是由于只有两个线程,最终还是执行完了两个任务。

tips:sleep和yield的区别:
当线程调用sleep方法时,调用线程会阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是出于就绪状态,线程调度器下一次调度时就可能调度到当前线程执行。

线程中断

java中的线程中断是一种线程间的协作模式。每个线程对象里都有一个boolean类型的标识(通过isinterrupted()方法返回),代表着是否有中断请求(interrupt()方法)。例如,当线程t1想中断线程t2,只需要在线程t1中将线程t2对象的中断标识置为true,然后线程2可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。
在上面章节中也讲到了线程中断的一些内容,此处就不再用代码来展开了。

java并发编程大纲

继续附上java编程的系统学习大纲以供参考:
java并发编程.png【原创】Java并发编程系列2:线程概念与基础操作

【参考资料】

  1. 《java并发编程之美》

本文由微型公众号【dali王的技术博客】原创,扫码关注获取更多原创技术文章。
【原创】Java并发编程系列2:线程概念与基础操作