并发编程之美(1)并发编程基础

第一章.并发编程线程基础

1.1什么是线程

线程本身不会独立存在在,它是进程的一个实体。进程是系统进行资源分配和调度的基本单位

main函数启动时就是一个JVM进程,称为主线程

一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域

在这里插入图片描述

程序计数器是一块内存区域,用来记录线程当前要执行的指令地址

那么为何要将程序计数器设计为线程私有的呢?前面说了线程是占用CPU 执行的基本单位,而CPU 一般是使用时间片轮转方式让线程轮询占用的,所以当前线程CPU 时间片用完后,要让出CPU ,等下次轮到自己的时候再执行。那么如何知道之前程序执行到哪里了呢?其实程序计数器就是为了记录该线程让出CPU 时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。另外需要注意的是,如果执行的是native 方法,那么pc 计数器记录的是undefined 地址,只有执行的是Java 代码时pc 计数器记录的才是下一条指令的地址。
另外每个线程都有自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的,除此之外栈还用来存放线程的调用栈帧。

堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new 操作创建的对象实例。
方法区则用来存放JVM 加载的类、常量及静态变量等信息,也是线程共享的。

1.2线程的创建与运行

Java 中有三种线程创建方式,分别为

实现Runnable 接口的run 方法,

继承Thread 类并重写run 的方法,

使用FuturereTask 方式。

需要注意的是
当创建完thread 对象后该线程并没有被启动执行,直到调用了start 方法后才真正启动了
线程。
其实调用start 方法后线程并没有马上执行而是处于就绪状态, 这个就绪状态是指该
线程已经获取了除CPU 资源外的其他资源,等待获取CPU 资源后才会真正处于运行状态。
一旦run 方法执行完毕, 该线程就处于终止状态。

小结

使用继承方式的好处是, 在run() 方法内获取当前线程直接使用this 就可以了,无须使用Thread.currentThread() 方法;你可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递,而如果使用Runnable 方式,则只能使用主线程里面被声明为final 的变量。

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

前两种方式都没办法拿到任务
的返回结果,但是Futuretask 方式可以。

FutureTask后面我们将会详细补充

1.3线程等待与通知

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

1.wait()函数

当一个线程调用一个共享变量的wa i t()方法时, 该调用线程会被阻塞挂起, 直到发生
下面几件事情之一才返回:

(1 ) 其他线程调用了该共享对象的notify ()或notifyAll() 方法;
( 2 )其他线程调用了该线程的interrupt() 方法, 该线程抛出InterruptedExce ption 异常返回。
另外需要注意的是,如果调用wait() 方法的线程没有事先获取该对象的监视器锁,则
调用wait()方法时调用线程会抛出IllegalMonitorState Exce ption 异常。
那么一个线程如何才能获取一个共享变量的监视器锁呢?
( 1 )执行synchronized 同步代码块时, 使用该共享变量作为参数。
synchron 工zed (共享变量){

//doSomething

}

(2 )调用该共享变量的方法,并且该方法使用了synchroni ze d 修饰。
synchronized void add (int a , int b ) {
//doSomething

}

另外需要注意的是,一个线程可以从挂起状态变为可以运行状态( 也就是被唤醒),即使该线程没有被其他线程调用notify()、notify A ll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒

如何避免我们的虚假唤醒

做法就是不停地去测试该线程被唤醒的条件是否满足,不满足则继续等待,也就是说在一个循环中调用wait () 方法进
行防范。退出循环的条件是满足了唤醒该线程的条件

synchronized (Obj) {
while (条件不满足){
	obj.wait() ;
	}
}

关于这点,我的另一篇博客有详细的介绍

https://blog.csdn.net/qq_22155255/article/details/109749311

2.wait(long timeout)函数

