xiaobenchi

导航

并发编程基础1

基础篇之并发编程基础

1. 进程与线程

image-20220719181300340

堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建的时候分配的,堆里面主要存放使用new操作创建的对象实例。

方法区则用来存放JVM加载的类,常量及静态变量等信息,也是线程共享的。

2. 线程的创建与运行

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

  • 2.1 继承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();
    }
}

好处:在run()方法内获取当前线程直接使用this就可以了,无需使用Thread.currentThread()方法;不好的地方就在于Java不支持多继承,任务与代码没有分离,多个线程执行一样的任务时需要多份任务代码,而Runnable则没有这个限制。

  • 2.2 实现Runnable接口
public static class RunnabTask implements Runnable{
    @override
    public void run(){
        System.out.println("I am a child thread");
    }
}

public static void main(String[] args) throws InterruptedException{
    
    RunnableTask task = new RunnableTask();
    new Thread(task).start();
    new Thread(task).start();
}

上面的两种方式,任务没有返回值。

  • 2.3 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函数内首先创建一个FutureTask对象(构造函数为CallerTask的实例),然后使用创建的FutureTask对象作为任务创建了一个线程并启动它,最后通过futureTask.get()等待任务执行完毕并返回结果。

3. 线程的等待与通知

3.1 wait() 函数

当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,知道发送下面几件事情之一才返回:(1) 其它线程调用了该共享对象的notify()或者notifyAll( )方法;(2) 其它线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。

另外需要注意的是,如果调用wait()方法的线程没有实现获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常.

线程如何获取共享变量的监视锁:

  1. 执行synchronize同步代码块时,使用该共享变量作为参数。
synchronized(共享变量){
    //doSomething
}
  1. 调用该共享变量的方法,并且该方法使用了synchronized修饰
synchronized void add(int a, int b){
    //doSomething
}

可以预防虚假唤醒的例子

synchronized (obj){
    while(条件不满足){
        obj.wait();
    }
}
//生产线程
synchronized (queue){
    //消费队列满,则等待队列空闲
    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();
}

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

3.2 wait(long timeout)函数

该方法比Wait()方法多了一个超时参数,在wait()方法内部就是调用了wait(0)。

3.3 wait(long timeout, int nanos) 函数

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);
}

3.4 notify() 函数

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

3.5 notifyAll() 函数

唤醒一个共享变量上调用wait系列方法后被挂起的线程。

3.4 等待线程终止的join方法

