java多线程编程从入门到卓越(超详细总结)

Posted huangjiahuan1314520

tags:

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

导读:java多线程编程不太熟?或是听说过?或是想复习一下?找不到好的文章?别担心我给你们又安利一波,文章内容很全,并且考虑到很多开发中遇到的问题和解决方案。循环渐进,通俗易懂,文章较长,建议收藏再看!

1.多线程的概念

  • 什么是进程?什么是线程?(了解这个还是很重要的,利于后面的学习,面试也常考)
    • 首先我们应该了解进程,在操作系统中进程是程序的一次执行。(也可理解为对静态层序的一次动态实例化的过程),(在引入线程的操作系统中)线程是系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元。对于操 作系统而言,其调度单元是线程。一个进程至少包括一个线程,通常将该线程称为主线程。一个进程从主线程的执行开始进而创建一个或多个附加线程,就是所谓基于多线程的多任务。 那进程与线程的区别到底是什么?进程是执行程序的实例。
      技术图片
    • 为什么要引进进程?
      进程的引入是为了使多个程序并发执行以改善系统资源的利用率和系统的吞吐量。
      技术图片
    • 有了进程为什么还要引入线程呢?
      虽然进程能够改善系统资源的利用率和系统的吞吐量。
      但是现实是:在多程序执行的情况下会生成多个进程,为了实现并发,系统会根据一定的算法进行进程切换来实现并发效果,但是进程的切换都会消耗较大的时空开销来进行系统资源的重新分配,保存和释放,如果进程一多,就会为其花费不少的处理机时间。
      线程的引入就是为了减少程序并发执行时所付出的时空开销。线程本身不拥有资源。
      技术图片
      技术图片
      技术图片
    • 那么进程与线程之间有什么联系和区别呢?
      技术图片
    • 一个进程可以启动多个线程。
    • 一个对应一个应用程序。(软件)
  • 对于java程序员来说,当在DOD窗口中输入:java HelloWorld 层序来说:
    • 会先启动JVM,而JVM就是一个进程。JVM在启动一个主线程调用main方法。同时再启动一个垃圾回收线程负责看护,回收垃圾。
  • 在没有引入多线程编程之前程序实例都以顺序方式执行,而多线程编程是一种并发的执行方式。
  • 进程之间内存不共享。
  • 线程之间内存共享吗?
    • 在java中:
    • 线程之间共享堆内存和方法区内存;但是栈内存是独立,一个线程一个栈。
    • 启动10个线程,会有10个栈空间每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。多线程提高了程序的处理效率。
    • 火车站,可以看作是一个进程,而火车站的每个售票窗口可以看作是一个线程。
  • 注意:使用了多线程之后,main方法结束,有可能程序还没有结束。main方法结束只是主线程结束了,主栈空了,其他的线程可能还在压栈弹栈。
    技术图片

2.多线程并发

  • 什么是真正的多线程并发?
    线程之间互不影响。
  • 对于单核CPU来说,真的可以做到真正的多线程并发吗?
    • 多核CPU可以做到多线程并发。
    • 但是单核CPU不能真正做到,但是可以做到一种“多线程并发”的感觉。(多个线程之间频繁切换执行,给人一种多个事情同时在做)

3.多线程程序设计

  • 分析以下程序中存在几个线程:(除垃圾回收之外)
package Day3;

public class ThreadTest1 {
    public static void main(String[] args) {
        System.out.println("main begin");
        m1();
        System.out.println("main end");
    }
    private static void m1(){
        System.out.println("m1 begin");
        m2();
        System.out.println("m1 end");
    }
    private static void  m2(){
        System.out.println("m2 begin");
        m3();
        System.out.println("m2 end");
    }
    private static void m3(){
        System.out.println("m3 begin");
        System.out.println("m3 end");
    }


}

技术图片

  • 以上代码顺序执行,只有一个线程。
    java语言中,实现多线程有两种方式。
    第一种是:编写一个类继承java.lang包下Thread类。
    第二种是:编写一个类实现Runnable接口。

