day22

day22

一、多线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FXZUUjJy-1620435124477)(img/image-20210507093823663.png)]

1.1 和多线程有关的几组概念
程序和进程
程序:是一组固定资源的集合,是安装在电脑硬盘上的,是一个静态的概念(比如:安装一个游戏 英雄联盟,就是一个程序)
进程:是一个正在执行的程序,是程序向系统申请资源的独立单位(比如:双击打开 英雄联盟这个程序,那么此程序就会生成一个进程向操作系统申请相关的系统资源:CPU、内存等,没有系统资源就无法正常运行程序)

如何查看进程:
	可以使用快捷键打开任务管理器: CTRL + ALT + . 或者 CTRL + SHIFT + ESC

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7qCZ8OWW-1620435124479)(img/image-20210507094725046.png)]

进程和线程
进程:是一个正在执行的程序,是程序是系统申请资源的独立单位。
线程:进程可以包含多个线程,但是至少要有一个线程。一个线程就是一个执行任务。(也就是说一个程序可以同时执行多个任务)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a65OyPTv-1620435124480)(img/image-20210507095212302.png)]

并行和并发
并行:多个CPU或者多核心的情况下,程序的同时执行(这个是物理上的同时执行)
并发:单个CPU的情况下,程序的同时执行(这个是逻辑上的同时执行)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BBL0HmtD-1620435124483)(img/并行和并发.jpg)]

1.2 创建多线程的方式
【1】第一种方式:继承Thread类
1. 声明一个类,并且继承Thread类
2. 在Thread的子类中重写run方法
3. 创建子类对象并通过start方法启动线程
注意事项
1. 启动线程必须要使用start方法,而不是run方法
2. 一个线程只能通过start方法启动一次
3. run方法中一般放的都是比较耗时的操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IVaEWhJD-1620435124484)(img/image-20210507102740239.png)]

//1. 声明一个类,并且继承Thread类
public class MyThread extends Thread {

    //2. 在Thread的子类中重写run方法

    @Override
    public void run() {

        for(int i = 0; i < 10; i++){

            System.out.println(i);
        }
    }
}

public class Demo2 {

    public static void main(String[] args) {

        //3. 创建子类对象并通过start方法启动线程
        MyThread mt = new MyThread();

        mt.start();

    }
}
使用匿名内部类创建线程
匿名内部类格式:
new 类名或者接口名(){

    方法的重写;
}

本质:类或接口的对象
public class Demo3 {

    public static void main(String[] args) {

        new Thread(){

            @Override
            public void run() {

                for(int i = 0; i < 10; i++){

                    System.out.println(i);
                }
            }
        }.start();

    }
}
【2】第二种方式:实现Runnable接口
1. 声明一个类实现Runnable接口
2. 重写Runnable接口的run方法
3. 创建一个Runnable接口子类的对象,并将此对象传入Thread的构造方法中 (构造方法:Thread(Runnable target) )
4. 创建Thread的对象,并调用start方法启动线程

注意:要想启动线程,肯定需要使用Thread类的start方法
//1. 声明一个类实现Runnable接口
public class MyRun implements Runnable {

    //2. 重写Runnable接口的run方法
    @Override
    public void run() {

        for (int i = 0; i < 10; i++) {

            System.out.println(i);
        }
    }
}

public class Demo {

    public static void main(String[] args) {

        //3. 创建一个Runnable接口子类的对象,并将此对象传入Thread的构造方法中 (构造方法:Thread(Runnable target) )
        MyRun mr = new MyRun();

        //4. 创建Thread的对象,并调用start方法启动线程
        Thread t = new Thread(mr);
        t.start();

    }
}
使用匿名内部类实现线程
public class Demo2 {

    public static void main(String[] args) {

//        Runnable r = new Runnable() {
//
//            @Override
//            public void run() {
//                for (int i = 0; i < 10; i++) {
//                    System.out.println(i);
//                }
//            }
//        };
//        Thread t = new Thread(r);
//        t.start();

        //简化写法:
        new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(i);
                }
            }
        }).start();

    }
}
【3】第三种方式:实现Callable接口

返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做 call 的方法。

Callable 接口类似于 Runnable些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。

分析
1. Thread类有一个构造方法Thread(Runnable target) 可以接收Runnable接口的对象
2. Runnable接口有一个实现类FutureTask(也就说明FutureTask的对象可以传入Thread类的构造方法中)
3. FutureTask有一个构造方法FutureTask(Callable<V> callable) ,可以接收Callable接口的对象
4. Callable接口中有一个call方法类似run方法,可以执行任务。
	
