java高级之多线程

1.什么是多线程

首先引入程序与进程概念:

  • 程序(program)

    程序是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码(还没有运行起来),静态对象。

  • 进程(process)

    进程是程序的一次执行过程,也就是说程序运行起来了,加载到了内存中,并占用了cpu的资源。这是一个动态的过程:有自身的产生、存在和消亡的过程,这也是进程的生命周期。

    进程是系统资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。

  • 线程(thread)

    进程可进一步细化为线程,是一个程序内部的执行路径:线程是以cpu调度分配的单位。

    若一个进程同一时间并行执行多个线程,那么这个进程就是支持多线程的。

    【当然,一个进程最少包括一个线程,要不然无意义】

2.线程创建的几种方法

2.1创建方式一:继承Thread

public class MyThread extends Thread{

}

继承Thread重写run方法:

public class MyThread extends Thread{
 public void run(){
 
 }
}

这个run,当主线程运行时就会执行run里面的程序【注:main方法就是主线程】

这里举例:

public class MyThread extends Thread{
    public void run(){
        for (int i = 0; i < 10; i++) {
            System.out.println("我是run线程=="+i);
        }
    }
}

在run中我写上for循环,以便一会演示而用。

--------

新建测试类并声明main方法

随后创建MyThread类对象

并调用start()方法启用run线程

例:

public static void main(String[] args) {
     MyThread myThread = new MyThread();
        myThread.start();

}

我在main方法中同样写上一个for循环

public static void main(String[] args) {
     MyThread myThread = new MyThread();
        myThread.start();
   
     for (int i = 0; i < 10; i++) {
            System.out.println("我是main线程=="+i);
        }
}

 此时运行,run线程与主线程同为竞争管理

因为他们会同时抢夺cpu调度资源

至于哪个线程抢的多哪个线程抢的少就全凭运气

 

 运行结果:

 确实随机...

2.1.1 线程的命名和获取

首先是获取当前线程名称:

getName();

System.out.println(this.getName()+"我是run线程=="+i);

 这里可以直接使用this用来指代当前new对象

 当然,如果你不先给线程设置名称的话就会使用默认名称,以 Thread-? 的形式

 这里再次使用 setName() 来为线程命名 

MyThread myThread = new MyThread();
myThread.setName("线程A-");//为线程命名
myThread.start();

 再次运行:

当然,getName() 虽说好用,但是他有缺陷

他只能被用于继承Thread的子类身上

你把 getName()  用在main身上就不行了

所以推出另一种万能获取线程名称的方法

Thread.currentThread().getName()

 这个不管是什么牛马,都可Thread加身,非常的牛13

System.out.println(Thread.currentThread().getName()+"我是main线程=="+i);

 这里直接加在main中,效果可见:

2.2 创建方式二:实现 Runnable  接口

public class MyThread2 implements Runnable{
  
}
 public class MyThread2 implements Runnable{
    public void run() {
        
    }
}

 我还是在其中写上一个for循环

 public class MyThread2 implements Runnable{
    public void run() {
          for (int i = 0; i < 10; i++) {
            System.out.println("我是run线程=="+i);
        }
    }
}

 

 在main方法中同样写上for循环

现在的话不单单是直接new MyThread2就能行的

还得new Thread才阔以,并将对象传入Thread中,同时还可以有第二个参数,可以直接为此线程命名

MyThread2 myThread2 = new MyThread2();
Thread thread = new Thread(myThread2,"窗口1");

 剩下的直接运行,与第一种方法相同

  优缺点:

  • 第一种确实方便,但是不利于效率,因为只能继承一个,也不能重写什么接口了,限制很大
  • 第二种相对于麻烦了一步,但是它可以同时继承,同时重写多个接口

2.3创建方式三:Callable创建线程(实现接口)

创建一个线程类,并实现Callable的call方法,这里类名称就用ThreadCall指代

与前两种创建方式不同,但是又和Runnable实现的创建方式相似,这个更可以看成Runnable的进阶版

与Runnable不同的是,Callable不仅有泛型,还有返回值,并且实现的方法也不同此处实现的是call方法

public class ThreadCall implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        
        return null;
    }
}

 创建一个测试类

这里有些许不一样,我都感觉薛薇有点麻烦,但是不要紧,后面会讲到线程池,到时候就直接用线程池创建,也是很方便的

