Java核心技术卷一 -第十二章:多线程

系列文章目录

Java核心技术卷一 -第一章:java“白皮书”的关键术语
Java核心技术卷一 -第三章:数据类型
Java核心技术卷一 -第三章:变量与常量
Java核心技术卷一 -第三章:运算符
Java核心技术卷一 -第三章:字符串
Java核心技术卷一 -第三章:输入与输出
Java核心技术卷一 -第三章:数组
Java核心技术卷一 -第四章:类之间的关系-依赖
Java核心技术卷一 -第四章:预定义类-LocalDate类小应用
Java核心技术卷一 -第四章:构造器
Java核心技术卷一 -第四章:null引用
Java核心技术卷一 -第四章:方法参数
Java核心技术卷一 -第四章:对象构造
Java核心技术卷一 -第五章:覆盖方法与super
Java核心技术卷一 -第五章:super关键字
Java核心技术卷一 -第五章:类的强制类型转换与instanceof操作符
Java核心技术卷一 -第五章:抽象类与abstract关键字
Java核心技术卷一 -第五章:Object类的toString方法
Java核心技术卷一 -第五章:数组列表初识
Java核心技术卷一 -第五章:装箱和拆箱
Java核心技术卷一 -第五章:枚举类再认识
Java核心技术卷一 -第七章:异常
Java核心技术卷一 -第九章:集合
Java核心技术卷一 -第六章:抽象类和接口
Java核心技术卷一 -第十章:IO流

文章目录

  • 系列文章目录
  • 前言
  • 一、多线程
    • 1.1、什么是进程?什么是线程?
    • 1.2、基本线程数量
    • 1.3、进程和线程是什么关系?
      • 举个例子:
      • 注意:
    • 1.4、思考一个问题:
    • 1.5、分析一个问题:对于单核的CPU来说,真的可以做到真正的多线程并发吗?
    • 1.6、java语言中,实现线程有两种方式,那两种方式呢?
      • 第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法。
      • 代码展示:
      • 第二种方式:编写一个类,实现java.lang.Runnable接口,实现run方法。
      • 代码展示:
    • 1.7、线程的生命周期
    • 1.8、线程的方法
      • 代码展示:
      • 代码展示(在java中怎么强行终止一个线程的执行):
        • 直接终止:
        • 间接终止:这种方法安全
    • 1.9、线程安全
      • 为什么这个是重点?
      • 什么时候数据在多线程并发的环境下会存在安全问题呢?
      • 怎么解决线程安全问题呢?
      • 说到线程同步这块,涉及到这两个专业术语:
        • 异步编程模型:
        • 同步编程模型:
    • 1.10、模拟俩个线程对同一账号取款
      • Account类:
      • AccountThread类:
      • Test类:
      • 代码执行原理(锁池):
      • 提示:
      • 小总结:
    • 1.11、死锁
      • 图示:T1从上往下,T2从下往上,T1锁住第一个后再锁第二个;T2锁住第二个后再锁第一个,此时双方都锁住了要锁的第一个,而要锁的第二个被对方锁住,从而造成死锁。
      • 代码展示:
      • 我们以后开发中应该怎么解决线程安全问题?
    • 1.12、守护线程:
      • 图示:
      • 代码展示:
    • 1.13、定时器:
      • 了解:
      • 定时器的作用:
      • 代码展示:
    • 1.14、关于Object类中的wait和notify方法。(生产者和消费者模式!)
      • 图示:
      • (生产者和消费者模式!):
      • 代码展示:
  • 总结


前言

本人为java初学者,文章内容仅为个人学习总结分享,其中包含了大量Java核心技术卷一里面的文章内容以及一些网站文章内容,由于参考文章过多,在此并不一一列出,如有侵权,请联系删除。

一、多线程

1.1、什么是进程?什么是线程?

进程是一个应用程序(1个进程是一个软件)
线程是一个进程中的执行场景/执行单元。
一个进程可以启动多个线程。

1.2、基本线程数量

对于java程序来说,当在DOS命令窗口中输入:java HelloWorld回车之后。会先启动JVM,而JVM就是一个进程。
JVM再启动一个主线程调用main方法。
同时再启动一个垃圾回收线程负责看护,回收垃圾。
最起码,现在的java程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行main方法的主线程。

1.3、进程和线程是什么关系?

举个例子:

阿里巴巴:进程
马云:阿里巴巴的一个线程
童文红:阿里巴巴的一个线程

京东:进程
强东:京东的一个线程
妹妹:京东的一个线程

进程可以看做是现实生活当中的公司,
线程可以看做是公司当中的某个员工。

