JAVA多线程

JAVA多线程

什么是线程

谈起线程,我们必须得先来聊聊进程,其实在我们在使用电脑工作的时候,无时无刻都接触着进程,只是可能大家不了解而且,用通俗的话来讲,每个程序都是一个进程,我们常用的杀毒软件就是一个进程,当我们使用杀毒软件的时候,里面有很多功能,比如病毒查杀,垃圾清理,雷达扫描等,这些就是我们所谓的线程。

JAVA多线程_第1张图片

进程和线程的区别
  • 进程单独占有一定的内存空间,所以进程间存在内存隔离,数据存放在不同的内存地址下,数据共享复杂但是同步简单。线程共享所属进程的内存空间和资源,数据共享简单但是同步复杂。
  • 进程之间相互独立,一个进程出现问题不会影响其他进程,而一个线程崩溃则可能会影响整个进程的稳定性。
  • 进程是操作系统进行分配资源的基本单位,线程是操作系统进行调度的基本单位。

JAVA多线程_第2张图片

线程共享进程的堆和方法区,本地方法栈,程序计数器,虚拟机栈属于线程私有。

并行和并发

并发: 在某个时间段内,多线程执行任务时,将CPU运行时间划分为若干个时间段,再将时间段分配给线程执行,在一个时间段线程运行时,其他线程处于挂起状态。(单核CPU)

并行:在某个时间段,多线程执行时,CPU可以同时处理这些线程请求。(多核CPU)

如何创建一个线程
  • 继承Thread类
/**
*  由于JAVA单继承的特性,继承Thread具有单继承的局限性
*  
*/
public class Demo1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        //开启线程
        myThread.start();

    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("继承Thread创建线程");
    }
}
  • 实现Runnable接口
package org.thread;

import java.util.concurrent.*;

public class Demo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        //使用FutureTask包装实现Callable的接口,FutureTask本质上也是一个Runnable
        FutureTask futureTask = new FutureTask<String>(new MyThread3());
        Thread thread = new Thread(futureTask);
        thread.start();
        String result = (String) futureTask.get();
        System.out.println(result);
    }
}
class   MyThread3 implements Callable<String>{
    @Override
    public String call() throws Exception {
        return "使用Callable接口创建线程";
    }
}

线程组

我觉得大部分初学者应该都没听过这个概念,我在别人的博客里也很少能看到这个概念,偶然间看到一本书里讲了这个概念,才进行了学习,线程组就像我们工作中的部门一样,对部门里的员工进行统一管理,线程组就是对当前组下的线程进行统一管理,每个线程必然都有一个线程组,如果没有显示的指定,默认将父线程设置为自己的线程组。

获取当前线程组名称
package org.thread;

public class Demo4 {
    public static void main(String[] args) {
        System.out.println("执行main方法线程名称为:"+Thread.currentThread().getName());
        System.out.println("执行main方法线程组名称为:"+Thread.currentThread().getThreadGroup().getName());

       Thread t1 =new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t1当前线程名称为:"+Thread.currentThread().getName());
                System.out.println("t1当前线程组名称为:"+Thread.currentThread().getThreadGroup().getName());

                //在t1线程里继续开一个线程
               Thread t2 =new Thread(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("t2当前线程名称为:"+Thread.currentThread().getName());
                        System.out.println("t2当前线程组名称为:"+Thread.currentThread().getThreadGroup().getName());
                    }
                },"t2");
                t2.start();
            }
        },"t1");
       t1.start();

    }
}

// 输出结果
执行main方法线程名称为:main
执行main方法线程组名称为:main
t1当前线程名称为:t1
t1当前线程组名称为:main
t2当前线程名称为:t2
t2当前线程组名称为:main
线程组统一异常处理
package org.thread;

public class Demo5 {
    public static void main(String[] args) {
        ThreadGroup threadGroup = new ThreadGroup("tg1"){
            //继承ThreadGroup并重写uncaughtException方法,当此线程组的线程抛出异常时,则会执行此方法
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println("线程名称为"+t.getName()+"异常信息:"+e.getMessage());
                //打印堆栈信息
                e.printStackTrace();
            }
        };

        //创建一个线程,并加入上面创建的线程组中
        Thread t1=new Thread(threadGroup,new Runnable() {
            @Override
            public void run() {
                //抛异常
                throw new NullPointerException("空指针异常");
            }
        });
        t1.setName("t1");

        t1.start();
    }
}

线程组复制
package org.thread;

