第 1 章 并发编程线程基础

目录

1.1 什么是线程 

1.2 线程创建与运行 

1、继承 Thread 类方式的实现。

2、实现 Runnable 接口的 run 方法

3、使用 FutureTask 方式

1.3 线程通知与等待

1.wait() 函数

2.wait(long timeout) 函数

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

4.notify() 函数

5.notifyAll() 函数

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

1.5 让线程睡眠的 sleep 方法 

1.6 让出 CPU 执行权的 yield 方法 

sleep 与 yield 方法的区别

1.7 线程中断 

void interrupt() 方法 :

boolean isInterrupted() 方法 :

boolean interrupted() 方法 :

 interrupted()和 isinterrupted()的区别

1.8 理解线程上下文切换 

1.9 线程死锁 

1.10 守护线程与用户线程 

1.11ThreadLocal

1.11.1 ThreadLocal 使用示例

1.11.2 ThreadLocal 的实现原理

1.11.3 ThreadLocal 不支持继承性

1.11.4 InheritableThreadLocal 类 


1.1 什么是线程 

进程-线程-协程-优快云博客

1.2 线程创建与运行 

Java 中有三种线程创建方式

1、继承 Thread 类方式的实现

        使用继承方式的好处是,在 run() 方法内获取当前线程直接使用 this 就可以了,无须
使用 Thread.currentThread() 方法;
        不好的地方是 Java 不支持多继承,如果继承了 Thread 类, 那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码。
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();

    }
}

如上代码中的 MyThread 类继承了 Thread 类,并重写了 run() 方法。在 main 函数里面创建了一个 MyThread 的实例,然后调用该实例的 start 方法启动了线程。需要注意的是, 当创建完 thread 对象后该线程并没有被启动执行,直到调用了 start 方法后才真正启动了 线程, 处于就绪状态,这个就绪状态是指该 线程已经获取了除 CPU 资源外的其他资源,等待获取 CPU 资源后才会真正处于运行状态。 一旦 run 方法执行完毕,该线程就处于终止状态。

2、实现 Runnable 接口的 run 方法

任务与代码有分离,当多个线程执行一样的任务时需要一份任务代码。
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();
}

3、使用 FutureTask 方式

如上面代码所示,两个线程共用一个 task 代码逻辑,如果需要,可以给 RunableTask 添加参数进行任务区分。另外,RunableTask 可以继承其他类。但是上面介绍的两种方式 都有一个缺点,就是任务没有返回值。下面看最后一种,即使用 FutureTask 的方式。
//创建任务类,类似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();
}
}
如上代码中的 CallerTask 类实现了 Callable 接口的 call() 方法。在 main 函数内首先创建了一个 FutrueTask 对象(构造函数为 CallerTask 的实例),然后使用创建的 FutrueTask 对象作为任务创建了一个线程并且启动它,最后通过 futureTask.get() 等待任务执行完毕并返回结果。
小结 :使用 继承方式的好处是方便传参 ,你可以在子类里面添加成员变量,通过 set 方法设置参数或者通过构造函数进行传递,而如果使用 Runnable 方式,则只能使用主线 程里面被声明为 final 的变量。不好的地方是 Java 不支持多继承 ,如果继承了 Thread 类,那么子类不能再继承其他类,而 Runable 则没有这个限制。前两种方式都没办法拿到任务 的返回结果,但是 Futuretask 方式可以。

1.3 线程通知与等待

1.wait() 函数

当一个线程调用一个共享变量的 wait() 方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:
1 )其他线程调用了该共享对象的 notify() 或者 notifyAll() 方法;
2 )其他线程调用了该线程的 interrupt() 方法,该线程抛出 InterruptedException 异常返回。
如果调用 wait() 方法的线程没有事先获取该对象的监视器锁,则 调用 wait() 方法时调用线程会抛出 IllegalMonitorStateException 异常。
那么一个线程如何才能获取一个共享变量的监视器锁呢?使用 synchronized关键字。
1 )执行 synchronized 同步代码块时,使用该共享变量作为参数。
synchronized(共享变量){
//doSomething
}
2 )调用该共享变量的方法,并且该方法使用了 synchronized 修饰。
synchronized void add(int a,int b){
//doSomething
}

唤醒:一个线程可以从挂起状态变为可以运行状态;
虚假唤醒: 该线程没有被其他线程调用 notify()、notifyAll() 方法进行通知,或者被中断,或者等待超时
在一个循环中调用 wait() 方法对虚假唤醒进行防范,退出循环的条件是满足了唤醒该线程的条件。
synchronized (obj) {
while (条件不满足){
obj.wait();
}
}
例子:生产者和消费者
//生产线程
synchronized (queue) { //queue 为共享变量,生产者线程在调用 queue 的 wait() 方法前,使用         
                       //synchronized 关键字拿到了该共享变量queue 的监视器锁,所以调用 wait() 
                       //方法才不会抛出 IllegalMonitorStateException 异常
 
 //消费队列满,则等待队列空闲,这里使用循环就是为了避免上面说的虚假唤醒问题。
 while (queue.size() == MAX_SIZE) { 
 try { 
 //挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取该锁,然后 
 //获取队列里面的元素
 queue.wait(); 
 } catch (Exception ex) { 
 ex.printStackTrace(); 
 } 
 }
 //空闲则生成元素,并通知消费者线程
 queue.add(ele); 
 queue.notifyAll(); 
 } 
} 

//消费者线程
synchronized (queue) { 
 
 //消费队列为空
 while (queue.size() == 0) { 
 try{
 //挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者线程可以获取该锁,将生
 //产元素放入队列
 queue.wait(); 
 } catch (Exception ex) { 
 ex.printStackTrace(); 
 } 
 }
 //消费元素,并通知唤醒生产者线程
 queue.take(); 
 queue.notifyAll(); 
 } 
}
        假如生产者线程 A 首先通过 synchronized 获取到了 queue 上的锁,那么 后续所有企图生产元素的线程和消费线程将会在获取该监视器锁的地方被阻塞挂起。线程 A 获取锁后发现当前队列已满会调用 queue.wait() 方法阻塞自己,然后释放获取的 queue 上的锁,这里考虑下为何要释放该锁?如果不释放,由于其他生产者线程和所有消费者线 程都已经被阻塞挂起,而线程 A 也被挂起,这就处于了死锁状态。这里线程 A 挂起自己 后释放共享变量上的锁,就是为了打破死锁必要条件之一的持有并等待原则。线程 A 释放锁后,其他生产者线程和所有消费者线程中会有一个线程获 取 queue 上的锁进而进入同步块,这就打破了死锁状态。
当前线程调用共享变量的 wait() 方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。
 // 创建资源
    private static volatile Object resourceA = new Object();
    private static volatile Object resourceB = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 创建线程
        Thread threadA = new Thread(new Runnable() {
            public void run() {
                try {
                    // 获取resourceA共享资源的监视器锁
                    synchronized (resourceA) {
                        System.out.println("threadA get resourceA lock");

                        // 获取resourceB共享资源的监视器锁
                        synchronized (resourceB) {
                            System.out.println("threadA get resourceB lock");
                            // 线程A阻塞,并释放获取到的resourceA的锁
                            System.o
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值