与第一个相比,这个函数多了一个超时参数它的不同之处在于,如果一个线程调用共享对象的该方法挂起后, 没有在指定的timeoutms 时间内被其他线程调用该共享变量的notify() 或者notifyA ll() 方法唤醒,那么该函数还是会因为超时而返回。如果将timeout 设置为0 则和wait 方法效果一样,因为在wa it 方法内部就是调用了wa it(O) 。需要注意的是,如果在调用该函数时, 传递了一个负的timeout 则会抛出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 I I nanos > 999999) {
    
    
throw new IllegalArgumentException(” nanosecond timeout value out of range " );
if (nanos > 0) {
    
    
timeout++ ;
wait (timeout);
}

如果timeout与nanos参数都为0,则不会超时,会一直进行等待,改为wait()方法

4.notify ()函数

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

此外,被唤醒的线程不能马上从wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁 , 只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
类似wait 系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的notify() 方法,否则会抛出Illega!MonitorStateException 异常。

5.notifyAll ()函数

不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程notifyAll () 方法则会唤醒所有在该共享变量上由于调用wait 系列方法而被挂起的线程。

public class notifyAll {
    
    
    private static volatile Object resourceA = new Object();

    public static void main(String[] args) throws InterruptedException {
    
    
        Thread threadA = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                //获取资源A的监视器锁
                synchronized (resourceA)
                {
    
    
                    System.out.println("线程A获得了A的锁");
                    try {
    
    
                        System.out.println("线程A开始等待");
                        resourceA.wait();
                        System.out.println("线程A等待结束");
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }

            }
        });
        Thread threadB = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                //获取资源A的监视器锁
                synchronized (resourceA)
                {
    
    
                    System.out.println("线程B获得了A的锁");
                    try {
    
    
                        System.out.println("线程B开始等待");
                        resourceA.wait();
                        System.out.println("线程B等待结束");
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }

            }
        });
        Thread threadC = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                synchronized (resourceA){
    
    
                    System.out.println("线程C释放全部锁");
                    resourceA.notify();
                }
            }
        });
        threadA.start();
        threadB.start();
        Thread.sleep(1000);
        threadC.start();
        //等待线程结束
        threadA.join();
        threadB.join();
        threadC.join();
        System.out.println("main over");
    }
}
线程A获得了A的锁
线程A开始等待
线程B获得了A的锁
线程B开始等待
线程C释放全部锁
线程A等待结束

这是notify方法,我们可以看出,调度器先调动了线程A,在线程A中resourceA挂起释放锁后再给B线程调度,然后B同样的操作后给了线程C,C调用共享变量resouceA的notify方法,只释放一个,线程A先被拿到,输出了A等待结束,但是这个时候线程B还在阻塞,程序没有结束,所以我们想打印的main over是不会输出的

如果我们换成notifyAll方法,则执行结果就是

线程B获得了A的锁
线程B开始等待
线程A获得了A的锁
线程A开始等待
线程C释放全部锁
线程A等待结束
线程B等待结束
main over
注意线程AB谁先被拿到是随机的,但是一定是成对出现的,不成对出现就会死锁

1.4等待线程执行终止的join方法

在项目实践中经常会遇到一个场景,就是需要等待某几件事情完成后才能继续往下执行, 比如多个线程加载资源, 需要等待多个线程全部加载完毕再汇总处理。Thread 类中有一个join 方法就可以做这个事情,前面介绍的等待通知方法是Obj ect 类中的方法, 而join方法则是Thread 类直接提供的join是无参且返回值为void 的方法

public class Join {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread threadOne = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("ThreadOne 完毕");
            }
        });
        Thread threadTwo =new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("线程ThreadTwo完毕");
            }
        });
        //启动线程
        threadOne.start();
        threadTwo.start();
        System.out.println("等待所有线程通过");
        //等待子线程执行完毕
        threadOne.join();
        threadTwo.join();
        System.out.println("子线程全部通过,程序结束");
    }
}

运行结果

等待所有线程通过
ThreadOne 完毕
线程ThreadTwo完毕
子线程全部通过,程序结束
但凡上面One或者Two线程有一个被阻塞或者出错,那么这个程序就不会输出子线程全部通过,程序结束

这里只是为了演示join 方法的作用, 在这种情况下使用后面会讲到的CountDownLatch 是个不错的选择。

CountDownLatch 在我另一篇JUC入门中有讲到怎么快速使用

https://blog.csdn.net/qq_22155255/article/details/109749311

中断我们的线程会发生什么

public class Join2 {
    
    
    public static void main(String[] args) {
    
    
        //线程A
        Thread threadA = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("线程A开始运行");
                for(;;){
    
    

                }
            }
        });
        //获取主线程
        final Thread mainThread = Thread.currentThread();
        //线程B
        new Thread(()->{
    
    
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            //中断主线程
            mainThread.interrupt();
        },"B").start();
        //启动子线程A
        threadA.start();
        //启动线程B
        try {
    
    
            threadA.join();
        } catch (InterruptedException e) {
    
    
            System.out.println("main thread:"+e);
        }
    }
}

线程A开始运行
main thread:java.lang.InterruptedException

注意这是我们中断主线程,因为我们的A线程一直在for循环,他是不会出来的

如果我们改成中断我们的线程A会发生什么

我们发现我们程序打印出线程A开始运行后就一直卡死了,

我们不是用threadA.interrupt();中断了线程A么?

经过查找我们发现

在这里插入图片描述

也就是说我们只是给了一个中断标志,程序依然在运行

1.5让线程睡眠的sleep方法