public class Demo6 {
    public static void main(String[] args) {
        //获取到当前线程组
        ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
        //创建一个长度为此线程组中活动的线程估计数的数组
        Thread[] threads=new Thread[threadGroup.activeCount()];
        //复制线程
        threadGroup.enumerate(threads);
    }
}

线程上下文切换

多线程在运行的时候,由于CPU资源有限,所以线程都是在不断的切换,给我们一种在同时运行的错误,线程切换就是我们所说的上下文切换,线程切换那么如何保证切换回去再上次的位置呢,每个线程都有一个程序计数器,保存着当前线程执行的位置。大家有兴趣的可以网上深入了解一下。

什么时候会发生线程上下文切换
  • 线程的CPU时间片用完
  • 主动让出CPU,调用了wait(),sleep() 等方法
  • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
线程优先级

线程的优先级会影响线程执行的顺序,但不一定优先级高的就一定先执行。CPU忙时,优先级高的获取更多的时间片,CPU闲时,优先级基本没用。

   
    //最小的优先级为1
    public final static int MIN_PRIORITY = 1;

   /**
     * The default priority that is assigned to a thread.
     */
     //优先级默认为5
    public final static int NORM_PRIORITY = 5;

    //最大优先级为10
    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;





  //设置优先级的方法

    public final void setPriority(int newPriority) {
        ThreadGroup g;
        //判断当前运行的线程是否拥有修改此线程的权限
        checkAccess();
        //判断优先级是否符合范围
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        //判断当前线程线程组是否为空,如果不为空,则判断线程优先级是否比线程组的优先级高
        //线程的优先级不能高于所属线程组的优先级
        //线程组的默认优先级为10,所以大家一般设置的时候都不会有问题
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }
package org.thread;

public class Demo7 {
    public static void main(String[] args) {
        //获取main线程组
        ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
        threadGroup.setMaxPriority(5);
        System.out.println("main线程组优先级为:"+threadGroup.getMaxPriority());
        Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("lssssss");
            }
        });
        t1.setPriority(10);
        t1.start();

        System.out.println("线程优先级为"+t1.getPriority());
    }
}

//输出结果
main线程组优先级为:5
线程优先级为5
lssssss
守护线程

一般来说,java进程需要等到所有的线程完成后才会结束,有一种例外,那就是守护线程,当所有的普通线程结束后,守护线程哪怕没有执行完,也会结束。GC就是守护线程。

   //设置守护线程方法,true为守护线程,默认为false
   public final void setDaemon(boolean on) {
        //判断当前运行的线程是否有修改此线程的权限
        checkAccess();
        //判断线程是否活着,如果或者则抛异常
        //这里就说明一个问题,我们必须在线程启动前设置为守护线程
        //main线程是不能设置为守护线程的
        if (isAlive()) {
            throw new IllegalThreadStateException();
        }
        daemon = on;
    }
package org.thread;

public class Demo8 {
    public static void main(String[] args) {
        Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("lsssssss");
            }
        });
        //由于设置为了守护线程,如果守护线程先拿到CPU执行权,则会输出内容,如果main线程先拿到CPU执行权,则守护线程不会执行
        t1.setDaemon(true);
        t1.start();
        //大家可以试试,线程启动后设置为守护线程则会抛异常
        //t1.setDaemon(true);
    }
}

线程礼让

谈起礼让,我突然想起小时候的一片语文课文,孔融让梨,融四岁,能让梨,小孔融把大的梨子让给哥哥吃,那我们的线程礼让又是什么呢?线程礼让在我看来让的是CPU的执行权,暂停当前的线程,执行其他的线程(当然也有可能当前线程再次抢到CPU执行权)

 package org.thread;

public class Demo9 {
    public static void main(String[] args) {
       Thread t1 =new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程礼让前");
                Thread.yield();
                System.out.println("线程礼让后");
            }
        });
       Thread t2=new Thread(new Runnable() {
           @Override
           public void run() {
                   System.out.println("lsssss");
           }
       });
       t2.start();
       t1.start();
    }
}

//有三种输出结果

//1. t1线程礼让后,t1线程继续抢到CPU时间片
  //线程礼让前
 //线程礼让后
 //lsssss

//2.t2线程先抢到CPU执行权,那么t1线程的礼让毫无意义
  // lsssss
  //   线程礼让前 
  // 线程礼让后


//3.t1线程抢到CPU执行权,然后礼让,t2线程执行,t2线程执行完后,让出cpu执行权,t1线程抢到时间片,继续执行
 // 线程礼让前
 // lsssss
 // 线程礼让后
