Java多线程

本文详细介绍了Java多线程的相关概念,包括进程与线程的关系、多线程的实现方式、线程的生命周期、常用方法及调度。重点讨论了多线程并发环境下数据安全问题,如线程安全、同步机制、synchronized的使用和面试题,并涉及死锁、守护线程以及Callable接口等内容。
摘要由CSDN通过智能技术生成

多线程

1. 多线程概述

1.1 进程和线程

  • 进程
    • 一个应用程序(一个软件)
  • 线程
    • 线程是一个进程中的执行场景(执行单元)
    • 一个进程可以启动多个线程
  • 举例
    • 在DOS命令窗口输入 java HelloWorld 之后(运行HelloWorld类)
    • 会先启动JVM,而JVM就是一个进程
      • JVM再调用一个主线程调用main方法
      • 同时调用一个垃圾回收线程负责看护,回收垃圾

1.2 进程和线程的关系

  • 阿里巴巴:进程

    • 马云:阿里巴巴的一个线程
    • 阿里员工:阿里巴巴的一个线程
  • 京东:进程

    • 强东:京东的一个线程
    • 京东员工:京东的一个线程
  • 进程可以看作公司,线程可以看作公司中某个员工

  • 进程A和进程B

    • 内存独立不共享(阿里巴巴和京东资源不共享)
  • 同一进程中的线程a和线程b

    • 堆内存和方法区内存共享
    • 栈内存独立,一个线程一个栈
  • 多线程并发

    • 假设有10个线程,那么会有10个栈,互不干扰,各自执行各自的任务
    • 目的是提高程序的处理效率
  • main方法结束

    • 使用多线程机制之后,main方法结束只是一个主线程结束,主栈空了
    • 其它的线程仍然在压栈,弹栈

1.3 单核CPU和多核CPU实现多线程

  • 多核CPU真正实现多线程是没问题的
    • 多核相当于多个大脑,可以真正的有多个进程同时并发
    • 比如4核CPU同一时间可以真正的有4个进程同时并发
  • 单核CPU
    • 只有一个大脑,不能真正的多线程并发
    • 但可以给人一种多线程并发的假象
    • 只需要快速的在a线程和b线程之间来回切换,快到人脑反应不过来,就可以制造这种感觉
    • 就像很多图片以足够快的速度连续放映就像真的动起来一样

2. 实现线程的两种方式

  • 主要的目的是传入run方法
  • 第一种使用重写的方式
  • 第二种使用实现接口并传入构造方法的方式

注意:

  • 使用第二种方法的比较多
  • 因为实现了接口还可以继承其它的类,更灵活

2.1 第一种方式

  • 编写一个类,直接继承 java.lang.Thread 重写run方法

  • run方法在分支进程的栈的最底部,地位等同于主栈中的main方法

  • start方法作用是在JVM开辟一个新的栈空间,建立一个分支线程,建立完start方法就结束

public class Main {
    public static void main(String[] args) {
        //main方法在主线程,在主栈中运行
        //新建一个分支线程对象
        Mythread mythread=new Mythread();
        mythread.run();//这里如果直接执行run方法,并没有开辟新的栈空间,还是在一个线程里
        //在JVM开辟一个新的栈空间,开辟完之后,start方法结束
        //mythread.start();
        //下面的代码还是在主线程中运行,run方法在分支线程运行
        for (int i = 0; i < 1000; i++) {
            System.out.println("主线程--->"+i);
        }
    }
}

class Mythread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("分支线程--->"+i);
        }
    }
}
  • 如果新开辟了线程,两个线程互不干扰,但只有一个控制台,所以输出的时候会交替,不均匀的输出

2.2 第二种方式

  • 编写一个类,实现Runnable接口
  • 创建线程对象,传入可运行类,调用线程对象的start方法
public class Main {
    public static void main(String[] args) {
        //创建一个线程对象,放入一个可运行对象
        Thread T=new Thread(new MyRunnable());
        //启动线程
        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);
        }
    }
}
  • 匿名内部类写法
public class Main {
    public static void main(String[] args) {
        //创建一个线程对象,放入一个可运行对象
        Thread T=new Thread(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);
        }
    }
}
  • 原理内存图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qeCt6zxO-1622909094796)(/Users/mac/Library/Containers/com.tencent.qq/Data/Library/Caches/Images/53E005BB1EC357CFC7BE21E812EA80AD.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ktuBbYzb-1622909094799)(/Users/mac/Library/Containers/com.tencent.qq/Data/Library/Caches/Images/D86BDA71414179324784963E4220CB26.jpg)]

