17、多线程

多线程

目录

一、基本概念:程序、进程、线程
二、线程的创建和使用
三、线程的生命周期
四、线程的同步
五、线程的通信
六、JDK 5.0新增的线程创建方式

一、基本概念:程序、进程、线程

1、程序(program),是为完成特定任务、用某种语言编写的一组指令的集合。即++指一段静态的代码++,静态对象。
2、进程(process),是程序的一次执行过程,或是++正在运行的一个程序++。是一个动态的过程:有它自身的产生、存在和消亡的过程。 ——生命周期
  • 如:运行中的QQ,运行中的MP3播放器
  • 程序是静态的,进程是动态的
  • ++进程作为资源分配的单位++, 系统在运行时会为每个进程分配不同的内存区域
3、线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。
  • 若一个进程同一时间并行执行多个线程,就是支持多线程的
  • ++线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc)++,线程切换的开销小
  • 一个进程中的多个线程共享相同的内存单元/内存地址空间它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来++安全的隐患。++
4、并行与并发
  • 并行: 多个CPU同时执行多个任务。比如:多个人同时做不同的事。
  • 并发: 一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。

二、线程的创建和使用

方式一
1、创建一个继承于Thread类的子类
2、重写Thread类的run()方法:将此线程要执行的操作声明在润方法里
3、创建Thread类的子类的的对象
4、通过此对象调用start()方法①启用当前线程②调用当前线程
5、例子:遍历100以内的所有偶数
public class ThreadTest {
    public static void main(String[] args) {
        //3、创建Thread类的子类的的对象
        MyThread t1 = new MyThread();

        //4、通过此对象调用start()方法
        t1.start();

        for (int i = 0; i < 100; i++) {
            if(i%2==0){
                System.out.println("***");
                 System.out.println(Thread.currentThread().getName())//看是哪个线程
            }
        }//穿插出现
    }
    
}


//1、创建一个继承于Thread类的子类
class MyThread extends Thread{
    //2、重写Thread类的run()方法:将此线程要执行的操作声明在润方法里
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if(i%2==0){
                System.out.println(i);
                System.out.println(Thread.currentThread().getName())//看是哪个线程
            }
        }
    }
}
6、两个问题
  • 我们不能通过直接调用run()的方式启动线程->run()在主线程中
  • 不可以让同一个对象再次调用Start()方法,会报IllegalThreadStartException异常->重新建一个对象
方式二:创建Thread子类的匿名对象
//方法二:创建Thread子类的匿名对象
        new Thread(){
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    if(i%2==0){
                        System.out.println(Thread.currentThread().getName()+":"+i);
                    }
                }
            }
        }.start();
方式三:实现Runnable
1、创建一个实现了Runnable类的接口
2、在实现类中重写Runnable中的抽象方法:run()
3、创建实现类的对象
4、将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5、通过Thread类的对象调用start()①启动线程②调用当前线程的run()方法–>调用了Runnable类中的target的run()方法
public class WindowTest {
    public static void main(String[] args) {
        //例子:创建三个窗口卖票,总票数为100张
        Window window = new Window();

        Thread w1 = new Thread(window);
        Thread w2 = new Thread(window);
        Thread w3 = new Thread(window);


        w1.setName("窗口一");
        w2.setName("窗口二");
        w3.setName("窗口三");

        w1.start();
        w2.start();
        w3.start();
    }
}
class Window implements Runnable{
    private int ticket = 100;

    @Override
    public void run() {
        while(true){
            if(ticket > 1){
                System.out.println(Thread.currentThread().getName()+"你的票号为:"+ticket);
                ticket--;
            }else {
                System.out.println("没票了");
                break;
            }
        }
    }
}

在这里插入图片描述

比较方式一和方式三
开发中:优先选择,实现Runnable接口的方式
原因:
  • 1、实现的方式没有类的单继承的局限性。
  • 2、实现的方式更适合来处理多个线程有共享数据的情况。
联系:
  • 1、public class Thread implements Runnable
  • 2、相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
