Java多线程总结

Posted 赵jc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程总结相关的知识,希望对你有一定的参考价值。

并行与并发

  • 并发:多个进程在一个CPU下采用时间片轮转的方式,在一段时间之内,让多个进程都得以推进,称之为并发(实际上并没有同时进行)。
  • 并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行。

并发与并行类似于工厂中的流水线,要扩大产量,1是考虑建造多个工厂,这就是并行,2是考虑每个工厂中新增流水 线,这就类似并发。

上下文切换

多核cpu下,多线程是并行工作的,如果线程数多,单个核又会并发的调度线程,运行时会有上下文切换的概念cpu执行线程的任务时,会为线程分配时间片,以下几种情况会发生上下文切换。

  • 线程的cpu时间片用完
  • 垃圾回收
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当发生上下文切换时,操作系统会保存当前线程的状态,并恢复另一个线程的状态,jvm中有块内存地址叫程序计数器,用于记录线程执行到哪一行代码,是线程私有的。

进程与线程

进程(系统资源分配得最小单位)

  • 当一个程序被运行,就开启了一个进程, 比如启动了qq
  • 程序由指令和数据组成,指令要运行,数据要加载,指令被cpu加载运行,数据被加载到内存,指令运行时可由cpu调度硬盘、网络等设备

线程(系统调度的最小单位)

  • 一个进程内可分为多个线程(一个进程中最少有一个主线程)
  • 一个线程就是一个指令流,cpu调度的最小单位,由cpu一条一条执行指令

进和线程的区别
在这里插入图片描述

多线程的好处

  • 程序运行的更快!
  • 充分利用cpu资源。

多线程的应用场景

  • 工作量大,执行时间比较长的任务
  • 让阻塞的代码不影响后续代码的执行(后续的代码在其他线程执行)

Thread类的常见方法

静态方法:作用在当前线代码所在的线程

  • static Thread currentThread() 获取代码行所在的当前线程
  • static void sleep(long millis) 让当前线程休眠给定的时间,会抛出InterruptedException异常
  • static void **yield()**当前线程让步,从运行态变为就绪态
  • static boolean interrupted() 判断当前线程的中断标志被设置,清除中断标志

实例构造方法:作用在调用的线程对象上

  • void start() 启动线程,申请系统调度该线程
  • void run()定义线程的任务
  • void interrupt()中断一个线程
  • booelan isIntrrupted()
  • void join()无条件等待:当前线程阻塞并等待,一直等到调用的线程执行完毕,也可以传入一个参数,表示限时等待:当前线程阻塞并等待,直到调用线程执行完毕,或者时间到了,再往下执行
  • boolean isAlive()是否存活,即简单的理解,为 run 方法是否运行结束了
  • String getName()获取线程名称
  • int getPriority() 获取线程优先级 0-10的数值
  • boolean isDaemon()是否为后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。

多线程的基本使用

线程的状态 state

在这里插入图片描述
大家不要被这个状态转移图吓到,我们重点是要理解状态的意义以及各个状态的具体意思。
在这里插入图片描述
我们来举个例子:

  • 刚把李四、王五找来,给他们在安排任务,没让他们行动起来,就是 NEW 状态;
  • 当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE
    状态。该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;
  • 当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入 BLOCKED 、WATING 、 TIMED_WAITING 状态
  • 如果李四、王五已经忙完,为 TERMINATED 状态。