3. 线程的生命周期

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oIMptJxV-1622909094802)(/Users/mac/Library/Containers/com.tencent.qq/Data/Library/Caches/Images/8DE1FE54415DA6F843699C753088CBAE.jpg)]

4. 线程常用方法

  • 线程有默认的名字
    • Tread-0
    • Tread-1…

4.1 获取线程对象的名字

myThread.getName();

4.2 修改线程对象的名字

myThread.setName("tttt");

4.3 获取当前线程对象

Thread T=Thread.currentThread();
  • 测试
public class Main {
    public static void main(String[] args) {
        //获取当前线程对象
        //出现在main方法,所以获取的就是主线程
        Thread T=Thread.currentThread();
        System.out.println(T.getName());//main
        //创建线程对象
        MyThread myThread=new MyThread();
        String name=myThread.getName();
        System.out.println(name);//Thread-0
        //设置线程的名字
        myThread.setName("tttt");
        //重新获取名字
        name=myThread.getName();
        System.out.println(name);
        //启动线程
        myThread.start();
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            //获取调用run方法的线程
            //谁调用,获取的就是谁
            Thread T=Thread.currentThread();
            System.out.println(T.getName()+"--->"+i);
        }
    }
}

4.4 线程的sleep方法

4.4.1 sleep基本用法
static void sleep(long millis)
  • sleep是一个静态方法
  • 作用
    • 让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用
    • 出现在哪个线程就让哪个线程休眠
    • 可以让某段代码间隔一定的时间在去执行
public class Main {
    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            Thread T=Thread.currentThread();
            String name=T.getName();
           //每隔一秒输出一行
            System.out.println(name+"--->"+i);
            
            try {
                Thread.sleep(1000);//休眠一秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • sleep面试题
public class Main {
    public static void main(String[] args) {
        //创建线程对象
        Thread t=new Mythread();
        t.setName("t");
        t.start();

        //调用sleep方法
        try {
            t.sleep(1000*5);//关键点
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //这里是否会休眠?
        System.out.println("hello");
    }
}

class Mythread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println(Thread.currentThread().getName()+"--->"+i);
        }
    }
}
  • t.sleep(1000*5),按理说是不能这样调用的
  • 因为sleep是静态方法,只能通过“类名.”的方式调用
  • 如果通过引用来调用,编译器会自动转换为 Thread.sleep()
  • 本质是在main方法调用Thread.sleep()方法
  • 所以主线程main会休眠五秒,与线程t无关
4.4.2 终止线程的睡眠
  • interrupt
    • 线程终止,同时抛出线程终止异常
  • stop(已过时)
    • 存在很大的缺点
    • 原理是直接将线程杀死
    • 线程没有报错的数据将直接丢失,不建议使用
public class Main {
    public static void main(String[] args) {
        //实现Runnable接口的方式创建线程对象
        Thread t=new Thread(new MyRunnable());
        t.start();

        //5秒之后让t线程终止休眠
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //t线程终止,同时抛出线程终止异常
        t.interrupt();
      //t.stop(); 强行终止,容易丢失数据,已过时
    }
}

class MyRunnable implements Runnable {
    //run中的异常不能throw抛出
    //因为父类run方法没有抛出异常,子类不能比父类抛出更多异常
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"---> begin");
        try {
            Thread.sleep(1000*60*60*24*365);//休眠一年
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}
4.4.3 一种合理的,常用的终止方法
public class Main {
    public static void main(String[] args) {
        MyRunnable m=new MyRunnable();
        //传入实现Runnable接口的类,创建线程对象
        Thread t=new Thread(m);
        //启动线程
        t.start();

        //模拟5秒后终止线程
        try {
            Thread.sleep(5*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //终止线程
        m.run=false;
    }
}

class MyRunnable implements Runnable{
    //用来控制线程的终止与正常运行
    boolean run=true;

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            if(run){
                System.out.println(Thread.currentThread().getName()+"--->"+i);
            }
            else {
                //终止之前在这里保存还没保存的数据
                //save...
                //终止线程
                return;
            }
        }
    }
}

5. 线程调度