项目实践中遇到这样的场景,就是需要等待某几件事情完成之后才能继续往下执行。Thread类中有一个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("child threadOne over!");
    }
   });
    
    
    Thread threadTwo = new Thread(new Runnable(){
          @override
    public void run(){
        try{
            Thread.sleep(1000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("child threadTwo over!");
    }
   });
    
    //启动子线程
    threadOne.start();
    threadTwo.start();
    
    System.out.println("wait all chile thread over!");
    
    //等待子线程执行完毕,返回
    threadOne.join();
    threadTwo.join();
    
    System.out.println("all child thread over!");
}
  

3.5 让线程睡眠的sleep方法

线程调用sleep方法之后会让出指定时间的执行权。

线程在睡眠时拥有的监视器资源不会被释放。

3.6 让出CPU执行权的yield方法

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

3.7 线程中断

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

  • void interrupt() 方法:中断线程,设置线程A的中断标志为true。线程实际并未被中断。
  • boolean isInterrupted() 方法:检测当前线程是否被中断。
  • boolean interrupted(): 与isInterrupted不同的是,此方法会清除标志

4. 理解线程上下文切换

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

6. 线程死锁

6.1 什么是死锁

image-20220720182016520

死锁产生的四个条件:

  • 互斥条件:对获取的资源进行排他性使用。
  • 请求并持有条件:持有资源并请求新的资源。
  • 不可剥夺条件:获得的资源使用完之前不可以被其他线程抢占。
  • 环路等待:发生死锁时,必然存在一个线程—资源环形链。

6.2 如何避免死锁

破坏构造死锁的必要条件:其中只有请求并持有条件和环路等待条件是可以破坏的。

破坏持有并等待条件:设定释放资源的时间,不让其永久持有;

破坏环路等待条件:各线程按照相同的顺序来获取资源。

7 . 守护线程与用户线程

用户线程结束后,JVM进程马上结束,无论此时守护进程是否结束。

//设置守护线程的方式
Thread daemonThread = new Thread(new Runnable(){
    public void run(){
        
    }
});

	daemonThread.setDaemon(true);

8. ThreadLocal

多线程访问同一个共享变量的时候特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程的安全,一般使用者在访问共享变量时需要进行适当的同步。

image-20220720184156615

同步的措施一般是加锁。ThreadLocal可以使得每个线程对其访问的时候访问的是自己线程的变量。

8.1 ThreadLocal使用实例

本例开启了两个线程,在每个线程内部都设置了本地变量的值,然后调用print函数打印当前本地变量的值。如果打印后调用了本地变量的remove方法,则会删除本地内存中的该变量。

public class ThreadLocalTest{
    
    //(1) print函数
    static void print(String str){
        //1.1 打印当前线程本地内存中localVariable变量的值
        System.out.println(str + ":" + localVariable.get());
        //1.2 清除当前线程本地内存中的localVariable变量
        //localVaribable.remove();
    }
    
    //(2) 创建ThreadLocal变量
    static ThreadLocal<String> localVariable = new ThreadLocal<>();
    public static void main(String[] args){
        
        //(3)创建线程one
        Thread threadOne = new Thread(new Runnable(){
            public void run(){
                //3.1 设置线程One中本地变量localVariable的值
                localVariable.set("threadOne local variable");
                //3.2调用打印函数
                print("threadOne");
                //3.3打印本地变量值
                System.out.println("threadOne remove after" + ":" + localVariable.get());
            }
        });
        
          //(4)创建线程two
        Thread threadTwo = new Thread(new Runnable(){
            public void run(){
                //4.1 设置线程two中本地变量localVariable的值
                localVariable.set("threadTwo local variable");
                //3.2调用打印函数
                print("threadTwo");
                //3.3打印本地变量值
                System.out.println("threadTwo remove after" + ":" + localVariable.get());
            }
        });
        
        //(5) 启动线程
        threadOne.start();
        threadTwo.start();
    }
}

8.2 ThreadLocal的实现原理

  • ThreadLoacl相关类的类图结构

image-20220720201725874

ThreadLocal类型的本地变量存放在具体的线程内存空间中。

  • ThreadLocal的基本方法的实现逻辑
  1. void set(T value)

    public void set(T value){
        //(1) 获取当前线程
        Thread t = Thread.currentThread();
        //(2)将当前线程作为key,去查找对应线程的变量,找到则设置
        ThreadLoaclMap map = getMap(t);
        if(map != null)
            map.set(this,value);
        else
        //(3)第一次调用就创建当前线程对应的HashMap
            createMap(t,value);
    }
    
  2. T get()

    public T get(){
        //(4)获取当前线程
        Thread t = Thread.currentThread();
        //(5)获取当前线程的threadLocals变量
        ThreadLocalMap map = getMap(t);
        //(6)如果threadLocals不为null,则返回对应本地变量的值
        if(map != null){
            ThreadLoaclMap.Entry e = map.getEntry(this);
            if(e != null){
            	@SuppressWarnings("unchecked")
            	T result = (T)e.value;
            	return result;
        	}
        }
        //(7)threadLocals为空则初始化当前线程的threadLocals成员变量
        return setInitialValue();
    }
    
  3. void remove()

    public void remove(){
        ThreadLocalMap m = getMap(Thread.currentThread());
        if(m != null){
            m.remove(this);
        }
    }
    

在每个线程内部都有一个名为threadLocals的成员变量,该变量的类型为HashMap,其中key为我们定义的ThreadLocal变量的this引用,value则为我们使用set方法设置的值。

image-20220720204517450

8.3 ThreadLocal不支持继承性

也就是说,同一个ThreadLocal变量在父进程中被设置值后,在子线程中是获取不到的。

8.4 InheritableThreadLocal类

这个类的产生就使得子线程可以访问到父线程中的值。

InheritableThreadLocal继承自ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量。

InheritableThreadLocal继承了ThreadLocal,并重写了三个方法。由代码(3)可知,InheritableThreadLocal重写了createMap方法,那么现在当第一次调用set方法时,创建的是当前线程的inheritableThreadLocals变量的实例而不再是threadLocals。由代码(2)可知,当调用get方法获取当前线程内部的map变量时,获取的是inheritableThreadLocals而不再是threadLocals

image-20220721123238880

posted on 2022-07-21 12:36  小迟在努力  阅读(19)  评论(0)    收藏  举报