线程的状态

从操作系统的层面来说,线程分为五个状态,分别为,初始,就绪,运行,阻塞,结束。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AgRQwGh3-1642903934004)(https://pic.imgdb.cn/item/61ce71222ab3f51d915ccfe8.png)]

  • 初始状态:线程被创建后的状态
  • 就绪状态:调用start()方法后线程进入就绪状态
  • 运行状态:线程获取到CPU的时间片,执行run()方法
  • 阻塞状态:线程被阻塞,等待阻塞结束后回到就绪状态
  • 结束:线程执行结束或抛出异常

从JAVA的角度来看,线程状态被分为六种

 public enum State {
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }
  • NEW:当线程新建后,没有调用start()方法
  • RUNNABLE: Java的RUNNABLE包括传统操作系统的ready和running两种状态
  • BLOCKED: 处于BLOCKED状态的线程正在等待锁的释放以进入同步区,没获取到锁的状态。(可以通过死锁打印状态看一下)
  • WAITING: 处于等待状态的线程变成RUNNABLE需要其他线程唤醒。
    • Object.wait() :使当前线程处于等待状态,直到另一个线程唤醒他
    • Thread.join():等待线程执行完毕,底层调用的是Object.wait()
    • LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。
  • TIMED_WAITING:超时等待状态,线程等待一个具体的时间,时间到后会自己唤醒。
    • Thread.sleep(long millis):使当前线程睡眠指定时间
    • Object.wait(long timeout):线程休眠指定时间,可以通过notify()/notifyAll()唤醒
    • Thread.join(long millis): 等待当前线程最多执行millis毫秒,如果millis则会一直执行
    • LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
    • LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;
  • TERMINATED,线程终止
线程中断

在某些情况下,我们在启动线程后发现并不需要他继续执行下去,需要中断线程。目前在JAVA里还没有安全直接的方法停止线程,但是提供了线程中断机制来处理需要线程中断的情况

线程中断机制是一种协作机制。线程中断并不能直接终止一个线程,而是需要通知被中断的线程自行处理。

//中断线程。这里的中断线程并不会立即停止线程,而是将线程的中断标志设置为true
//可以打断sleep,wait,join等显示的抛出InterruptedException方法的线程,但是打断后,线程的中断标志依旧是false
public void interrupt() {
}
//获取线程的中断标志,调用后不会修改线程的中断标志
public boolean isInterrupted() {
}
//获取线程的中断标志,调用后清空打断标记,即如果获取为true,调用后为false
public static boolean interrupted() {
}
线程安全

首先我们得知道什么时候会有线程安全问题,单线程肯定是线程安全的,多线程读共享资源时也不会有线程安全问题,但是当多线程对共享资源(临界区)进行读写时,就会遇到线程安全的问题。

package org.thread;

import java.util.concurrent.ConcurrentHashMap;
// 按照正常的思维来看count++和--都是500次,应该还是0
// 但是多线程情况下和单线程不一样
       //             count -- 原理,count++类似
       //             int temp=count;
       //             count=count-1;
       //             return temp;
// 假设当t1线程执行到for循环里面,这个时候count=0,然后进行线程切换,然后切换到t2,这个时候count还是0,++后count=1,这个时候线程在切换回t1,count还是0,这样子就会导致数据有问题,出现了线程安全问题
public class Demo15 {
    private static int count =0;

    public static void main(String[] args)  {
        Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 500; i++) {
                    count--;
                }
            }
        });

            //new ConcurrentHashMap<>()

       Thread t2 =new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 500; i++) {
                    count++;
                }
            }
        });

       t1.start();
       t2.start();
       t1.join();
       t2.join();
       //等待线程执行完毕打印count
        System.out.println(count);
    }
}
JAVA内存模型(JMM)

JMM定义了线程和主内存之间的抽象关系。