Thread类中有一个静态的s leep 方法,当一个执行中的线程调用了Thread 的sleep 方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU 的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU 的调度,获取到CPU 资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的intenupt()方法中断了该线程,则该线程会在调用sleep 方法的地方抛出IntermptedException 异常而返回。
下面举一个例子来说明,线程在睡眠时拥有的监视器资源不会被释放。

public class Sleep {
    
    
    //创建一个独占锁  写锁
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) {
    
    
        //创建线程A
        new Thread(()->{
    
    
            //加锁
            lock.lock();
            try {
    
    
                System.out.println("子线程A要睡了");
                Thread.sleep(10000);
                System.out.println("子线程A醒了");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }finally {
    
    
                //释放锁
                lock.unlock();
            }
        },"线程A").start();
        //创建线程B
        new Thread(()->{
    
    
            //获取独占锁
            lock.lock();
            try {
    
    
                System.out.println("子线程B要睡了");
                Thread.sleep(10000);
                System.out.println("子线程B醒了");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        },"线程B").start();

    }
}
子线程A要睡了
子线程A醒了
子线程B要睡了
子线程B醒了

结果是绝对不会出现交叉输出的,肯定是A睡了在醒过来,线程B才会拿到锁的,睡眠的那10S中,lock锁是不会释放的!!

当一个线程处于睡眠状态时,如果另外一个线程中断了它, 会不会在调用sleep 方法处抛出异常。

子线程在睡眠期间, 主线程中断了它,所以子线程在调sleep 方法处抛出了InterruptedException 异常。另外需要注意的是,如果在调用Thread.sleep(long millis)时为millis 参数传递了一个负数, 则会抛出Illega!ArgumentException 异常!!

1.6让出CPU执行权的yield方法

当一个线程调用yield 方法时, 当前线程会让出CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU 的那个线程来获取CPU 执行权

public class yeild implements Runnable {
    
    
    yeild(){
    
    
        //创建启动线程
        Thread thread = new Thread(this);
        thread.start();
    }
    @Override
    public void run() {
    
    
        for (int i=0;i<5;i++)
        {
    
    
            //当i=0时让出CPU执行权,放弃时间片,进行下一轮调度
            if ((i%5) ==0){
    
    
                System.out.println(Thread.currentThread()+"让出CPU...");
                //当前线程让出CPU执行权,放弃时间片,进行下一轮调度
               //Thread.yield();
            }
        }
        System.out.println(Thread.currentThread()+"is Over");
    }

    public static void main(String[] args) {
    
    
        new yeild();
        new yeild();
        new yeild();
    }
}

Thread .yield()方法生效了,三个线程分别在i=O 时调用Thread. yield()方法,所以三个线程自己的两行输出没有在-起,因为输出了第一行后当前线程让出了CPU 执行权。

总结:

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

1.7线程中断

Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行, 而是被中断的线程根据中断状态自行处理。

void interrupt()方法:

中断线程, 例如,当线程A 运行时,线程B 可以调用钱程A的interrupt() 方法来设置线程A 的中断标志为true 并立即返回。设置标志仅仅是设置标志, 线程A 实际并没有被中断, 它会继续往下执行。如果线程A 因为调用了wait 系列函数、join 方法或者sle ep 方法而被阻塞挂起,这时候若线程B 调用线程A 的interrupt() 方法,线程A 会在调用这些方法的地方抛出InterruptedException 异
常而返回。

boolean isinterrupted() 方法:

检测当前线程是否被中断,如果是返回true , 否则返回false 。

public boolean isInterrupted() {
    
    
    return isInterrupted(false);
}

那上面的那个例子,线程A不是被中断了么,为什么还在无线循环,他是不是没有被中断呢?

//线程B
new Thread(()->{
    
    
    try {
    
    
        System.out.println("线程B开始睡眠");
        Thread.sleep(10000);
        System.out.println("线程B睡眠结束");
        //中断主线程
        System.out.println("线程B去中断线程A");
        threadA.interrupt();
        boolean interrupted = threadA.isInterrupted();
        System.out.println(interrupted);
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
},"B").start();

结果是

线程A开始运行
线程B开始睡眠
线程B睡眠结束
线程B去中断线程A
true

我们可以确定A确实被中断了,只是程序还在运行

boolean interruptedO 方法:

检测当前线程是否被中断, 如果是返回true , 否则返回false 。与is lnterrupted 不同的是,该方法如果发现当前线程被中断, 则会清除
中断标志,并且该方法是static 方法, 可以通过Thread 类直接调用。另外从下面的代码可以知道, 在interrupted()内部是获取当前调用线程的中断标志而不是调用interrupted() 方法的实例对象的中断标志。

public static boolean interrupted() {
//清除中断标志
return currentThread().isinterrupted(true);
}
public class Interrupted {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread = new Thread(() -> {
    
    
            //如果当前线程被中断则退出循环
            while (!Thread.currentThread().isInterrupted())
                System.out.println(Thread.currentThread() + "hello");
        });
        thread.start();
        //主线程休眠1s,以便中断子线程输出
        Thread.sleep(1000);
        //中断子线程
        System.out.println("主线程要中断thread");
        thread.interrupt();
        //等待子线程执行完毕
        thread.join();
        System.out.println("主线程完毕");
    }
}
结果
Thread[Thread-0,5,main]hello
Thread[Thread-0,5,main]hello
主线程要中断thread
Thread[Thread-0,5,main]hello
主线程完毕
中间这条只是特例

