多线程概述

多线程

以下代码均使用IntelliJ IDEA 2020.1.4 x64运行

一、多线程基础概述

线程与进程


关系:

1.一个进程最少有一个线程。

2.线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。

多线程为了让多条执行路径能均分,能更合理的交替执行。

线程调度


分别为分时调度和抢占式调度。

Java使用的调用机制是抢占式调度。

多线程并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

同步与异步


同步:排队执行,效率低但是安全。

异步:同时执行,效率高但是数据不安全。

并发与并行


并发:指两个或多个事件在同一个时间段内发生。

并行:指两个或多个事件在同一时刻发生(同时执行)。

两者不是一个概念,同一时间段和同一时刻有很大的区别。



二、继承Thread

原理看如下代码:

 public static void main(String[] args) {
        //Thread
        MyThread myThread = new MyThread();
        myThread.start();//与下边的代码并发执行,抢占式分配,输出顺序会不一样
        for (int i = 0; i <10 ; i++) {
            System.out.println("汗滴禾下土"+i);
        }
    }

public class MyThread extends Thread{
    /**
     * run方法就是线程要执行的任务方法
     *
     * 每个线程都拥有自己的栈空间,共用一份堆内存
     */
    @Override
    public void run() {
        //这里的代码就是一条新的执行路径
        //这个执行路径的处罚方式,不是调用run方法,而是通过thread对象的start()来启动任务
        for (int i = 0; i <10 ; i++) {
            System.out.println("锄禾日当午"+i);
        }
    }
}

run()方法就是自己编写的想让线程执行的任务方法,他与main函数中的方法并发执行,由于是抢占式分配,所以输出顺序会不一样。


Thread常用方法

1.start(开启线程,start是通过线程来调用run方法)

2.run 此run非彼run (不是在run方法实现线程的逻辑,而是thread.run(),这个run方法是直接调用了线程中的run)

3.yield(暂停当前线程,并执行其他线程)

4.sleep(使当前线程由运行状态变成阻塞状态,若睡眠时其他线程调用了interrupt方法,会导致sleep抛出异常InterruptException)

5.join(保证当前线程在其他线程开始时会结束)(如下,A线程想运行的话,必须等B线程结束才能运行(将处于阻塞状态))

6.interrupt(中断线程)

7.wait/notify(从Object类继承下来的方法)

8.setPriority(设置线程优先级(只能在线程开始前设置))



三、实现Runnable

原理看如下代码:

public static void main(String[] args) {
        //实现Runnable
        //1.创建一个任务对象
        MyRunnable r = new MyRunnable();
        //2.创建一个线程,并为其分配一个任务
        Thread t = new Thread(r);
        //3.执行这个线程
        t.start();
        for (int i = 0; i <10 ; i++) {
            System.out.println("疑是地上霜"+i);
        }
    }
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //线程的任务
        for (int i = 0; i <10 ; i++) {
            System.out.println("床前明月光"+i);
        }
    }
}

与继承Thread不同,Thread只可以单继承,而Runnable可以多实现。


实现Runnable相比于继承Thread的好处

  1. 是通过创建任务,然后给线程分配的方式来实现的多线程,更适合多个线程同时执行相同任务的情况。
  2. 可以避免单继承所带来的局限性。
  3. 任务与线程本身是分离的,提高了程序的健壮性。
  4. 后续学习的线程池技术,接受Runnable类型的任务,不接受Thread类型的线程。

使用线程还可以使用匿名内部类的方式,具体代码如下:

public static void main(String[] args) {
        //匿名内部类
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i <10 ; i++) {
                    System.out.println("一二三四五"+i);
                }
            }
        }.start();

        for (int i = 0; i <10 ; i++) {
            System.out.println("六七八九十"+i);
        }
    }

线程名称的获取以及线程休眠

1、线程名称获取
  • new Thread(new MyRunnable(), “锄禾日当午”).start():给线程命名
  • Thread.currentThread().getName():获取当前运行线程的名称,如果未命名,则获取默认的命名。
2、线程休眠

Thread.sleep(1000):线程休眠1秒,如果在循环中的话,则一秒之后再次循环。