线程的创建 new

  • 继承Thread类
    // 方法一 继承Thread 单继承
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("子线程" + Thread.currentThread().getName());
        }
    }
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        System.out.println("主线程" + Thread.currentThread().getName());
    }
  • 实现Runnanle接口(重写run()方法)
 //方法一 实现Runnable接口
    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("线程名:" +
                    Thread.currentThread().getName());
        }
    }
    public static void main(String[] args) {
        // 创建 Runnable 子对象
        MyRunnable myRunnable = new MyRunnable();
        // 创建线程
        Thread thread = new Thread(myRunnable);
        // 启动线程
        thread.start();
    }


    //方法二 创建一个匿名 Runnable 常用
    public static void main(String[] args) {
        // 创建一个匿名 Runnable 类
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("当前线程:" +
                        Thread.currentThread().getName());
            }
        });
        thread.start();
    }

    //方法三 lambda + runnable 常用
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("当前线程" + Thread.currentThread().getName());
        });
        thread.start();
    }
  • 实现Callable接口(重写call()方法)可以拿到线程的返回值(常用)
    //方法以 可以拿到线程的返回值(常用)
    static class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            int num =  new Random().nextInt(10);
            System.out.println(String.format("线程%s 产生的随机数: %d",Thread.currentThread().getName(),num));
            return num;
        }
    }
    public static void main6(String[] args) throws ExecutionException, InterruptedException {
        // 1.创建 Callable 子对象
        MyCallable callable = new MyCallable();
        // 2.使用 FutrueTask 接收 Callable
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        // 3.创建线程并设置任务
        Thread thread = new Thread(futureTask);
        // 执行线程
        thread.start();
        // 得到线程的执行结果
        int ret = futureTask.get();
        System.out.println("拿到的随机数为ret:" + ret);
    }

线程的启动 start()

之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。

  • 覆写 run 方法是提供给线程要做的事情
  • 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。

start()run()的区别:

  • start()是线程的开启方法,它使用新的线程来执行任务,run()是一个对象的普通方法,它使用当前线程来执行任务
  • start()方法可以执行一次,但run()可以调用多次
  • 如果不调用start(),而是直接调用run(),相当于java对象直接调用普通的实例方法

线程的休眠 sleep()

休眠一个线程,会抛出InterredExcep异常

  • 方式一
    Thread.sleep( 1000);// 休眠 1 秒

  • 方式二

    TimeUnit.SECONDS.sleep(1); // 休眠 1 秒
    TimeUnit.HOURS.sleep(1); // 休眠 1 小时

  • 方式三
    Thread.sleep(TimeUnit.SECONDS.toMillis(1));//休眠一秒

线程的等待 join()

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。
在这里插入图片描述

线程的中断 Interrupt

当线程进入运行态时,如果发生紧急情况,我们可以中断线程。
目前常见的有以下两种方式:
在这里插入图片描述

  • 1.使用自定义的全局变量来终止(但当线程阻塞时不行终止线程,舍弃)

  • 2.实例方法Thread.currentThread().interrupt() 设置代用线程的中断标志位为true,至于是否被中断,由当前线程决定(如果线程处于阻塞(调用wait/join/sleep),则会中断并且抛出InterruptedException异常,并重置标志位

  • 3.实例方法Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
    在这里插入图片描述

  • 4.静态方法Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志(静态的,大家都可以用,所以使用完后要恢复,方便下一次使用)

在这里插入图片描述

线程通信 wait()notify()

所谓的线程通信是指在一个线程中的操作可以影响到另一个线程。
wait()线程等待
其实wait()方法就是使线程停止运行。

  • wait方法在执行前必须先加锁(wait配合synchronized一起使用)
  • wait和notifiy在在配合使用时一定要操作同一把锁
  • wait在不传递任何参数的情况下会进入waiting状态(其实底层调用了wait(0)这个方法),当传入一个大于0的整数时,它会进入timed_waiting状态
  • wait在执行时会释放锁

notify()线程唤醒(随机唤醒一个)

notifyAll()线程唤醒(全部唤醒)

wait和sleep的区别
相同点

  • wait和sleep都是让线程进入休眠状态
  • wait和sleep在执行的过程中都可以接收到线程终止的通知

不同点

  • wait必须配合synchronized一起使用,而sleep不用
  • wait会释放锁,而sleep不会释放锁
  • wait是Object的方法,而sleep是Thread(线程)的方法
  • 默认情况下wait(不传递任何参数或者参数为0的情况下)它会进入waiting状态,而sleep会进入timed_waiting状态
  • 使用wait时可以主动的唤醒线程,而使用sleep时不能主动地唤醒线程

sleep(0)和wait(0)的区别

  • sleep(0)表示过了0ms之后会继续执行,而wait(0)会一直休眠
  • sleep(0)会重新出发一次CPU竞争

为什么wait会释放锁,而sleep不会释放锁?

  • sleep必须要传入一个最大等待时间,也就是说sleep是可控的(对于时间层面来说),而wait是不可以传递参数的,如果wait不主动释放锁的话就,没被唤醒前就会一直阻塞

为什么wait是Object的方法,而sleep是Thread的方法?
wait需要操作锁,而锁是属于对象级别的(存放在对象头当中)它不是线程级别的,一个线程可以有多把锁,为了灵活起见,所以讲wait放在了Object当中

解决wait/notify随机唤醒的问题(指定唤醒某个线程)

  • LockSupport park()/unpark(线程)
public class Test {
    public static void main(String[] args) throws InterruptedException {


        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 让线程进行休眠
                LockSupport.park();
                System.out.println("唤醒 t1");
            }
        }, "t1");


        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 让线程进行休眠
                LockSupport.park();
                System.out.println("唤醒 t2");
            }
        }, "t2");

        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 让线程进行休眠
                LockSupport.park();
                System.out.println("唤醒 t3");
            }
        }, "t3");

        t1.start();
        t2.start();
        t3.start();

        LockSupport.unpark(t2);

    }
}
  • LockSupporrt()虽然不会报Interrupt的异常,但依然可以监听到线程终止的指令
