栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

多线程(一)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

多线程(一)

并发编程:现如今电脑都支持同一时间运行多个程序了,那么并发编程也就成为了刚需.

 

并发编程的作用:提升效率.所谓人多力量大,一群人干活肯定比一个人干活要快不少.

各种语言都是如何实现并发编程的:

     

java实现并发编程:标准库中提供了Thread类,通过Thread就能够实现多线程.其实操作系统本身就提供了一组API,是C语言风格的,因为操作系统本身就是C/C++写的,所以提供的事C语言风格的也就不奇怪了.但java本身的特性就是为了方便,统一而生,如果换了个系统又要学习一套新的API就太麻烦了,所以java就索性直接把各个系统提供的用于并发编程的API都封装成了Thread类,这样的话就只用学习Thread类就可以应付各种操作系统了,也就降低了程序员的学习成本.

创建Thread类的几种基本方法:

 第一种方法:

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello thread");
    }
}
public class Demo {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
    }
}

第二种方法:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("hello thread");
    }
}
public class Demo {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
}

第三种方法:

public class Demo {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        };
        t.start();
    }
}

第四种方法:

public class Demo {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
        t.start();
    }
}

第五种方法:

public class Demo {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello thread");
        });
        t.start();
    }
}

接下来我们解析一下第一种方法的代码:

 对于第二种方法里的Runnable:

sleep方法:sleep方法可以让线程进入阻塞状态,也就是暂时不会被cpu调度执行.

演示:假如我执行下面这串代码,创建了个线程,且这个线程在无限循环打印这个hello thread字符串,会非常快.

public class Demo {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true){
                System.out.println("hello thread");
            }
        });
        t.start();
    }
}

如果我想让他慢一点,隔个1s钟打印一次,就可以用sleep方法了.sleep方法里的参数是时间,单位是ms.

public class Demo {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

sleep方法是属于线程的,哪个线程调用它,哪个线程就进入阻塞状态.

一个进程至少有一个线程就是main方法线程.

两个线程并发执行,都调用sleep方法进入阻塞状态,且时间相同,再次唤醒先执行哪个线程是不确定的,这是由操作系统决定的.因此这两个线程相当于抢占式执行.这也会带来一些麻烦.因为有的时候我们是希望线程按照我们所预期的顺序来执行的。

解决方法:使用wait()和notify().

既然多线程比顺序执行执行效率高,那我们就来试验一下.分别创建两个方法,方法一使用顺序执行,方法二使用多线程执行.这两个方法的目的就是自增到10_0000_0000.看看谁的执行时间更短.

public class Demo {
    private static long num = 10_0000_0000L;
    public static void ser() {
        long begin = System.currentTimeMillis();
        int a = 0;
        for (long i = 0; i < num; i++) {
            a++;
        }
        for (long i = 0; i < num; i++) {
            a++;
        }
        long end = System.currentTimeMillis();
        long time = end - begin;
        System.out.println(time);
    }
    public static void mul() {
        long begin = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            for (long i = 0; i < num; i++) {
                i++;
            }
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            for (long i = 0; i < num; i++) {
                i++;
            }
        });
        t2.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        long time = end - begin;
        System.out.println(time);
    }
    public static void main(String[] args) {
        ser();
//        mul();
    }
}

当我调用ser()方法也就是只有一个线程,执行时间为500ms左右,但当我调用mul()方法也就是两个线程同时工作时,执行时间为200多ms.这样一比较就可以明显的得出一个结论,多线程效率是更高的.但要注意一点的是.你要计算的数据必须要够大.因为本身创建线程也是要时间的.如果你计算这个东西只用20ms,而创建两个线程就需要60ms,那肯定就不如直接使用串行执行了.本身多线程就是用来执行较大计算的.

举个例子,你今天要搬1000块砖,那肯定叫多几个人来搬,搬得快一点,那如果只要搬20块砖,那你就直接自己搬就完了呗,因为你要去叫人也是花费时间的,等你叫来人,20块砖早搬完了.

线程等待join()方法:join方法是用来控制执行顺序的,当执行到这个方法后,当前线程会进入阻塞状态,直到调用的线程执行完毕后才会继续执行下面的代码.因为在刚才计算时间的代码其实是在main这个主线程里的,也就是说主线程和其他的几个线程是并发执行的,如果不使用join方法,当创建完线程之后,计算时间这个代码也就执行完毕,那肯定是不对的.

join方法里的参数是时间,单位是ms,可以写也可以不写,当不写时就意味着死等.直到t线程结束才继续执行下面的代码

