[多线程] | 实例演示三种创建多线程的方式,初识线程同步以及解决线程安全问题(超卖)


前言

读这篇文章前请先了解什么是程序,什么是线程,什么是进程以及并行与并发的概念,这里主要是进行代码的实现去了解具体创建线程的操作,认识线程同步,锁以及如何解决线程安全问题

1 创建多线程的三种方式

我们考虑用一个情景来演示,假设我们有三个跑步参赛者,我们记录每秒参赛者跑的距离

不加线程

public class FirstThread {
    public static void main(String[] args) {
        //前一行执行下一行才能执行,并不能做到并行执行
        System.out.println("参赛者A 10秒跑了 1");
        System.out.println("参赛者B 10秒跑了 10");
        System.out.println("参赛者C 10秒跑了 100");
    }
}

显而易见,只通过打印时无法实现多线程的与实现每秒进行的。

1 继承Thread类来实现多线程

1 单线程

public class FirstThread {
    //线程内部类
    class Runner extends Thread{
        //重写run方法
        @Override
        public void run() {
            //跑步速度 生成一个随机数
            Integer speed = new Random().nextInt(10);
            //10秒跑了多少
            for(int i = 1;i <= 10;i++){
                try {
                    Thread.sleep(1000);//休眠一秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("第" + i +"秒" +this.getName() + "跑了:" + i*speed +"米");//this.getName()获取当前线程名
            }
        }
    }
    public void start(){
        Runner threadA = new Runner();
        threadA.setName("参赛者A");
        threadA.start();//开始新线程的创建与运行
    }
    public static void main(String[] args) {
        //前一行执行下一行才能执行,并不能做到并行执行
        new FirstThread().start();//对类进行实例化并调用里面的start方法
    }
}

2 多线程

public class FirstThread {
    //线程内部类
    class Runner extends Thread{
        //重写run方法
        @Override
        public void run() {
            //跑步速度 生成一个随机数
            Integer speed = new Random().nextInt(10);
            //10秒跑了多少
            for(int i = 1;i <= 10;i++){
                try {
                    Thread.sleep(1000);//休眠一秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("第" + i +"秒" +this.getName() + "跑了:" + i*speed +"米");//this.getName()获取当前线程名
            }
        }
    }
    public void start(){
        Runner threadA = new Runner();
        threadA.setName("参赛者A");
        Runner threadB = new Runner();
        threadB.setName("参赛者B");
        Runner threadC = new Runner();
        threadC.setName("参赛者C");
        threadA.start();//开始新线程的创建与运行
        threadB.start();
        threadC.start();
    }

    public static void main(String[] args) {
        //前一行执行下一行才能执行,并不能做到并行执行
        new FirstThread().start();//对类进行实例化并调用里面的start方法
    }
}

仔细观察同一秒abc的顺序并不是按照我们启动线程的顺序,他们谁先拿到时间片谁先执行,简单的做到了并发执行。

注:此方法只是为了演示可行性,实际工作中由于java对于继承的特性我们一边选择接口的方式去完成,也就是下一种方法

2 Runnable接口

public class SecondThread {
    //内部类
   class Runner implements Runnable{

        @Override
        public void run() {
            //跑步速度 生成一个随机数
            Integer speed = new Random().nextInt(10);
            //10秒跑了多少
            for(int i = 1;i <= 10;i++){
                try {
                    Thread.sleep(1000);//休眠一秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //Thread.currentThread().getName() 线程类的静态方法.currentThread()获取到当前的线程
                System.out.println("第" + i +"秒" +Thread.currentThread().getName() + "跑了:" + i*speed +"米");
            }
        }
       
   }
    public void start(){
       Runner runner = new Runner();
       Thread threadA = new Thread(runner);//将Runnable对象作为构造参数传入其中
       threadA.setName("参赛者A");
       Thread threadB = new Thread(new Runner());
       threadB.setName("参赛者B");
        Thread threadC = new Thread(new Runner());
        threadC.setName("参赛者C");
        threadA.start();
        threadB.start();
        threadC.start();
    }