public static void main(String[] args) {
    ThreadCall threadCall = new ThreadCall();
    FutureTask<Integer> integerFutureTask = new FutureTask<>(threadCall);
    Thread thread = new Thread(integerFutureTask);
    thread.start();
}

反正就是隔这套娃呢!!!

3.线程卖票例子

3.1继承Thread方式卖票

新建MyThread3并继承Thread重写run方法

并定义变量ticket票数等于100

public class MyThread3 extends Thread{
    private int ticket = 100;
    @Override
    public void run() {
        while (ticket > 0){
            ticket--;
            System.out.println(Thread.currentThread().getName()+"卖了1张票,还剩"+ticket+"票");
        }
    }
}

 再次新建测试类并什么main方法中创建三个窗口同时卖票,【注意,是三个窗口同时各卖100张票】,创建三个窗口也就意味着是创建三个线程

public static void main(String[] args) {
    MyThread3 myThread3 = new MyThread3();
    myThread3.setName("窗口一");
    myThread3.start();

    MyThread3 myThread4 = new MyThread3();
    myThread4.setName("窗口二");
    myThread4.start();

    MyThread3 myThread5 = new MyThread3();
    myThread5.setName("窗口三");
    myThread5.start();
}

 运行测试:

太多了,太多了,太多了这里盛不下省略..................................

3.2 实现接口Runnable 方式卖票

新建MyThread4实现 Runnable 接口并重写run方法,

 并定义变量ticket票数等于100

public class MyThread4 implements Runnable{
    private int ticket = 100;

    @Override
    public void run() {

        while (ticket > 0){
            ticket--;
            System.out.println(Thread.currentThread().getName()+"卖了1张票,还剩"+ticket+"票");
        }
    }
}

 新建测试类文件并声明main方法中创建三个窗口同时卖100张票,没错,你没看错,是三个窗口同时同100张票

public static void main(String[] args) {
    MyThread4 myThread4 = new MyThread4();
    Thread thread = new Thread(myThread4,"窗口1");
    thread.start();

    Thread thread2 = new Thread(myThread4,"窗口2");
    thread2.start();

    Thread thread3 = new Thread(myThread4,"窗口3");
    thread3.start();
}

 运行测试:

你会发现问题的

无论是继承Thread的方式卖票,还是重写 Runnable 方法的方式卖票,结果都有一个共同点,三个窗口会重复卖掉同一张票,并且会使得库存呈现负数,这是极其不合理的

所以,这里就开始涉及到 线程安全 的方向

4.线程安全--锁的概念

4.1自动锁  synchronized(共享资源){}

这一概念用大白话来说就是一间厕所只有一个坑位,但是这个时候却有三个人要嘘嘘,当然不可能三个人同时对着坑位嘘嘘,这个时候某一个人率先出手抢到了坑位,然后给厕所门一锁

另外两人只能等在门后,待某人解决后开门,临到下一人,继续关门上锁嘘嘘,循环往复,自动锁的概念就此简单化

当然,在上锁之前,我们的代码需要简单改动一下在上锁

public class MyThread4 implements Runnable{
    private int ticket = 100;

    @Override
    public void run() {

        while (true){
            synchronized(this){
            if(ticket>0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ticket--;
                System.out.println(Thread.currentThread().getName()+"卖了1张票,还剩"+ticket+"票");
            }
           else {
               break;
            }
        }
        }
    }
}

 如果你在测试的时候发现效果不明显,那就加上 Thread.sleep(100); 这个方法,效果杠杠的

至于什么作用,稍后说道说道

4.2 手动锁 lock

 演示,效果跟 那个自动锁一样,只是变成了手动

首先就是new 一个  ReentrantLock();

public static ReentrantLock lock = new ReentrantLock();
public class MyThread4 implements Runnable{
    private int ticket = 100;
   public static ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {

        while (true){
            lock.lock();//手动开启锁
            if(ticket>0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                ticket--;
                System.out.println(Thread.currentThread().getName()+"卖了1张票,还剩"+ticket+"票");
            }
           else {
               break;
            }
            lock.unlock();//手动关闭锁
        }
    }
}

测试运行:效果奇好

 这样大大有利于线程安全的漏洞