方式四 实现Callable
Thread类中的方法
  • void start(): 启动线程,并执行对象的run()方法
  • run(): 线程在被调度时执行的操作,通常需要重写Thread类中的此方法,将创建的线程要执行的方阿飞声明在此方法中
  • String getName(): 返回线程的名称
  • void setName(String name):设置该线程名称
  • 也可以用构造器设置名字,注意要在子类中重写构造器
  • static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
  • void yield():释放当前cpu的执行权
  • void join():在线程a中调用线程b的join()方法,此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态
  • void sleep():暂时阻塞当前线程 n 毫秒,++不会释放锁++
  • boolean isAlive():判断当前线程是否还存货
  • void stop():已过时,强制结束线程
线程的调度

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MogCs1em-1610982285039)(A47DD0EC71AF4605AC7C556B90A7A1AB)]

线程的优先级
  • MAX_PRIORITY:10
  • NORM_PRIORITY:5 ——>默认优先级
  • MIN_PRIORITY:1
如何设置当前线程的优先级
  • getPriority():获取线程的优先级
  • setPriority(int p):设置线程的优先级
  • 优先级高只是在统计概率上的,高概率被执行,并不意味着等高优先级的执行完,低优先级才执行。

三、线程的生命周期

在这里插入图片描述

四、线程的同步

例子:创建三个窗口卖票,总票数为100张
  • 1、问题:卖票过程中,出现了重票、错票
  • 2、问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票
  • 3、如何解决:但一个线程 a 操作共享数据(ticket)时,其他线程不能参与进来,直到线程 a 操作完共享数据(ticket)时,其他线程才开始操作 共享数据(ticket)。这种情况,即使线程 a 出现了阻塞,也不能被改变。
  • 在Java中我们通过同步机制,来解决线程的安全问题
方式一:同步代码块
  • 说明:
  • ① 操作共享数据的代码即为需要被同步的代码。–>不能多了,也不能少了
  • ② 共享数据:多个线程共同操作的变量,比如:ticket就是共享数据
  • ③ 同步监视器,俗称:锁。任何一个类的对象都可以来充当锁,注:只能由一个锁(所以要在run()方法外创建对象创建对象)
  • 好处:同步代码块方式,解决了线程的安全问题
  • 局限性:操作同步代码时只能由一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低。
synchronized(同步监视器){
    需要被监视的代码
}
买票窗口 最终答案1(实现Runnable版)
  • 注意:可以考虑用synchronized(this)来充当同步监视器

public class WindowTest {
    public static void main(String[] args) {
        //例子:创建三个窗口卖票,总票数为100张
        Window window = new Window();

        Thread w1 = new Thread(window);
        Thread w2 = new Thread(window);
        Thread w3 = new Thread(window);

        w1.setName("窗口一");
        w2.setName("窗口二");
        w3.setName("窗口三");

        w1.start();
        w2.start();
        w3.start();
    }
}
class Window implements Runnable{
    private int ticket = 100;
    Object obj = new Object();//对象要放在外面,这样才是唯一的锁

    @Override
    public void run() {
        while(true){
            synchronized (obj) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() +"," +"你的票号为:" + ticket);
                    ticket--;
                } else {
                    System.out.println("没票了");
                    break;
                }
            }
        }
    }
}
买票窗口 最终答案2(实现Thread版)
  • 在声明Object(锁)的时候用 static 修饰
  • 慎用synchronized(this)来充当同步监视器,this在(继承Thread版)代表三个对象
    private static int ticket = 100;
    private static Object  obj = new Object();
  • 可以用synchronized(Window.class),用当前的类充当同步监视器 --> 类也是对象,++而且类只会加载一次++
synchronized (this) 
方式二:同步方法
说明:
  • 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的
  • 因为synchronized不能包多,也不能包少,所以间接地创建一个show()方法来存放要循环的代码块
总结:
  • 同步方法中仍然涉及到同步监视器,只是不需要我们显式的去声明。
  • 非静态的同步方法,同步监视器是:this
  • 静态的同步方法,同步监视器是:当前类本身(Window.class)