5.1 常见的线程调度模型

  • 抢占式调度模型
    • 哪个线程的优先级较高,抢到的CPU时间片就多一些/概率高一些
    • Java采用的就是抢占式调度模型
  • 均分式调度模型
    • 平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样
    • 有些编程语言采用这种方式

5.2 Java提供的与线程调度有关的方法(了解)

  • 实例方法
void setPriority(int newPriority);//设置线程的优先级
int getPriority();//获取线程的优先级

最低优先级 1

默认优先级5

最高优先级10

  • 静态方法
static void yield();//让位方法

暂停当前正在执行的线程对象,并执行其他线程

yield()方法不是阻塞方法,让当前线程让位,让给其他线程使用

yield()方法的执行会让当前线程从 运行状态 回到 就绪状态(还有可能再次抢到)

  • 实例方法
void join();//合并线程

t.join();

当前线程进入阻塞状态,t线程开始执行,t线程结束后,当前线程才能继续执行

6.多线程并发环境下,数据安全问题(重要)

  • 最重要的就是
    • 自己编写的程序放在多线程环境下是否安全

6.1 多线程的安全问题

  • 出现安全问题的三个条件
    • 多线程并发
    • 有共享数据
    • 共享数据有修改的行为

6.2 解决线程安全问题

  • 线程排队执行(线程同步机制)
    • 这样会牺牲一部分效率,但为了数据安全,必须这么做

6.3 同步编程模型和异步编程模型

  • 同步编程模型
    • 线程排队执行(一个线程在执行,另一个需要排队,不能同时执行)
  • 异步编程模型
    • 多线程并发(两线程互不干扰,各自执行)

6.4 通过代码模拟线程安全隐患

  • 编写账户类
public class Account {
    //账户
    private String actno;
    //余额
    private double balance;

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款方法
    public void withdraw(double money){
        //取款之前的余额
        double before=balance;
        //取款之后的余额
        double after=balance-money;

        //模拟网络延迟1秒
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //更新余额
        this.setBalance(after);
    }
}
  • 编写账户线程类
public class AccountThread extends Thread{
    //两个线程共享的账户对象
    private Account act;

    //通过构造方法传递账户对象
    AccountThread(Account act){
        this.act=act;
    }

    //run方法
    public void run(){
        //表示取款操作
        //每次取款5000
        double money=5000;

        act.withdraw(money);

        System.out.println(Thread.currentThread().getName()+"对"+act.getActno()+"取款"+money+" 余额"+act.getBalance());
    }
}
  • 测试,主方法
public class Main {
    public static void main(String[] args) {
        //创建共享账户对象
        Account act=new Account("act-01",10000);

        //创建两个线程对象,共用一个账户
        Thread t1=new AccountThread(act);
        Thread t2=new AccountThread(act);

        //设置线程名字
        t1.setName("t1");
        t2.setName("t2");

        //启动线程
        t1.start();
        t2.start();
    }
}
  • 运行结果
t1对act-01取款5000.0 余额5000.0
t2对act-01取款5000.0 余额5000.0
  • 正确的取款顺序
    • 余额10000,取钱5000,更新余额为5000
    • 余额5000,取钱5000,更新余额为0
  • 有延迟的情况下取款顺序
    • 余额10000,取钱5000
    • 余额10000,取钱5000
    • 更新余额为5000
    • 更新余额为5000

6.5 用synchronized解决问题(同步机制)

  • 如果让取钱的线程对象排队
  • 等一个线程对象完整的进行取钱操作,更新余额后,下一个用户在进行取钱操作
  • 这样就可以避免余额更新延迟带来的损失
synchronized(填入需要排队的线程所共享的对象内存地址){
	//排队执行的代码块
}
  • 修改后的取钱方法