5.Thread类的常用方法

  • start() : 启动当前线程, 调用当前线程的run()方法
  • run() : 通常需要重写Thread类中的此方法, 将创建的线程要执行的操作声明在此方法中
  • currentThread() : 静态方法, 返回当前代码执行的线程
  • getName() : 获取当前线程的名字
  • setName() : 设置当前线程的名字
  • yield() : 释放当前CPU的执行权
  • join() : 在线程a中调用线程b的join(), 此时线程a进入阻塞状态, 知道线程b完全执行完以后, 线程a才结束阻塞状态
  • stop() : 已过时. 当执行此方法时,强制结束当前线程.
  • sleep(long militime) : 让线程睡眠指定的毫秒数,在指定时间内,线程是阻塞状态

 这里方法太多,就不一一测试了,感兴趣的小伙伴可以尝试着调用其中的方法,看看是什么效果

 

6.线程死锁

线程死锁,怎么理解,一个画板是需要一根画笔和颜料组成才能画画,张三把画笔拿走了,李四把颜料拿走了,李四想画画,但是定睛一瞅,张三去厕所了,完了,画不成了,等等等等等好久没见张三出来,一听在厕所便秘,WC,真画不成了。如果张三一直便秘,就一直画不了画,等价与程序无响应,等价于线程死锁

6.1利用代码实现线程死锁

声明一个锁对象,这里暂代用MyThread6来作为

public class MyThread6 {
    public static Object lockA = new Object() ;
    public static Object lockB = new Object();

}

为了方便演示,这里不在一 一建立Thread类

直接使用匿名创建的方式演示:

如下:

在测试类文件中声明main方法,并创建两个 线程,在其中声明匿名 new Runnable 实现run方法

public static void main(String[] args) {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (MyThread6.lockA){
                System.out.println("张三获取到了锁A");
                synchronized (MyThread6.lockB){
                    System.out.println("张三获取到了锁B");
                    System.out.println("可以绘画了");
                }
            }
        }
    });
    thread.start();

    Thread thread2 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (MyThread6.lockB){
                System.out.println("李四获取到了锁B");
                synchronized (MyThread6.lockA){
                    System.out.println("李四获取到了锁A");
                    System.out.println("可以绘画了");
                }
            }
        }
    });
    thread2.start();
}

6.2 运行测试

 当运行多次后,只会得到两种结果,一种就是直接出现死锁情况,一种是未锁

 

7.线程通信

【有一种业务需求,是需要功能一边相加,另一边相减,比值相等】

线程通信利用同一种共享资源进行增加和减少,利用 wait();  notify();

 wait();  //指代线程无期限休眠,且释放CPU资源,通过notify() 来唤醒其休眠的状态,继续竞争CPU资源的抢夺,如果没有唤醒该线程,将会永无休止的沉眠

notify(); //指代唤醒wait(),且是随机唤醒等待队列中一个线程

7.1银行取钱/存钱

这里将会通过银行的存钱和取钱的方式来演示线程通信的使用

【注:这里的银行取钱存钱指代,张三与李四共享一张银行卡,共享余额,

这里会有要求:

1.当银行卡有钱时李四必须将钱取出来,当银行卡没钱时李四必须等待

2.当银行卡没钱时张三必须将钱存入,当银行卡有钱时,张三必须等待】

  1. 创建银行卡类

   并声明余额、与银行卡状态两个属性

public class BankCard {
    private int balane;//余额
    private boolean flag;//状态:当flag = true,代表银行卡有钱,当flag = false,代表银行卡没钱
}

   2.创建两个方法:存钱方法、取钱方法

   并给其 方法上锁

public class BankCard {
    private int balane;
    private boolean flag;

    //存钱方法
    public synchronized void save(int money) throws InterruptedException {
    
    }
    
    //取钱方法
        public synchronized void take(int money) throws InterruptedException {
        
        }
}

   3.依次按照要求

 

 

public class BankCard {
    private int balane;//余额
    private boolean flag;//银行卡状态

    public synchronized void save(int money) throws InterruptedException {
        if(flag == true){//当银行卡有钱时
            this.wait();//线程休眠,等待唤醒
        }
        balane += money;//否则就是存钱
        System.out.println(Thread.currentThread().getName()+"往卡中存了"+money+"元; 余额剩:"+balane);
        flag = true;//代表银行卡有钱
        this.notify();//唤醒正在休眠的线程,这里是指代当前,即银行卡没钱的时的休眠线程
    }