四、线程中断

interrupt():中断标记,添加之后当线程运行到中断标记时就会停止运行。
原理代码如下:

public static void main(String[] args) {
        //线程阻塞-所有比较消耗时间的操作
        //线程中断
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //给线程t1添加中断标记
        t1.interrupt();
    }

    static class MyRunnable implements Runnable{

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                    System.out.println("发现中断标记,线程自杀");
                    return;
                }
            }
        }
    }

由上边的代码可以看到,当main函数循环完毕之后,代码运行到中断标记,run()方法发现中断标记,线程自杀,线程运行结束。


用户线程和守护线程

用户线程:当一个进程不包含任何的存活的用户线程时,进行结束。
守护线程:守护用户线程,当最后一个用户线程结束时,所有守护线程自动死亡。



五、线程安全问题

举个栗子

有一个活动的售票点要准备开始售票,有三个售票窗口,总共要售出10张票,用代码实现。
首先第一种方法:

 public static void main(String[] args) {

        //线程不安全
        //解决方案1,同步代码块
        //格式:   synchronized(锁对象){
        //
        //        }
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //票数
        private int count = 10;
       
        @Override
        public void run() {

                while (true) {
                   
                        if (count > 0) {
                        System.out.println("正在准备买票");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName()+"出票成功,余票:" + count);
                    }else {
                            break;
                        }
                }
           
        }
    }

运行结果如下:
在这里插入图片描述
可以看到余票在最后出现了负数情况,单看代码逻辑是永远不会出现负数的,因为循环只有在票数大于0才会运行,但是依旧出了问题,这是为什么呢?
假设三个售票口分别为A,B,C,余票只剩一张,当A进入循环时,他在循环里可能费了点时间,在运行过程中丢失了时间片,这个时候B抢到了时间片,也进入了循环,这个时候票数还没有减,因为A可能还在count–前,同样的情况,这个时候C也抢到了时间片进入了循环,三个逻辑同时都处在循环中。这个时候A 拿到时间片,运行count–,这个时候count变成了0;然后B拿到时间片,运行count–,count变成了-1;最后C拿到时间片,运行count–,count变成了-2。这就是线程不安全问题。


1、线程安全1-同步代码块

格式:

private static Object o = new Object();
synchronized(锁对象:o){
        
}

作用:加锁,可以使线程排队执行。

 static class Ticket implements Runnable{
        //票数
        private int count = 10;
        private Object o = new Object();//多个线程只能看同一把锁
        @Override
        public void run() {

                while (true) {
                    synchronized (o) {
                        if (count > 0) {
                        System.out.println("正在准备买票");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName()+"出票成功,余票:" + count);
                    }else {
                            break;
                        }
                }
            }
        }
    }

注意:多个线程只能看同一把锁,不然无法起到排队的作用。

加锁之后运行结果:
在这里插入图片描述
这样就解决了线程不安全问题。


2、线程安全2-同步方法

原理代码如下:

 public static void main(String[] args) {

        //线程不安全
        //解决方案1,同步方法

        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //票数
        private int count = 10;

        @Override
        public void run() {
                while (true) {
                    boolean flag = sale();
                    if (!flag){
                        break;
                    }
                }
        }
        public synchronized boolean sale(){
            //this 锁
            //静态:类名,class 锁
            if (count > 0) {
                System.out.println("正在准备卖票");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count--;
                System.out.println(Thread.currentThread().getName() + "出票成功,余票:" + count);
                return true;
            }
            return false;

        }
    }

3、线程安全3-显示锁 Lock 子类 ReentrantLock

原理代码如下:

public static void main(String[] args) {
        //线程不安全
        //解决方案3,显示锁 Lock 子类 ReentrantLock
        

        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //票数
        private int count = 10;
        //显示锁 :参数为true,就表示是公平锁
        private Lock lock = new ReentrantLock(true);
        @Override
        public void run() {
                while (true) {
                    lock.lock();
                        if (count > 0) {
                        System.out.println("正在准备买票");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName()+"出票成功,余票:" + count);
                    }else {
                            break;
                        }
                        lock.unlock();
                }

        }
    }

