Skip to content

Latest commit

 

History

History
566 lines (390 loc) · 24.4 KB

多线程知识点-2.md

File metadata and controls

566 lines (390 loc) · 24.4 KB

1.线程的状态

1.1线程相关的方法

1.1.1线程通知

  • wait() :由Object类提供,当一个 线程调用了一个共享变量的wait()方法时,该线程将会被阻塞挂起,直到发生以下事件才返回。
  1. 其它线程调用了该共享对象的notify()或者notiftAll()方法
  2. 其它线程调用了该线程的 interrupt()方法,该线程抛出IllegalMonitorStateExcception异常返回。

若调用wait()方法时没有事先获取该对象的监视器锁,则会抛出IllegalMonitorStateExcception异常。

获取该对象的监视器锁方法如下:

(1)使用线程同步代码块时,将该共享变量作为参数;

(2)使用同步方法来实现线程安全。

为防止线程发生虚假唤醒(即从挂起状态变为可运行状态,而没有被其它线程调用notify()等方法通知,被中断,被等待超时),需要不停测试改线程被唤醒的条件是否满足。

//线程同步代码块获得监视器锁对象!
synchronized (obj){
    	//调用while()循环不断检查线程唤醒条件是否满足
        while (条件不满足){
            obj.wait()
        }
    }
  • notify()函数,由Object类提供,一个线程调用共享对象的该方法后会唤醒一个在该变量上调用wait()系列方法后别挂起的线程,若有多个这种线程,被唤醒的等待线程是随机的。

    同样,这样被唤醒的线程不能马上从wait()方法返回并继续执行,必须获取改共享对象的监视器锁后才能返回,因为唤醒它的线程在释放该共享变量的监视器锁后,需要该线程与其他线程一起竞争该锁,竞争到才能继续执行。

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

  • join()方法:由Thread类提供,该方法无参且无返回值,作用是等待该线程执行完毕后再返回。

1.1.2线程睡眠

  • sleep()方法:由Thread类提供,调用该方法将使该线程暂时让出指定时间的执行权,不参与CPU调度,但该线程所有的监视器资源(比如锁)是不让出的,指定时间到之后,线程正常返回,并出于就绪状态,参与CPU调度,获取到CPU资源就可以继续运行。

睡眠期间的线程如果被其它线程调用该线程interrupt()方法中断,则该线程会在调用sleep()方法的地方抛出InterruptedExceptinon异常。

1.1.3线程中断

  • void interrupt() 方法:中断线程。调用该方法可以将该线程的中断标志位设置为true并立即返回。设置标志位后线程并没有立即被中断,它会继续向下执行。若一个线程因调用wait()系列函数、join方法或sleep方法二被阻塞挂起,另一个线程再调用该线程的interrupt()方法则会抛出InterruptedExceptinon异常。

  • boolean isInterrupted() 方法:检测当前线程是否被中断,是则返回true,否返回false,不清除中断标志。

    public boolean isInterrupted(){
            //传递false,说明不清楚中断标志
            return isInterrupted(false);
        }
  • static boolean interrupted() 方法: 检测当前线程是否被中断,是则返回true,否返回false。与前者不同的是,此方法若发现当前线程被中断,则会清楚中断标志。

1.2线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态呢?在API中java.lang.Thread.State这个枚举中给出了六种线程状态:

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread只有线程对象,没有线程特征。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪态
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(超时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

如图所示:

alt

2.线程池

使用线程池的原因:频繁的创建与销毁线程池不仅繁琐且影响系统的稳定性,浪费资源,为了使得创建的线程能过复用,因此引入了线程池。

主要解决的两个问题:

  1. 为执行大量异步任务提供较好的性能;
  2. 提供了一种资源限制和管理的手段。如限制线程的数量,动态新增线程

2.1什么是线程池?

  • 线程池:就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

原理图简单理解如下:

alt

使用线程池的好处:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

2.2线程池的使用

  1. Java里面线程池的顶级接口是java.util.concurrent.Executor
  2. 真正的线程池接口是java.util.concurrent.ExecutorService

配置一个线程池是比较复杂,官方建议使用Executors工程类来创建线程池对象。

方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行

    Future接口:用来记录线程任务执行完毕后产生的结果。

使用线程池创建对象的步骤如下:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

示例代码:

定义Runnabel任务对象:

class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i =0; i<3; i++){
            System.out.println(Thread.currentThread().getName()+"===>"+i);
        }
    }
}

