Java进阶第九章——多线程

多线程

本章多线程原理能够在操作系统学习记录中找到原理:咖啡ice的操作系统学习记录

1.多线程概述

  • 进程:是一个应用程序,或者说是一个软件。进程之间内存的资源不会共享。
  • 线程:是进程中的执行场景/执行单元。一个进程可以启动多个线程。线程之间堆内存和方法区内存共享,但栈内存独立,一个线程一个栈。

2.实现多线程方式

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

    创建线程对象:new

    启动线程:调用线程对象的start()方法

  • 注:start方法作用:启动一个分支线程,在JVM中开辟一个新的栈空间,启动完这段代码就结束了,启动成功后新的栈空间会自动调用run方法开始执行自己的任务,且run方法在新的分支栈底部。

  • run方法在分支栈底部,main方法在主栈底部,run和main是平级的。

/*
实现线程的第一种方式:
    编写一个类,继承java.lang.Thread,重写run方法
 */
public class ThreadTest {
    public static void main(String[] args) {
        //这里的main方法,这里代码属于主线程,在主栈中运行
        //新建一个分支线程对象
        MyThread myThread = new MyThread();
        //启动线程

        myThread.start();  //start方法作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码启动完就结束了,新的栈空间会开始执行它的任务。
        //如果直接调用MyThread.run(),会导致一直执行到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);
        }
    }
}
  • 第二种:编写一个类,实现java.lang.Runable接口,实现run方法。
  • 一般使用实现接口的方法,还能够继承(extends)其他类。
public class ThreadTest {
    public static void main(String[] args) {
        //创建一个可运行的对象,封装成一个线程对象
        Thread t = new Thread(new MyRunable());
        //启动线程
        t.start();

        //其他代码继续在主线程中执行
        for (int i = 0; i <100; i++) {
            System.out.println("主线程——>" + i);
        }

    }
}

//这不是一个线程类,只是一个可运行的类。不是一个线程。
class MyRunable implements Runnable{
    @Override
    public void run() {
        //编写程序,这段程序运行在分支线程中(分支栈)。
        for (int i = 0; i <100; i++) {
            System.out.println("分支线程——>" + i);
        }
    }
}

​ 通过常用匿名内部类实现

public class ThreadTest {
    public static void main(String[] args) {
        //创建线程对象采用匿名内部类方式
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("t线程" + i);
                }
            }
        });

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

        for (int i = 0; i < 100; i++) {
            System.out.println("main线程" + i);
        }
    }
}
  • 第三种:实现Callable接口(JDK8新特性),这种方式优点能够获取返回执行结果,缺点是获取线程执行结果时,本线程会阻塞执行效率低。

    这种方式实现线程可以获取线程的返回值,前两种方式的run返回void无法获取返回值。

package studyThread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;  //属于JAVA的并发包。

//实现线程的Callable接口
public class ThreadTest13 {
    public static void main(String[] args) throws Exception{
        //创建一个“未来任务”类对象
        //这里需要给一个Callable接口实现类对象。
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception {  //call()方法相当于run方法,只不过这里有返回值。
                //线程执行一个任务,执行完会有一个结果。
                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;

            }
        });
        //创建线程对象
        Thread t = new Thread(task);
        //启动线程
        t.start();
        //这里main方法是在主线程当中
        //在主线程中获取线程的返回结果使用get()方法
        System.out.println(task.get());
        //main方法下面的程序想要执行必须等待get()方法结束
        //get方法可能需要很久,因为get方法为了拿另一个线程的执行结果。另一个线程需要时间。
        //get方法执行会导致当前线程执行阻塞
        System.out.println("get()获取完毕");
    }
}

3.线程生命周期

  • 刚new出来的线程对象为新建状态(创建状态)。
  • 调用了start方法进入就绪状态。就绪状态的线程有抢夺CPU时间片(执行权)的权利。
  • 当一个线程抢夺到CPU时间片后,执行run方法,标志着线程进入运行状态。
  • 当占有的CPU时间片用完后,会线程会重新回到就绪状态继续抢夺CPU时间片,再次得到CPU时间片时会重新进入run方法接着上一次代码继续往下执行。
  • run方法执行结束,标志着线程进入死亡状态(终止状态)。
  • 当遇到阻塞事件,例如要接受用户键盘输入或者sleep方法等,程序会从执行状态进入到阻塞状态,放弃当前占用的CPU时间片。
  • 阻塞解除之后,线程会进入到就绪状态。