继承Thread类创建线程

  • 如何创建线程对象呢? new就完事了。
  • 怎么启动线程呢? 调用线程对象的start()方法就OK了。
  • 详情请看下列代码示意:(注释是重点)
package Day3;
//第一种:直接继承Thread
public class ThreadTest2 {
    public static void main(String[] args) {
        //这里是主线程,在主栈中执行
        //新建一个分支线程
        MyThread myThread = new MyThread();
        //启动线程
        /*start方法的作用:启动一个分支线程,在JVM种开辟一个新的栈空
         间,这段代码完成后瞬间就结束了,线程就启动成功了*/
        /*启动成功的线程会自动调用run方法,并且run方法在分支栈的底部
        run和main方法平级*/
        myThread.start();
        //myThread.run();//不会启动动线程(为单线程)
        for(int i=0 ;i<100;i++){
            System.out.println("主线程--->"+i);
        }
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        //编写代码,这段程序在分支线程中执行
        for(int i=0 ;i<100;i++){
            System.out.println("分支线程--->"+i);
        }
    }
}

  • 直接使用run方法的内存分析图:
    技术图片
  • run方法执行,其结果跟之前编写的程序没有区别。
  • 使用start方法内存图分析:
    技术图片
  • 使用start方法,输出结果有先有后,有多又少,这是为什么?
    • 控制台只有一个。
    • 占有CPU时间片的多少。
    • 抢时间片等等
  • 上述代码多线程的执行结果。
    技术图片

新建类实现Runnable接口创建线程

  • 详情请看注释:
package Day3;

public class ThreadTest3 {
    public static void main(String[] args) {
        //创建一个线程的对象
        MyRunnable aa = new MyRunnable();
        //将可运行的对象封装成一个线程对象
        Thread t =new  Thread(aa);
        //启动线程
        t.start();
        for(int i =0 ;i<1000;i++){
            System.out.println("主线程-->"+i);
        }
    }
}
//这并不是一个线程类,是一个可运行的类,他还不是一个线程
class MyRunnable implements Runnable{

    @Override
    public void run() {
        for(int i =0 ;i<1000;i++){
            System.out.println("分支线程-->"+i);
        }
    }
}
  • 第二种:一个类实现了接口还可以去继承其它的类,更加灵活。

改进(匿名内部类方式)

package Day3;

public class ThreadTest4 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {//new一个对象,且实现了Runnable接口
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("分支线程--->" + i);
                }
            }
        });
        //启动线程
        t.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("主线程--->" + i);
        }
    }
}

获取线程的名字和当前线程对象

  • 怎么获取当前线程对象?
    • 所用的方法static Thread currectThread()为静态方法。
    • Thread currentThread = Thread.currectThread();
  • 获取线程对象的名字?
    • String name = 线程对象.getName();
  • 修改线程对象的名字?
    • 当线程没有设置名字之前,默认有个名字Thread-0,Thread-1,Thread-2
    • 线程对象.setName(“s1”);方法修改线程的名字。
  • 口说无凭,举个栗子:
package Day3;

public class ThreadTest5 {
    public static void main(String[] args) {
        //以下代码出现在main方法中,所以当前线程就是主线程,获取当前线程对象
        Thread current1 = Thread.currentThread();
        System.out.println(current1.getName());
        //创建线程对象
        MyThread2 t1 = new MyThread2();
        //设置线程的名字
        t1.setName("sss");
        //获取线程的名字
        System.out.println(t1.getName());
        //创建线程对象
        MyThread2 t2 = new MyThread2();
        System.out.println(t2.getName());
        //启动线程
        t1.start();
        for(int i=0;i<10;i++){
            Thread current2 = Thread.currentThread();
            System.out.println(current2.getName()+"-->"+i);
        }
    }
}
class  MyThread2 extends Thread{
    @Override
    public void run() {
        //获取当前对象
        for (int i =0;i<1000;i++){
            //获取当前线程对象
            Thread current2 = Thread.currentThread();
            System.out.println(current2.getName()+"--->"+i);
        }
    }
}
  • 运行部分结果
    技术图片