如果写了,比如写了1000ms也就是1s后如果t线程还没执行完毕主线程也不管了直接执行下面的代码.

线程也可以取名字:

 可以使用jconsole这个工具来进行调试.这是JDK自带的调试工具,只要安装了JDK就有,在JDK目录下的bin文件里.

 在jconsole上会展示所有的java进程,连接对应的IDEA就可已看到各个线程的基本信息.

前台线程:影响进程的退出.比如现在有A线程和main主线程,如果A是前台线程,即使main线程执行完了也不能直接退出,需要等A线程也执行完才能结束进程.

后台线程:如果A是后台线程,那main线程只要执行完毕就直接结束进程了,不管A进程有没有执行完,所有后台线程不影响进程的退出.

默认创建的Thread都是前台线程.

start()和run()的区别:

中断线程:

中断线程有两种办法:

演示:

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(5000);
        t.interrupt();
    }
}

调用interrupt()方法后并没有达到我们想让它停下来的效果.

 加入break后:打印了5个hello后就结束了程序

进程中的状态(针对只有一个线程):就绪状态、阻塞状态。这是操作系统对于线程的状态描述

java线程中的状态:对操作系统的状态由进一步作出了细分

问题 :为什么要分这么细呢?

答:开发中碰到线程卡死或一些线程阻塞了可以通过观察各个线程的状态来判定具体原因,对后期调试有很大帮助。

线程安全问题

什么叫线程不安全:就是在多线程执行时产生bug。也就是说多线程程序并没有完成你想让他完成的事情。举个栗子你想让你朋友给你买个苹果,结果他给你买了个梨。虽然都是水果但是苹果跟梨还是有点区别的。

一个典型案例:让两个线程自增到10000