本小结涉及到操作系统中的进程管理:操作系统学习第二章进程管理

4.获取线程名字

  • 获取线程名字:线程对象.t.getName()
  • 设置线程名字:线程对象.setName(“线程名字”);
  • 当线程没有设置名字时,默认名字:Thread-0、Thread-1、……
public class ThreadTest {
    public static void main(String[] args) {
        //创建线程对象
        MyThread2 t = new MyThread2();//默认名字:Thread-0
        //设置线程名字
        t.setName("t1");//修改为t1
        //获取线程的名字
        System.out.println(t.getName());  //t1

        MyThread2 t2 = new MyThread2(); //默认名字:Thread-1
        //不修改名称
        System.out.println(t2.getName());  //Thread-1
    }
}
class MyThread2 extends Thread{
    public void run(){
        for (int i = 0; i < 100; i++) {
            System.out.println("分支线程——>" + i);
        }
    }
}
  • 获取当前线程对象:static Thread currentThread()
public class ThreadTest {
    public static void main(String[] args) {

        //currentThread表示当前线程对象
        //这个代码出现在main方法当中,所以当前线程就是主线程。
        Thread currentThread = Thread.currentThread();
        System.out.println(currentThread.getName());  //main

        //创建线程对象
        MyThread2 t = new MyThread2();//默认名字:Thread-0
        //设置线程名字
        t.setName("t1");//修改为t1
        t.start();
        //获取线程的名字
        System.out.println(t.getName());  //t1


        MyThread2 t2 = new MyThread2(); //默认名字:Thread-1
        t2.start();
        //不修改名称
        System.out.println(t2.getName());  //Thread-1
    }
}
class MyThread2 extends Thread{
    public void run(){
        for (int i = 0; i < 100; i++) {
            Thread currentThread = Thread.currentThread();
            System.out.println(currentThread.getName() + "——>" + i);
        }
    }
}

5.线程的休眠和终止

  • static void sleep(long millis):静态方法、参数是毫秒,作用是让当前进程进入休眠(阻塞)状态,放弃CPU时间片让给其他线程使用。
public class ThreadTest {
    public static void main(String[] args) {
        //让当前进程进入休眠,睡眠时间5秒
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("咖啡加点ice"); //5秒之后才会打印
    }
}
  • 可以通过此方法,执行一段特定的代码,每隔一段时间执行一次。
public class ThreadTest {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "——>" + i);
            //睡眠一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
  • sleep()这是一个静态方法,在哪个线程调用那个线程进入休眠。
public class ThreadTest {
    public static void main(String[] args) {
        //创建线程对象
        Thread t = new MyThread3();
        t.setName("t");
        t.start();
        //调用sleep方法
        try {
            t.sleep(1000*5);  //sleep作为静态方法,在这里调用t.sleep会转换为:Thread.sleep(1000*5)
            //这段代码是让当前线程进入休眠,即main方法休眠
            //这方法出现在在main方法中,是main方法休眠,而不是t休眠
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("主线程");  //5秒钟后才能打印
    }
}
class MyThread3 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println("分支线程——>" + i);
        }
    }
}
  • sleep()休眠时间过长,如果需要提前终止线程的休眠:interrupt()
public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunable2());
        t.setName("t");
        t.start();

        //希望5秒后t线程终止休眠
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //终止t线程的睡眠,这种中断睡眠的方式,依靠了Java的异常处理机制
        t.interrupt();  //这个方法调用,t中的sleep会捕获到异常,就会提前结束
    }
}
class MyRunable2 implements Runnable{

    //重点:run()方法当中的异常不能throws,只能try.catch
    //因为run()方法在父类中没有任何异常,子类不能比父类抛出更多的异常
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "——> begin" );
        try {
            //睡眠一小时
            Thread.sleep(1000*60*60);
        } catch (InterruptedException e) {
            //打印异常信息
            e.printStackTrace();
        }
        //1小时后才会执行这里
        System.out.println(Thread.currentThread().getName() + "——> over" );
    }
}
  • 强制终止线程:stop(),此方法容易丢失数据,不建议使用。

  • 合理的终止一个线程执行,通过设置一个标记进行终止。

public class ThreadTest {
    public static void main(String[] args) {
        MyRunnable3 r = new MyRunnable3();
        Thread t = new Thread(r);
        t.setName("t");
        t.start();

        //主程序延迟5秒执行t线程的终止
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //5秒到了,强制终止t线程
        //t.stop();会直接丢失数据不建议使用
        r.run=false;
    }
}