注意:

进程A和进程B的内存独立不共享。(阿里巴巴和京东资源不会共享的!)线程A和线程B呢?
在java语言中:

线程A和线程B,堆内存和方法区内存共享。
但是栈内存独立,一个线程一个栈。

假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。

火车站,可以看做是一个进程:
火车站中的每一个售票窗口可以看做是一个线程。
我在窗口1购票,你可以在窗口2购票,你不需要等我,我也不需要等你。
所以多线程并发可以提高效率。
java中之所以有多线程机制,目的就是为了提高程序的处理效率。

1.4、思考一个问题:

使用了多线程机制之后,main方法结束,是不是有可能程序也不会结束。
main方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在压栈弹栈。

1.5、分析一个问题:对于单核的CPU来说,真的可以做到真正的多线程并发吗?

对于多核的CPU电脑来说,真正的多线程并发是没问题的。
4核CPU表示同一个时间点上,可以真正的有4个进程并发执行。

什么是真正的多线程并发?
t1线程执行t1的。
t2线程执行t2的。
t1不会影响t2,t2也不会影响t1。这叫做真正的多线程并发。

单核的CPU表示只有一个大脑:
不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。
对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于
CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是:多个事情
同时在做!!!!!
线程A:播放音乐
线程B:运行魔兽游戏
线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在运行,
给我们的感觉是同时并发的。

电影院采用胶卷播放电影,一个胶卷一个胶卷播放速度达到一定程度之后,
人类的眼睛产生了错觉,感觉是动画的。这说明人类的反应速度很慢,就像
一根钢针扎到手上,到最终感觉到疼,这个过程是需要“很长的”时间的,在
这个期间计算机可以进行亿万次的循环。所以计算机的执行速度很快。

1.6、java语言中,实现线程有两种方式,那两种方式呢?

java支持多线程机制。并且java已经将多线程实现了,我们只需要继承就行了。

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

  // 定义线程类
  public class MyThread extends Thread{
   public void run(){
   
   }
  }
  // 创建线程对象
  MyThread t = new MyThread();
  // 启动线程。
  t.start();

代码展示:

public class Test02 {
    public static void main(String[] args) {
        AZX azx=new AZX();
        //启动线程
        //start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
        //这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了,线程就启动成功了。
        //启动成功的线程会自动调用run方法,并且run方在分支的栈底部(压栈)。
        //run方法在分支的底部,main方法在主栈的栈底部。run和main是平级的。
        azx.start();

        for (int i=0;i<1000;i++){
            System.out.println("主线程------>"+i);
        }
    }
}

class AZX extends Thread{
    @Override
    public void run() {
        for (int i=0;i<1000;i++){
            System.out.println("分支线程------>"+i);
        }
    }
}

第二种方式:编写一个类,实现java.lang.Runnable接口,实现run方法。

  // 定义一个可运行的类
  public class MyRunnable implements Runnable {
   public void run(){
   
   }
  }
  // 创建线程对象
  Thread t = new Thread(new MyRunnable());
  // 启动线程
  t.start();

代码展示:

public class Test02 {
    public static void main(String[] args) {
        //创建一个可运行的对象
        MyRunnable r=new MyRunnable();
        //将可运行的对象封装成一个线程对象
        Thread t=new Thread(r);

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

        for (int i=0;i<100;i++){
            System.out.println("主线程------>"+i);
        }
    }
}

class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i=0;i<100;i++){
            System.out.println("分支线程------>"+i);
        }
    }
}

注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承
其它的类,更灵活。

1.7、线程的生命周期

Java核心技术卷一 -第十二章:多线程_第1张图片

1.8、线程的方法

代码展示:

public class Test02 {
    public static void main(String[] args) {
        //让当前线程进行休眠,5秒后唤醒
        try {
            //注意:sleep方法是静态方法,无论是谁调用,最后它都只会让当前线程睡眠,即便是用其它线程的名字调用也没用
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //5秒后执行这里的代码
        System.out.println("HelloWorld!!!");

        //获取当前对象线程的名字
        Thread currentThread =Thread.currentThread();
        System.out.println(currentThread.getName());

        //创建一个线程对象
        AZX r=new AZX();

        //设置线程的名字
        r.setName("ttttt");

        //获取线程的名字
        String tName= r.getName();
        System.out.println(tName);

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

        for (int i=0;i<10;i++){
            System.out.println("主线程------>"+i);
        }
    }
}

class AZX extends Thread{
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            //currentThread就是当前线程对象。当前线程是谁呢?
            //当t1我程执行run方法,那么这个当前线程就是t1
            //当t2线程执行run方法,那么这个当前线程就是t2
            Thread currentThread =Thread.currentThread();
            System.out.println(currentThread.getName()+":分支线程------>"+i);
        }
    }
}