以上:就是实现多线程的第三种方式Callable接口和Thread类之间的关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rNcocbtc-1620435124486)(img/image-20210507110907844.png)]

步骤
1. 声明一个类,实现Callable接口
2. 在类中重写Callable接口的call方法
3. 将Callable接口的实现类对象传递到FutureTask的构造方法中,来创建一个FutureTask对象
4. 因为FutureTask是Runnable接口的实现类,所以我们可以直接将FutureTask对象传递到Thread类的构造方法中
5. 创建Thread类的对象,并通过start方法启动线程
package com.ujiuye.demo01_多线程.第三种方式;

import java.util.concurrent.Callable;

//1. 声明一个类,实现Callable接口
public class MyCall implements Callable<String> {

    //2. 在类中重写Callable接口的call方法
    @Override
    public String call() throws Exception {

        for (int i = 0; i < 10; i++) {

            System.out.println(i);
        }

        return "OK";
    }
}

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

public class Demo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //3. 将Callable接口的实现类对象传递到FutureTask的构造方法中,来创建一个FutureTask对象
        MyCall mc = new MyCall();
        FutureTask<String> ft = new FutureTask<>(mc);

        //4. 因为FutureTask是Runnable接口的实现类,所以我们可以直接将FutureTask对象传递到Thread类的构造方法中
        Thread t = new Thread(ft);

        //5. 创建Thread类的对象,并通过start方法启动线程
        t.start();

        //6. 获取关键信息,也就是call方法的返回值
        String s = ft.get();
        System.out.println(s);

    }
}
1.3 线程调度

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MsbWVbto-1620435124486)(img/image-20210507135349987.png)]

1.4 Thread类的常见方法
1.4.1 设置和获取线程的名字
String getName() 				返回该线程的名称 
void setName(String name) 		改变线程名称,使之与参数 name 相同 
    
注意:获取当前线程对象的方法
static Thread currentThread() 	返回对当前正在执行的线程对象的引用 
/*
	Thread的子类可以直接使用getName和setName方法	
 */
public class MyThread extends Thread {

    @Override
    public void run() {

        for(int i = 0; i < 10; i++){

            System.out.println(getName() + "---" + i);
        }
    }
}

public class Demo {

    public static void main(String[] args) {

        MyThread mt = new MyThread();
        MyThread mt2 = new MyThread();

        mt.setName("张飞");
        mt2.setName("关羽");

        mt.start();
        mt2.start();

    }
}
/*
	在Runnable接口的实现类中:可以先通过currentThread()方法获取当前线程对象,再调用getName方法
*/
public class MyRun implements Runnable {
    @Override
    public void run() {

        for (int i = 0; i < 10; i++) {

            System.out.println(Thread.currentThread().getName() + "---" + i);
        }

    }
}

public class Demo2 {

    public static void main(String[] args) {

        MyRun mr = new MyRun();

        Thread t = new Thread(mr, "张飞飞");
        Thread t2 = new Thread(mr, "关羽羽");

        t.start();
        t2.start();
    }
}
思考练习:
	在main方法中,建立一个for循环,设置并获取主线程的名字
public class Demo3 {

    public static void main(String[] args) {

        Thread.currentThread().setName("刘备");
        for (int i = 0; i < 10; i++) {

            System.out.println(Thread.currentThread().getName() + "---" + i);
        }

    }
}
1.4.2 线程的睡眠
static void sleep(long millis) 		在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
import java.util.Date;

public class MyThread extends Thread {

    @Override
    public void run() {

        for(int i = 0; i < 10; i++){

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(getName() + "---" + i + "---" + new Date());
        }
    }
}

public class Demo4 {

    public static void main(String[] args) {

        MyThread mt = new MyThread();

        mt.start();

    }
}

/*
	输出结果:
		Thread-0---0---Fri May 07 14:31:23 CST 2021
        Thread-0---1---Fri May 07 14:31:24 CST 2021
        Thread-0---2---Fri May 07 14:31:25 CST 2021
        Thread-0---3---Fri May 07 14:31:26 CST 2021
        Thread-0---4---Fri May 07 14:31:27 CST 2021
        Thread-0---5---Fri May 07 14:31:28 CST 2021
        Thread-0---6---Fri May 07 14:31:29 CST 2021
        Thread-0---7---Fri May 07 14:31:30 CST 2021
        Thread-0---8---Fri May 07 14:31:31 CST 2021
        Thread-0---9---Fri May 07 14:31:32 CST 2021
 */