    public synchronized void take(int money) throws InterruptedException {
        if (flag == false){//银行卡没钱时
            this.wait();//线程休眠
        }
        balane -= money;//取钱
        System.out.println(Thread.currentThread().getName()+"从卡中取了"+money+"元; 余额剩:"+balane);
        flag = false;//取钱后银行卡没钱
        this.notify();//唤醒正在休眠的线程,指代银行卡有钱时休眠的线程
    }
}

  4.声明两个线程类,这里使用实现Runnable的方式

  这里Thread1代表张三存钱线程

  Thread2代表李四取钱线程

public class Thread1 implements Runnable{
    /**
     * 张三存钱
     */
    private static int money;
    public static BankCard bankCard;
    public Thread1(BankCard b){
        bankCard = b;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                bankCard.save(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Thread2 implements Runnable{
    /**
     * 李四取钱
     */
    public static BankCard bankCard;
  public Thread2(BankCard b){
      bankCard = b;
  }
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                bankCard.take(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

   5.新建测试类

public class test {
    public static void main(String[] args) {
        BankCard bankCard = new BankCard();
        Thread1 saves = new Thread1(bankCard);
        Thread2 takes = new Thread2(bankCard);

        Thread thread = new Thread(saves,"张三");//张三存钱
        Thread thread2 = new Thread(takes,"李四");//李四存钱
        thread.start();
        thread2.start();
    }
}

   6.测试运行

【注:这里是循环10次,每次存入/取出 1000元】

8.线程状态

  • NEW : 新建状态
  • RUNNABLE :  start()就绪状态-时间片-运行状态. 统称为RUNNABLE
  • BLOCKED :  堵塞状态。加锁时就如该状态
  • WAITING :  无期等待:  调用wait方法时会进入该状态
  • TIMED_WAITING :  有期等待---当调用sleep方法时就会进入该状态
  • TERMINATED :终止状态。线程的任务代码执行完毕或出现异常。

【注:线程的状态之间可以通过调用相应的方法,进行转换】

9.线程池

线程池,线程池的出现可以大大提高线程的利用效率,怎么说?

 假如有5个线程池,这时却有7个任务,那多出来两个怎么办?

好办,继续调用线程池已释放资源的线程,循环往复

理论说只要释放够快,可以一直这样循环下去

先了解创建线程池的几种状态

9.1创建线程池的几种状态

  1.创建一个固定长度的线程池。
   ExecutorService executorService = Executors.newFixedThreadPool(?个线程);

  2. 单一线程池。
   ExecutorService executorService = Executors.newSingleThreadExecutor();

  3. 可变线程池--缓存线程池
  ExecutorService executorService = Executors.newCachedThreadPool();

  4. 延迟线程池。
  ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);

9.2线程池的创建

新建测试类,并声明main方法

这里使用固定长度的线程池的方式创建

且使用匿名实现Runnable的方式创建线程

且使用for循环模拟多任务少线程的方式的情景,看看线程池如何循环往复

public static void main(String[] args) {
    //线程池的创建
    //使用创建固定长度的线程池
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    /**
     *  这里为了方便演示
     *  使用匿名实现Runnable的方式
     *  并使用for循环模拟6个任务需要5个线程的情景
     */
    for (int i = 0; i < 6; i++) {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });
    }
}

 测试运行

 

 结果很明显,当线程小于任务数时,线程池这时就会调用已释放资源的线程,很明显,线程1先释放,自然被线程池优先调用

【注:以上通过Executors工具类创建线程池,但是阿里巴巴不建议使用。阿里建议使用原生的模式创建线程池】

 9.3原生模式创建线程池

在这之前咱们先了解一下原生模式创建线程池的参数

         int corePoolSize :核心线程的个数

                          int maximumPoolSize :最多的线程个数

                          long keepAliveTime :线程空闲时间。

                          TimeUnit unit : 空闲的单位

                          BlockingQueue<Runnable> workQueue :等待队列

 

创建测试类并依然声明main方法,使用原生线程池模式创建

public static void main(String[] args) {
    //原生模式创建线程池
    ArrayBlockingQueue<Runnable> runnables = new ArrayBlockingQueue<>(5);
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 5, TimeUnit.SECONDS, runnables);
    for (int i = 0; i < 10; i++) {//for循环模拟线程与任务数
        threadPoolExecutor.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });
    }
}

 

---------

 以上便是java高级之线程中的部分内容,如有漏缺请在下方留言告知,我会及时补充

posted @ 2023-08-14 20:56  九极致之术  阅读(35)  评论(0编辑  收藏  举报