线程之间的共享变量存在主存中,每个线程都有一个私有的本地内存,存放着该线程读写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存,写缓冲区,寄存器等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z7xM2g2C-1642903934004)(https://pic.imgdb.cn/item/61d3c5152ab3f51d9112dbf8.png)]

1.所有的共享变量都在主存中

2.每个线程都可以在本地内存中保存该线程使用到的共享变量的副本

3.当线程A需要和线程B通信时,线程A首先将本地内存中的共享变量刷新到主存,然后线程B去主存中读取线程A更新后的共享变量

线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读取

​ 那么大家有没有想过线程本地内存怎么知道什么时候又是怎么样将数据同步到主内存,线程又怎么知道共享变量被其他线程更新了呢?这里就是JMM的功劳了

原子性:对共享内存的操作必须是要么全部执行直到执行结束,且中间过程不能被任何外部因素打断,要么就不执行

**可见性:**多线程操作共享内存时,确保执行结果能同步到主存中,并对其他线程可见

**有序性:**JVM在不影响程序正确性的情况下,可能会调整代码的执行顺序,这种情况被称为指令重排。单线程的情况下指令重排不会影响程序的正确性,但是多线程下指令重排就可能回带来线程安全问题

java提供了一系列的关键字和类来保证线程安全。

Synchronized

JAVA多线程的锁就是基于对象的,JAVA的每一个对象都可以做为锁,类锁其实也是对象锁。

Synchronized保证方法内部或代码块内部最多同一时刻只有一个线程在访问,大家不要理解成一旦线程获取了锁就会一直占用CPU执行结束,如果CPU时间片切换了,会执行未其他线程(不竞争当前锁的线程,因为当前锁还未释放),当线程继续抢到CPU执行权时,会继续执行

当一个线程执行完Synchronized内的代码时,会唤醒正在等待的线程

还有,大家在开发中一定加对锁,如果加不同的锁肯定是毫无影响的

// 关键字在实例方法上,锁为当前实例
public synchronized void lock() {
   System.out.println("Hello World");
}

// 关键字在静态方法上,锁为当前Class对象
public static synchronized void lock() {
    System.out.println("Hello World");
}

// 关键字在代码块上,锁为括号里面的对象
public void lock() {
    Object o = new Object();
    synchronized (o) {
        System.out.println("Hello World");
    }
}

我们分别来用 javap -c -s -v -l 命令 反编译一下以上三段代码

第一个例子,将synchronized加到普通方法上,我们可以看到有一个标识ACC_SYNCHRONIZED(同步方法)

JAVA多线程_第3张图片

第二个例子,将synchronized加到静态方法上,我们可以看到也有ACC_SYNCHROIZED(同步方法)标识,也多了一个ACC_STATIC(静态方法)标识

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xoKb8p6o-1642903934005)(https://pic.imgdb.cn/item/61d58ddf2ab3f51d91718d9a.jpg)]

第三个例子,synchronized代码块,我们可以看到有两个指令,一个是monitorenter,另一个是monitorexit

JAVA多线程_第4张图片

简单的来总结一下,同步方法中有一个标识ACC_SYNCHRONIZED标识,同步方法通过ACC_SYNCHRONIZED关键字对方法进行隐式的加锁,当线程要执行的方法被标注上ACC_SYNCHRONIZED关键字时,线程首先需要获取锁

同步代码块通过monitorenter和monitorexit指令的执行进行加锁和释放锁,当线程执行到monitorenter的时候要获得锁,才能执行代码块内的代码,执行到monitorexit的时候要释放锁。

每个对象自身维护了一个被加锁次数的计数器,当计数器的数字为0时,表示可以被任意线程获得锁,数字++,当计数器不为0时,只有当前获得锁的线程可以再次获得锁,也就是可重入锁。当执行monitorexit指令时,计数器的数字–。

ReentrantLock

一个可重入的互斥锁LOCK,它具有与使用Synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。

import java.util.concurrent.locks.ReentrantLock;

public class Demo22 {
    //new 一个锁对象
    private static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                test();
            }
        }).start();


        new Thread(new Runnable() {
            @Override
            public void run() {
               test();
            }
        }).start();

    }

    //切记,开发中一定不要忘了释放锁,如果不能确定代码一定不会出现异常,一定要把释放锁放到finally里,如果不释放锁,其他线程也都执行不了这段代码,就会卡住
    public static void test(){
        try {
            reentrantLock.lock();
            System.out.println("BestLee");
        }finally {
            reentrantLock.unlock();
        }
    }
}
Volatile

在java中,volatile是一个关键字,主要有两个功能(保证可见性,保证有序性)。volatile主要是通过内存屏障来实现的

  • 保证变量的内存可见性
  • 禁止指令重排

什么是内存屏障?硬件层面,内存屏障分为两种:读屏障和写屏障,内存屏障有两个作用

  • 阻止屏障两侧的指令重排序
  • 强制把写缓冲区/高速缓存中的脏数据等写回主存中,或者让缓存中对应的数据失效(这里的缓存指的是CPU缓存,如L1,L2,L3)