4.线程的生命周期

  • 什么是线程的生命周期?
    指:线程创建到运行完毕的整个过程。
    技术图片
  • 新建(new Thread)
  • 就绪 (Runnable)
  • 运行(Running)
  • 阻塞(Blocked)
  • 消亡(Dead)
  • JVM的线程调度程序会自动处理相关过程。那为啥还要了解呢?
    当程序员想要干预这个线程的运行时,了解线程生命周期和相关知识就很重要了。

5.多线程的调度管理

  • 当多个线程对象要求cpu控制权的时候,就需要系统进行一定的管理,也就是进行调度。
  • 大多数cpu环境下,cpu一个时间点只能做一件事情,也就是说只能执行一个线程体,因此调度管理是很重要的。
    • 超线程技术:超线程技术是在一颗CPU同时执行多个程序而共同分享一颗CPU内的资源,理论上要像两颗CPU一样在同一时间执行两个线程。虽然采用超线程技术能同时执行两个线程,但它并不象两个真正的CPU那样,每个CPU都具有独立的资源。当两个线程都同时需要某一个资源时,其中一个要暂时停止,并让出资源,直到这些资源闲置后才能继续。因此超线程的性能并不等于两颗CPU的性能。并且超线程:需要CPU支持,需要主板芯片组支持,需要Bios支持,需要操作系统支持,需要应用软件支持。目前很多应用软件不支持多线程技术。(这里对于超线程做简单了解)
    • Java中提供了优先级的概念,同一个优先级先来先服务,不同优先级线程对象按照优先级高低。
  • 常见的调度模型有哪些?
    • 抢占式调度模型:
      • 哪个线程的优先级比较高,抢到的cpu时间片的概率就高一些/多一些。Java采用的就是这个。
    • 均分式调度模型:
      • 均分cpu时间片,每个线程占有的cpu时间片长度一样。有些编程语言采用这种模型。
  • 状态切换的常用方法
  • void setPriority(int newPriotity)方法:设置线程的优先级。
  • int getPriority()方法:获取线程的优先级。
  • 最低优先级1(MIN_PRIORITY)
  • 最高优先级10(MAX_PRIORITY)
  • 默认优先级5(NORM_PRIORITY)
  • join()/join(long millis)实例方法(线程合并):使用该方法,当前线程进入阻塞状态,直到其它线程进入消亡后,才再次进入就绪状态。
    • 口说无凭,举个栗子:
package Day3;

public class ThreadTest10 {
    public static void main(String[] args) {
        System.out.println("main begin!");
        Thread f = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + "-->" + i);
                }
            }
        });
        f.setName("ttt");
        f.start();
        //合并到当前线程,当前线程受到阻塞,直到f线程执行结束
        try {
            f.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main over!");
    }
}

运行部分结果:
技术图片

  • yield()方法(线程让位):静态方法,暂停当前正在执行的线程对象(回到就绪状态,回到就绪可能会重新抢到),并执行其它对象。(让位方法,让给同等优先权的线程,否则不起作用)
    • 口说无凭,举个栗子:
package Day3;

public class ThreadTest8 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable3());
        t.setName("t");
        t.start();
        for(int i =0;i<1000;i++){
            System.out.println(Thread.currentThread().getName()+"-->"+i);
        }
    }
}
class  MyRunnable3 implements Runnable{
    @Override
    public void run() {
        for(int i =0;i<=1000;i++) {
            if (i %100==0){
                Thread.yield();
            }
            System.out.println(Thread.currentThread().getName()+"-->"+i);
        }
    }
}

  • Thread.sleep()/sleep(long millis)//静态方法
    • 参数为毫秒。
    • 作用:让当前线程进入休眠(重点需注意,面试常考:跟对象无关(静态方法),用一个线程对象调用sleep方法,并不意味着改线程对象进入休眠),进入“阻塞状态”,放弃占有cpu时间片,让给其它线程使用。
    • 出现在哪里就让当前线程睡觉。
    • 作用:不让当前线程独霸该进程获取的cpu资源,留给其它线程。或者项目开发时让某程序隔一段时间执行。
    • 口说无凭,举个栗子:
package Day3;

public class TreadTest6 {
    public static void main(String[] args) {
        //让当前线程睡眠10秒,主线程
        try {
            for(int i=0;i<10;i++) {
                System.out.println("Hello Wrold!");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  • 怎么终止线程的休眠呢?
    • 线程对象.interrupted()方法:
      此方法发出中断信号,并不会直接中断线程,当调用该方法时,如果线程正在被某些方法阻塞,它将收到一个中断异常的请求,提早终结这个线程被阻塞的状态。(wait(),join(),sleep(long),等方法阻塞。其靠异常处理机制。
    • 口说无凭,举个栗子:
package Day3;

public class ThreadTest7 {
    public static void main(String[] args) {
        Thread t =new Thread(new Runnable() {//匿名内部类实现接口
            @Override
            public void run() {
                try {
                    Thread.sleep(1000*60*60);
                } catch (InterruptedException e) {//interrupted()方法引发这个异常
                    e.printStackTrace();
                }
                System.out.println("Hello Wrold!");
            }
        });
        t.setName("se");
        t.start();
        t.interrupt();//中断阻塞
    }
}

  • 如何合理终止线程的执行?
    • 直接举个栗子:(注意看注释)
package Day3;

import java.util.concurrent.ThreadLocalRandom;

public class ThreadTest9 {
    public static void main(String[] args) {
        MyRunnable2 r =new MyRunnable2();//需要对象,合理终止需要用到对象的属性
        Thread t = new Thread(r);
        t.setName("Jia");
        t.start();
        try {
            Thread.sleep(5*1000);//5秒后终止线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //暴力终止t线程`
        //t.stop();//缺点:线程没有保存的数据会丢失,即容易丢失数据(已过时
        //合理终止线程的执行,标记法
        r.run=false;
    }
}
class MyRunnable2 implements Runnable{
    //run标记
    boolean run =true;
    @Override
    public void run() {
        for(int i =0;i<10;i++){
            if(run) {
                System.out.println(Thread.currentThread().getName() + "-->" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            else {
                //终止线程
                return;
            }
        }
    }
}
  • 运行结果:
    技术图片
    结果为啥是这样:请记住现在的编程是多线程并发执行,在没有终止的五秒前,运行了五秒。(可能一些小伙伴这里会转不过弯来,所以啰嗦一下)
  • 注意:强行终止的stop()方法容易丢失数据。
  • run方法不能抛出任何异常,因为子类不能比父类抛出更多异常。

6.线程安全问题(重点)

  • 多线程并发环境下,数据安全问题。
  • 为什么说线程安全是重点?
    • 开发中,我们项目运行在服务器中,而服务起已经将线程的定义,线程对象的创建,线程的启动等,都已经实现了,我们无需关心。
    • 我们要关心的是,我们编写的程序放到一个多线程环境下运行,这些数据是否安全。
  • 什么情况下在多线程环境下,我们的数据会存在安全问题。
    • 举个栗子:
      多线程并发对同一个账户进行取款:
      技术图片
package Day1;

public class AccountTest1 {
    public static void main(String[] args) {
        Account account = new Account("Jia",10000);
        //创建两个线程对象
        Thread t1 = new MyAccountThread(account);
        Thread t2 = new MyAccountThread(account);
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}
class Account {
    private String name;
    private double blance;

    public Account() {
    }

    public Account(String name, double blance) {
        this.name = name;
        this.blance = blance;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getBlance() {
        return blance;
    }

    public void setBlance(double blance) {
        this.blance = blance;
    }
    //取款
    public void GetMoney(double money){
        //两个线程并发,同时操作堆中的对象
        //取款之前
        double before =this.getBlance();
        //取款之后
        double after = before -money;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //如果t1线程执行到这之前余额还没更新,t2也进来执行获取余额了,这时就会出问题
        this.setBlance(after);
    }
}
class MyAccountThread extends Thread{
    //两个线程共享一个账户对象
    private Account account1 ;
    //通过构造方法传递过来账户对象
    public MyAccountThread(Account account1) {
        this.account1 = account1;
    }

    @Override
    public void run() {
        //取款5000
        double money =5000;
        account1.GetMoney(money);
        System.out.println("取款成功!"+"账户"+account1.getName()+"余款:"+account1.getBlance());
    }
}

运行结果:取了两次钱结果:
技术图片
说明:不同的类尽量写在不同的源文件中。这里是为了便于观察。

  • 总结以下情况会存在安全问题。
    • 条件一:多线程并发。
    • 条件二:有共享数据。
    • 条件三:共享数据有修改的行为。
    • 满足以上条件会存在线程安全问题。
  • 怎么解决线程安全问题?
    • 用排队执行解决线程安全问题。这种机制被称为:“线程同步机制”。(线程排队,即线程不能并发了)。

7.多线程的同步问题

  • 同步编程模型:
    • 线程之间的执行,需要等待另一个线程执行完毕,才能执行,效率较低。线程排队执行。
  • 异步编程模型:
    • 线程之间,各自执行各自的,互不影响,其实就是多线程并发。
  • 如何实现多线程之间的“线程同步机制”(线程排队)?
    • 方法为:给要操作的共享资源(重要)加锁。保证一个线程对象在对它操作的时候不被其它线程干扰。
    • Java提供了一个synchronized关键字:如果线程t1遇到了synchronzied关键字,这时候自动找后面“共享资源”的对象锁,找到后,并占有这把锁,然后执行同步代码块的程序。直到执行结束,才会释放该锁。线程t2在t1没有释放锁之前,只能排队。synchronized后面小括号”数据很关键“,必须是多线程共享的数据,才能排队。
    • Synchronized关键字可以队线程对象要操作的资源(如方法,对象等)进行加锁。(同步锁)
      技术图片
    • synchromized后面的小括号写什么看你想要哪几个线程同步,例如t1,t2…t5,如果你只希望t1,t2,t3,排队,你一定要在括号中写一个t1,t2,t3共享的对象,t4,t5不共享这个对象,就可以并发执行。(这就好比t1,t2,t3,为男生,共享男卫生间。t4,t5为女生就不需要在男卫生间门口排队,他们可以并发的去女卫生间执行(对于一些会去男卫生间上厕所的这里就不讨论,哈哈)。)
  • java语言中,任何对象都有“一把锁”,其实就是一个标记(只是称为锁)。
  • 口说无凭,举个栗子:
package Day1;
//使用同步机制解决线程安全问题
public class AccountTest1 {
    public static void main(String[] args) {
        Account account = new Account("Jia",10000);
        //创建两个线程对象
        Runnable tt1= new MyAccountThread(account);//多态
        Runnable tt2 = new MyAccountThread(account);
        Thread t1 = new Thread(tt1);
        Thread t2 = new Thread(tt2);
        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        t2.start();
    }
}
class Account {
    private String name;
    private double blance;

    public Account() {
    }

    public Account(String name, double blance) {
        this.name = name;
        this.blance = blance;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getBlance() {
        return blance;
    }

    public void setBlance(double blance) {
        this.blance = blance;
    }
    //取款
    public void GetMoney(double money){
        //以下代码必须是线程排队的,不能并发
        //一个线程吧这里的代码全部执行后,另一个才能进来执行
        /*synchronized后面小括号”数据很关键“,必须是多线程共享的数据,才能排队
        小括号写什么看你想要哪几个线程同步,例如t1,t2..t5,如果你只希望t1,t2,t3,排队
        你一定要在括号中写一个t1,t2,t3共享的对象。t4,t5不共享这个对象,就可以并发执行。*/

        synchronized (this) {//线程同步机制,线程同步代码块,需要传入共享资源;共享资源的不同,决定了什么不同的线程同步
            double before = this.getBlance();//这里写什么,取决于你想对于什么资源,对应的线程排队。
            double after = before - money;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBlance(after);
        }
    }
}
class MyAccountThread implements Runnable{
    //两个线程共享一个账户对象
    private Account account1 ;
    //通过构造方法传递过来账户对象
    public MyAccountThread(Account account1) {
        this.account1 = account1;
    }

    @Override
    public void run() {
        //取款5000
        double money =5000;
        account1.GetMoney(money);
        System.out.println("取款成功!"+"账户"+account1.getName()+"余款:"+account1.getBlance());
    }
}

运行结果:
技术图片

  • 共享资源的理解:不同的账户,不共享,所以对不同的账户取款,不需要排队。只有同时对同一账户进行多个操作时,其共享一个账户信息,需要加锁进行排队。
  • Java的三大变量哪些存在线程安全问题?
    • 实例变量(堆内存):堆只有一个。
    • 静态变量(方法区):方法区只有一个。
    • 堆和方法区都是多线程共享的,所以可能存在线程安全问题。
  • 局部变量(在栈中):局部变量永远不会存在线程安全问题。因为局部变量不共享在栈中,一个线程一个栈。
  • 同步代码块越小,效果越好。
  • 常量不会有线程安全问题。
  • 实例方法中可以使用sychronized。但是出现在实例方法上,一定锁的是this.没得挑,只能是this.这种方式不灵活。
    • synchronized使用的优点:减少了代码冗余。
    • 如果共享的就是this,并且需要同步的代码块就是方法体,建议使用这种方式(声明同步方法,这种方式不会被继承)。(即public Synchronized 返回值 方法名(){
      //方法的实现
      })
  • 回顾:线程安全:Vector,Hashtale
    • 非线程安全:ArrayList,HashMap,BashMap。
  • 总结:Synchronized有三种方法:
    • 第一种:同步代码块。灵活
      Synchronized(线程共享对象){
      同步代码块
      }
    • 第二种:在实例方法上使用Synchronzed。
      表示共享对象一定是this.并且同步代码块是整个方法体。
    • 第三种:在静态方法上使用Synchronized
      表示找类锁。类锁永远只有一把(保证了静态变量的安全)不管对象有多少,所以对于类锁而言,如果加类锁的话,只要属于该类的对象都是共享资源。
  • 死锁的概念:
  • 死锁会造成程序不出异常,也不出错误,一直僵持在那里。
  • 口说无凭,举个栗子:
package Day1;

import java.util.concurrent.SynchronousQueue;

public class ExamTest1 {
    //死锁要会写
    public static void main(String[] args) {
        //两个线程共享o1,o2
        Object o1 = new Object();
        Object o2 = new Object();
        Thread t1 = new MyThread1(o1,o2);
        Thread t2 = new MyThread2(o1,o2);
        t1.start();
        t2.start();
    }
}
class MyThread1 extends  Thread {
    Object o1;
    Object o2;

    public MyThread1(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        synchronized (o1) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2) {
            }
        }
    }
}
class  MyThread2 extends Thread {
    Object o1;
    Object o2;

    public MyThread2(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }

    @Override
    public void run() {
        synchronized (o2) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1) {
            }
        }
    }
}

技术图片

  • 总结:
    • 尽量不要使用两个嵌套synchronized,容易造成死锁。
    • 对于线程调度状态的方法:destroy(),stop(),suspend()等调用后不会释放线程本身的对象锁,容易造成死锁,不建议使用。而resume方法是用来唤醒suspend方法暂定的线程,因而也不建议使用。
  • 开发中如何解决线程安全问题?
    • synchronized会使执行效率降低,让用户体验差,在不得已的时候在选取。
    • 第一种方案:尽量使用局部变量代替”实例变量和静态变量。
    • 第二种方案:如果必须使实例变量,那么可以考虑创建对各对象,即不共享。一个线程一个对象。这样就没有安全问题了。(多搞几个卫生间,如果每个人都有一个卫生间或那啥是吧,你说还需要排队吗?)
    • 第三种方案:前两者都不能用,采用synchronized,线程同步机制。
  • 什么是守护线程?(了解)
    • java语言中线程分为用户线程(主线程等)和守护线程。
    • 守护线程其实就是后台线程,其中比较有代表性的是:垃圾回收线程(守护线程)
  • 守护线程有什么作用呢?
    • 比如我们的定时数据自动备份,这个需要用到计时器,当到某个特定点后就自动备份一次。所以用户线程退出后,守护线程自动退出,没有必要进行数据备份了。
    • 话不多说,举个栗子说明一下吧:
package Day1;

public class TheadTest1 {
    public static void main(String[] args) {
        Thread JiSi=new MyDaemon();
        JiSi.setName("备份数据");
        //设为守护线程
        JiSi.setDaemon(true);
        JiSi.start();
        //主线程
        for(int i =0 ;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"-->"+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class MyDaemon extends Thread{
    @Override
    public void run() {
        int i = 0;
        //即使是死循环,但由于改线程是守护者,当用户结束,守护线程自动终止。
        while (true) {
            System.out.println(Thread.currentThread().getName() + "-->" + (++i));
            try {
                Thread.sleep(1000);//模拟一秒记录一次
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:
技术图片
从结果可以看出当用户线程结束的时候,守护线程也终止。

  • 如何设置一个守护线程呢?
    • 使用setDaemon(True);方法即可。
  • 什么是定时器
    • 定时器的作用?
      • 间隔特定的时间,执行特定的程序。比如隔一段时间备份数据,或进行总账操作。在实际开发中很常见。
    • 那如何实现呢?
      • 可以采用sleep方法,最原始的定时器。
      • java.util.Timer计时器,可以来拿用。
      • 实际开发中,使用Spring框架中提供的SpirngTask框架,简单配置就可以完成计时器的任务,其原理跟Timer相同。所以我们看一下Timer。
      • 多说无益,举个栗子:
package Day2;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.AbstractMap;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.zip.DataFormatException;

public class TimerTest1 {
    public static void main(String[] args) {
        //创建定时器对象
        Timer timer =new Timer();
        //Timer timer = new  Timer(true);//守护线程的方式
        //定时任务
        //timer.schedule(定时任务,第一次执行时间,间隔多久一次)//定时任务属于,TimerTask类
        //创建对象
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        try {
            //将时间字符串转换成可处理的时间
            Date firstTime = sdf.parse("2020-04-11 10:40:00");
            timer.schedule(new LogTimerTask(),firstTime,1000*5);//每隔5秒备份一次
        } catch (ParseException e) {
            e.printStackTrace();
        }

    }
}
//设为定时任务类,假设为记录日志的定时任务
class LogTimerTask extends TimerTask{//TimerTask为抽象类,因为任务的类型为这个类
    @Override
    public void run() {
        //创建一个日期格式对象
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String strTime = sdf.format(new Date());//将当前系统时间转换为字符串
        System.out.println(strTime+"成功完成了一次数据备份!");
    }
}

部分运行结果:
技术图片

  • 实现线程的第三种方式:(JDK8新特性)
    • 实现Callable接口。
    • 在java.util.concurrent.FutureTask包下。
    • 这种方式实现的线程可以获取现场的返回值。
    • 执行一个任务,需要返回一个值时,可以用这种方式。
    • 缺点:获取另一个线程返回结果时,当前线程受到阻塞,效率较低。
    • 多说无益,举个栗子:
package Day2;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

//实现Callable接口,实现线程的第三种方式
public class CallableTest1 {
    public static void main(String[] args) {
        //第一步:创建一个“未来任务类”对象
        FutureTask task =new FutureTask(new Callable() {//匿名内部类实现接口
            @Override
            public Object call() throws Exception {//相当于run方法,只不过有返回值
                //线程执行一个任务,可能有一个执行结果
                System.out.println("Call method begin!");
                Thread.sleep(1000*5);
                System.out.println("Call method over!");
                int a,b;
                a=100;
                b=200;
                return a+b;//自动装箱(Interger)
            }
        });
        //创建线程对象
        Thread t = new Thread(task);
        //启动线程
        t.start();
        //这里时main方法,在主线程
        //用get()方法,获取t线程的返回结果,但是当前线程受到阻塞
        Object obj = null;
        try {
            obj = task.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        //主线程要执行以下程序必须等待get()方法执行结束。
        System.out.println("线程执行结果:"+obj);
        System.out.println("Hello World!");
    }
}

8.生产者与消费者模型

  • 了解生产者与消费者模型之前我们先来看一下wait和notify方法。
  • wait和notify方法不是线程对象的方法,是java中任何一个对象都有的方法,因为这两个方法是Object类自带的。wait方法和notify方法不是通过线程对象调用。(重点)
  • Object.wati()方法的作用?
    • 当一个线程执行到wait()方法时,进入一个和该对象相关的等待池,同时失去了对象锁的拥有权,该线程进入阻塞状态,直到调用notify或notifyAll()方法将其唤醒。当前线程必须拥有该对象的锁,如果不拥有,会抛出异常,所以wait()方法必须在synchronized block中调用。(重点)
    • 以下举个栗子说明一下:(如下图)
      技术图片
  • Object.notify()/notifyAll()方法的作用?
    • Object o =new Object();
    • o.notify()/notifyAll()表示唤醒正在o对象等待的第一个线程/所有线程。这个方法不会释放对象锁,这个方法同样必须有其对象锁,否则会抛出异常(IllegalMonitorStateException)。
  • 生产者和消费者模型。
    • 学过操作系统的朋友这里应该比较熟悉,没学过的朋友不要担心,我们来唠唠,学过的就当好好复习一下。
      技术图片
    • 什么是“生产者和消费者模型”?
      • 生产线程负责生产,消费线程负责消费。
      • 生产线程和消费线程要达到均衡。
      • 这是一个特殊的业务需求,需要使用wait方法和notify方法。
  • wait和notify方法不是线程对象的方法,是普通java对象的方法。(又啰嗦一遍,因为很多人对这有误解)
  • wait和notify方法建立在线程同步的基础上,因为多线程共享一个资源(仓库),存在线程安全问题。
  • 举个生产者和消费者比较经典的栗子(重点):
package Day2;

import java.util.ArrayList;
import java.util.List;

//List模拟仓库,容量为1,即生产一个消费一个
public class ProductTest1 {
    public static void main(String[] args) {
        //创建一个仓库对象,共享的
        List list = new ArrayList();
        //创建两个线程对象
        //生产者线程
        Thread t1 = new Thread(new Producer(list));
        //消费者线程
        Thread t2 = new Thread(new Consumer(list));
        t1.setName("生产者线程:");
        t2.setName("消费者线程:");
        t1.start();
        t2.start();
    }
}
//生产线程
class Producer implements Runnable{
    //仓库
    private List list;

    public Producer(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        //一直生产
        while(true) {
            synchronized (list) {//给仓库资源加锁
                if (list.size() > 0) {
                    //当前线程进入等待状态,释放锁,不放锁的话,消费者线程无法访问资源(生产者线程)
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //程序能执行到这里说明仓库为空,可以生产
                Object obj = new Object();
                list.add(obj);
                System.out.println(Thread.currentThread().getName()+"-->"+obj);
                //唤醒消费者消费
                list.notify();
            }
        }
    }
}
//消费线程
class Consumer implements Runnable{
    //同一个仓库
    private List list;
    public Consumer(List list) {
        this.list = list;
    }
    @Override
    public void run() {
        //一直消费
        while (true){
            synchronized (list){//没有得到锁,以下代码都不能执行
                if(list.size()==0){
                    //仓库空了,停止消费,消费线程进入阻塞,释放list集合的锁
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //程序能够执行到这,说明仓库有数据,进行消费
                Object obj= list.remove(0);
                System.out.println(Thread.currentThread().getName()+"-->"+obj);
                //唤醒生产者进行生产
                list.notify();

            }
        }
    }
}

部分执行结果:
技术图片

码字不易,点个赞再走吧!

以上是关于java多线程编程从入门到卓越(超详细总结)的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程学习(吐血超详细总结)

Java多线程学习(吐血超详细总结)

java多线程编程详细总结

Java多线程学习(吐血超详细总结)

超详细的JAVA多线程学习

Java多线程学习(吐血超详细总结)