//取款方法
public void withdraw(double money){
   synchronized (this){
       //取款之前的余额
       double before=balance;
       //取款之后的余额
       double after=balance-money;

       //模拟网络延迟1秒
       try {
           Thread.sleep(1000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       //更新余额
       this.setBalance(after);
   }
}
  • 运行结果
t1对act-01取款5000.0 余额5000.0
t2对act-01取款5000.0 余额0.0

6.6 synchronized的深入理解

  • 锁的概念

    • 一个对象有一把锁
    • 锁是一个标志,只是取个名字叫锁
    • t1和t2排队执行synchronized代码块中的语句,假设t1先进入,会占有synchronized()括号中对象的锁,执行完后释放锁
    • 相当于t1和t2共享同一个坑,t1先上厕所,然后锁门(占有锁),只有上完出去才会把锁打开(释放锁)
  • synchronized()括号中参数的理解

  • 该线程会去锁池找括号内的对象的锁(可以理解为一种阻塞状态),有几把找几把
    + [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nofYjWpQ-1622909094804)(/Users/mac/Library/Containers/com.tencent.qq/Data/Library/Caches/Images/7DDF35DF54427EFF250E60F61C8A9493.jpg)]

  • 括号中传入的具体是谁不重要,可以是普通对象也可以是一个字符串对象(只有一把锁,字符串常量池所有线程共享,都排队)

  • 重要的是他到底有几把锁(是几个对象),以及线程的共享关系

    • 如果这个对象唯一,只有一把锁,都要排队
    • 如果有两个对象,t1和t2共享一个对象(共享一把锁,操作同一个对象),t3有另一个对象(自己用一把锁)
      • t1和t2排队,t3不用排队,自己用
  • synchronized使用在实例方法上(共享对象默认this)

public synchronized void withdraw(){}
  • 优点
    • 代码量减少
    • 如果整个方法都需要同步,而且共享的对象是this,建议使用这种简洁的写法
  • 缺点
    • 整个实例方法同步,扩大同步范围,效率降低

6.7 不会发生安全问题的变量

  • 三大变量

    • 实例变量 堆区 共享
    • 静态变量 方法区 共享
    • 局部变量 栈中 不共享
  • 局部变量,常量不会发生安全问题

    • 栈中的数据是不共享的,一个线程一个栈
    • 所以局部变量不会发生安全问题
    • 常量不能改变,也不会发生安全问题
  • 建议

    • 建议使用StringBuilder,因为局部变量不存在线程安全问题
    • StringBuffer某些实例方法中用了关键字synchronized,效率比较低
    • 线程安全的
      • Vector
      • HashTable
    • 非线程安全的
      • ArrayList
      • HashMap
      • HashSet

6.8 总结synchronized的三种写法

  • 同步代码块

    • synchronized(线程共享对象){
      		同步代码块;
      }
      
  • 实例方法上使用synchronized

    • 共享对象一定是this
    • 同步代码块是整个方法体
  • 静态方法上使用synchronized

    • 表示找到类锁
    • 类锁只有一把,创建多少对象都是一把

6.9 synchronized面试题

第一题
  • 问:doOther 方法是否需要等待 doSome 方法结束才执行
public class Main {
    public static void main(String[] args) {
        Myclass test=new Myclass();
        //两个线程共享一个对象
        MyThread t1=new MyThread(test);
        MyThread t2=new MyThread(test);

        //给线程对象设置名称
        t1.setName("t1");
        t2.setName("t2");

        //线程t1启动
        t1.start();
        //延迟一秒,为了让t1先启动
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //线程t2启动
        t2.start();
    }
}

class MyThread extends Thread{
    //线程对象中的一个共享对象属性
    Myclass test;
    MyThread(Myclass test){
        this.test=test;
    }

    //线程t1调用doSome
    //线程t2调用doOther
    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("t1")){
            test.doSome();
        }

        if(Thread.currentThread().getName().equals("t2")){
            test.doOther();
        }
    }
}

class Myclass{
    //上锁
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000*10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
		
    public void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}
  • 不需要,因为虽然只有一把锁,t1占用了,但doOther方法不需要锁的限制,直接执行

  • 执行顺序

    • doSome begin
      doOther begin
      doOther over
      doSome over
      
第二题
  • 问:doOther 方法是否需要等待 doSome 方法结束才执行
public class Main {
    public static void main(String[] args) {
        Myclass test=new Myclass();
        //两个线程共享一个对象
        MyThread t1=new MyThread(test);
        MyThread t2=new MyThread(test);

        //给线程对象设置名称
        t1.setName("t1");
        t2.setName("t2");

        //线程t1启动
        t1.start();
        //延迟一秒,为了让t1先启动
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //线程t2启动
        t2.start();
    }
}

class MyThread extends Thread{
    //线程对象中的一个共享对象属性
    Myclass test;
    MyThread(Myclass test){
        this.test=test;
    }