class MyRunnable3 implements Runnable{

    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) {
                    throw new RuntimeException(e);
                }
             }else {
             //return就结束了,在return将需要保存的数据进行保存。
            return;
            }
        }
    }
}

6.线程调度

  • 常见的线程调度模型:

    抢占式调度模型:线程的优先级比较高,抢到的CPU时间片概率高。Java采用的就是抢占式调度模型。

    均分式调度模型:平均分配CPU时间片,每个线程占有的CPU时间片长度一样,平均分配一切平等。

  • java中提供与线程调度有关的方法:

    • 实例方法

      更改线程优先级:void setPriority(int newPriority)

      获取线程优先级:int getPriority()

      等待某个线程结束:void join()

      //线程合并
      public class ThreadTest {
          public static void main(String[] args) {
              System.out.println("main begin");
              Thread t = new Thread(new MyRunnable5());
              t.setName("t");
              t.start();
      
              try {
                  t.join();//t合并到当前线程中,当前线程受阻塞,t线程执行到结束
              } catch (InterruptedException e) {
                  throw new RuntimeException(e);
              }
      
              System.out.println("main over");  //t执行结束才会执行这里。
          }
      }
      class MyRunnable5 implements Runnable{
          @Override
          public void run() {
              for (int i = 0; i < 100; i++) {
                  System.out.println(Thread.currentThread().getName() + "——>" +i);
              }
          }
      }
      

      最低优先级1常量:MAX_PRIORITY

      默认优先级5常量:MIN__PRIORITY

      最高优先级10常量:NORM__PRIORITY

    //关于线程优先级
    public class ThreadTest {
        public static void main(String[] args) {
            System.out.println("最高优先级:" + MAX_PRIORITY);  //最高优先级:10
            System.out.println("最低优先级:" + MIN_PRIORITY);  //最低优先级:1
            System.out.println("默认优先级:" + NORM_PRIORITY); //默认优先级:5
    
            //获取当前线程对象,获取当前线程优先级
            Thread currentThread = Thread.currentThread();
            System.out.println(currentThread.getName() + "线程的默认优先级是:" + currentThread.getPriority());  //main线程的默认优先级是5
    
            Thread t = new Thread(new MyRunable3());
            t.setName("t");
            t.start();
    
        }
    }
    
    class MyRunable3 implements Runnable{
        @Override
        public void run() {
            //获取当前线程优先级
            System.out.println(Thread.currentThread().getName() + "线程没有设置时的优先级是:" + Thread.currentThread().getPriority());  //t线程没有设置时的优先级是:5
        }
    }
    
    • 静态方法

      暂停当时线程,执行其他线程:static void yield()

      注:yield方法不是阻塞方法,让当前线程让位从运行状态回到就绪状态,将CPU时间片让给其他线程使用。

    //让当前进行暂停,回到就绪状态,让给其他线程
    //静态方法:Thread.yield();
    public class ThreadTest {
        public static void main(String[] args) {
            Thread t = new Thread(new MyRunnable4());
            t.setName("t");
            t.start();
            for (int i = 1; i <=10000; i++) {
                System.out.println(Thread.currentThread().getName() + " ——> " + i);
            }
        }
    }
    class MyRunnable4 implements Runnable {
        @Override
        public void run() {
            for (int i = 1; i <=10000; i++) {
                //每100让位一次
                if(i % 100 == 0){
                    Thread.yield();  //当前线程暂停一下,让给主线程
                }
                System.out.println(Thread.currentThread().getName() + " ——> " + i);
            }
        }
    }
    

7.synchronized线程同步

  • 线程安全:在多线程并发环境下,数据的安全问题。

  • 多线程并发环境下存在安全问题的条件:①多线程并发、②有共享数据、③共享数据有修改的行为。

  • 线程同步:线程排队执行,用排队执行解决线程安全问题。

  • 同步与异步:

    异步(并发)模型:线程t1与线程t2互相不影响,各自执行。即多线程并发。

    同步(排队)模型:线程t1和线程t2之间,一个线程在执行时,另一个线程要等待前面线程执行结束才能执行。线程排队执行。

  • synchronized(共享对象){ }:假设t1与t2线程并发,t1遇到了synchronized,就占用这把锁,直到代码执行结束这把锁才会释放,在未释放前,t2遇到synchronized只能等待t1执行结束synchronized里边的代码才能够执行synchronized里边的代码。

