Java并发编程线程基础——更新中

一、线程创建的三种方式

Java中有三中线程创建方式,分别是实现Runnable接口的run方法,继承Thread类并重写run的方法,使用FutureTask方式。

继承Thread类并重写run的方法

当创建完thread对象后该线程并没有被启动执行,知道调用了start方法后才真正启动了线程。

其实调用start方法后线程并没有马上执行,而是处于就绪状态,这个就绪状态是指该线程已经获取了除CPU之外的其他资源,等待获取CPU资源后才会真正处于运行状态。一旦run方法执行完毕,该线程就处于终止状态。

优点:

  1. 在run方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法。
  2. 方便传参,可以在子类里面添加成员变量,通过set方法设置参数或通过构造函数进行传递。

缺点:

  1. Java不支持多继承,如果继承了Thread类,那么就不能再基础其他类。
  2. 任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而Runable则没有这个限制。

代码实现:

public class ThreadTest {

    //继承Thread类并重写run方法
    public static class MyThread extends Thread{

        @Override
        public void run(){
            System.out.println("I am a child thread");
        }

    }

    public static void main(String[] args){
        //创建线程
        MyThread thread = new MyThread();

        //启动线程
        thread.start();
    }
}
复制代码

实现Runnable接口的run方法

缺点:
如果要传递参数,只能使用主线程里面被声明为final的变量。

代码实现

public class ThreadTest {

    public static class RunableTask implements Runnable{

        @Override
        public void run(){
            System.out.println("I am a child thread");
        }
    }

    //中断异常
    public static void main(String[] args) throws InterruptedException{
        RunableTask task = new RunableTask();
        new Thread(task).start();
        new Thread(task).start();
    }
}
复制代码

使用FutureTask方式

上面介绍的两种方式都有一个缺点,就是任务没有返回值。

代码实现

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

public class ThreadTest {

    //创建任务类,类似Runable
    public static class CallerTask implements Callable<String> {

        @Override
        public String call() throws Exception{
            return "hello";
        }
    }

    public static void main(String[] args) throws InterruptedException{
        //创建异步任务
        FutureTask<String> futureTask = new FutureTask<>(new CallerTask());

        //启动线程
        new Thread(futureTask).start();
        try{
            //等待任务执行完毕,并返回结果
            String result = futureTask.get();
            System.out.println(result);
        }catch(ExecutionException e){
            e.printStackTrace();
        }
    }
}

复制代码

二、线程通知和等待

Java中的Object类是所有类的父类,鉴于继承机制,Java把所有类都需要的方法放到了Object类里面,其中就包含了本节要讲的通知和等待系列函数———wait()、notify()、notifyAll()

1. wait函数

当一个线程调用一个共享变量的wait()方法时,该掉员工线程会被阻塞挂起,直到发生下面几件事之一才返回:
(1)其他线程调用了该共享对象的notify()或者notifyAll()方法;

(2)其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

注意:如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法是调用线程会抛出IllegalMonitorStateException异常。

线程获取共享变量的监视器锁的方法:

(1)执行synchronized同步代码块是,使用该共享变量作为参数。

synchronized (共享变量){
    //doSomething
}
复制代码

(2)调用该共享变量的方法,并且该方法使用了synchronized修饰

synchronized void add(int a, int b){
    //doSomething
}
复制代码
虚假唤醒

如果一个线程没有被其他线程调用notify()、notifyAll()方法进行通知,或者被中断,或者等待超时,就从挂起状态变为可以运行状态(也就是被唤醒)。

虚假唤醒在应用实践中很少发生,但要防患于未然,做法就是不停的去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait()方法进行防范。退出循环的条件是满足唤醒该进程的条件。

synchronized (obj){
    while(条件不满足){
        obj.wait();
    }
}
复制代码
wait方法只会释放当前共享变量上的锁

当前线程调用共享变量的wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。

举例

public class ThreadTest {

    //创建资源
    private static volatile Object resourceA = new Object();
    private static volatile Object resourceB = new Object();

    public static void main(String[] args) throws InterruptedException{

        //创建线程threadA
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    //获取resourceA共享资源的监视器锁
                    synchronized(resourceA){
                        System.out.println("threadA get resourceA lock");

                        synchronized(resourceB){
                            System.out.println("threadA get resourceB lock");

                            //线程A阻塞,并释放(release)获取到的resourceA的锁
                            System.out.println("threadA release resourceA lock");
                            resourceA.wait();
                        }
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        });