编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。编译器选择了一个

比较保守的JMM内存屏障插入策略,这样可以保证在任何处理器平台,任何屏障中都能得到正确的Volatile内存语义。

  • 在每个volatile写操作之前加一个StoreStore屏障
  • 在每个volatile写操作之后加一个StoreLoad屏障
  • 在每个volatile读操作之后加一个LoadLoad屏障
  • 在每个volatile读操作之后再加一个LoadStore屏障

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FRVndOLm-1642903934006)(https://pic.imgdb.cn/item/61d84b642ab3f51d914dde1b.png)]

  • StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,这个屏障会把Store1强制刷新到内存,保证Store1的写入操作对其它处理器可见。

  • StoreLoad屏障,对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读操作执行前,保证Store1的写入对所有处理器可见。

  • LoadLoad屏障,对于这样的语句Load1;LoadLoad;Load2,在Load2及后续读取操作的数据访问前,保证Load1要读取的数据读取完毕。

  • LoadStore屏障,对于这样的语句Load1;LoadStore;Store2,在Store2及后续写入操作被刷出前,保证Load1的数据要读取完毕。

package org.thread;

public class Demo25 {
    //共享变量
    private static volatile int num=0;
    public static void main(String[] args) throws InterruptedException {
       Thread t1 =new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 500; i++) {
                    num++;
                }
            }
        });
        Thread t2 =new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 500; i++) {
                    num--;
                }
            }
        });

        t1.start();
        t2.start();
        //使t1,t2线程执行完在打印num,否则可能num优先于t1,t2线程先打印
        t1.join();
        t2.join();

        //如果在单线程下,num结果肯定是0,但是在多线程下只有保证了JMM的三大特性才能确保结果的正确,Volatile能保证变量的有序性,可见性,但是不能保证原子性,i++又不是一个原子性操作,所以可能会出现线程安全问题
        System.out.println(num);
    }
}
  • volatile只对基本类型(byte,short,int,long,boolean,char,double,float)的赋值操作和对象的引用赋值操作有效。
  • 对于i++此类复合操作,volatile无法保证其原子性
乐观锁和悲观锁

**悲观锁:**我们常说的锁就是悲观锁,悲观锁认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加锁,以保证临界区内的程序同一时间只有一个线程执行。

乐观锁:乐观锁又称为"无锁",乐观锁总是假设对共享资源的访问没有冲突,线程可以不停的执行,无需加锁也无需等待,而一旦多个线程发生冲突,乐观锁通常使用CAS技术保证线程的安全性,乐观锁由于不用锁,不会出现死锁的问题

乐观锁适用于读多写少,避免频繁加锁影响性能。悲观锁适用于写多读少,避免频繁失败和重试影响性能。

CAS

CAS,全称Compare And Swap(比较并交换)

  • V:要更新的变量(var)
  • E:预期值(excepted)
  • N:新值(new)

判断V和E是否相等,如果相等,则将V的值设置为N,如果不等,说明已经有了其他线程更新V,当前线程自旋重试。

CAS是一种原子操作,是一条CPU的指令,从CPU层面保证原子性。

ABA问题

一个值原来是A,接着变成了B,然后又变成了A,这就是ABA问题,CAS是检测不出来变化的,但实际上值已经变化了两次。一般来说我们可以加上版本号或者时间戳来解决ABA问题。Atomic包里提供了一个AtomicStampedReference来解决ABA问题。

Atomic原子类

Atomic翻译过来就是原子的意思。在化学中,构成物质的最小单位是夸克(刚在百度搜的),那么这和我们要谈论的Atomic有什么关系呢?在很早以前,当时还没有提出质子,中子,夸克这些,所以构成物质的最小单位是原子,原子是不可分割的,在我们这里,Atomic是指一个操作是不可中断的,即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰

JAVA多线程_第5张图片

基本类型

使用原子的方式更新基本类型

  • AtomicInteger:整型原子类
  • AtomicBoolean:布尔型原子类
  • AtomicLong:长整型原子类
数组类型

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray:引用类型数组原子类
引用类型
  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子类型带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用CAS时可能发生的ABA问题。
  • AtomicMarkableReference:原子更新带有标记位的引用类型。
对象的属性修改类型
  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

以AtomicInteger的getAndAdd(int delta) 为例,看一看如何实现原子操作的

 //Unsafe对象