下面再来看一种情况。当线程为了等待一些特定条件的到来时,一般会调用sleep函数、wait 系列函数或者join ()函数来阻塞挂起当前线程。比如一个线程调用了Thread.sleep(3000),那么调用线程会被阻塞, 直到3s 后才会从阻塞状态变为激活状态。但是有可能在3s 内条件己被满足,如果一直等到3s 后再返回有点琅费时间,这时候可以调用该线程的interrupt() 方法, 强制sleep 方法抛出InterruptedException 异常而返回,线程恢复到激活状态。下面看一个例子。

public class Interrupted2 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread threadA = new Thread(() -> {
    
    
            try {
    
    
                System.out.println("线程A睡眠2000s");
                Thread.sleep(2000000);
                System.out.println("线程A睡醒了");
            } catch (InterruptedException e) {
    
    
                System.out.println("线程A中断了睡眠");
                return;
            }
        });
        //启动线程
        threadA.start();
        //确保子线程进入休眠,主线程先睡眠1s
        Thread.sleep(1000);
        //打断子线程睡眠
        threadA.interrupt();
        //等待子线程执行完毕
        threadA.join();
        System.out.println("主线程结束");
    }
}

结果是

线程A睡眠2000s
线程A中断了睡眠
主线程结束

我们发现,线程A被打断后,我们的程序直接从

catch (InterruptedException e) {
    
    
    System.out.println("线程A中断了睡眠");
    return;
}

这里溜走了,而我们的System.out.println(“线程A睡醒了”);并没有被执行

下面再通过一个例子来了解interrupted()与isInterrupted()方法的不同之处。

public class Interrupdted3 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread = new Thread(() -> {
    
    
            for (; ; ) {
    
    

            }
        });
        //启动线程
        thread.start();
        //设置中断
        thread.interrupt();
        //获取中断标志
        System.out.println("中断状态是:"+thread.isInterrupted());
        //获取中断标志并重置
        System.out.println("中断状态:"+thread.interrupted());
        //获取中断标志并重置
        System.out.println("中断状态:"+ Thread.interrupted());
        //获取中断标志
        System.out.println("中断状态是:"+thread.isInterrupted());
        thread.join();
    }
}

结果是

中断状态是:true
中断状态:false
中断状态:false
中断状态是:true

第一行输出tru e 这个大家应该都可以想到, 但是下面三行为何是false 、fa lse 、true 呢,不应该是true 、false 、false 吗?如果你有这个疑问, 则说明你对这两个函数的区别还是不太清楚。

上面我们介绍了在interrupted()方法内部是获取当前线程的中断状态,这里虽然调用了子线程的interrupted 方法,但是获取的是主线程的中断标志,因为主线程是当前线程。thread.interrupted()和Thread.interrupted()方法的作用是一样的,目的都是获取当前线程的中断标志

修改上面的例子为如下

public class Interrupdted4 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread = new Thread(() -> {
    
    
            //里面将我们的主线程的也设置成中断
            while (!Thread.currentThread().interrupted()) {
    
    
            }
            System.out.println("子线程的中断状态是" + Thread.currentThread().isInterrupted());

        });
        thread.start();
        //设置中断标志
        thread.interrupt();
        thread.join();
        System.out.println("主线程结束");
    }
}

子线程的中断状态是false
主线程结束

由输出结果可知,调用interrupted() 方法后中断标志被清除了。

1.8理解上下文切换

在多线程编程中,线程个数一般都大于CPU 个数,而每个CPU 同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU 执行任务。

当前线程使用完时间片后,就会处于就绪状态并让出C PU 让其他线程占用, 这就是上下文切换,从当前线程的上下文切换到了其他线程。

那么就有一个问题,让出CPU 的线程等下次轮到自己占有CPU 时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存
当前线程的执行现场, 当再次执行时根据保存的执行现场信息恢复执行现场。

线程上下文切换时机有: 当前线程的CPU 时间片使用完处于就绪状态时,当前线程被其他线程中断时。

猜你喜欢

转载自blog.csdn.net/qq_22155255/article/details/109827153