1、实现Runnable版
public class WindowTest3 {
    public static void main(String[] args) {

        Window3 window3 = new Window3();

        Thread w1 = new Thread(window3);
        Thread w2 = new Thread(window3);
        Thread w3 = new Thread(window3);

        w1.setName("窗口 1");
        w2.setName("窗口 2");
        w3.setName("窗口 3");

        w1.start();
        w2.start();
        w3.start();
    }
}
class Window3 implements Runnable{
    private int ticket = 100;

    @Override
    public void run() {
        while(true){
            show();
            if(ticket == 0){
                break;
            }
        }
    }
    public synchronized void show(){
        if (ticket > 0) {
            try {
                Thread.sleep(35);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() +"," +"你的票号为:" + ticket);
            ticket--;
        }else{
            System.out.println("没票了");
        }
    }
}

2、继承Thread版

  • 用static修饰show()方法
  • 注:getName()要改成Thread.currentThread.getName()
  • 原因:getName()不是静态的(可以调用多次),但类是静态的(只调用一次)–>用类来调用getName()
public static synchronized void show()//同步监视器:Window.class
线程安全的单例模式之懒汉式
用同步机制将懒汉式改写为线程安全的
public class BankTest {
}
class Bank{
    private Bank(){}

    private  static  Bank instance = null;

