并发基础与多线程

Posted by Eric on October 5, 2019

多线程基础

每个线程都有自己的资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的,除此之外枝还用来存放线程的调用技帧。 是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new 操作创建的对象实例 。

一、线程的创建与运行

Java中有三种线程创建方式:

1.继承Thread类并重写run方法

public class ThreadTest{
    public static class MyThread extends Thread{
    @Override
    public void run(){
        System.out.println("A new thread");
     }
   }
    public static void main(String[] args){
    //创建线程
    MyThread mythread = new MyThread();
    //启动线程
    mythread.start();
   }
}

优点: run方法中获取当前线程只需使用this,无须使用Thread.currentThread()方法

缺点: Java不支持多继承,如果继承了Thread类则无法继承其他类

2.实现Runnable接口

a. 定义一个Runnable接口的线程实现类,并重写run方法

b. 创建Runnable实现类的实例,以此实例作为Thread类的target来创建Thread对象,该Thread对象才是真正的线程对象

public class ThreadTest{
    public static class RunnableTest implements Runnable{
    @Override
    public void run(){
        System.out.println("A new Thread")
    }
    }
 
    public static void main(String[] args){
        RunnableTest rt = new RunnableTest();
        new Thread(rt,"新线程1").start();
        new Thread(rt,"新线程2").start();
    }
}

3. 使用Callable和Future接口

使用Runnable接口创建线程的实现类虽然可以继承其它类,但是不能有返回值返回。然后Callable接口的call()方法可以很好地解决这个问题

a. 创建Callable接口的实现类,call()方法为线程的执行体,且call()方法有返回值

b. 使用FutureTask类来包装Callable对象

c. 使用FutureTask对象作为Thread对象的target来启动线程

d. 调用FutureTask对象的get()方法来获得线程执行结束后的返回值

public class ThreadTest{
    public static class CallerTask implements Callable<String>{
        @Override
        public String call() throws Exception{
            return "A new Thread";
        }
    }
    public static void main(String[] args){
        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();
        }
    }
}

采用Runnable、Callable接口创建多线程的优缺点:

  • 线程类只是实现了接口,仍然可以继承其它类
  • 多个线程共享一个target对象,所以非常适合多个相同线程来处理同一份资源的情况
  • 缺点是,编程有些复杂。如果访问当前线程,需要使用Thread.currentThread()方法

二、线程的生命周期

线程的五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)

image-20200401212623671.png

1.新建和就绪状态

当程序使用new创建一个线程后,线程就处于新建状态。由Java虚拟机分配内存并初始化成员变量的值。

当线程对象调用start()方法后,该线程处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器。注意:此时线程并没有开始运行,只是表示线程现在可以运行了,具体什么时候运行还要看JVM里线程调度器的调度。

永远不要调用线程对象的run()方法,直接调用run()方法的话,run()方法会立即执行。run()方法此时被当做一个普通方法,而不是线程的方法

2.运行和阻塞状态

RUNNABLE

如果处于就绪状态的线程获得了CPU,开始执行run()的线程执行体,则该线程处于运行状态。如果计算机只有一个CPU , 那么在任何时刻只有一个线程处于运行状态。如果在一个多处理器的机器上,将会有多个线程并行执行;当线程数大于处理器数时,依然会存在多个线程在同一个CPU 上轮换的现象。

当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了) ,线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务:当该时间段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。

BLOCKED

处于BLOCKED状态的线程等待其他线程释放锁后进入同步区。

3.等待状态

wait()函数

当一个线程调用一个共享变量的wait()方法时,该调用线程被阻塞挂起。当以下操作发生时才会返回:

  • 其他线程调用了该共享对象的notify()notifyAll()方法
  • 其他线程调用了该线程的interrupt()方法,该线程抛出了InterruptedException异常返回

注意:如果调用wait()方法的线程没有事先获取该对象的监视器锁,则会抛出IllegalMonitorStateException异常。要通过执行synchronized关键字来获取监视器锁。

synchronized(共享变量){
    //业务内容
}
//或者
synchronized void show(int a,int b){
    //业务内容
}

线程在通过synchronized获取到锁后,根据业务内容进行判断来调用wait(方法来阻塞自己,这样就释放了共享变量上的锁。这样做是为了防止死锁,打破死锁的持有并等待原则。

wait(long timeout)

当超过timeout的时间后没有被其他线程的notify()notifyAll()唤醒,该线程会自动释放同步监视器的锁。

notify()

一个线程调用共享对象的notify()后,会唤醒一个在该共享变量上调用wait()方法后被挂起的线程。一个变量上可能有多个线程在等待,具体哪个被唤醒是随机的。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法) ,才可以执行被唤醒的线程。

需要注意的是wait()或notify()方法使⽤的是同⼀个对象锁,如果你两个线程使⽤的是不同的对象锁,那它们之间是不能相互通信的。

三、线程通信

1. join线程

当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞, 直到被join()方法加入的join线程执行完为止。 join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作。

2.线程睡眠

sleep()方法可以让线程暂停一段时间并进入阻塞状态

sleep()yield()的区别:

  • sleep()方法暂停当前线程后, 会给其他线程执行机会,不会理会其他线程的优先级:但yield()方法只会给优先级相同,或优先级更高的线程执行机会。
  • sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态; 而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程被yield()方法暂停之后, 立即再次获得处理器资源被执行。
  • sleep()方法声明抛出了InterruptedException 异常,所以调用sleep()方法时要么捕捉该异常, 要么显式声明抛出该异常; 而yield()方法则没有声明抛出任何异常。
  • sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。

四、线程同步

1. 同步监视器

同步监视器作用:阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。

一般通过加锁方式就可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也被称为临界区) ,所以同一时刻最多只有一个线程处于临界区内,从而保证了线程的安全性。

五、线程优先级

Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都⽀持10级优先级的划分(⽐如有些操作系统只⽀持3级划分:低,中,⾼),Java只是给操作系统⼀个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。

通常情况下,⾼优先级的线程将会⽐低优先级的线程有更⾼的⼏率得到执⾏。我们使⽤⽅法 Thread 类的 setPriority() 实例⽅法来设定线程的优先级。

守护线程

守护线程默认的优先级比较低。如果一个线程是守护线程,当其他非守护线程都结束时,这个守护线程也会自动结束。

守护线程常应用于后台支持任务,比如垃圾回收、释放未使用对象的内存、从缓存中删除不需要的条目。

END