1.4.3 线程的优先级
void setPriority(int newPriority) 		更改线程的优先级 
int getPriority() 						返回线程的优先级 
    
注意:
    1. 线程的优先级范围为1-10
    2. 线程的优先级只是代表了线程被调用的概率。
    		如果线程优先级高,那么就证明此线程出现的概率较高
    		如果线程优先级低,那么就证明此线程出现的概率较低
    3. 如果优先级不在 MIN_PRIORITY 到 MAX_PRIORITY 范围内,那么会报IllegalArgumentException异常

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zQ5T2R9B-1620435124487)(img/image-20210507145130428.png)]

public class MyThread extends Thread {

    @Override
    public void run() {

        for(int i = 0; i < 10; i++){

            System.out.println(getName() + "---" + i);
        }
    }
}

public class Demo5 {

    public static void main(String[] args) {

        MyThread mt = new MyThread();
        MyThread mt2 = new MyThread();

        mt.setName("张飞");
        mt2.setName("关羽");

        //如果想要设置线程的优先级为最高,那么建议使用常量MAX_PRIORITY,而不是整数10(因为不方便记忆)
        mt2.setPriority(Thread.MAX_PRIORITY);

        System.out.println(mt.getPriority());
        System.out.println(mt2.getPriority());

        mt.start();
        mt2.start();

    }
}
1.4.4 线程的礼让
static void yield() 		暂停当前正在执行的线程对象,并执行其他线程 
    
注意:
   	1. 礼让线程的作用就是避免一个线程过度使用CPU(极端点来说:就是一直使用CPU,而其它线程没有使用的机会)
    2. 礼让线程和优先级一样,都是代表了一种概率情况。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8FOdxhwz-1620435124488)(img/yield方法.jpg)]

public class MyThread extends Thread {

    @Override
    public void run() {

        for(int i = 0; i < 10; i++){

            System.out.println(getName() + "---" + i);
        }
    }
}

public class Demo6 {

    public static void main(String[] args) {

        MyThread mt = new MyThread();
        MyThread mt2 = new MyThread();

        mt.setName("张飞");
        mt2.setName("关羽");

        mt.yield();

        mt.start();
        mt2.start();

    }
}
1.4.5 线程的中断

我们可能会启动多个线程做同一个事情,比如买票!!!,这样抢到票的概率会更高一点。

这多个线程分别去不同的渠道去买票,如果有一个渠道买到票了,那么其它渠道就不需要买票了,就可以关闭其它线程了。

void interrupt() 		中断线程 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EUBcmQFf-1620435124489)(img/image-20210507151020952.png)]

1.4.6 线程的守护
void setDaemon(boolean on) 			将该线程标记为守护线程或用户线程 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bjnrnVh2-1620435124490)(img/image-20210507153254427.png)]

public class MyThread extends Thread {

    @Override
    public void run() {

        for(int i = 0; i < 100; i++){

            System.out.println(getName() + "活了:" + i);
        }
    }
}

public class Demo8 {

    public static void main(String[] args) {

        MyThread mt = new MyThread();
        MyThread mt2 = new MyThread();

        mt.setName("张飞");
        mt2.setName("关羽");

        mt.setDaemon(true);
        mt2.setDaemon(true);

        mt.start();
        mt2.start();

        Thread.currentThread().setName("刘备");
        for(int i = 0; i < 50; i++){

            System.out.println(Thread.currentThread().getName() + "活了:" + i);
        }

    }
}
练习:
	目前电影院正在热映《葫芦娃救爷爷》这部电影,但是只剩下来100张票了。这100张票同时被3个商家售卖(淘票票、猫眼电影、美团),请写一个程序,模拟以上买票过程。比如:淘票票卖出了1张票,还剩99张票;猫眼电影卖出了1张票,还剩98张票。 直到卖完为止。
	
	注意:
		1. 三个窗口肯定是需要三个线程
		2. 一共有100张票,而不是每一个都有100张
		3. 使用Thread或者Runnable都可以
package com.ujiuye.demo03_卖票案例;

public class SellTicketsRunnable implements Runnable {

    int tickets = 100;