代码展示(在java中怎么强行终止一个线程的执行):

直接终止:

这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了,线程没有保存的数据将会丢失。

public class Test02 {
    public static void main(String[] args) {
        Thread t=new Thread(new AZX());
        t.setName("t");
        t.start();
        //模拟5秒
        try {
            //注意:sleep方法是静态方法,无论是谁调用,最后它都只会让当前线程睡眠,即便是用其它线程的名字调用也没用
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //5秒后强行终止t线程
        t.stop();//已过时,不建议使用
    }
}

class AZX extends Thread{
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            //执行十秒
            Thread currentThread =Thread.currentThread();
            System.out.println(currentThread.getName()+":分支线程------>"+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

结果:
Java核心技术卷一 -第十二章:多线程_第2张图片

间接终止:这种方法安全
public class Test02 {
    public static void main(String[] args) {
        MyRunnable r=new MyRunnable();
        Thread t=new Thread(r);
        t.setName("t");
        t.start();
        //模拟5秒
        try {
            //注意:sleep方法是静态方法,无论是谁调用,最后它都只会让当前线程睡眠,即便是用其它线程的名字调用也没用
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //5秒后强行终止t线程
        r.run=false;
    }
}

class MyRunnable implements Runnable{
    //打一个布尔标记
    boolean run=true;

    @Override
    public void run() {
            for (int i=0;i<10;i++){
                if(run){
                //执行十秒
                Thread currentThread =Thread.currentThread();
                System.out.println(currentThread.getName()+":分支线程------>"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
                    //终止当前线程
                    return;
            }
        }
    }
}

结果:
Java核心技术卷一 -第十二章:多线程_第3张图片

1.9、线程安全

为什么这个是重点?

以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。

最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。(重点:*****)

什么时候数据在多线程并发的环境下会存在安全问题呢?

三个条件:
条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为。

满足以上3个条件之后,就会存在线程安全问题。

怎么解决线程安全问题呢?

当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?

线程排队执行。(不能并发)。

用排队执行解决线程安全问题。这种机制被称为:线程同步机制

专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。

线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。

说到线程同步这块,涉及到这两个专业术语:

异步编程模型:

线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。
其实就是:多线程并发(效率较高。)

异步就是并发。

同步编程模型:

线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。

效率较低:线程排队执行

同步就是排队。

1.10、模拟俩个线程对同一账号取款

Account类:

public class Account {
    //账户
    private String actno;
    //余额
    private double balance;
    //构造方法
    public Account(){

    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }
    //set与get方法

    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){
        synchronized (this){
            //取款之前的余额
            double before=this.getBalance();
            //取款之后的余额
            double after=before-money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //更新余额
            this.setBalance(after);
        }
    }
}

AccountThread类:

public class AccountThread extends Thread{
    //俩个线程共享同一个账户对象
    private Account act;

    //构造方法
    public AccountThread(Account act) {
        this.act = act;
    }
    public void run(){
        //run方法的执行表示取款操作
        double money=5000;
        //取款
        act.withdraw(money);
        System.out.println(Thread.currentThread().getName()+"对"+act.getActno()+"取款成功,余额:"+act.getBalance());
    }
}

Test类:

public class AccountTest01 {
    public static void main(String[] args) {
        //创建账户对象(只创建1个)
        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();
        t2.start();
    }
}

结果:
Java核心技术卷一 -第十二章:多线程_第4张图片

代码执行原理(锁池):

Java核心技术卷一 -第十二章:多线程_第5张图片
Java核心技术卷一 -第十二章:多线程_第6张图片

提示:

这里需要注章的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。

小总结:

Java核心技术卷一 -第十二章:多线程_第7张图片

1.11、死锁

图示:T1从上往下,T2从下往上,T1锁住第一个后再锁第二个;T2锁住第二个后再锁第一个,此时双方都锁住了要锁的第一个,而要锁的第二个被对方锁住,从而造成死锁。

Java核心技术卷一 -第十二章:多线程_第8张图片

代码展示:

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

        //t1和t2俩个线程共享o1、o2
        Thread t1=new MyThread1(o1,o2);
        Thread t2=new MyThread22(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;
    }
    public void run(){
        synchronized (o1){
            try {
                System.out.println("MyThead1-begin");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2){
                System.out.println("MyThead1-end");
            }
        }
    }
}

class MyThread22 extends Thread{
    Object o1;
    Object o2;

    public MyThread22(Object o1, Object o2) {
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o2){
            try {
                System.out.println("MyThead22-begin");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1){
                System.out.println("MyThead22-end");
            }
        }
    }
}

结果:卡住了
Java核心技术卷一 -第十二章:多线程_第9张图片

我们以后开发中应该怎么解决线程安全问题?

是一上来就选择线程同步吗?synchronized
不是,synchronized会让程序的执行效率降低,用户体验不好。
系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择线程同步机制。

第一种方案:尽量使用局部变量代替“实例变量和静态变量”。

第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了。)

第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。

1.12、守护线程:

图示:

Java核心技术卷一 -第十二章:多线程_第10张图片

代码展示:

package Package03;
public class ThreadTest04 {
    public static void main(String[] args) {
        Thread t=new BakDataThread();
        //启动线程之前,将线程设置为守护线程
        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 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) {
                e.printStackTrace();
            }
        }
    }
}