ublic class Main {
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("park 之前 Interrupt 状态:" +
                        Thread.currentThread().isInterrupted());
                // 线程进入休眠
                LockSupport.park();
                System.out.println("park 之后 Interrupt 状态:" +
                        Thread.currentThread().isInterrupted());
            }
        }, "t1");
        // 启动线程
        t1.start();

        Thread.sleep(100);
        // 中止线程
        t1.interrupt();
        // 唤醒线程 t1
        LockSupport.unpark(t1);
    }
}

线程安全

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的,否则即为不安全的。

多线程不安全的原因

CPU是抢占式执行的(万恶之源)
非原子性
什么是原子性
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还 没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
一条 java 语句不一定是原子的,也不一定只是一条指令
比如 n++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU
  2. 进行数据更新
  3. 把数据写回到 CPU

或者new 一个对象, ListNode tmp = new ListNode();

  1. 创建初始化内存空间
  2. new对象
  3. 赋值给变量

内存可见性
主内存-工作内存
在这里插入图片描述
为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题

编译器优化/指令重排序

操作的是一个变量

解决线程不安全问题

volatile关键字

  • 可以解决内存不可见和指令重排序的问题,
  • 但不可以解决原子性的问题
    使用场景:
  • 写操作不依赖共享变量,赋值的是一个常量(依赖共享变量是原子性操作)
  • 作用在读,写依赖其他手段(加锁)

synchronized 关键字(监视器锁monitor lock)

synchronized的底层是使用操作系统的mutex lock实现的。(jvm层面来解决问题的)

  • 当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中
  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量

synchronized用的锁是存在Java对象头里的(Java层面)。
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题; 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
在这里插入图片描述
synchronized实现:

  • 针对操作系统层面,它是依靠互斥锁mutex
  • 针对JVM层面,它是依靠monitor来实现
  • 针对Java语言来说,是将锁信息存放在对象头(标识,锁的状态,所得拥有者)

synchronized的三种使用场景

  • 使用synchronized修饰代码块(可以给任意对象进行加锁)
  • 使用synchronized来修是静态方法(对当前的类进行加锁)
  • 使用synchronized来修饰普通实例方法(对当前类实例进行加锁)

sunchronized锁升级的过程(JDK1.6以后)
在这里插入图片描述

lock()手动加锁

加锁的方式
在这里插入图片描述
lock的使用场景

  • lock只能修饰代码块

注意事项:
lock()操作一定要放在try外面,如果放在try里面可能会造成两个问题:

  • 如果try里面抛异常了,还没有加锁成功就执行了finally里面的释放所得操作(但此时还没有得到锁呢)
  • 如果放在try里面,如果没有锁的情况下试图释放锁,这个时候产生的异常就会将业务代码(也就是try里面的异常)给覆盖掉,增加了代码调试的难度