定义Callable任务对象:

class MyCallable implements Callable<String>{
    //需求:使用线程池,计算1-100,1-200,1-300的和并返回。
    private int n;

    public MyCallable(int n) {
        this.n = n;
    }

    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i =0; i<n; i++) {
            System.out.println(Thread.currentThread().getName() + " isCallable===>" + i);
            sum += i;
        }
        return Thread.currentThread().getName()+"执行的结果是"+sum;
    }
}

创建线程池,开启任务并接收结果。

public class ThreadPoolDemo {
    public static void main(String[] args) {

        //1.创建一个线程池,指定线程的固定数量是3
        ExecutorService pools = Executors.newFixedThreadPool(3);
        //2.添加线程任务让线程池处理
//        Runnable target = new MyRunnable();
//        Callable callable = new MyCallable();
//        FutureTask<String> futureTask = new FutureTask(callable);
//        Thread newThread = new Thread(futureTask);
//        newThread.start();
//        try {
//            String name = futureTask.get();
//            System.out.println(name);
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
//        pools.submit(target); //第一次提交任务,此时线程池创建新线程
//        pools.submit(target); //第二次提交任务,此时线程池创建新线程
//        pools.submit(target); //第三次提交任务,此时线程池创建新线程
//        pools.submit(target); //第四次提交任务,复用之前的线程

        //提交Callable任务对象后返回一个未来任务对象!
        Future<String> t1 = pools.submit(new MyCallable(100));
        Future<String> t2 = pools.submit(new MyCallable(200));
        Future<String> t3 = pools.submit(new MyCallable(300));

        //获取当前线程池执行的结果
        try {
            String task1 = t1.get();
            String task2 = t2.get();
            String task3 = t3.get();
            System.out.println(task1);
            System.out.println(task2);
            System.out.println(task3);
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

3.线程上下文切换

简单理解:就是当前线程使用完时间片之后,就会处于就绪状态并让出CPU让其它线程占用。

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

4.线程死锁

4.1什么是线程死锁

死锁是指两个或者两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在没有外力干扰的情况下会一直等待下去的场景。

alt

4.2产生死锁的四个条件

  • 互斥条件:线程对持有的资源进行排他性使用,其它线程想要使用该资源只能等待。
  • 请求并持有条件:指一线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已经被其他线程占有,导致当前线程被阻塞,同时又不释放自身的资源。
  • 不可剥夺条件:指线程获取到的资源在自己使用完毕之前不能被其他线程抢占,只有在自己使用完才由自己释放。
  • 环路等待条件:在发生死锁时,必然存在一个线程-----资源的环形链。(如上图死锁示意图)

必然死锁案例:

package 死锁案例;

/**
 * Create By Chenhui Peng on 2021/3/3.
 */
public class ThreadDead {
    //定义一个资源对象
    public static Object rescourse01 = new Object();
    public static Object rescourse02 = new Object();

    public static void main(String[] args) {
        //定义2个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (rescourse01){
                    System.out.println("线程1占用资源1请求资源2");
                    try {
                        //使线程1睡1秒后再请求资源2,必然发生死锁
                        Thread.sleep(1000);
                        synchronized (rescourse02){
                            System.out.println("线程1成功占用资源2");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (rescourse02){
                    System.out.println("线程2占用资源2,请求资源1");
                    try {
                        //使线程2睡1秒后再请求资源1,必然发生死锁
                        Thread.sleep(1000);
                        synchronized (rescourse01){
                            System.out.println("线程2成功占用资源1");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }
        }).start();

    }

}

4.3如何避免死锁

要避免死锁,只需要破坏掉至少一个构成死锁的必要条件即可。

目前只有请求并持有和环路等待条件是可以被破坏的。

5.线程相关的关键字

5.1synchronized关键字

synchronized块是Java提供的一种原子性内置锁,Java中的每一个对象都可以把他当做一个同步锁来使用。

5.2volatile关键字

问题引入:并发编程下,多线程访问变量的不可见性问题

产生原因:多个线程访问共享变量,会出现一个线程修改变量的值后,其它线程看不到最新值的情况。

并发编程下,多线程修改变量会出现线程间的变量不可见性。

示例代码:

package volatile关键字;

import examples.TheadDemo;

/**
 * Create By Chenhui Peng on 2021/3/3.
 */
public class VolatileDemo extends Thread{
    private boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //线程中修改变量
        flag = true;
        System.out.println("flag:"+flag);
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

class visibilityDemo{
    public static void main(String[] args) {
        //1.启动子线程,修改flag的变量为true,
        VolatileDemo t = new VolatileDemo();
        t.start();

        //2.主线程检查子线程flag变量是否为true
        while (t.isFlag()){
            System.out.println("主线程进入执行!");
        }
    }
}

运行结果如下:

flag:true

Process finished with exit code 0

此时主线程并没有访问到线程里已经改变的flag值.

5.2.1变量不可见行的内存语义

Java内存模型(之和java并发编程有关):JMM(java memory model)

(重点)JMM是java虚拟机规范中定义的一中内存模型,JMM是标准化的,屏蔽了底层不同计算机之间的区别。他描述了JAVA程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样底层的细节。

JMM的规定如下:

  • 所有的共享变量都存储于主内存。这些共享变量指实例变量和类变量,不包含局部变量,因局部变量为线程私有,无需竞争。
  • 每个线程拥有自己的工作内存,在这里保留了线程所使用的变量的工作副本。
  • 线程对变量的所有操作(读、取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
  • 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。

本地内存和主内存之间的关系:

alt

前一例代码的原因如图所示:

alt

内存不可见性的原因:

  • 每个线程都有自己的工作内存,线程都是从主内存拷贝共享变量的副本值。
  • 每个线程都是在自己的工作内存中操作共享变量的。

5.2.2变量不可见性的解决方案

两种常见解决方案:

  • 加锁
  • 对共享变量进行volatile关键字修饰

加锁(synchronized)的解决流程如下:

  1. 线程获得锁
  2. 线程清空工作内存(由于加了锁)
  3. 线程从主内存拷贝共享变量的最新值到工作内存成为副本
  4. 执行线程任务代码
  5. 将修改后的副本的值刷新到主内存中
  6. 线程释放锁
class visibilityDemo{
    public static void main(String[] args) {
        //1.启动子线程,修改flag的变量为true,
        VolatileDemo t = new VolatileDemo();
        t.start();
        
        //加锁!避免并发编程的变量的不可见性。
        synchronized (visibilityDemo.class){
            //2.主线程检查子线程flag变量是否为true
            while (t.isFlag()){
                System.out.println("主线程进入执行!");
            }
        }

    }
}

使用volatile关键字修饰该变量:

public class VolatileDemo extends Thread{
    //volatile关键字修饰
    private volatile boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

volatile关键字工作原理:

当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其它地方,而是会把值刷新到主内存。当其它线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的副本值。

当线程写入volatile变量值就相当于线程退出synchronized同步块,当线程读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。

6.java中的原子性操作

原子性操作即执行一系列操作时,要么全部一起执行,要么全部都不执行,不存在只执行其中一部分操作的情况。

举例说明:自己给对方账户转账500,对方账户也需要同时+500,这两个操作必须同时完成,要么都失败,要么都成功。

注意: volatile关键字只能保证线程解决共享变量的不可见性问题(访问最新的变量值),但不能保证各个线程操作的原子性。

解决方案

  • 使用锁机制,直接给线程任务加锁,但性能较差(因为一次只有一个线程在对该共享变量执行)。
  • 使用原子类(性能高效,线程安全)。

什么是原子类

概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。

AtomicInteger

原子型Integer,可以实现原子更新操作。

  • public AtomicInteger():初始化一个默认值为0的原子型Integer
  • public AtomicInteger(int initialValue):初始化一个指定值的原子型Integer
  • int get(): 获取值
  • int getAndIncrement(): 以原子方式将当前值加1,注意,这里返回的是自增前值。
  • int incrementAndGet(): 以原子方式将当前值加1,注意,这里返回的是自增后的值。
  • int addAndGet(int data): 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
  • int getAndSet(int value): 以原子方式设置为new Value的值,并返回旧值。

示例:

class MyRunnable02 implements Runnable{
    //创建一个Integer原子类 AtomicInteger来更新,初始为0
    private AtomicInteger atomicInteger = new AtomicInteger();

    @Override
    public void run() {
        synchronized (MyRunnable.class){
            for (int i =1; i<=100 ; i++){
				//使用原子类的方法来操作共享变量,保证各个线程访问该共享变量的操作原子性
                System.out.println("count ======> " + atomicInteger.incrementAndGet());
            }
        }

    }
}

7.Java中的CAS操作(原子类原理)

CAS即Compare and Swap,是JDK提供的非阻塞原子性操作,通过一系列硬件保证了比较--更新一系列操作的原子性,实现了read-modify-check-write的原子性操作。

jdk里面的Unsafe类提供了一系列的CAS方法,实现操作的原子性。

CAS的四个操作数:

  • 对象内存位置obj
  • 对象中的变量的偏移量 valueOffset
  • 变量的预期值 except
  • 新的值 update

原理:如果对象Obj中的内存偏移量为valueOffset的变量值为except,则使用新的值update替换旧的值except。

CAS中的ABA问题:

比如说现在有三个线程来操作同一个地址A中的变量X,X的初始值是l。线程1和线程2的作用都是将变量X的值修改为m,但线程2在访问到变量X后被阻塞(BLocked),线程1成功执行完毕,讲变量X的值修改为m,此时线程3开始启动。线程3将变量X的值m再修改为l。线程3执行完毕之后线程2从阻塞中恢复,继续执行操作,通过检查自己的工作内存中的值是否与主内存中的变量X的值相等,此时发现相等,再继续执行,将变量X的值再修改为m。

(变量X的值发生了环形转换!)

ABA问题的影响很严重,比如说取钱,初始余额100,重复提交了两次取50操作,理想情况下应该是只执行1次,结果执行了一次操作的的时候你妈给你又转了50,在第一次取钱成功的情况下,余额又从50 变为了100,然后此时第二次取钱的操作开始生效,检查发现此时余额的值和自己当时保存的副本都是100元,继续执行!余额又变为50,而你则少了50!

解决ABA问题的方案是给每个变量的状态值都加上一个版本号!若发现版本号不对应,则取消本次操作,重新从主内存中获取新值继续操作。

CAS与Synchronized:乐观锁,悲观锁。

CAS和Synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?

Synchronized是从悲观的角度出发(悲观锁)

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁

共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。性能较差!!

CAS是从乐观的角度出发:

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!

8.Java中的 指令重排序

java内存模型(JMM)允许编译器和处理器对指令进行重新排序以提高运行性能,并且只会对不存在数据依赖性的指令进行重排序。如以下代码:

int a = 1; //(1)
int b = 2; //(2)
int c = a + b; //(3)

前两条指令的执行顺序并不会造成影响,但第三条指令的顺序则有影响。