公平锁:排队
不公平锁:不排队,抢



六、显示锁Lock和隐式锁synchronized的区别

1、层面不同

synchronized:Java中的关键字,是由JVM来维护的,时JVM层面的锁

Lock:使用lock是调用对应的API,是API层面的锁

2、使用方式不同

synchronized:程序能够自动获取锁和释放锁。非逻辑问题的话,不会出现死锁。

Lock:需要手动获取锁和释放锁。不释放锁,就可能导致死锁

3、等待是否可中断

synchronized:不可中断,除非抛出异常或者正常运行完成

Lock:可以中断

中断方式:

  • 调用设置超时方法tryLock(long timeout ,timeUnit unit)
  • 调用lockInterruptibly()放到代码块中,然后调用interrupt()方法可以中断

4、加锁的时候是否可以设置成公平锁

synchronized:只能为非公平锁

Lock:两者都可以,默认是非公平锁。

在其构造方法的时候可以传入Boolean值。true:公平锁、false:非公平锁

5、锁绑定多个条件来condition

  • synchronized
    不能精确唤醒线程。要么随机唤醒一个线程;要么是唤醒所有等待的线程。
  • Lock
    用来实现分组唤醒需要唤醒的线程,可以精确的唤醒。

6、性能区别

  • synchronized
    托管给JVM执行,Java1.5中,由于需要调用操作接口,可能导致加锁消耗时间过长,与Lock性比性能低。1.6以后,语义定义更加清晰,有适应自旋、锁粗化、锁消除、轻量级锁、偏向锁等,可进行许多优化,性能提高了,与Lock差不多。
  • Lock
    java写的控制锁的代码,性能高。


七、线程死锁

举个栗子

两个顾客在听一家卖衣服的商店,商店中有两个试衣间,A顾客进入第一个试衣间之后锁上了门,B顾客进入第二个试衣间之后锁上了门,两位顾客都进入试衣间之后,A顾客突然发现他进的试衣间没灯,没灯怎么换衣服,于是他想换一间试衣间,但是他进这个试衣间之前看到B顾客进入了另外一个试衣间,于是他决定等B顾客出来之后他再换试衣间;同一时间,B顾客发现他进的试衣间全是水,根本没法换衣服,于是他也想换一个试衣间,但是他进试衣间之前看到A顾客进入到另一个试衣间,于是他也决定等A顾客出来之后他再换试衣间,他们等啊等…等啊等…
线程死锁问题也就像这个栗子一样,示例代码如下:

 public static void main(String[] args) {
        //线程死锁
        Culprit c = new Culprit();
        Police p = new Police();
        new MyThread(c,p).start();
        c.say(p);

    }

    static class MyThread extends Thread{
        private Culprit c;
        private Police p;
        public MyThread(Culprit c,Police p){
            this.c = c;
            this.p = p;
        }

        @Override
        public void run() {
            p.say(c);
        }
    }
    //罪犯
    static class Culprit{
        public synchronized void say(Police p){
            System.out.println("罪犯:你放了我,我放了人质");
            p.fun();
        }
        public synchronized void fun(){
            System.out.println("罪犯被放走了,罪犯也放了人质");
        }
    }
    //警察
    static class Police{
        public synchronized void say(Culprit c){
            System.out.println("警察:你放了人质,我放过你");
            c.fun();
        }
        public synchronized void fun(){
            System.out.println("警察救到了人质,但是罪犯跑了");
        }
    }

运行结果如下:
在这里插入图片描述
从上图可以看到,罪犯和警察的fun()方法都没有执行,这就是线程死锁造成的。



八、线程的状态

new:表示线程刚被创建,但是未被启动

Runnable:在Java虚拟机中执行的线程处于此状态

Blocked:排队时的线程的状态

Waiting:无限休眠状态,等待唤醒

TimedWaiting:没有被唤醒,等到时间到了会自己醒的状态

Trminated:已退出(已死亡)的线程的状态



九、第三种线程的创建方式-带返回值的线程Callable

Callable: 返回结果并且可能抛出异常的任务。
优点:

  • 可以获得任务执行返回值;
  • 通过与Future的结合,可以实现利用Future来跟踪异步计算的结果。