公平锁和非公平锁

  • 公平锁:一个线程释放锁,(主动)唤醒“需要得到锁”的就绪队列里的线程来得到锁
  • 非公平锁:当一个线程释放锁之后,另一个线程刚好执行到获取锁的代码就直接可以获取锁(效率更高)

在Java语言中所有的锁默认都是非公平锁(synchronized和ReentrantLock()默认都是非公平锁),但lock可以显示声明公平锁

 Lock lock = new ReentrantLock(true);

synchronized和lock的区别

  • synchronized自行进行加锁和释放锁,二lock需要手动进行加锁和解锁
  • lock是Java层面锁的实现的,二synchronized是JVM层面实现的
  • synchronized可以修饰代码块、静态方法、实例方法,而lock只能修饰代码块
  • synchronized只能实现非公平锁,但lock可以实现非公平锁和公平锁
  • lock的灵活性更高(tryLock)

死锁

死锁:在两个或者两个以上的线程运行中,因为资源抢占而造成线程一直等待的问题。
在这里插入图片描述
简易的死锁代码

public class text {
    public static void main(String[] args) {
        //定义两个锁对象
       Object lockA = new Object();
       Object lockB = new Object();
       Thread t1 = new Thread(new Runnable() {
           @Override
           public void run() {
               synchronized(lockA) {
                   String threadName = Thread.currentThread().getName();
                System.out.println(threadName + "得到lockA等待lockB");
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   synchronized(lockB) {
                       System.out.println("Wait B");
                   }
               }
           }
       }, "t1");
       Thread t2 = new Thread(new Runnable() {
           @Override
           public void run() {
               String threadName = Thread.currentThread().getName();
                System.out.println(threadName + "得到lockB等待lockA" );
               synchronized(lockB) {
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   synchronized(lockA){
                       System.out.println("Wait A");
                   }
               }
           }
       }, "t2");
       t1.start();
       t2.start();
    }
}

造成死锁的四个条件:

  • 互斥条件:当资源被一个线程拥有之后,就不能被其他的线程拥有了(不可更改)
  • 请求拥有条件:当一个线程拥有了一个资源之后又试图请求另一个资源(可以解决)
  • 不可剥夺条件:当一个资源被一个线程拥有之后,如果不是这个线程主动释放此资源的情况下,其他线程不能拥有此资源(不可更改)
  • 环路等待条件:两个或两个以上的线程在拥有了资源之后,试图获取对方资源的时候形成了一个环路(可以解决)

如何解决死锁

  • 控制加锁的顺序(解决环路的等待条件)

多线程案例

单例模式

  • 饿汉模式
class Singleton {
    private static Singleton instance = new Singleton(); 
    private Singleton() {}
    public static Singleton getInstance() { 
        return instance;
    }
}
  • 懒汉模式(单线程版)
class Singleton {
    private static Singleton instance = null; 
    private Singleton() {}
    public static Singleton getInstance() { 
        if (instance == null) {
        instance = new Singleton();
        }
        return instance;
    }
}
  • 懒汉模式(多线程版,效率低)

class Singleton {
    private static Singleton instance = null; 
    private Singleton() {}
    public synchronized static Singleton getInstance() { 
        if (instance == null) {
        instance = new Singleton();
        }
        return instance;
    }
}

  • 懒汉模式(双重校验锁版,性能高)
class Singleton {
    private static volatile Singleton instance = null; 
    private Singleton() {}
    public static Singleton getInstance() { 
            if (instance == null) {
            synchronized (Singleton.class) { 
                if (instance == null) {
                instance = new Singleton()经验总结:Java高级工程师面试题-字节跳动,成功跳槽阿里!

学习java第19天个人总结

号称史上最全Java多线程与并发面试题总结—基础篇

java多线程总结

Java多线程-线程池的使用与线程总结(狂神说含代码)

第十周java学习总结