【例子】模拟两个线程对同一个账户取款。

银行账户设置了线程安全synchronized(){ },如果没有设置线程安全将会出现数据安全问题。

//银行账户
public class Account {
    //账号
    private String actno;
    //余额
    private double balance;
    public Account() { }

    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 withdrad(double money){
       //线程同步机制
        //synchronized ()括号中数据必须是多线程共享的数据,才能达到线程同步。
        //()里填需要线程同步的共享对象
        synchronized (this){
            //线程同步机制代码块
            //一个线程把这里代码全部执行结束后,另一个线程才能进来。
            double before = this.getBalance();
            double after = before - money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if(after>=0){
                this.setBalance(after);
            }else {
                System.out.println("余额不足");
            }
        }
    }
}

设置线程模拟取款

public class AccountThread extends Thread{
    //两个线程必须共享一个账户对象
    private Account act;
    //通过构造方法传过来账户对象
    public AccountThread(Account act){
        this.act = act;
    }
    public void run() {
        //run表示取款操作
        //假设取款5000
        double money = 5000;
        //取款
        act.withdrad(money);
        System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款成功,余额:" + act.getBalance());
    }
}

实现测试类

public class Test {
    public static void main(String[] args) {
        //创建账户对象(只创建一个)
        Account act = new Account("act-001",10000);
        //创建两个线程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);
        //设置线程name
        t1.setName("t1");
        t2.setName("t2");
        //启动线程取款
        t1.start();  //t1对act-001取款成功,余额:5000.0
        t2.start();  //t2对act-001取款成功,余额:0.0
    }
}
  • 可以直接在实例方法上加上synchronized,有个缺点是会扩大同步的范围,导致程序的执行效率低。
 //取款方法
public synchronized void withdrad(double money){
            double before = this.getBalance();
            double after = before - money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if(after>=0){
                this.setBalance(after);
            }else {
                System.out.println("余额不足");
            }
}
  • synchronized使用在实例方法上,代码比较简洁了,如果共享的对象是this,并且需要同步的是整个方法体,使用这种方式。

  • synchronized出现在静态方法上(带static方法),是类锁,不管创建几个对象这个方法同步。

8.守护线程

  • 守护线程:这是一个死循环,所有用户线程只要结束,守护线程自动结束。
package studyThread;

public class ThreadTest {
    public static void main(String[] args) {
        Thread t = new BakDataThread();
        t.setName("这是一个守护线程");
        //在启动线程之前将线程设置成一个守护线程,
        t.setDaemon(true);//设置成守护线程之后,如果这里线程结束,即使是死循环线程也会结束
        t.start();

        //主线程:主线程是用户线程
        //在没有设置守护线程时,主线程的for循环结束后,死循环的线程还会继续执行
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "——>" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

//写一个死循环的线程
class BakDataThread extends Thread{
    public void run(){
        int i = 0;
        while (true){
            System.out.println(Thread.currentThread().getName() + "——>" + (++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

9.定时器

  • 定时器的作用:间隔特定的时间执行特定的程序。

  • 定时器在java中实现方式:

    第一种:使用sleep方法,睡眠。设置睡眠时间,每到这个时间点醒来执行任务。

    第二种:java.util.Timer可以直接用,目前开发很少用,很多高级框架支持定时任务。

    第三种:目前使用最多的是Spring框架中的SpringTask框架。

public class TimerTest {
    public static void main(String[] args) throws Exception {
        //创建定时器对象
        Timer timer = new Timer();
        //Timer timer = new Timer(true);守护线程的方式

        //指定定时任务
        //timer.schedule(定时任务,第一次执行时间,间隔多久执行一次);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date firstTime = sdf.parse("2024-01-22 16:39:00");
        timer.schedule(new LogTimerTask(),firstTime,1000*10);
    }
}

//编写一个定时任务类
//一个记录日志的定时任务
class LogTimerTask extends TimerTask{
    @Override
    public void run() {
        //编写需要执行的任务
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(sdf.format(new Date()) + ":完成了一次定时任务");
    }
}

——本章节为个人学习笔记。学习视频为动力节点Java零基础教程视频:动力节点—JAVA零基础教程视频

你可能感兴趣的:(咖啡ice的Java学习记录,java,开发语言)