JAVA基础知识五(多线程)

本文详细介绍了Java中的多线程概念,包括进程、线程的基本概念,线程的创建(继承Thread、实现Runnable、实现Callable)以及线程间的安全问题,如死锁和线程安全。还探讨了线程池的使用,以及线程的生命周期和状态转换。最后,枚举的概念和用法也进行了说明。
摘要由CSDN通过智能技术生成

九、多线程

1.基本概念

程序:按照一定的逻辑编写的代码,存储到文件中,文件存放到磁盘---静态状态

进程:字面理解-正在进行中的程序。

资源:内存 + CPU

程序从启动到结束的过程叫做进程。

我们看到多个进程在同时执行的效果,实际上cpu在某一时刻只能执行其中一个进程

是因为cpu在做着高速的切换动作。

每个进程执行的时间也是不确定的,多个进程需要抢夺cpu的执行权。

线程:属于进程,是进程中一个可以独立运行执行单元(执行顺序,执行路径、代码片段)。

一个进程最少有一个线程。一个进程有多个可以独立运行的执行单元---多线程。

我们之前所写的代码就是单线程进程,因为只有一个执行单元。

这个执行单元中的代码都被放在main方法中----主线程。

实现效果:一个进程中,出现多个执行路径,各自执行各自的。

cpu进行切换:实际切换的各个进程中的线程。

在main方法中创建其他线程----子线程

子线程要运行的代码封装到一个叫run方法中。

多线程的目的:让CPU的使用率提高,多个程序都能执行,提高程序执行效率。

注意:cpu也是有极限的,不能无限开启线程。

并行:

前提:多个cpu。

多个任务同时发起,在某一时刻,每个任务都在执行。

并发:

单个cpu。

多个任务同时发起,在某一时刻,只有一个任务在执行,其他任务等待cpu。

2.创建线程

java是面向对象思想,任何事物都可以叫做对象,因此线程也是一类事物,就会有对应的对象来进行描述,java.lang.Thread类。

1.继承Thread类

步骤:

1.定义类继承Thread,子类就是一个线程类

2.重写run方法

3.创建子类对象

4.启动线程,调用start方法

//定义类继承Thread,这个类就是线程类
class MyThread extends Thread {
    @Override
    public void run(){                  //重写Run方法 
        for (int i = 0 ; i<1000;i++)
            System.out.print("☆☆☆☆☆"+i);
    }
}
​
//创建子类对象
 MyThread1 myThread = new MyThread1();
        myThread1.start();      //启动线程
//使用匿名内部类
        new Thread(){
            @Override
            public void run(){
                for (int i = 0; i <10 ; i++) {
                    System.out.println("i==="+i);
                }
            }
        }.start();

注意:

  1. 为什么要重写Run方法?

    因为Run方法封装的是被线程执行的代码,因为自定义类中不是所有的内容都是要被线程执行的,所以用Run封装区分一下。

    2.Run和Start方法有什么区别?

    Run 封装线程执行的代码,直接调用的话相当于普通的方法

    Start 启动线程,然后由Jvm调用此线程的Run方法

2.实现接口Runnable

步骤:

1.定义类实现Runnable接口,重写接口中的run方法 (run方法中就是线程任务)

2.创建实现类对象

3.创建Thread对象,将实现类对象作为参数传给Thread的构造方法

4.调用start方法,启动线程

//定义类并且实现Runable接口
class myRun implements Runnable{
        @Override
        public void run() {                 //重写方法
            for (int i = 0;i <= 20000;i++)
                System.out.println("i="+i);
        }
    }
​
//创建实现类对象
     myRun m = new myRun();
            Thread t = new Thread(m);       //创建Theread 对象,并将实现类对象作为参数传入
            t.start();                      //启动线程
        //使用匿名块创建
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <10 ; i++) {
                    System.out.println("i==="+i);
                }
            }
        }).start();

3.实现接口java.util.concurrent.Callable

步骤:

1.定义类实现Callable<V> ,重写call方法,方法的返回值是V类型 (线程任务)