    @Override
    public void run() {

        while(true){

            //证明有票可卖
            if(tickets > 0) {  //因为是抢占式调度,所有三个线程都有可能过来: 淘票票、美团、猫眼电影都在这等

                /*
                    需要明确的知识点:
                        1. CPU处理的操作都是原子性的
                            比如:--tickets是分为两步的:
                                    1. tickets先减1
                                    2. 将结果再赋值给tickets

                        2. Java虚拟机中是抢占式调度
                            也就说明了有可能在同一时间,这三个线程都到达了CPU的临界点


                     问题过程分析:
                        理想中的情况:
                             一个线程执行完毕,另一个线程再过来
                             比如:
                                淘票票过来卖了一张票,剩99
                                美团过来卖了一张票, 剩98
                                ...
                        现实中的情况:
                             三个线程同时都过来了
                             比如:
                                还剩最后一张票的时候,三个线程都可以通过if的判断>0,从而进入到if语句中
                                (三个线程都是拿着tickets=1进来的)
                                淘票票过来卖了1张票:
                                      tickets先减1          0
                                      ...
                                      中间有很大的空档
                                      (万一就是这个空档被美团抢到了)
                                            tickets先减1   -1
                                            ...
                                            中间有很大的空档
                                            (万一就是这个空档被猫眼电影抢到了)
                                            tickets先减1   -2
                                            然后赋值给tickets并输出-2
                                            ...
                                            然后赋值给tickets并输出-1
                                      ...
                                      然后赋值给tickets并输出0


                 */

                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + "卖出了1张票,还剩" + --tickets + "张票");
            }else{
                break;
            }
        }

    }
}
1.5 线程安全问题
1.5.1 出现线程安全问题的环境
1. 多线程环境
2. 有共享的数据
3. 多个线程同时操作了共享的数据
1.5.2 如何解决线程安全问题
只需要将出现问题的环境给解决了(解决其中一个就可以)就可以解决线程安全问题

1. 多线程环境	【无法解决】
2. 有共享的数据   【无法解决】
3. 多个线程同时操作了共享的数据  【可以解决】
		解决的方式就是:让多个线程一个一个的去访问共享的数据,而不是同时去访问共享的数据
1.5.3 解决方案

同步代码块和同步方法,都可以保证让线程一个一个的去执行共享的数据。

原理:就是在一个线程访问数据的时候,给数据先加一把锁,等操作完这个数据之后,再将这个锁给放开。锁放开之后,其它的线程又可以去争抢了,然后一直这么循环下去。

理解:火车上的卫生间。

使用同步代码块
synchronized(){
	
	可能出现线程安全问题的代码;
}

锁是谁?  锁可以是任何对象,但是必须要保证是同一把锁。
使用普通同步方法
修饰符 synchronized  返回值类型 方法名(参数列表) {
	
    可能出现线程安全问题的代码;
}

锁是谁?	this
使用静态同步方法
修饰符 static synchronized  返回值类型 方法名(参数列表) {

	可能出现线程安全问题的代码;
}

锁是谁?   类名.class
public class SellTicketsRunnable implements Runnable {

    static int tickets = 100;
    Object obj = new Object();
    @Override
    public void run() {

        while(true){

            test();
			//使用同步代码块
//            synchronized (obj){
//                //证明有票可卖
//                if(tickets > 0) {
//
//                    try {
//                        Thread.sleep(200);
//                    } catch (InterruptedException e) {
//                        e.printStackTrace();
//                    }
//
//                    System.out.println(Thread.currentThread().getName() + "卖出了1张票,还剩" + --tickets + "张票");
//                }else{
//                    break;
//                }
//            }

        }

    }

    //普通的同步方法
//    public synchronized  void test() {
//
//        if(tickets > 0) {
//
//            try {
//                Thread.sleep(200);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//
//            System.out.println(Thread.currentThread().getName() + "卖出了1张票,还剩" + --tickets + "张票");
//        }
//    }

    //使用静态的同步方法
    public static synchronized  void test() {

        if(tickets > 0) {

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + "卖出了1张票,还剩" + --tickets + "张票");
        }
    }
}
}

//普通的同步方法

// public synchronized void test() {
//
// if(tickets > 0) {
//
// try {
// Thread.sleep(200);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
//
// System.out.println(Thread.currentThread().getName() + “卖出了1张票,还剩” + --tickets + “张票”);
// }
// }

//使用静态的同步方法
public static synchronized  void test() {

    if(tickets > 0) {

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "卖出了1张票,还剩" + --tickets + "张票");
    }
}

}










  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值