private static final Unsafe unsafe = Unsafe.getUnsafe();
//valueOffset
private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    //原子修改值并返回前一个值
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    
    // UnSafe的getAndAddInt方法,var1代表AtomicInteger对象,var2代表valueOffset(value 的偏移量),var 4代表新值
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //旧值
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); //如果CAS操作失败则继续循环,CAS成功则返回旧值

        return var5;
    }
AQS

AQS全称AbstractQueuedSynchronizer,即抽象队列同步器

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效的构造出应用广泛的同步器,比如ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask

AQS的核心思想

如果请求的共享资源空闲,则将当前的请求线程设置为有效的工作线程,并给共享资源加锁,如果被请求的共享资源被占用,则需要一个线程阻塞等待和线程唤醒时锁分配的机制,这个机制就是AQS,底层是用CLH队列实现的。

CLH队列是一个虚拟的双向队列(不存在队列实例,只存在各个节点之间的关联关系)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TZICQrAV-1642903934006)(https://pic.imgdb.cn/item/61e6bf7f2ab3f51d91467d0b.jpg)]

AQS使用一个int成员变量来表示同步状态,通过内置的CLH队列完成获取资源线程的排队。

//同步状态值
private volatile int state;

//获取同步状态值
protected final int getState() {
    return state;
}
//设置同步状态值
protected final void setState(int newState) {
    state = newState;
}
//通过CAS操作对state进行设置
protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
资源共享模式
  • 共享模式:可以同时被多个线程获取,如Semaphore,CountDownLatch
  • 独占模式:在同一时间只能被一个线程获取,如ReentrantLock
    • 公平锁:按照线程在队列中的顺序,在前的先获取锁
    • 非公平锁:各凭实力,谁先抢到就是谁的
static final class Node {
    
    //标记一个节点(节点对应的线程)在共享模式下等待
    static final Node SHARED = new Node();
    //标记一个节点(节点对应的线程)在独占模式下等待
    static final Node EXCLUSIVE = null;
    //waitStatus的值,表示该节点(对应的线程)已被取消
    static final int CANCELLED =  1;
    //waitStatus的值,表示后继节点(对应的线程)需要被唤醒
    static final int SIGNAL    = -1;
    //waitStatus的值,表示该节点(对应的线程)正在等待条件
    static final int CONDITION = -2;
    //waitStatus的值,表示有资源可用,新head节点需要唤醒后继节点(共享模式下,多线程并发释放资源,head唤醒其后继节点后,需要把多余的资源让给后面的节点,设置新head节点,会继续唤醒后面的节点)
    static final int PROPAGATE = -3;

    //等待状态
    volatile int waitStatus;
    //前驱节点
    volatile Node prev;
    //后继节点
    volatile Node next;
    //节点对应的线程
    volatile Thread thread;
    //等待队列里下一个等待条件的节点
    Node nextWaiter;
    //判断是否为共享模式
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    //返回当前节点的前驱节点
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {    // Used to establish initial head or SHARED marker
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

AQS的设计是基于模板方法设计模式的,它有一些方法必须需要子类去实现

    protected boolean tryAcquire(int arg); //在独占模式下,尝试获取资源,成功则true,失败false
    protected boolean tryRelease(int arg);//在独占模式下,尝试释放资源,成功则true,失败false
    protected int tryAcquireShared(int arg);//在共享模式下,尝试获取资源,负数代表失败,0代表获取资源成功,但无剩余可用资源,正数代表成功,返回值代表剩余可用资源数目
    protected boolean tryReleaseShared(int arg);//在共享模式下,尝试释放资源,true代表成功,false代表失败
    protected boolean isHeldExclusively();//该线程是否独占资源,如果独占返回true,否则为false,只有用到Condition才用去实现这个方法
获取资源

获取资源的方法是acquire(int arg) 方法。arg是要获取资源的个数,在独占模式下始终为1。

    public final void acquire(int arg) {
    //首先调用tryAcquire()方法,如果失败了则调用acquireQueued方法
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //中断当前线程
            selfInterrupt();
    }

我们来看一下addWaiter(Node.EXCLUSIVE)方法

private Node addWaiter(Node mode) {
   //以独占模式生成对应的节点
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    //将tail(尾节点)赋值给一个节点
    Node pred = tail;
    //判断pred是不是为空,如果为空,则说明队列为空,不为空则将生成的节点添加到尾结点的后面
    if (pred != null) {
        //新节点的上一个节点为尾节点赋值的节点
        node.prev = pred;
        //CAS操作,如果成功,则将pred的next节点设置为node,并返回节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果队列为空或者CAS失败,则将节点放到队列里
    enq(node);
    return node;
}

接下来我们再看acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法

final boolean acquireQueued(final Node node, int arg) {
    //定义一个标记failed默认为true
    boolean failed = true;
    try {
        //定义一个中断标记默认为false
        boolean interrupted = false;
        for (;;) {
            //获取node的前一个节点
            final Node p = node.predecessor();
            //如果前一个节点是头节点并且尝试获取资源成功
            if (p == head && tryAcquire(arg)) {
                //将node节点设置为头节点
                setHead(node);
                //并将原本头节点的next节点设置为Null
                p.next = null; // help GC
                //failed标记设置为false
                failed = false;
                //返回false,表示未在等待时中断
                return interrupted;
            }
            //检查并更新未能获取的节点的状态。 如果线程应该阻塞,则返回 true。 这是所有采集循环中的主要信号控制。 要求 pred == node.prev
            if (shouldParkAfterFailedAcquire(p, node) &&
                //中断线程,底层调用的是LockSupport.park();
                parkAndCheckInterrupt())
                //中断标记设置为true
                interrupted = true;
        }
    } finally {
        //如果failed标记为true
        if (failed)
            //取消正在进行的尝试获取
            cancelAcquire(node);
    }
}
释放资源
public final boolean release(int arg) {
    //如果尝试释放资源成功
    if (tryRelease(arg)) {
        //将头节点赋给h节点
        Node h = head;
        //判断h节点是否不为空和waisStatus是否不为0
        if (h != null && h.waitStatus != 0)
           //唤醒节点的下一个节点(如果存在)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
private void unparkSuccessor(Node node) {
    
    int ws = node.waitStatus;
    //如果waitStatus小于0,则将waitStatus设置为0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    //获取node节点的后置节点
    Node s = node.next;
    //
    if (s == null || s.waitStatus > 0) {
        s = null;
        //等待队列的所有使用的节点向前移动
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //如果s的后置节点不为Null,则唤醒对应的线程
    if (s != null)
        LockSupport.unpark(s.thread);
}
线程池
为什么使用线程池?
  • 降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁的资源消耗。
  • 提高响应速度,当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 方便线程管理。
ThreadPoolExecutor的构造方法
  //五个构造参数
  public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
    
    
        //六个构造参数
        public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }
        //六个构造参数,与上边不同的是最后一个参数上边的是线程工厂,下边的是拒绝策略
        public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }
     
        //七个参数
        public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

我们来分别看一下构造方法的每个参数分别是啥

  • corePoolSize:核心线程数量,线程池中有两类线程,一种是核心线程,一种是非核心线程。核心线程默认情况下会一直存在于线程池里(核心员工),非核心线程如果长时间闲置,就会销毁(外包员工,干完活滚蛋)。

  • **maximumPoolSize:**线程池中允许的最大线程数量(线程数量=核心线程数+非核心线程数)

  • **keepAliveTime:**非核心线程闲置超时时长。(如果设置了allowCoreThreadTimeOut(默认为true),则核心线程也会在keepAliveTime时间到后被销毁)

  • **workQueue:**阻塞队列,维护着等待执行的Runnable对象。

    常用的阻塞队列如下

    • LinkedBlockingQueue,链式阻塞队列,容量为Integer.MAX_VALUE
    • ArrayBlockingQueue,数组阻塞队列
    • SynchronousQueue,同步队列,内部容量为0
    • DelayQueue,延迟队列,只有在延迟时间到了后,才能从队列中取出元素。
  • **threadFactory:**创建线程的工厂,用于批量创建线程,如果不指定,则使用DefaultThreadFactory

  • **RejectedExecutionHandler handler:**拒绝处理策略,线程数大于最大线程数量,就会执行拒绝处理策略。

    • **ThreadPoolExecutor.AbortPolicy:**默认的拒绝处理策略,丢弃任务,抛出RejectedExecutionException
    • **ThreadPoolExecutor.CallerRunsPolicy:**由调用线程直接处理任务,除非程序已关闭,将丢弃任务。
    • **ThreadPoolExecutor.DiscardPolicy:**丢弃任务,并且不抛出异常。
    • **ThreadPoolExecutor.DiscardOldestPolicy:**丢弃队列最头部的请求,然后尝试执行此任务(如果失败则继续重试),如果程序已关闭,将丢弃任务。
线程池状态
//线程池创建后处于RUNNING状态
private static final int RUNNING    = -1 << COUNT_BITS;
//调用shutdown()方法后处于SHUTDOWN状态,线程池不会接受新的任务,清除一下空闲Worker,不会等待阻塞队列里的任务完成
private static final int SHUTDOWN   =  0 << COUNT_BITS;
//调用shutdownNow()方法处于STOP状态,中断所有线程,丢弃阻塞队列中的任务。
private static final int STOP       =  1 << COUNT_BITS;
//当所有的任务已终止,线程池为RIDYING状态,然后执行terminated()方法,线程池为TERMINATEDz
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;
解析excute()方法
public void execute(Runnable command) {
    //判断任务是否为null
    if (command == null)
        throw new NullPointerException();
    
    int c = ctl.get();
    //判断工作线程数量是否小于核心线程数
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //如果大于核心线程数,则加入到任务队列中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        //如果不处于RUNNIG状态,则将任务在队列里移除
        if (! isRunning(recheck) && remove(command))
            reject(command);
        //如果处于RUNNING状态,但没有线程,则创建线程
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    //如果加入队列失败,则执行拒绝策略
    else if (!addWorker(command, false))
        reject(command);
}
execute执行流程

JAVA多线程_第6张图片

addWorker方法解析
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        //线程池状态,任务,队列的判断条件
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
            //工作的线程数量
            int wc = workerCountOf(c);
            //如果工作的线程数量大于容量或者大于核心线程数或者最大线程数(看core的值是啥)
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            //对workerCount进行原子递增,如果递增成功,则跳出循环
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            //如果rs的值发生变化,则继续循环
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    //定义两个循环和一个worker对象
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        //实例化上面的worker对象
        w = new Worker(firstTask);
        //定义一个线程对象,使用worker的thread
        final Thread t = w.thread;
        //如果线程对象不为Null
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            //加锁
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());
                //判断线程池是否处于RUNNING状态或者处于SHUNTDOWN状态但是任务为Null
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    //判断线程是否处于运行状态,如果处于运行状态,则抛异常
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    //加入到集合中,workers底层是一个HashSet(无序不重复)
                    workers.add(w);
                    //获取workers集合的长度
                    int s = workers.size();
                    //如果集合的长度大于跟踪获得的最大池大小,则将集合的长度赋值给largestPoolSize
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    //将workerAdded字段设置为true
                    workerAdded = true;
                }
            } finally {
                //释放锁
                mainLock.unlock();
            }
            //如果workerAdded字段为true
            if (workerAdded) {
                //则启动线程
                t.start();
                //线程执行后将workerStarted字段设置为true
                workerStarted = true;
            }
        }
    } finally {
        //如果线程启动失败,则执行addWorkerFailed(w)f
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}
Worker源码解析