    public static Bank getInstance(){
        //方式一:效率低
        /* 
        synchronized (Bank.class){
            if(instance == null){
                instance = new Bank();
            }
            return instance;
        }
        */
        
        //方法二:效率高
        if(instance == null){
            synchronized (Bank.class){
                if(instance == null){
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}
演示线程的死锁问题

1、死锁的理解:不同的线程分别蝉蛹对方需要的同步资源不放弃,都在等待对方放弃自己寻妖的同步资源,就形成了死锁

2、说明①出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态无法继续②我们是同同步时,要比避免出现死锁

Lock(锁)
解决线程安全问题方式三(JDK 5.0 新增)
* 1、实例化ReentrantLock
* 2、把需要同步的代码块用 try-finally 包起来
* 3、在 try 开头调用锁定的方法 luck();
* 4、在 finally 里调用解锁的方法 unlock();
public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
class Window implements Runnable{

    private int ticket = 100;
    private ReentrantLock lock = new ReentrantLock(true);
    @Override
    public void run() {
        while(true){
            try {
                //调用lock()方法:锁定
                lock.lock();

                if (ticket > 0) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ",你的票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }finally {
                //3、调用unlock()方法:解锁
                lock.unlock();
            }
        }
    }
}
面试题1:synchronized和Lock的方式有什么异同

异:

  • 1、synhronized机制在执行完行医的同步代码块后,自动的释放同步监视器
  • 2、Lock需要手动的启动同步(lock()),同时结束同步也需要手动地解锁(unlock())
  • 3、在开发中一般用Lock,更灵活
    在这里插入图片描述
练习
题目
两个人分别向一个账户存4000块钱,一次存1000,存完后打印余额
答案
public class BankTest2 {
    public static void main(String[] args) {
        Bank bank = new Bank(0);

        Customer customer = new Customer(bank);

        Thread c1 = new Thread(customer);
        Thread c2 = new Thread(customer);

        c1.setName("甲,");
        c2.setName("乙,");

        c2.start();
        c1.start();

    }
}
class Customer implements Runnable{
    private Bank bank;
    private ReentrantLock lock = new ReentrantLock(true);

    public Customer(Bank bank) {
        this.bank = bank;
    }

    @Override
    public void run() {

        for (int i = 0; i < 5; i++) {
            lock.lock();
            bank.deposit();
            lock.unlock();
        }
    }
}

class Bank {

    private double balance = 0;

    public Bank(double balance) {
        this.balance = balance;
    }

    public void deposit(){
        balance+=1000;

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

        System.out.println(Thread.currentThread().getName()+"存钱成功,余额为:"+balance);
    }
}

五、线程的通信

在这里插入图片描述

说明:
  • 1、这三个方法只能出现在同步代码块和同步方法中(Lock不行)
  • 2、这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则,会出现IllegalMonitorSatateException
  • 3、这三个方法不是定义在在Thread中的,是定义在java.lang.Object类中的。
  • 4、推导:调用者是同步监视器,任何一个类都可以充当同步监视器–>Object类–>着三个方法定义在Object类中
两个线程交替打印1-100的数
public class CommunicationTest {
    public static void main(String[] args) {
        Print print = new Print();

        Thread t1 = new Thread(print);
        Thread t2 = new Thread(print);

        t1.start();
        t2.start();
    }
}
class Print implements Runnable{
    private int num = 1;
    @Override
    public void run() {
       while (true){
           synchronized (this) {
               notify();
               System.out.println(Thread.currentThread().getName() + num);
               num++;
               if (num == 100) {
                   break;
               }

               try {
                   //使用wait()方法使线程进入阻塞状态
                   wait();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }
    }
}
面试题:sleep()和wait()方法的异同
相同点:
  • 一旦执行方法都可以是当前的线程i将纳入阻塞状态。
不同点:
  • 两个方法声明的位置不同:① sleep()方法声明在Thread类中。 ②wait()方法声明在Object类中
  • 调用的要求不同:①sleep()方法可以在任何需要的场景下调用 ②wait()必须在同步代码块和同步方法中调用
  • 关于是否释放同步监视器的问题:①sleep()不会释放锁,wait()会释放锁。

六、JDK 5.0新增的线程创建方式

方式一:实现Callable接口
与使用Runnable相比, Callable功能更强大些
  • 在Callable的实现类中,重写call()方法;相比run()方法,可以有返回值;
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结果
Future接口
  • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
  • FutrueTask是Futrue接口的唯一的实现类
  • FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
步骤
  • 1、创建一个实现Callable的实现类
  • 2、实现call方法,将此线程需要执行的操作声明在call()中
  • 3、创建Callable接口实现类的对象
  • 4、将此Callable接口实现类的对象传递到FutureTask的构造器中,创建FutureTask的对象
  • 5、将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread的对象,并调用start()方法,开始线程
  • 6、获取Callable中call()方法的返回值(不想要返回值可以return null)
public class ThreadTest3 {
    public static void main(String[] args) {
        NewThread newThread = new NewThread();

        FutureTask futureTask = new FutureTask(newThread);

        new Thread(futureTask).start();
        try {
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
            Object sum = futureTask.get();//获取返回值
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
class NewThread implements Callable {

    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i%2==0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}
实现Callable接口 vs 实现 Runnable接口
  • 1、call()可以有返回值
  • 2、call()可以抛出异常,被外面的操作捕获,可以通过异常排除程序的错误
方式二:使用线程池

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8RmKYeLn-1610982285045)(1622AC572DEC431ABC93EFFD6C0B5060)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z4cWtc5R-1610982285046)(79571F1A678D45F3A917A81FE9A248C3)]

步骤
  • 1、提供指定线程量的像城池
  • 2、执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
  • 3、关闭线程池
使用线程池的好处
  • 1、提高响应速度(减少了创建新线程的时间)
  • 2、降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
  • 3、便于线程管理
public class ThreadPool {
    public static void main(String[] args) {
        //1、提供指定线程量的像城池
        ExecutorService service = Executors.newFixedThreadPool(10);

        //设置线程池的属性
        System.out.println(service.getClass());//java.util.concurrent.ThreadPoolExecutor
        ThreadPoolExecutor service2 = (ThreadPoolExecutor) service;//强转
        service2.setCorePoolSize(10);//设置线程个数
        //service2.setKeepAliveTime(); //设置活跃时间

        //2、执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumThread1());//适用于Runnable
        service.submit(new NumThread2());//适用于Callable

        //3、关闭线程池
        service.shutdown();
    }
}
class NumThread1 implements Runnable{

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if(i%2==0) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
class NumThread2 implements Callable {

    @Override
    public Object call() throws Exception {
        for (int i = 1; i <= 100 ; i++) {
            if(i%2 != 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
        return null;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值