结果:主线程即main线程结束,守护线程也结束(其死循环结束)
Java核心技术卷一 -第十二章:多线程_第11张图片

1.13、定时器:

了解:

定时器:Timer类
Java核心技术卷一 -第十二章:多线程_第12张图片
注意schedule类:
Java核心技术卷一 -第十二章:多线程_第13张图片

定时器的作用:

间隔特定的时间,执行特定的程序。

列如:每周要进行银行账户的总账操作、每天要进行数据的备份操作。

在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的,那么在java中其实可以采用多种方式实现:

可以使用sleep方法,睡眠,设置睡眠时间,每到这个时间点醒来,执行任务。这种方式是最原始的定时器。(比较low)

在java的类库中已经写好了一个定时器: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 TimerTest {
    public static void main(String[] args) throws Exception {
        //创建定时器对象
        Timer timer=new Timer();
        //指定定时任务
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
        Date firstTime=sdf.parse("2022-02-14 11:15:00");
        timer.schedule(new LogTimerTask(), firstTime, 1000*10);
    }
}

//编写一个定时任务类
class LogTimerTask extends TimerTask{
    public void run(){
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
        String strTime=sdf.format(new Date());
        System.out.println(strTime+":成功完成了一次数据备份!");
    }
}

结果:
Java核心技术卷一 -第十二章:多线程_第14张图片

1.14、关于Object类中的wait和notify方法。(生产者和消费者模式!)

Java核心技术卷一 -第十二章:多线程_第15张图片

图示:

Java核心技术卷一 -第十二章:多线程_第16张图片

(生产者和消费者模式!):

Java核心技术卷一 -第十二章:多线程_第17张图片
Java核心技术卷一 -第十二章:多线程_第18张图片
Java核心技术卷一 -第十二章:多线程_第19张图片

代码展示:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThreadTest01 {
    public static void main(String[] args) throws Exception{
        //创建1个仓库对象,共享的
        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;
    //数组大小
    private int size=0;
    //构造方法
    public Producer(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        //一直生产(使用死循环来模拟一直生产)
        while (true){
            //给仓库对象List加锁
            synchronized (list){
                if (list.size()>0){
                    try {
                            list.wait();
                    } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                }
                //程序能够执行到这里说明仓库是空的,可以生产
                Object obj=new Object();
                //获取数组大小
                for(int i = 0 ; i < list.size() ; i++) {
                    size=i+1;
                }
                obj=size;
                list.add(obj);
                System.out.println(Thread.currentThread().getName()+"--->"+obj);
                //唤醒消费者进行消费
                list.notify();
            }
        }
    }
}

//消费线程
class Consumer implements Runnable{
    //仓库
    private List list;
    //数组大小
    private int size=0;
    //构造方法
    public Consumer(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        //一直消费
        while (true){
            //给仓库对象List加锁
            synchronized (list){
                if (list.size()==0){
                    try {
                            //仓库已经空了
                            //消费者线程等待
                            list.wait();
                    } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                }
                //程序能够执行到这里说明仓库是有数的,可以消费
                //获取数组大小
                for(int i = 0 ; i < list.size() ; i++) {
                    size=i;
                }
                Object obj=list.remove(size);
                System.out.println(Thread.currentThread().getName()+"--->"+obj);
                //唤醒生产者进行消费
                list.notify();
            }
        }
    }
}

结果:(生产和消费交替运行)
Java核心技术卷一 -第十二章:多线程_第20张图片

总结

以上就是本文的内容,记录了一些关于java“多线程”的内容,本人也是刚开始接触java,不能保证总结内容的正确性,若是有错误的话,欢迎大家指出,谢谢!

你可能感兴趣的:(java学习记录,java,经验分享)