【Java】多线程与JUC

2024-01-09 22:20:34


多线程是什么?

让一个程序同时做多件事,能够提高效率。

主要是当我想让多个功能同时运作的时候可以使用多线程。

举例: 放置游戏、某软件后台挂载更新、聊天软件、后台服务器…

JUC是什么?

Java处理线程相关的类
api
juc


一、并发与并行

并发
概念上: 同一时刻,单个CPU执行多条指令的交替行为。
个人理解:一个人同时操作多个事件,快速、轮换、交替进行。

并行
概念上:同一时刻,多个CPU执行多条指令
个人理解: 多个人同时操作多个事件,每个人都有分工 然后同时执行 (即:并在一起行动)

二、实现多线程的方式

方式一 :自定义类继承Thread

  1. 自定义类继承Thread类
  2. 重写run方法
  3. 创建自定义类对象,调用start()启动线程
 // 测试类:     
        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();

        m1.setName("线程1 ");
        m2.setName("线程2 ");

       m1.start();
       m2.start();
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + " HelloWorld!");
        }
    }
}

线程实现运行方式一

方式二 :实现Runnable接口

  1. 自定义类实现Runnable接口,重写run()
  2. new 自定义类
  3. 创建Thread对象,引入自定义类对象参数
  4. 调用start()启动线程
        MyRun myRun = new MyRun();

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

        t1.setName("线程一: ");
        t2.setName("线程二: ");

        t1.start();
        t2.start();
public class MyRun implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "HelloThread!");
        }
    }
}

在这里插入图片描述

方式三 :实现Callable接口 (有返回值

前面两种都没有返回值

  1. 自定义类实现Callable接口,泛型是返回值类型,实现call方法
  2. 创建自定义类对象
  3. 创建FutureTask<> 类 ( 用于管理获取返回值类型
  4. 创建Thread线程,开启线程
  5. FutureTask<> 对象的 get()方法获取返回值
        MyCallable myC = new MyCallable();
        FutureTask<Integer> iFTask = new FutureTask<Integer>(myC);
        Thread t1 = new Thread(iFTask);
        t1.start();

        Integer sum = iFTask.get();
        System.out.println(sum);
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

三种实现方法对比

实现优缺点

三、常见的成员方法

多线程常见成员方法

四、线程的生命周期

线程生命周期

五、线程安全问题 同步代码块

需求:电影院卖票,共100张票,三个窗口卖。

没有加上同步代码块 会导致的问题:

  • 超出总票数
  • 重复卖同一张票
    卖票运行效果
    使用同步代码块方式避免以上问题:
    同步代码块:可以将需要共享的代码锁起来。
    同步代码块
    特点:
  1. 锁是默认打开,当一个线程进入,就锁上,其他线程不能进入
  2. 等到这个线程 执行完毕出来后,锁才会自动打开
        WicketThread w1 = new WicketThread("窗口一:");
        WicketThread w2 = new WicketThread("窗口二:");
        WicketThread w3 = new WicketThread("窗口三:");

        w1.start();
        w2.start();
        w3.start();
public class WicketThread extends Thread {

    public WicketThread() {
    }

    public WicketThread(String name) {
        super(name);
    }

	// 这里使用的是实现线程方式一,会创建多个对象,所以需要static确定只有一个票仓
    static int tickets = 0;
	//锁对象 随意 , 但是一定要唯一
	//不唯一会出现重复
    //static Object obj = new Object();

    @Override
    public void run() {
        while (true) {
        	//锁 同步代码块
            synchronized (WicketThread.class) {
                if (tickets < 100) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    tickets++;
                    System.out.println(getName() + "卖出第" + tickets + "张票");
                } else {
                    break;
                }
            }
        }
    }
}

六、同步方法 synchronized

当我们需要把整个方法锁起来,就不需要使用同步代码块,而是直接加到方法定义上。

格式:
格式

特点:

  • 锁对象 不能自己指定
    • 静态 : 当前类的字节码文件对象(Xxx.class)
    • 非静态: this

先写出同步代码块 然后提取代码块中的方法 在方法定义上加上synchronized即可

public class WicketRunnable implements Runnable {

	//这里使用的是实现线程方式二,不会创建多个对象,所以不用static修饰也可以
    int tickets = 0;

    @Override
    public void run() {
        while (true) {
            if (method()) break;
        }
    }

    private synchronized boolean method() {
        if (tickets == 100) {
            return true;
        } else {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            tickets++;
            System.out.println(Thread.currentThread().getName() + "卖出第" + tickets + "张票");
        }
        return false;
    }
}

七、Lock锁

使用同步锁 是属于 自动上锁 自动解锁,
但需要 手动设置 开关锁 的时候 使用 Lock 接口的ReentrantLock实现类
void lock();
void unlock() 解锁

public class WicketThread extends Thread {

    public WicketThread() {
    }

    public WicketThread(String name) {
        super(name);
    }

    static int tickets = 0;

	//创建了多个线程对象,所以static修饰,只能有一个锁对象
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            //synchronized (WicketThread.class) {
                lock.lock();
            try {
                if (tickets < 100) {
                    Thread.sleep(100);
                    tickets++;
                    System.out.println(getName() + "卖出第" + tickets + "张票");
                } else {
                    break;
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
                // 不管什么情况 finally 都会执行 也就是都会解锁
            } finally {
                lock.unlock();
            }
            //}
        }
    }
}