public class Demo2 {
    private static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                num++;
            }
        });
        Thread t2 = new Thread(() ->  {
            for (int i = 0; i < 5000; i++) {
                num++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(num);
    }
}

 

 按理说应该结果是10000,但最终结果确是7876.

分析原因:

解决方法:使用synchronized加锁,把这个操作变成原子性的。自增前lock,自增后unlock。

 

问题:加锁后不就变成串行执行了吗,那效率不就没提高吗

答:一个线程里要做的事有很多,这只是其中一件事而已,并且线程不安全的代码也比较少,只需要把线程不安全的代码给加上锁就好了。所以总的效率还是高出不少的。只是这个例子里代码只有这么一个任务就是自增。

产生线程不安全问题 :

 synchronized的使用:

要注意的是加在代码块的两种分别与加在普通方法和静态方法的对应。

多个线程一定要针对同一个对象加锁,要不然是不起作用的。因为你只有针对同一个对象加锁才有锁竞争。举个例子,现在只有一个厕所,但是两个人都想进去,这样才会产生竞争,那如果有两个厕所了,那就不必竞争了。

类对象:java程序编译后生成了字节码文件(.class文件),让后JVM加载字节码文件到内存中生成的特殊对象就叫做类对象。类对象中有着java源代码中类的一切属性和方法,还能知道各个属性和方法的访问权限。反射机制就是基于类对象实现的。

加锁干了啥:每个对象都是继承自Object类的,因此这个类不仅有它自己的属性还继承着Object的一些属性。放置Object属性的地方就叫做对象头,加锁其实就是给对象头里设置了一个标志位。

可重入:同一线程加锁两次不会产生死锁

不可重入:同一线程加锁两次会产生死锁

synchronized是可重入锁,如何实现:

死锁:由于内部逻辑不同导致每个线程都拿不到锁

死锁的几种场景:

为什么要有可重入锁:降低了程序员的开发负担。

死锁的四个必要条件:

标准库中线程安全的类:主要就是给线程不安全的地方加了锁

 JMM(Java内存模型):java基于计算机硬件自己整了个新的名称

 

 wait和notify:保证线程的执行顺序

设计模式:类似于一种模板。为了帮助程序员写出更安全的代码,使用大佬们的模板就可以减轻我们的开发负担。

对于小白目前只用掌握单例模式和工厂模式就好

1.饿汉模式:唯一实例的创建比较着急,类加载过程中直接创建示例(程序中用到了这个类就会立即加载)

2.懒汉模式:对于实例的创建并不是那么着急,用到了才去实例化

如何解决线程不安全问题:加锁

问题1:只是在第一次实例化的时候有线程不安全问题,后续就不会在产生线程不安全了,所以只需要在第一次实例化的时候加上锁,后面就不用加锁了。毕竟加锁也是需要时间开销的,并且还会产生大量的锁竞争,不能无脑加锁,因此,还得改进一下。 

 问题2:虽然解决了无脑加锁的问题,但要知道,我们开发时肯定是有很多的线程,那这么多的线程访问instance == null,而cpu读到的数据都是一样的,就会导致编译器进行优化去读寄存器而不去读内存了。

解决办法:给instance加上volatile关键字,禁止编译器进行优化 

 最终的懒汉模式代码:

阻塞队列

什么是阻塞队列:

基于阻塞队列可以实现生产者消费者模型:此时的阻塞队列相当于交易场所,也就是中间人。

生产者消费者模型是实际开发中非常有用的一种多线程开发手段,尤其是在服务器开发的场景中 

 

1.低耦合:让两个服务器关联不要太紧密

2.削峰填谷:起到一个缓冲的作用

java标准库中的阻塞队列用法:

自己实现一个简单的生产者消费者模型:

1.实现循环队列:

问题一:当head/tail到达队尾时,如何实现循环:

问题二:当head与tail重合时如何判断队列空还是满:

2.实现线程安全:

3.实现阻塞效果:

完整代码:

class MyBlockingQueue {
    // 保存数据的本体
    private int[] data = new int[1000];
    // 有效元素个数
    private int size = 0;
    // 队首下标
    private int head = 0;
    // 队尾下标
    private int tail = 0;

    // 专门的锁对象
    private Object locker = new Object();

    // 入队列
    public void put(int value) throws InterruptedException {
        synchronized (locker) {
            if (size == data.length) {
                // 队列满了. 暂时先直接返回.
                // return;
                locker.wait();
            }
            // 把新的元素放到 tail 位置上.
            data[tail] = value;
            tail++;
            // 处理 tail 到达数组末尾的情况
            if (tail >= data.length) {
                tail = 0;
            }
            // tail = tail % data.length;
            size++;  // 千万别忘了. 插入完成之后要修改元素个数
            // 如果入队列成功, 则队列非空, 于是就唤醒 take 中的阻塞等待.
            locker.notify();
        }
    }

    // 出队列
    public Integer take() throws InterruptedException {
        synchronized (locker) {
            if (size == 0) {
                // 如果队列为空, 就返回一个非法值.
                // return null;
                locker.wait();
            }
            // 取出 head 位置的元素
            int ret = data[head];
            head++;
            if (head >= data.length) {
                head = 0;
            }
            size--;
            // take 成功之后, 就唤醒 put 中的等待.
            locker.notify();
            return ret;
        }
    }
}

public class Demo22 {
    private static MyBlockingQueue queue = new MyBlockingQueue();

    public static void main(String[] args) {
        // 实现一个简单的生产者消费者模型
        Thread producer = new Thread(() -> {
            int num = 0;
            while (true) {
                try {
                    System.out.println("生产了: " + num);
                    queue.put(num);
                    num++;
                    // 当生产者生产的慢一些的时候, 消费者就得跟着生产者的步伐走.
                    // Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();

        Thread customer = new Thread(() -> {
            while (true) {
                try {
                    int num = queue.take();
                    System.out.println("消费了: " + num);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();


        // 简单验证看这个队列是否能正确工作.
//        MyBlockingQueue queue = new MyBlockingQueue();
//        queue.put(1);
//        queue.put(2);
//        queue.put(3);
//        queue.put(4);
//        int ret = 0;
//        ret = queue.take();
//        System.out.println(ret);
//        ret = queue.take();
//        System.out.println(ret);
//        ret = queue.take();
//        System.out.println(ret);
//        ret = queue.take();
//        System.out.println(ret);
    }
}

定时器:在到达你设定的时间后,唤醒并执行指定的任务

标准库定时器的用法:

public class Demo5 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello timer");
            }
        },3000);
    }
}

 

 自己实现一个简单的定时器:

1.描述任务:

2.组织任务:

3.执行时间到了的任务:

public MyTimer() {
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    // 先取出队首元素
                    MyTask task = queue.take();
                    // 再比较一下看看当前这个任务时间到了没?
                    long curTime = System.currentTimeMillis();
                    if (curTime < task.getTime()) {
                        // 时间没到, 把任务再塞回到队列中.
                        queue.put(task);
                    } else {
                        // 时间到了, 执行这个任务
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

 以上代码有两个问题

问题一:

问题二:

完整代码:

// 创建一个类, 表示一个任务.
class MyTask implements Comparable {
    // 任务具体要干啥
    private Runnable runnable;
    // 任务具体啥时候干. 保存任务要执行的毫秒级时间戳
    private long time;

    // after 是一个时间间隔. 不是绝对的时间戳的值
    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay;
    }

    public void run() {
        runnable.run();
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        // 到底是谁减谁, 才是一个时间小的在前? 需要背下来.
        return (int) (this.time - o.time);
    }
}

class MyTimer {
    // 定时器内部要能够存放多个任务
    private PriorityBlockingQueue queue = new PriorityBlockingQueue<>();

    public void schedule(Runnable runnable, long delay) {
        MyTask task = new MyTask(runnable, delay);
        queue.put(task);
        // 每次任务插入成功之后, 都唤醒一下扫描线程, 让线程重新检查一下队首的任务看是否时间到要执行~~
        synchronized (locker) {
            locker.notify();
        }
    }

    private Object locker = new Object();

    public MyTimer() {
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    // 先取出队首元素
                    MyTask task = queue.take();
                    // 再比较一下看看当前这个任务时间到了没?
                    long curTime = System.currentTimeMillis();
                    if (curTime < task.getTime()) {
                        // 时间没到, 把任务再塞回到队列中.
                        queue.put(task);
                        // 指定一个等待时间
                        synchronized (locker) {
                            locker.wait(task.getTime() - curTime);
                        }
                    } else {
                        // 时间到了, 执行这个任务
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

public class Demo24 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello timer!");
            }
        }, 3000);
        System.out.println("main");
    }
}

线程池:类似于进程池、字符串常量池,能够提高程序运行效率

 

为什么线程放在池子里比在系统申请释放更快呢?

 

java标准库中线程池的使用:

 

ThreadPoolExecutor有很多构造方法,我们主要来看最复杂的一个:这里的参数bao'ha包含了其他构造方法的参数,是最全的也是最复杂的。
 

根据上面的描述,我们再来分析各个参数代表的意思:

 

 

问题:如果创建线程池的话,线程池的个数怎么确定呢?

 

 

Excutors:简化版的线程池(针对ThreadPoolExcutor进行了封装,提供了一些默认参数)

public class Demo25 {
    public static void main(String[] args) {
        // 创建一个固定线程数目的线程池. 参数指定了线程个数
        ExecutorService pool = Executors.newFixedThreadPool(10);
        // 创建一个自动扩容的线程池. 会根据任务量来自动进行扩容
        // Executors.newCachedThreadPool();
        // 创建一个只有一个线程的线程池.
        // Executors.newSingleThreadExecutor();
        // 创建一个带有定时器功能的线程池. 类似于 Timer
        // Executors.newScheduledThreadPool();

        for (int i = 0; i < 100; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello threadpool");
                }
            });
        }
    }
}

 