2.创建实现类对象

3.创建FutureTask对象,并将实现类对象作为参数传递给构造方法

4.创建线程对象,并将FutureTask对象作为参数传递给构造方法

5.调用start方法,启动线程

FutureTask实现了Runnable接口,类中提供get方法,可以获取call方法的返回值,即获取线程执行后返回的结果,还又判断线程任务状态的方法,如:isDone()...

//定义类实现Callable,重写Call方法
class call implements Callable<String>{
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 10; i++) {
            System.out.println("i++++++"+i);
        }
        return "null";
    }
}
​
        //2.创建类对象
        call c1 = new call();
        //3.创建Futuretask 对象
        FutureTask<String> futureTask = new FutureTask<>(c1);
        //4.创建线程对象
        Thread t = new Thread(futureTask);
        //5.运行线程
        t.start();
        System.out.println(futureTask.get());
        //使用匿名块创建       
        new  Thread(new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("线程任务");
                return "";
            }
        })).start();

三种方式区别:

继承:代码最为简单。继承了这个类就不能继承其他类,因为是单继承。想要使用继承需要是is-->a关系,如果子类和Thread没有这种关系,使用继承不是很合适。线程任务和线程对象邦定在一起,耦合度增高了。

原理:start-->start0--->run 由于子类重写了run方法,因此创建子类对象调用run

方法,运行时子类的内容(方法重写)

实现Runnable: 还可以去继承其他类。线程任务和线程对象是分离的,耦合度降低。

实际开发建议使用这种方式。

原理:start-->start0-->run(Thread类中的run),方法中:target是Runnable类型,

创建Thread对象时,给target变量赋值了,是Runnable的实现类对象,

因此方法中的判断是true,多态形式调用方法,编译看父类运行看子类。

实现Callable:好处:任务有返回结果,可以抛出异常。基本与Runnable类似。

原理:start-->start0-->run(Thread类中的run),方法中:target是Runnable类型,

创建Thread对象时,给target变量赋值了,是Runnable的实现类对象,

是FutureTask,因此方法中的判断是true,多态形式调用方法,编译看

父类运行看子类,执行的是FutureTask中的run方法。在该方法中。

常用方法:

\1. String getName(): 获取线程名

线程名的默认格式:Thread-编号,编号是从0开始

主线程默认的名称: main

\2. Thread static currentThread(): 放回当前正在执行的线程对象的引用

\3. void setName(String name): 将线程名称设置为name

线程启动前或启动后去设置都可以。

构造方法也可以给线程设置名称:

new Thread(String name)

new Thread(Runnable run, String name)

\4. static sleep(long millis) : 当前线程休眠millis时间 1000毫秒=1秒

主动放弃执行资格,cpu不在调度

时间到,线程醒过来,恢复执行资格,等待cpu调度

\5. int getPriority():获取线程的优先级

每个线程在创建时都有一个优先级,默认是5。一共有10个级别:1-10

1最小,10最大;通常优先级高的线程被执行的概率更高一些。

void setPriority(int ) :设置线程优先级

\6. isDaemon(): 判断线程是否是守护线程

守护线程也叫做后台线程,是为前台线程(非守护线程)提供服务

守护线程是依赖于前台线程而存在的,前台线程结束守护线程也会随之结束

默认线程是非守护线程

setDaemon(true) :设置线程为守护线程。在线程启动前去做设置。

\7. interrupt(): 中断线程执行,想要中断哪个线程,必须是在另一个线程中去调用。

\8. static void yield():让步的意思。某个线程指向到该方法,意思是把cpu的执行权让出来,让其他线程或自己优先执行。注意:调用后有可能该线程会得到cpu的执行权也能得不到。

3.线程间安全问题产生

代码演示:

//创建类Sell实现Callable接口,模拟卖票功能
class Sell implements Callable<String>{
    int count = 100 ;
    String s ;
    @Override
    public String call() throws Exception {
        while (true){
                if (count>0){
                    System.out.println(Thread.currentThread().getName()+"卖了一张"+count);
                    count--;
                }else if (count == 0){
                    String s = "票卖完了";
                    System.out.println(s);
                    break;
                }
        }
        return null;
    }
}
​
//创建两个线程,模拟两个窗口同时卖票
public static void main(String[] args) {
        Sell s1 = new Sell();
        FutureTask<String> f1 = new FutureTask<String>(s1);     //创建1线程
        Thread t1 = new Thread(f1);
        FutureTask<String> f2 = new FutureTask<String>(s1);     //创建2线程
        Thread t2 = new Thread(f2);
        t1.start();
        t2.start();
    }
​
//发现问题:执行后发现,出现了错误的票数,两个窗口卖了重复的票号,出现了0号票...这是为什么呢?

原因:

多线程执行时,共同操作同一个资源数据(ticket), 其中一个线程对共享资源数据的一次计算还没有执行完(一次if执行)还没有完成,另一个线程参与进来执行,从而导致错误数据的产生。

t1刚执行完判断和打印,没有执行减减计算,t2获取cpu执行权执行if和打印...

多线程安全问题前提:

1.有多线程环境

2.有共享数据

3.有多条语句操作共享数据 破坏这个环境

解决:

对ticket的一次计算某个线程必须执行完,没有计算结束前另一个线程不能参与执行,哪怕获取了执行权也不能进入执行。

线程安全问题解决

1.同步代码块

synchronized (锁对象){                 //synchronized   同步的,同时发生
    //共享资源操作的代码,即不可分割的代码 
}
//代码改进
    public String call() throws Exception {
        while (true){
            synchronized (this){        //使用synchronized把不能分割的代码包起来,作用就是一个线程抢到cpu执行后
                if (count>0){           //立即锁住,使得下一个线程不能进来,直到执行完synchronized大括号内的内容后解锁
                    System.out.println(Thread.currentThread().getName()+"卖了一张"+count);
                    count--;
                }else if (count == 0){
                    String s = "票卖完了";
                    System.out.println(s);
                    break;
                }
            }
        }
        return null;
    }

执行流程:

lock是一个对象,这个对象有两种状态:开和关,默认状态是开。

某个线程获取到执行权,执行到synchronized该语句,会先判断lock的状态,如果是关,则不能进入同步块中;如果是开,立即获取lock对象(线程持有该锁对象),将lock的状态由开改为关,线程执行到sleep后不会释放持有的锁。持有锁的线程一旦出了同步块,会立即释放持有的锁,将锁的状态由关改为开。

加锁的利弊:

好处:解决了安全问题

弊端:线程执行需要判断锁,只有持有了锁,才能执行,因此执行效率降低了。

前提:

1.需要保证多线程看到的是同一个锁对象
​
2.至少两个或两个以上的线程
​
3.不可分割的代码是否都在同步中

2. 同步方法

使用关键字synchronized修饰的方法,就叫做同步方法。

同步方法会有特点,就会有锁对象,锁对象是固定的,不需要手动指定。

方法分为:

同步方法: 锁对象是this

同步静态方法: 锁对象是静态方法所属的类的字节码对象, 类名.class

class  MyRun1  implements  Runnable{
    private  static int  ticket = 100;
    @Override
    public void run() {
        while(true){
            if("线程A".equals(Thread.currentThread().getName()))
                show();
            if("线程B".equals(Thread.currentThread().getName()))
                synchronized (MyRun1.class){
                    if(ticket > 0){
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+"..."+ticket);
                        ticket--;
                    }
                }
        }
    }
    
//将不可分割的代码封装到一个方法中,通过调用方法
    //静态同步方法  ---this
    synchronized static public  void  show(){
        if(ticket > 0){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"..."+ticket);
            ticket--;
        }
    }
}
​
//启动两个线程
 public static  void main(String[] args) {
        MyRun1 s1 =  new MyRun1();
        Thread t1 = new Thread(s1,"线程A");
        Thread t2 = new Thread(s1,"线程B");
        t1.start();
        t2.start();
    }

注意:run方法也是可以使用同步关键字进行修饰的。