八、死锁

不同的线程分别占用对方的资源不放,都在等待对方放弃,就形成死锁。
出现死锁不会发生异常或提示,而且 处于线程阻塞状态 , 无法继续。
锁的嵌套会导致死锁,要避免。

九、等待唤醒机制(生产者和消费者)

线程具有随机性 所以使用等待唤醒机制 避免随机性

常见方法

常见方法

消费者代码实现

public class Customer extends Thread {
    @Override
    public void run() {
        /*
         * 1. 循环
         * 2. 同步代码块/方法/lock
         * 3. 判断结束条件
         * 4. 判断没结束时候的执行(核心逻辑)
         * */

        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    if (Desk.productFlag == 0) {
                        //桌子上没东西,当前线程->消费者等待,生产者开做
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        //桌子有东西,开买
                        Desk.count--;
                        System.out.println("买了一个!当前还能再买" + Desk.count + "个");
                        //买完一个通知生产者
                        Desk.lock.notifyAll();
                        Desk.productFlag = 0;
                    }
                }
            }
        }
    }
}

生产者代码实现

public class Productor extends Thread {
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                	//桌子上已有东西就等待
                    if (Desk.productFlag == 1) {
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        System.out.println("放桌上一个了");
                        Desk.productFlag = 1;
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

生产者消费者中转代码

/**
 * @Description: 生产者消费者 的 中转站
 */
public class Desk {
    //判断桌子上是否有东西 0无,1有
    static public int productFlag = 0;
    //消费者只能买十个 限定条件
    static public int count = 10;
    //锁对象
    final static public Object lock = new Object();
}
       //测试main类
        Productor p = new Productor();
        Customer c = new Customer();

        p.setName("生产者");
        c.setName("消费者");

        p.start();
        c.start();

运行结果

十、等待唤醒机制(阻塞队列方式实现)

厨师生产食物给消费者(Consumer)
阻塞图修改
阻塞队列继承结构:
阻塞队列继承结构
其实就是使用内部封装的方法实现消费者生产者模式,是队列的形式
使用ArrayBlockingQueue<> 中的 put() take() 方法

举例put()的源码:内部是用了lock锁 所以外部调用不用再写。take() 是一样的。
put源码
两个线程类通过构造接收 的是 同一个ArrayBlockingQueue对象

public class Cook extends Thread {

    ArrayBlockingQueue<String> abq;

    public Cook(ArrayBlockingQueue<String> abq) {
        this.abq = abq;
    }

    @Override
    public void run() {
        while (true) {
            try {
                abq.put("食物"); // put
                System.out.println("放一碗吃的");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class Consumer extends Thread {
    ArrayBlockingQueue<String> abq;

    public Consumer(ArrayBlockingQueue<String> abq) {
        this.abq = abq;
    }

    @Override
    public void run() {
        while (true) {
            try {
                String food = abq.take(); // take
                System.out.println("消费者吃一碗" + food);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
//测试类
        ArrayBlockingQueue<String> abq = new ArrayBlockingQueue<String>(1);
        Cook cook = new Cook(abq);
        Consumer consumer = new Consumer(abq);

        cook.setName("厨师 ");
        consumer.setName("消费者 ");

        cook.start();
        consumer.start();

打印的语句存在问题是因为:输出语句sout 在锁的外面,但是里面的数值是没有问题的。
问题
打印

十一、线程的六种状态

自画简略状态图
API文档
六

十二、线程池

Executors 工具类提供的静态方法:
Executors

ExecutorService es = Executors.newCachedThreadPool();
        es.submit(new MyRunnable());
        Thread.sleep(1000);

线程池原理:
类似C#对象池
原理

十三、自定义线程池

自定义

综合练习

抢红包

需求:
? 假设:100块,分成了3个包,现在有5个人去抢。
? 其中,红包是共享数据。
? 5个人是5条线程。
? 打印结果如下:
? XXX抢到了XXX元
XXX没抢到…

方式一: Double

public class Person extends Thread {
    static double money = 100;//总共100元
    static  int count = 3; // 分三个包抢

    static final double MIN = 0.01;//最小红包

    @Override
    public void run() {
        synchronized (Person.class) {
            if(count == 0){
                System.out.println(getName()+" 没抢到");
            }else{
                double prize = 0;
                if(count == 1){
                    //最后一次
                    prize = money;
                }else{
                    Random r = new Random();
                    //最极限的情况: 第一次 最多只能抢到99.98 , 第二次 第三次 为0.01
                    double bounds = money - (count - 1) * MIN;
                    prize = r.nextDouble(bounds);
                    if(prize < MIN){
                        prize = MIN;
                    }
                }
                money -= prize;
                count--;
                System.out.println(getName() + " 抢到 " + prize+" 元");
            }
        }
    }
}

使用double 钱的数值会有问题。
运行效果

方式二:BigDecimal

public class Person extends Thread {
    static BigDecimal money = BigDecimal.valueOf(100);//总共100元
    static int count = 3; // 分三个包抢

    static final BigDecimal MIN = BigDecimal.valueOf(0.01);//最小红包

    @Override
    public void run() {
        synchronized (Person.class) {
            if (count == 0) {
                System.out.println(getName() + " 没抢到");
            } else {
                BigDecimal prize;
                if (count == 1) {
                    //最后一次
                    prize = money;
                } else {
                    Random r = new Random();
                    //最极限的情况: 第一次 最多只能抢到99.98 , 第二次 第三次 为0.01
                    double bounds = money.subtract(BigDecimal.valueOf(count - 1)).multiply(MIN).doubleValue();
                    prize = BigDecimal.valueOf(r.nextDouble(bounds));
                }
                prize = prize.setScale(2, RoundingMode.HALF_UP);
                money = money.subtract(prize);
                count--;
                System.out.println(getName() + " 抢到 " + prize + " 元");
            }
        }
    }
}

效果

抽奖

需求:
抽奖池中的奖项为 : {10,5,20,50,100,200,500,800,2,80,300,700};
创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
每次抽出一个奖项就打印一个(随机)
? 抽奖箱1 又产生了一个 10 元大奖
抽奖箱2 又产生了一个 500 元大奖 …

//测试类
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);

        MyThread m1 = new MyThread(list);
        MyThread m2 = new MyThread(list);

        m1.setName("抽奖箱1");
        m2.setName("抽奖箱2");

        m1.start();
        m2.start();
public class MyThread extends Thread {
    ArrayList<Integer> list;

    public MyThread(ArrayList<Integer> list) {
        this.list = list;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (MyThread.class) {
                if (list.size() == 0) {
                    break;
                } else {
                    // 打乱集合,每次拿到集合的第一个数并且删除
                    Collections.shuffle(list);
                    Integer num = list.remove(0);
                    System.out.println(getName() + "抽到了" + num + " 元大奖");
                }
            }
            //让结果均匀一点,写在锁的外面 让出来的线程休息10毫秒,让另一个线程进去
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

效果更好

抽奖 - 多线程统计并求最大值

需求:
? 在上一题基础上继续完成如下需求:
? 每次抽的过程中不打印,抽完时一次性打印(随机)

? 在此次抽奖过程中,抽奖箱1总共产生了6个奖项。
? 分别为:10,20,100,500,2,300最高奖项为300元,总计额为932元
? 在此次抽奖过程中,抽奖箱2总共产生了6个奖项。
? 分别为:5,50,200,800,80,700最高奖项为800元,总计额为1835元

public class MyThread extends Thread {
    ArrayList<Integer> list;

    int count = 0;//几个奖项
    int sum = 0; //总和多少钱
    int max = 0;

    public MyThread(ArrayList<Integer> list) {
        this.list = list;
    }

    @Override
    public void run() {
        ArrayList<Integer> newList = new ArrayList<Integer>();
        while (true) {
            synchronized (MyThread.class) {
                if (list.size() == 0) {
                    System.out.println(getName() + "一共产生了" + count + "个奖项" + "分别是:" + newList + "总计额为" + sum + "元,"+"最大奖为"+max+"元");
                    break;
                } else {
                    // 打乱集合,每次拿到集合的第一个数并且删除
                    Collections.shuffle(list);
                    Integer num = list.remove(0);
                    sum += num;
                    count++;
                    newList.add(num);
                    if(num > max){
                        max = num;
                    }
                }
            }
            //让结果均匀一点,写在锁的外面 让出来的线程休息10毫秒,让另一个线程进去
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);

        MyThread m1 = new MyThread(list);
        MyThread m2 = new MyThread(list);

        m1.setName("抽奖箱1");
        m2.setName("抽奖箱2");

        m1.start();
        m2.start();

效果

抽奖 - 多线程之间的比较

在上一题基础上继续完成如下需求:
“在此次抽奖过程中,抽奖箱2中产生了最大奖项,该奖项金额为800元“”
对比两个线程之间的最大奖项。(也就是获取线程的结果

public class MyCallable implements Callable<Integer> {
    ArrayList<Integer> list;

    int count = 0;//几个奖项
    int sum = 0; //总和多少钱
    int max = 0;
    public MyCallable(ArrayList<Integer> list) {
        this.list = list;
    }

    @Override
    public Integer call() throws Exception {
        ArrayList<Integer> newList = new ArrayList<Integer>();
        while (true) {
            synchronized (MyCallable.class) {
                if (list.size() == 0) {
                    System.out.println(Thread.currentThread().getName() + "一共产生了" + count + "个奖项" + "分别是:"
                            + newList + "总计额为" + sum + "元," + "最大奖为" + max + "元");
                    break;
                } else {
                    // 打乱集合,每次拿到集合的第一个数并且删除
                    Collections.shuffle(list);
                    Integer num = list.remove(0);
                    sum += num;
                    count++;
                    newList.add(num);
                    max = Collections.max(newList);
                }
            }
            //让结果均匀一点,写在锁的外面 让出来的线程休息10毫秒,让另一个线程进去
            Thread.sleep(10);
        }
        if (newList.size() == 0) {
            return null;
        } else {
            return max;
        }
    }
}
//测试类  
       ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list, 10, 5, 20, 50, 100, 200, 500, 800, 2, 80, 300, 700);

        MyCallable mc1 = new MyCallable(list);
        MyCallable mc2 = new MyCallable(list);

        FutureTask<Integer> ift1 = new FutureTask<Integer>(mc1);
        FutureTask<Integer> ift2 = new FutureTask<Integer>(mc2);

        Thread t1 = new Thread(ift1);
        Thread t2 = new Thread(ift2);

        t1.setName("抽奖箱一:");
        t2.setName("抽奖箱二:");

        t1.start();
        t2.start();

        Integer max1 = ift1.get();
        Integer max2 = ift2.get();

        if(max1 > max2){
            System.out.println(t1.getName()+"产生最大奖"+max1);
        }else{
            System.out.println(t2.getName()+"产生最大奖"+max2);
        }

效果


总结

一个线程执行一段代码逻辑,多个线程就是执行多次同一段代码逻辑。

文章来源:https://blog.csdn.net/GavinGroves/article/details/135362206
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。