自己实现一个简单的线程池:

 

完整代码:

class MyThreadPool {
    // 1. 描述一个任务. 直接使用 Runnable, 不需要额外创建类了.
    // 2. 使用一个数据结构来组织若干个任务.
    private BlockingQueue queue = new LinkedBlockingQueue<>();
    // 3. 描述一个线程, 工作线程的功能就是从任务队列中取任务并执行.
    static class Worker extends Thread {
        // 当前线程池中有若干个 Worker 线程~~ 这些 线程内部 都持有了上述的任务队列.
        private BlockingQueue queue = null;

        public Worker(BlockingQueue queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            // 就需要能够拿到上面的队列!!
            while (true) {
                try {
                    // 循环的去获取任务队列中的任务.
                    // 这里如果队列为空, 就直接阻塞. 如果队列非空, 就获取到里面的内容~~
                    Runnable runnable = queue.take();
                    // 获取到之后, 就执行任务.
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    // 4. 创建一个数据结构来组织若干个线程.
    private List workers = new ArrayList<>();

    public MyThreadPool(int n) {
        // 在构造方法中, 创建出若干个线程, 放到上述的数组中.
        for (int i = 0; i < n; i++) {
            Worker worker = new Worker(queue);
            worker.start();
            workers.add(worker);
        }
    }

    // 5. 创建一个方法, 能够允许程序猿来放任务到线程池中.
    public void submit(Runnable runnable) {
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Demo26 {
    public static void main(String[] args) {
        MyThreadPool pool = new MyThreadPool(10);
        for (int i = 0; i < 100; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello threadpool");
                }
            });
        }
    }
}

 

 

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/999744.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号