        //创建线程threadB
        Thread threadB = new Thread(new Runnable() {

            @Override
            public void run(){
                try{
                    //休眠1秒
                    Thread.sleep(1000);

                    //获取resourceA共享资源的监视器锁
                    synchronized(resourceA){
                        System.out.println("threadB get resourceA lock");

                        System.out.println("threadB try get resouceB lock...");

                        //获取resourceB共享资源的监视器锁
                        synchronized(resourceB){
                            System.out.println("threadB get resourceB lock");

                            //线程B阻塞,并释放获取到的resourceA 的锁
                            System.out.println("threadB release resourceA lock");
                            resourceA.wait();
                        }
                    }
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        });
        
        //启动线程
        threadA.start();
        threadB.start();
        
        //等待两个线程结束
        threadA.join();
        threadB.join();
        
        System.out.println("main over");
    }
}
复制代码

运行结果:

image.png

结果分析:

在代码中,在main函数里面启动了线程A和线程B,为了让线程A先获取到锁,这里让线程B先休眠1s,线程A先获取共享变量resourceA 和 共享变量resourceB 上的锁,然后调用了resourceA 的wait()方法阻塞自己,阻塞自己后线程A释放掉获取的 resourceA 上的锁。

线程B休眠结束后会首先尝试获取resourceA 上的锁,如果当时线程A还没有调用 wait()方法释放该锁,那么线程B会被阻塞,当线程A释放了resourceA 上的锁后,线程B就会获取到resourceA 上的锁,然后尝试获取resourceB 上的锁。由于线程A调用的是resourceA 上的wait()方法,所以线程A挂起自己后并没有释放获取到的resourceB 上的锁,所以线程B尝试获取resourceB 上的锁时会被阻塞。

这就证明了:当线程调用共享对象的wait()方法时,当前线程自会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放。

中断阻塞挂起的线程会抛出异常

当一个线程调用共享对象的wait()方法被阻塞挂起后,如果其他线程中断了该线程,则该线程会抛出InterruptedException异常并返回。

示例

public class ThreadTest {

    static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException{

        //创建线程
        Thread threadA = new Thread(new Runnable(){
            public void run(){
                try{
                    System.out.println("---begin---");

                    //阻塞当前线程
                    synchronized(obj){
                        obj.wait();
                    }
                    System.out.println("---end---");
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        });

        threadA.start();

        Thread.sleep(1000);

        System.out.println("---begin interrupt threadA---");
        threadA.interrupt();                                    //主线程中断了线程threadA
        System.out.println("---end interrupt threadA---");
    }
}
复制代码

运行结果:

image.png

代码分析:

在如上代码中,threadA 调用共享对象obj的wait()方法后阻塞挂起了自己,然后主线程在休眠1s后中断了threadA线程,中断后threadA 在obj.wait()处抛出java.lang.InterruptedException异常而返回并终止。

2. wait(long timeout) 函数

与wait()方法相比多了一个超时参数,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeout ms时间内被其他线程调用该共享变量的notify() 或者 notifyAll() 方法唤醒,那么该函数还是会因为超时而返回。

如果timeout设置为0则和wait方法效果一样,因为在wait方法内部就是调用了wait(0)。

如果timeout < 0,则会抛出IllegalArgumentException异常。

3. wait(long timeout, int nanos)函数

在其内部调用的是wait(long timeout)函数,如下代码只有在nanos>0是才使参数timeout递增1

源码

public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }
复制代码

4. notify()函数

一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上掉员工wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。

此外,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程竞争该锁,只有该线程竞争到了共享变量的监视器后才可以继续执行。

类似wait系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify()方法,否则会抛出IllegalMonitorStateException异常。

5. notifyAll()函数

notifyAll()方法会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。

需要注意的是:在共享变量上掉员工notifyAll()方法只会唤醒调用这个方法前调用了wait系列函数而被放入共享变量等待集合里面的线程。 如果调用notifyAll()方法后,一个线程调用了该共享变量的wait()方法而被放入阻塞集合,则该线程是不会被唤醒的。

示例

public class NotifyAndNotifyAll {

    //创建资源
    private static volatile Object resourceA = new Object();

    public static void main(String[] args) throws InterruptedException{

        //创建线程
        Thread threadA = new Thread(new Runnable() {
            public void run(){

                //获取resourceA共享资源的监视器锁
                synchronized(resourceA){

                    System.out.println("threadA get resourceA lock");
                    try{
                        System.out.println("threadA begin wait");
                        resourceA.wait();
                        System.out.println("threadA end wait");
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                }
            }
        });

        //创建线程
        Thread threadB = new Thread(new Runnable() {
            public void run(){

                //获取resourceA共享资源的监视器锁
                synchronized(resourceA){
                    System.out.println("threadB get resourceA lock");
                    try{
                        System.out.println("threadB begin wait");
                        resourceA.wait();
                        System.out.println("threadB end wait");
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                }
            }
        });

        //创建线程
        Thread threadC = new Thread(new Runnable() {
            public void run(){
                synchronized(resourceA){
                    System.out.println("threadC begin notify");
                    //resourceA.notify();                 //唤醒一个在共享变量resourceA上调用wait方法所阻塞的线程
                    resourceA.notifyAll();                //唤醒所有在共享变量resourceA上调用wait方法所阻塞的线程
                }
            }
        });

        //启动线程
        threadA.start();
        threadB.start();

        Thread.sleep(1000);                         //主线程休眠1s
        threadC.start();

        //等待线程结束
        threadA.join();
        threadB.join();
        threadC.join();

        System.out.println("main over");
    }
}
复制代码

调用notify()的运行结果

只有一个线程A被唤醒,线程B没有被唤醒

image.png

调用notifyAll()的运行结果

线程A和线程B都会被唤醒

image.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享