    //线程t1调用doSome
    //线程t2调用doOther
    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("t1")){
            test.doSome();
        }

        if(Thread.currentThread().getName().equals("t2")){
            test.doOther();
        }
    }
}

class Myclass{
    //上锁
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000*10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
		//上锁
    public synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}
  • 这题两个方法都上锁了

  • 需要,因为一个对象只有一把锁,t1占用了

  • t2执行doOther方法的时候同样需要这把锁,但被占用,所以需要等待doSome结束,把锁释放

  • 执行顺序

    • doSome begin
      doSome over
      doOther begin
      doOther over
      
第三题
  • 问:doOther 方法是否需要等待 doSome 方法结束才执行
public class Main {
    public static void main(String[] args) {
        Myclass test1=new Myclass();
        Myclass test2=new Myclass();
        //两个线程两个对象
        MyThread t1=new MyThread(test1);
        MyThread t2=new MyThread(test2);

        //给线程对象设置名称
        t1.setName("t1");
        t2.setName("t2");

        //线程t1启动
        t1.start();
        //延迟一秒,为了让t1先启动
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //线程t2启动
        t2.start();
    }
}

class MyThread extends Thread{
    //线程对象中的一个共享对象属性
    Myclass test;
    MyThread(Myclass test){
        this.test=test;
    }

    //线程t1调用doSome
    //线程t2调用doOther
    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("t1")){
            test.doSome();
        }

        if(Thread.currentThread().getName().equals("t2")){
            test.doOther();
        }
    }
}

class Myclass{
  	//上锁
    public synchronized void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000*10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }
		//上锁
    public synchronized void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}
  • 这题new了两个对象,不共享对象

  • 不需要等待,因为两个对象两把锁

  • 虽然两个方法都需要锁,但现在有两个锁,自己拿自己的锁,不需要等待

第四题
  • 问:doOther 方法是否需要等待 doSome 方法结束才执行
public class Main {
    public static void main(String[] args) {
        Myclass test1=new Myclass();
        Myclass test2=new Myclass();
        //两个线程共享一个对象
        MyThread t1=new MyThread(test1);
        MyThread t2=new MyThread(test2);

        //给线程对象设置名称
        t1.setName("t1");
        t2.setName("t2");

        //线程t1启动
        t1.start();
        //延迟一秒,为了让t1先启动
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //线程t2启动
        t2.start();
    }
}

class MyThread extends Thread{
    //线程对象中的一个共享对象属性
    Myclass test;
    MyThread(Myclass test){
        this.test=test;
    }

    //线程t1调用doSome
    //线程t2调用doOther
    @Override
    public void run() {
        if(Thread.currentThread().getName().equals("t1")){
            test.doSome();
        }

        if(Thread.currentThread().getName().equals("t2")){
            test.doOther();
        }
    }
}

class Myclass{
    public synchronized static void doSome(){
        System.out.println("doSome begin");
        try {
            Thread.sleep(1000*10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }

    public synchronized static void doOther(){
        System.out.println("doOther begin");
        System.out.println("doOther over");
    }
}
  • 需要

  • 加了static关键字之后占用的是类锁

  • 虽然有两个对象,但类锁只有一把,t1占用了类锁,t2需要等待

  • 执行顺序

    • doSome begin
      doSome over
      doOther begin
      doOther over
      

6.10 开发中如何解决线程安全问题

  • 尽量不用synchronized,这样减少并发,降低效率

  • 尽量使用 局部变量 代替 实例变量 和 静态变量

  • 如果必须是实例变量,考虑创建多个对象,这样就不会有内存共享

  • 不能用局部变量,也不能创建多个对象,这时候在选择使用synchronized

7. 死锁

  • 死锁程序
    • 不会出现异常,不报错,一致僵持,永远不结束

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bMOn4hPk-1622909094806)(/Users/mac/Library/Containers/com.tencent.qq/Data/Library/Caches/Images/D04818B28A15625068FE1005A7DBB290.jpg)]

public class Main {
    public static void main(String[] args) {
        Object o1=new Object();
        Object o2=new Object();

        MyThread1 t1=new MyThread1(o1,o2);
        MyThread2 t2=new MyThread2(o1,o2);

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

    }
}

class MyThread1 extends Thread{
    Object o1;
    Object o2;

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

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

            }
        }
    }
}

class MyThread2 extends Thread{
    Object o1;
    Object o2;

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

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

            }
        }
    }
}