    public static void main(String[] args) {
        //前一行执行下一行才能执行,并不能做到并行执行
        new SecondThread().start();//对类进行实例化并调用里面的start方法
    }
    }

3 Callable接口

public class ThirdThread {
    class Runner implements Callable<Integer>{//允许线程执行完毕返回值
        public String name;
        @Override
        public Integer call() throws Exception {
            //跑步速度 生成一个随机数
            Integer speed = new Random().nextInt(10);
            //总和
            Integer sum = 0;
            for(int i = 1;i <= 10;i++){
                Thread.sleep(1000);
                sum = i * speed;
                //this.name指向当前Runner对象的name不是线程的名字
                System.out.println("第" + i +"秒" +this.name+ "跑到了:" + sum +"米");
            }
          return sum;
        }
    }
    public void start() throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        Runner threadA = new Runner();
        threadA.name="参赛者A";
        Runner threadB = new Runner();
        threadB.name="参赛者B";
        Runner threadC = new Runner();
        threadC.name="参赛者C";
        //由于语句执行速度快尽管语句有先后顺序还是可以忽略的
        Future<Integer> m1 = executorService.submit(threadA);
        Future<Integer> m2 = executorService.submit(threadB);
        Future<Integer> m3 = executorService.submit(threadC);
        executorService.shutdown();//关闭线程池,所有线程结束才关闭
        System.out.println(threadA.name + "累计跑了:" + m1.get() + "米");//get的是返回值sum
        System.out.println(threadB.name + "累计跑了:" + m2.get() + "米");//get的是返回值sum
        System.out.println(threadC.name + "累计跑了:" + m3.get() + "米");//get的是返回值sum
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        new ThirdThread().start();
    }
}

4 小结

继承Thread, Java对继承不友好,不推荐使用 实现Runnable接口,Java编程友好,但无法返回执行后数据 实现Callable接口,可以返回多线程执行结果,编程稍显复杂

2 线程同步

现实生活中:

扔牌游戏:如果我么有一个游戏,谁先把手里的牌扔光谁就是赢家,参与者每个人都一把扔完这样是无序的。但是如果我么通过指定规则,把他设计成一个有序的安排,通过一个小游戏实现把它变得有序,一轮完了以后还有下一轮去选出下一个优胜者,这就是现实中的同步机制。

代码中:

synchronized (同步锁)关键字的作用就是利用一个特定的对象设置一个锁lock (赢家),在多线程(参与者)并发访问的时候,同时只允许一个线程(参与者)可以获得这个锁,执行特定的代码(成为赢家)。执行后释放锁,继续由其他线程争抢。

代码实现

public class Sample {
    class Printer{
        Object lock = new Object();//创建锁,当有线程获取到锁其他线程会进入到阻塞的状态
        public void print() {
            synchronized (lock) {
                try {
                    Thread.sleep(500);
                    System.out.print("111");
                    Thread.sleep(500);
                    System.out.print("222");
                    Thread.sleep(500);
                    System.out.print("333");
                    Thread.sleep(500);
                    System.out.print("444");
                    System.out.println("");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    class PrintTask implements Runnable{
        public Printer printer;
        @Override
        public void run() {
            printer.print();
        }
    }
    public void start(){
        Printer printer = new Printer();
        for(int i = 0;i < 10;i++){
            PrintTask printTask = new PrintTask();
            printTask.printer = printer;
            Thread thread = new Thread(printTask);
            thread.start();
        }
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.start();
    }
}

加锁前

加锁后

如果不加锁输出时多线程同时进行大概率先打印1111 会出现1111 1111 1111 1111 1111 1111 的情况造成了混乱,如果加入锁,我们就能实现线程进入锁后其他线程会阻塞知道上一个线程执行完成,其他线程再去争夺进入锁的机会,就与之前扔牌游戏类似。

3 synchronized的锁对象

synchronized代码块 -任意对象即可 synchronized方法 – this当前对象 synchronized静态方法-该类的字节码对象

4 线程安全的产生及解决方式

在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

实例,杜绝电商秒杀活动中“超卖”现象。

新建一个包 包下有三个类

Consumer:

//消费者类
class Consumer implements Runnable{
    //所有消费者都来到同一个商城
    public Mall mall;
    @Override
    public void run() {
        //商城为每一名消费者销售商品
        mall.sale();
    }
}

Mall:

//模拟商城销售商品
public class Mall {
    //消费者调用synchronized
    public  void sale(){
        //检验库存
        if(Stock.count > 0 ){
            try {
                //模拟商城办理销售业务,用时5毫秒
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //销售成功库存减少
            Stock.count--;
            System.out.println("商品销售成功");
        }else{
            System.out.println("商品库存不足,请下次再来吧!");
        }
    }

    public static void main(String[] args) {
        //实例化唯一的商城对象
        Mall mall = new Mall();
        //模拟5名顾客同时涌入商城购买商品,库存仅3
        for(int i = 0 ; i < 10 ; i++){
            Consumer consumer = new Consumer();
            consumer.mall = mall;//将实例化的mall赋值给consumer.mall
            Thread thread = new Thread(consumer);
            thread.start();
        }
        try {
            //模拟库存清点
            Thread.sleep(1000);
            System.out.println("当前商品库存为:" + Stock.count);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

Stock:

//库存类
public class Stock {
    //当前商品库存剩余3个
    public static int count = 3;
}

如果不加锁就会出现超卖现象:

如果加上锁,实质就是方法上加入synchroized

//消费者调用
    public synchronized void sale(){
        //检验库存
        if(Stock.count > 0 ){
            try {
                //模拟商城办理销售业务,用时5毫秒
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //销售成功库存减少
            Stock.count--;
            System.out.println("商品销售成功");
        }else{
            System.out.println("商品库存不足,请下次再来吧!");
        }
    }

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/290887.html

(0)
上一篇 2022年10月11日
下一篇 2022年10月11日

相关推荐

发表回复

登录后才能评论