3. 显示锁

jdk5.0有一个接口,叫做Lock,可以显示加锁和显示释放锁,java.util.concurrent.locks;

方法分别是:lock()添加锁 , unLock()释放锁。

使用其子类 :ReentrantLock

class Myrun2 implements Runnable{
    private int ticket = 100;
    ReentrantLock lock = new ReentrantLock();
    @Override
    public void run(){
        while(true){
            //共享资源操作的代码,即不可分割的代码
            //加锁
            lock.lock();        //luck 和 unluck不能包含整个方法,不然跟单线程没区别
            try {
                if(ticket > 0){
                    try {
                        Thread.sleep(30);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"---"+ticket);
                    ticket--;
                }else
                    break;
            }finally {
                //释放锁
                lock.unlock();
            }
​
        }
    }
}
​
​
public static void main(String[] args) {
     Myrun2 m2 = new Myrun2();          //创建类对象切记不要创建两个,不然就是两个对象两个锁。
    Thread t1 = new Thread(m2,"线程A");
    Thread t2 = new Thread(m2,"线程B");
    t1.start();
    t2.start();
​
    }

4.死锁

多线程并发执行时,各自持有资源,都想要对方的资源,但是持有资源的线程又不释放资源,

导致程序不能向下执行,就会出现所谓的”卡”的现象,叫做死锁。

注意:实际开发中千万不要写出死锁。

模拟实现死锁: 同步嵌套

//自定义类实现Runable接口
class Myrun implements Runnable{
    //做标记
    private boolean flag;
    public Myrun(boolean flag){
        this.flag = flag;
    }
    @Override
    public void run() {
        //锁嵌套
        //线程1 运行
        if (flag){
            synchronized (locks.A){
                System.out.println(Thread.currentThread().getName()+"==运行了AA");
                synchronized (locks.B){
                    System.out.println(Thread.currentThread().getName()+"==运行了BB");
                }
            }
            //线程2 运行
        }else{
            synchronized (locks.B){
                System.out.println(Thread.currentThread().getName()+"==运行了BB");
                synchronized (locks.A){
                    System.out.println(Thread.currentThread().getName()+"==运行了AA");
                }
            }
        }
    }
}
//创建锁对象
class locks {
    static Object A = new Object();
    static Object B = new Object();
​
}
​
//创建对象,运行线程
public static void main(String[] args) {
    Thread t1 = new Thread(new Myrun(true));
    Thread t2 = new Thread(new Myrun(false));
    t1.start();
    t2.start();
}

总结:实际开发中尽量不要使用同步的嵌套。

工具:

可以检测是否有死锁。

jstack + pid编号

打开命令行窗口,cmd --->tasklist, java/javaw

5.线程池

引导:在我们使用线程的时候,使用一次创建一次,使用过后就会删除,我们需要花费大量的时间在创建线程上。就比如我们需要短暂的出行,我们没有必要造一个自行车,因为我们每个人都造一个自行车的话,在不使用的时候就会造成资源的浪费,我们可以骑共享单车,如果需要了就使用,如果不需要的话就放着让其他需要的人先使用,可以发挥资源的最大利用。线程池就是这么一个存在,里边提供了一定量的准备好的线程,谁需要谁就用,不需要的话就在线程池里放着。

使用Executors中的工具包创建:

        //创建指定大小线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //开始执行
        es.execute()    //适用于Runable
        es.submit()     //适用于Callable
        //关闭线程池
        es.shutdown
​
        //ExecutorService 是一个接口,缺少好多功能,因此我们可以研究他的子类ThreadPoolExecutor
        //创建指定大小线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //创建默线程池
        ExecutorService es = Executors.newCachedThreadPool();
        //向下转型
        ThreadPoolExecutor tpe = (ThreadPoolExecutor)es;
​
        //获取线程池中线程的个数
        System.out.println(tpe.getPoolSize());
        //获取线程池的最大线程个数
        System.out.println(tpe.getMaximumPoolSize());
        //关闭线程池,试图停止正在执行的任务,并返回到列表中
        List<Runnable> runnables = tpe.shutdownNow();