8. 守护线程

8.1 守护线程

  • Java语言线程分类

    • 用户线程
      • 例如main方法
    • 守护线程(后台线程)
      • 死循环,所以用户线程结束,守护线程才结束
      • 例如
        • 垃圾回收线程
        • 定时自动备份
  • 模拟守护线程

public class Main {
    public static void main(String[] args) {
        Thread t=new MyThread();
        t.setName("守护线程");
        //设置为守护线程
        t.setDaemon(true);
        //启动线程
        t.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 MyThread 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();
            }
        }
    }
}

8.2 定时器

  • 定时器的作用

    • 特定时间执行特定程序
  • 定时器的实现

    • 原始的方法
      • 使用sleep方法,设置睡眠时间
    • 实际开发中
      • java.util.Timer(使用的较少)
      • Spring框架中的SpringTask框架(使用较多)
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.Timer;
    import java.util.TimerTask;
    
    public class Main {
        public static void main(String[] args) throws ParseException {
            //创建定时器对象
            Timer timer=new Timer();
    
            //指定定时任务
            SimpleDateFormat sdf=new SimpleDateFormat("yyyy 年 MM 月 dd 日 hh 时 mm 分 ss秒");
            //设置启动时间,转换为date类型
            Date firstTime=sdf.parse("2021 年 05 月 30 日 04 时 39 分 00秒");
            //三个参数:执行任务,起始时间,间隔时间
            timer.schedule(new LogTimerTask(),firstTime,1000*10);
            //这里TimerTask是抽象类,不能直接new,可以用匿名内部类,也可以写个类继承
        }
    }
    
    class LogTimerTask extends TimerTask{
        @Override
        public void run() {
            SimpleDateFormat sdf=new SimpleDateFormat("yyyy 年 MM 月 dd 日 hh 时 mm 分 ss秒");
            String strTimer=sdf.format(new Date());
            System.out.println(strTimer+":完成了异常数据备份");
        }
    }
    

9. 实现线程的第三种方式

  • 实现Callable接口
    • 优点:
      • 可以获取当前线程的执行结果
    • 缺点:
      • 效率比较低,获取结果的时候可能导致线程受阻
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //第一步,创建一个未来任务对象
        //参数需要给一个Callable接口
        FutureTask task=new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {
                System.out.println("call method begin");
                Thread.sleep(1000*10);
                System.out.println("call method over");
                int a=100;
                int b=200;
                return a+b;
            }
        });

        //传入FutureTask,创建线程对象(类似传入Runnable接口)
        Thread t=new Thread(task);

        //启动线程
        t.start();

        //获取t线程的返回结果
        //get方法的执行可能会导致当前线程的阻塞
        Object obj=task.get();

        System.out.println(obj);//300
    }
}


10. wait和notify方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8dRoNbeu-1622909094809)(/Users/mac/Library/Containers/com.tencent.qq/Data/Library/Caches/Images/3BF681289CB814EA6CC854A15BA60E67.jpg)]

  • wait和notify方法

    • 两个方法是所以对象都有的方法,因为它们是Object类中的方法
    • 这两个方法不是通过线程对象调用的
  • wait方法作用

    • Object o=new Object();
      o.wait;
      
    • 表示让o对象进入等待状态,并释放锁,直到被唤醒

  • notify方法作用

    • Object o=new Object();
      o.notify;
      
    • 唤醒o对象,不释放锁

    • notifyAll方法

      • 唤醒o对象处于等待的所有线程
10.1 生产者和消费者模式
  • 这种模式是实际开发中可能遇到的情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vFcIyuPy-1622909094811)(/Users/mac/Library/Containers/com.tencent.qq/Data/Library/Caches/Images/3E5385802B17B968527F5F15630997F3.jpg)]

  • 模拟生产者和消费者模式
    • 生产一个消费一个
    • 如果仓库有货,停止生产,消费后在继续生产
    • 如果仓库没货,停止消费,生产后在继续消费
import java.util.ArrayList;
import java.util.List;

public class Main {
    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{
    //仓库
    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.notifyAll();
            }
        }

    }
}

//消费线程
class Consumer implements Runnable{
    //仓库
    List list;

    public Consumer(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=list.remove(0);
                System.out.println(Thread.currentThread().getName()+"--->"+obj);
                //消费后就没货了,唤醒消费线程生产
                list.notifyAll();
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值