Java 基础学习笔记(九) 多线程

    科技2022-08-03  112

    多线程


    一、序章

    程序: 指令+数据byte序列;

    进程:(进程就是程序执行的过程)进程是程序的一次执行,它是一个动态概念,是系统资源分配的单位;

    通常一个进程中包含多个线程,(一个进程中至少有一个线程);线程就是独立执行的路径;在程序执行时,即使没有自己创建线程,后台也会有多个线程,如主线程,GC线程;main()称之为主线程,为系统的入口,用于执行整个程序;在一个进程中如果开辟了多个线程,线程的运行有调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为干预的;对同一份资源时,会存在资源抢夺问题,需要加入并发控制;线程会带来额外的开销,如CPU调度时间,并发控制开销;

    线程:线程是一个动态执行的过程,它也有一个从产生到死亡的过程。

    并发:进程是并发运行的,系统将时间分为很多时间片段,尽可能均分给正在运行的程序;

    ​ 微观上:进程是走走停停的状态;

    ​ 宏观上:都在运行;这种现象就简称为并发;

    普通执行和线程执行:

    二、线程的创建

    创建一个新的执行线程有两种方法。 一个是将一个类声明为Thread的子类。 这个子类应该重写run类的方法Thread 。 然后可以分配并启动子类的实例(调用start()方法启动)。 自定义线程类继承Thread类重写run()方法,编写线程执行体;创建线程对象,调用start()方法启动线程;注意:线程启动并不一定立即执行;由CPU调度执行; 另一种方法来创建一个线程是声明实现类Runnable接口。 那个类然后实现了run方法。 然后可以分配类的实例,在创建Thread时作为参数传递,并启动; 定义MyRunnable线程类继承TRunnable接口重写run()方法,编写 线程执行体;创建线程对象,调用start()方法启动线程;注意:线程启动并不一定立即执行;由CPU调度执行;

    三、线程的生命周期

    线程是一个动态执行的过程,它也有一个从产生到死亡的过程。

    新建状态:

    实现Runnable接口和继承Thread类可以得到一个线程类,new一个实例出来,线程就进入了初始状态

    就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

    可运行状态只是说你有资格运行,调度程序没有挑选到你,你就永远是可运行状态(就绪状态)调用线程的start()方法,此线程进入可运行状态。当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入可运行状态。当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入可运行状态。锁池里的线程拿到对象锁后,进入可运行状态。

    运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

    阻塞状态:

    当前线程T调用Thread.sleep() ;suspend(挂起)等方法,当前线程进入阻塞状态。运行在当前线程里的其它线程t2调用join()方法,当前线程进入阻塞状态。等待用户输入的时候,当前线程进入阻塞状态。

    在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    等待阻塞:运行状态中的线程执行wait()方法,使线程进入到等待阻塞状态。同步阻塞:线程在获取synchronized 同步锁失败(因为同步锁被其他线程占用)。其他阻塞:通过调用线程的sleep()或join()发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep()状态超时,join()等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

    死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

    、等待队列(本是Object里的方法,但影响了线程)

    调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj)代码段内。

    锁池状态

    当前线程想调用对象A的同步方法时,发现对象A的锁被别的线程占有,此时当前线程进入锁池状态。简言之,锁池里面放的都是想争夺对象锁的线程。当一个线程1被另外一个线程2唤醒时,1线程进入锁池状态,去争夺对象锁。锁池是在同步的环境下才有的概念,一个对象对应一个锁池。

    四、线程状态的控制

    static voidsleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。static voidsleep(long millis, int nanos) 在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。voidstart() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。static voidyield() 暂停当前正在执行的线程对象,并执行其他线程。voidinterrupt() 中断线程。voidjoin(long millis) 等待该线程终止的时间最长为 millis 毫秒。voidjoin(long millis, int nanos) 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。voidsetDaemon(boolean on) 将该线程标记为守护线程或用户线程。voidsetPriority(int newPriority) 更改线程的优先级。

    线程睡眠——sleep

    如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep方法,从上面可以看到sleep方法有两种重载的形式,

    每个对象都有一个锁,sleep不会释放锁;

    public class Test1 { public static void main(String[] args) throws InterruptedException { for(int i=0;i<100;i++){ System.out.println("main"+i); Thread.sleep(100); } } }

    线程让步——yield

    yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。ield()方法和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态 package thread; public class Test1 { public static void main(String[] args) throws InterruptedException { new MyThread("低级", 1).start(); new MyThread("中级", 5).start(); new MyThread("高级", 10).start(); } } class MyThread extends Thread { public MyThread(String name, int pro) { super(name);// 设置线程的名称 this.setPriority(pro);// 设置优先级 } @Override public void run() { for (int i = 0; i < 30; i++) { System.out.println(this.getName() + "线程第" + i + "次执行!"); if (i % 5 == 0) Thread.yield(); } } }

    sleep()方法和yield()方的区别

    ①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。

    ②、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。

    ③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。

    线程合并——join

    线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。 从上面的方法的列表可以看到,它有3个重载的方法: void join() ​ 当前线程等该加入该线程后面,等待该线程终止。 void join(long millis) ​ 当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度 void join(long millis,int nanos) ​ 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度 package thread; public class DemoJoin { public static void main(String[] args) throws InterruptedException { MyThread2 thread=new MyThread2(); thread.start(); thread.join(1);//将主线程加入到子线程后面,不过如果子线程在1毫秒时间内没执行完,则主线程便不再等待它执行完,进入就绪状态,等待cpu调度 for(int i=0;i<30;i++){ System.out.println(Thread.currentThread().getName() + "线程第" + i + "次执行!"); } } } class MyThread2 extends Thread { @Override public void run() { for (int i = 0; i < 200; i++) { System.out.println(this.getName() + "线程第" + i + "次执行!"); } } }

    几个方法的比较

    Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入阻塞,但不释放对象锁,millis后线程自动苏醒进入可运行状态。作用:给其它线程执行机会的最佳方式。Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。t.join()/t.join(long millis),当前线程里调用其它线程1的join方法,当前线程阻塞,但不释放对象锁,直到线程1执行完毕或者millis时间到,当前线程进入可运行状态。obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。

    守护线程

    前台线程执行完后,不管后台线程(守护线程)有没有执行完,程序结束运行;守护线程与普通线程写法上基本么啥区别,调用线程对象的方法setDaemon(true),则可以将其设置为守护线程,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。setDaemon方法的详细说明: public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。 参数: ​ on - 如果为 true,则将该线程标记为守护线程。 抛出: ​ IllegalThreadStateException - 如果该线程处于活动状态。 ​ SecurityException - 如果当前线程无法修改该线程。 /** * Java线程:线程的调度-守护线程 */ public class Test { public static void main(String[] args) { Thread t1 = new MyCommon(); Thread t2 = new Thread(new MyDaemon()); t2.setDaemon(true); //设置为守护线程 t2.start(); t1.start(); } } class MyCommon extends Thread { public void run() { for (int i = 0; i < 5; i++) { System.out.println("线程1第" + i + "次执行!"); try { Thread.sleep(7); } catch (InterruptedException e) { e.printStackTrace(); } } } } class MyDaemon implements Runnable { public void run() { for (long i = 0; i < 9999999L; i++) { System.out.println("后台线程第" + i + "次执行!"); try { Thread.sleep(7); } catch (InterruptedException e) { e.printStackTrace(); } } } } 后台线程第0次执行! 线程10次执行! 线程11次执行! 后台线程第1次执行! 后台线程第2次执行! 线程12次执行! 线程13次执行! 后台线程第3次执行! 线程14次执行! 后台线程第4次执行! 后台线程第5次执行! 后台线程第6次执行! 后台线程第7次执行!

    线程的优先级

    java线程执行时有不同的优先级,优先级范围 1~10;默认值 5;可以通过Thread中的setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级先设置优先级在启动;

    五、Lamda 表达式

    new Thread (()->System.out.println("多线程学习。。。。")).start(); 函数式接口任何接口吧,如果自包含唯一抽象方法,那么它就是一个函数式接口;Lamda表达式只有一行的情况下才能简写为一行;如果有多行,就得使用代码块;如果有多个参数也可以去掉参数类型,但是要去就都去,必须加上括号;、 代码推导λ表达式 package com.haiyang.lamda; public class TestLamda01 { //3、静态内部类,同样实现ILike接口 static class Like2 implements ILike{ @Override public void lamda() { System.out.println("i like lamda2"); } } public static void main(String[] args) { ILike like = new Like(); like.lamda(); like = new Like2(); like.lamda(); //4、局部内部类 class Like3 implements ILike{ @Override public void lamda() { System.out.println("i like lamda3"); } } like = new Like3(); like.lamda(); //5、匿名内部类,没有类的名称。必须借助接口或者父类 like = new ILike() { @Override public void lamda() { System.out.println("i like lamda4"); } }; like.lamda(); //6、lamda表达式 like = ()->{ System.out.println("i like lamda5"); }; like.lamda(); } } //1、定义一个只有一个抽象方法的接口 interface ILike{ abstract void lamda(); } //2、实现类 class Like implements ILike{ @Override public void lamda() { System.out.println("i like lamda1"); } }

    带一个参数的Lamda简化

    package com.haiyang.lamda; public class TestLamda02 { public static void main(String[] args) { ILive live = null; //Lamda简化 live = (int a)->{ System.out.println("I live you"+a); }; live.live(1); //Lamda简化参数类型 live = (a)->{ System.out.println("I live you"+a); }; live.live(2); //Lamda简化括号 live = a->{ System.out.println("I live you"+a); }; live.live(3); //Lamda简化花括号(只有一条语句时) live = a->System.out.println("I live you"+a); live.live(4); } } interface ILive{ abstract void live(int a); } package thread; //lamda表达式;前提函数式接口 public class DemoLamda { public static void main(String[] args) { ILove love =new ILove() { @Override public void love(int a) { System.out.println("I love you "+a); } }; love.love(2); ILove love1 =(int a)->{ System.out.println("I love you "+a); }; love1.love(520); love =(a)->{ System.out.println("I love you "+a); }; love.love(3); love=a -> { System.out.println("I love you "+a); }; love.love(4); love=a -> System.out.println("I love you "+a); love.love(111); } } interface ILove{ void love(int a); } /*class Love implements ILove{ @Override public void love(int a) { System.out.println("I love you "+a); } }*/

    带多个参数的Lamda简化

    package com.haiyang.lamda; public class TestLamda02 { public static void main(String[] args) { ILive live = null; //Lamda简化 live = (int a,int b)->{ System.out.println("I live you"+a+b); }; live.live(1,1); //Lamda简化参数类型(要去掉就要都去掉) live = (a,b)->{ System.out.println("I live you"+a+b); }; live.live(2,2); //Lamda简化花括号(只有一个语句时) live = (a,b)->System.out.println("I live you"+a+b); live.live(3,3); } } interface ILive{ abstract void live(int a,int b); }

    六、线程同步

    1.同步方法

    通步方法:即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, ​ 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。synchronized关键字修饰的方法的默认锁对象本身(this) synchronized void save(){...} synchronized void max(){...}

    注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

    2.同步代码块

    即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步synchronized关键字修饰的语句块锁的对象一定是变化的量(增删改) synchronized(object){ }

    3.使用特殊域变量(volatile)实现线程同步

    volatile关键字为域变量的访问提供了一种免锁机制,使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,因此每次使用该域就要重新计算,而不是使用寄存器中的值volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

    4.使用重入锁实现线程同步

    在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。

    // ReenreantLock类的常用方法有: ReentrantLock() : 创建一个ReentrantLock实例 lock() : 获得锁 unlock() : 释放锁 package thread; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; //多个线程同时操作同一个对象; //买火车票 //发现问题:多个线程同时操作同一个资源的情况下,线程不安全,数据紊乱; public class Demo3 implements Runnable { private int p = 10; //声明这个锁 private Lock lock = new ReentrantLock(); @Override public void run() { while (true) { // 同步代码块; /* synchronized (this) {。。。}*/ lock.lock();//上锁 try { if (p <= 0) { break; } Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock();//解锁 } System.out.println(Thread.currentThread().getName() + "-->拿到了第" + p-- + "票"); } } public static void main(String[] args) { Demo3 demo3 = new Demo3(); new Thread(demo3, "xiao").start(); new Thread(demo3, "an").start(); new Thread(demo3, "shuai").start(); } }

    七、线程池的优势

    (1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

    (2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

    (3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

    1.Executors创建线程池

    Java中创建线程池很简单,只需要调用Executors中相应的便捷方法即可,比如Executors.newFixedThreadPool(int nThreads),但是便捷不仅隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。

    Executors创建线程池便捷方法列表:

    方法名功能newFixedThreadPool(int nThreads)创建固定大小的线程池newSingleThreadExecutor()创建只有一个线程的线程池newCachedThreadPool()创建一个不限线程数上限的线程池,任何提交的任务都将立即执行

    2.ThreadPoolExecutor构造方法

    Executors中创建线程池的快捷方法,实际上是调用了ThreadPoolExecutor的构造方法(定时任务使用的是ScheduledThreadPoolExecutor),该类构造方法参数列表如下:

    // Java线程池的完整构造函数 public ThreadPoolExecutor( int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。 int maximumPoolSize, // 线程数的上限 long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长, // 超过这个时间,多余的线程会被回收。 BlockingQueue<Runnable> workQueue, // 任务的排队队列 ThreadFactory threadFactory, // 新线程的产生方式 RejectedExecutionHandler handler) // 拒绝策略

    3.Runnable和Callable

    可以向线程池提交的任务有两种:Runnable和Callable,二者的区别如下:

    方法签名不同,void Runnable.run(), V Callable.call() throws Exception是否允许有返回值,Callable允许有返回值是否允许抛出异常,Callable允许抛出异常。

    Callable是JDK1.5时加入的接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。

    三种提交任务的方式:

    提交方式是否关心返回结果Future<T> submit(Callable<T> task)是void execute(Runnable command)否Future<?> submit(Runnable task)否,虽然返回Future,但是其get()方法总是返回null
    Processed: 0.017, SQL: 8