通过Executors创建的线程还是有些使用的灵活度问题,建议使用ThreadPoolExecutor对象。

注意: corePoolSize <0 maximumPoolSize <= 0 corePoolSize > maximumPoolSize

keepAliveTime < 0 这样设置不可以,如果这样设置会出现非法参数异常。

workQueue、threaDFactory、handler 都不能为null,如果为null这出异常。

TimeUnit 是时间单位,是一个枚举类,直接类名.枚举值访问。

workQueue:

提交队列:,默认工作模式,任务执行阻塞式的,

该任务执行完,才会执行下一个。

无界队列:,maximumPoolSize 设置无效,任务处理

依赖于corePoolSize 。

有界队列:,最多能处理的线程数:

maximumPoolSize + 容量(手动指定的)。

handler:任务超出范围时,采用的拒绝模式

6.线程生命周期

生命周期:是一个过程,从创建到终止的过程,在过程中可以有不同的阶段。

线程生命周期:线程从创建到消亡的过程,在这个过程中,也会有不同的阶段,这些阶段可以进行转换。别名:线程的状态图、线程的声明状态图。

新建状态:线程对象被创建,执行了new Thread()后。

就绪状态(可运行):调用start方法后的状态,有运行资格,等待cpu

运行状态:执行run方法的状态

消亡状态: run方法结束,出现异常、调用stop/destroy方法

阻塞状态:主动放弃执行资格,CPU不在调度,sleep方法

wait和sleep方法的区别:

sleep方法不会释放锁对象,时间到回到就绪态

wait方法会释放锁对象,需要通过notify或notifyAll来唤醒才能回到就绪态

以上描述的线程各个状态都是理论上的状态,实际上线程某一个时刻只能处于一种状态,这个状态可以通过方法getState获取到,方法的返回值是Thread.State,State是一个枚举类。

7.枚举

引用类型: 数组 类 接口 枚举

枚举类型:也是一个类类型,是一个特殊的类,jdk5.0

不能new对象,不能实现也不能继承

格式:

修饰符 class 类名{}

修饰符 interface 接口名{}

修饰符 enum 枚举类名{}

枚举类中的每个枚举值其实就是该类的一个实例对象,枚举值之前使用逗号进行分隔,最后一个后边可以加分号也可以不加分号,但是如果枚举值后边还有其他内容,必须加。

当某个类型的对象个数是有限个,可数的过来的,这时候就使用枚举来定义:

如:季节、月份、星期、性别...

枚举类中成员:

1.枚举值,该类对象

2.成员变量,要在枚举值的下边

3.构造方法,要私有化,可以重载,调用重载需在枚举值后边传参

4.成员方法

5.抽象方法,一定要重写(每个枚举值都要进行重写,重写格式有点类似于匿名内部类)

enum Level{
    //实例对象
    A,B,C,D
    //如果有抽象方法的话实例对象
    A(100){
        @Override
        public void method() {
        }
    },B(80){
        @Override
        public void method() {
        }
    },C(70){
        @Override
        public void method() {
        }
    };
    //变量
    int score;
    //构造方法
    private Level(){
        System.out.println("执行了");
    };
    private Level(int score){
        System.out.println("执行了===="+score);
    }
    //方法
    public void show(){
        System.out.println("show");
    }
    //抽象方法
    abstract public void method();

switch语句:表达式结果是byte short int String char 枚举

注意:枚举值名按照常量名的命名规范编写,通常都是大写字母或单词表示。

枚举类的继承体系:

我们定义的枚举都是java.lang.Enum的子类,Enum是Object的子类。

        //常用方法
        Level a = Level.A;
        Level b = Level.B;
        //打印枚举值名
        System.out.println(a.toString());
        //打印枚举值名
        System.out.println(a.name());
        //返回序号
        System.out.println(a.ordinal());
        //序号差值比较
        System.out.println(a.compareTo(b));
        //
        Level[] values = Level.values();
        System.out.println(Arrays.toString(values));

###

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值