原理代码如下:

 public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<Integer> c = new MyCallable();
        FutureTask<Integer> task = new FutureTask<>(c);
        new Thread(task).start();
        Integer j = task.get();
        System.out.println("返回值为:"+j);
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);
        }
    }

    static class MyCallable implements Callable<Integer>{
        @Override
        public Integer call() throws Exception {
            //Thread.sleep(3000);
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(i);
            }
            return 100;
        }
    }

运行结果如下:
在这里插入图片描述
Callable获取返回值:
Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

Runnable与Callable的异同

Runnable 与 Callable的相同点

  • 都是接口
  • 都可以编写多线程程序
  • 都采用Thread.start()启动线程

Runnable 与 Callable的不同点

  • Runnable没有返回值;Callable可以返回执行结果
  • Callable接口的call()允许抛出异常;Runnable的run()不能抛出


十、线程池

1、缓存线程池

 * (长度无限制)
 * 任务加入后的执行流程
 *      1.  判断线程池是否存在空闲线程
 *      2.  存在则使用
 *      3.  不存在,则创建线程 并放入线程池,然后使用

2、定长线程池

*(长度是指定的数值)
* 任务加入后的执行流程:
*      1.  判断线程池是否存在空闲线程
*      2.  存在则使用
*      3.  不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池,然后使用
*      4.  不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程

3、单线程线程池

 * 执行流程:
 *      1.  判断线程池的那个线程是否空闲
 *      2.  空闲则使用
 *      3.  不空闲,则等待 池中的单个线程空闲后使用

4、周期性任务定长线程池

* 执行流程:
 *      1.  判断线程池是否存在空闲线程
 *      2.  存在则使用
 *      3.  不存在空闲线程,且线程池未满的情况下,则创建线程并放入线程池,然后使用
 *      4.  不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
 *
 * 周期任务执行时:
 *      定时执行,当某个时机触发时,自动执行某任务

 * 1.定时执行一次
     *  参数1。定时执行任务
     *  参数2.时长数字
     *  参数3.时长数字的时间单位,TimeUnit的常量指定

代码如下:

 service.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("锄禾日当午");
            }
        },5, TimeUnit.SECONDS);

* 周期性执行任务
     * 参数1.任务
     * 参数2.延迟时长数字(第一次执行在什么时间以后)
     * 参数3.周期时长数字(每隔多久执行一次)
     * 参数4.时长数字的单位         

代码如下:

  service.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("汗滴禾下土");
            }
        },5,1, TimeUnit.SECONDS);


十一、Lambda表达式

原理代码如下:

 public static void main(String[] args) {
        print((int x, int y)  -> {
                return x+y;
        },100,200);
    }
    public static void print(MyMath m,int x,int y){
        int num = m.sum(x,y);
        System.out.println(num);
    }
    static interface MyMath{
        int sum(int x,int y);
    }

运行结果如下:
在这里插入图片描述


总结

  • 公平锁与非公平锁的使用;

private Lock lock = new ReentrantLock(true);
括号内为true即为公平锁,否则为非公平锁。

  • 1.什么是线程?线程和进程的区别?

线程:线程是CPU调度的最小单位,也是程序执行的最小单位。没有单独地址空间,线程属于进程,不能独立执行,每个进程至少要有一个线程,称为主线程。

进程:进程是系统进行资源分配的基本单位,有独立的内存地址空间;

  • 2.描述CPU和多线程的关系

第一阶段,单CPU时代,单CPU在同一时间点,只能执行单一线程。

第二阶段,单CPU多任务阶段,计算机在同一时间点,并行执行多个线程。但这并非真正意义上的同时执行,而是多个任务共享一个CPU,操作系统协调CPU在某个时间点,执行某个线程,因为CPU在线程之间切换比较快,就好像多个任务在同时运行。

第三阶段,多CPU多任务阶段,真正实现的,在同一时间点运行多个线程。具体到哪个线程在哪个CPU执行,这就跟操作系统和CPU本身的设计有关了。

  • 3.什么是线程安全/线程不安全?

线程安全:就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。

线程不安全:就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值