我们上边t.start()方法调用后将会调用对应线程的run()方法

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{

     Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
        
        
        
        public void run() {
            runWorker(this);
        }

}
runWorker方法
final void runWorker(Worker w) {
    //获取当前线程
    Thread wt = Thread.currentThread();
    //拿到worker的任务,也就是我们传进来的runnale对象,在addWorker方法里,我们将Runable传了进来
    Runnable task = w.firstTask;
    //将worker的firstTask字段设置为Null
    w.firstTask = null;
    //尝试释放资源,底层使用的是AQS的release()方法
    w.unlock(); // allow interrupts
    //定义一个变量为true
    boolean completedAbruptly = true;
    try {
        //如果我们传过来的runnable不为空或者getTask()不为空,则进入循环  getTask是从队列里取任务
        while (task != null || (task = getTask()) != null) {
            //尝试获取资源
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            //检查线程池状态,倘若线程池处于中断状态,当前线程将中断
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                //执行beforeExecute方法
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    //执行任务
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    //执行afterExecte方法
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                //尝试释放资源
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}
getTask方法
private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        //判断一下线程池的状态以及队列是否为空
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        //定义一个标识timed,allowCoreThreadTimeOut为核心线程是否也需要在指定时间内空闲后销毁,判断工作的线程是否大于核心线程
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        //工作线程数量,队列的一些判断,如果成立,则递减worker
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            //如果timed为true,则会调用workQueue.poll(),如果为Null,则继续循环
            //如果timed为false,则执行 workQueue.take()方法,将线程阻塞,take()方法如果结果为Null是不返回的,底层使用了Condition的await()方法
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

你可能感兴趣的:(java,开发语言,后端,线程池,多线程)