多线程(小白笔记)


笔者对 Java 的学习目前并未深入,很多东西笔者也没有弄的很懂,所以如果有错误的话,烦请各位指出。

一、多线程

在过去的学习中,我们知道了 main 方法执行方式,就是一行代码一行代码这样持续下去,直到结束为止。但是在很多情况下,这种情况不符合我们的使用需求。比如,我们知道服务器和客户端,是分为两个部分执行的,但是客户端一定有很多而服务器不一定。如果只有一个 main 方法,那么 Java 不能做到一个服务器响应多个客户端。此时多线程的引入就十分有必要。

1. 线程与进程

线程和进程是很容易被混淆的两个概念,但是二者却又本质的不同,所以我先来简单说说线程和进程。

1)进程

你可以想象一下,当你在电脑上打开了浏览器,那么此时,运行浏览器就是一个进程。又或者,你打开了游戏,打开了 steam 客户端,打开了暴雪战网……这些都是进程。通俗易懂(拿捏了!)。

2)线程

线程的理解比起进程稍微复杂那么一点点,就拿上面说的浏览器来说吧。当你打开了浏览器,搜索 “欧美大片” ,你就开启了一个新的线程,那么你再次搜索 “变形金刚” ,你就又开启了一个新的线程。虽然比起进程稍微有些复杂,但也不是很难,我相信各位大佬已经动了。当然真实情况肯定开启的线程不止这么点,这里就比喻一下,各位别当真哦。

2. 同步与异步

这个概念也很简单,没啥好唠叨的,我这就解释一波。

1)同步

排队进行,效率不高但是安全。
就看上面这么一句话可能很难想象我为什么说简单,那么我们老规矩,来举个栗子:假如你是个学生,你们班有个大佬学生买了一套《Java从入门到放弃》来学习,这不太好,你们都想看。然后这位大佬想了个办法,排队来,今天一号看,看完还给他,然后二号再看。不管一号看多久,反正二号得等着,不管一号看的是第几本,反正二号不能拿。这样就保证了书的位置大佬很清楚,很容易就能找到。
同步就是这么个意思啦。

2)异步

相信大家看完我同步的栗子,已经知道异步是咋回事了。咳咳,但是吧,毕竟我是在做博客,不说不太好。
同样是上面的栗子,现在大佬换了个方式,只要有人来借书,大佬就直接给。今天一号借走了第一部,二号借走了第二部,明天三号又来借走第四部,后天二号刚还的第二部就被四号借走了……这样做的方式效率可谓是极其高哇,大家都有书看。但是弊端也显而易见,没人知道现在大佬那里还有哪些书,大佬自己可能也搞混了借给哪些人了。

3. 并发与并行

相比较于同步和异步,并发与并行可难搞多了,隔段时间不看就很容易混,听我给给我细细道来。

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

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

有些许的绕,我给各位看官解释一下。并发就好比一个马路口,绿灯亮了,然后积攒的人潮哗哗哗地就开始走了,假如绿灯亮了 15 秒,那么我们就可以说,这个马路口 15 秒的并发量超级大!(虽然现实里没谁闲的没事干这么搞吧)
并行就比并发好搞多了,同样是马路口,这次我们来说车。假如你拿了个相机,路过了一个路口。此时正是红灯,马路上的车辆呼啸而过,你心血来潮,拿起相机对着对面就是一拍。那么在这一瞬间,冲过这个马路口的车辆全部进入了你的照片。假如照片上有 13 辆车吧,那这个马路口这一瞬间的车辆并行量就是13.

4. Thread

现在我们已经了解了关于线程的几个基本概念,该是实操一把的时候了!

我们来创建一个类(虽然我还没有写这个东西,但是我默认大家都知道了奥,后面我再补上)

public class MyThread extends Thread {

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("第" + i + "号大佬");
        }
    }
}

这就是 Thread 的使用方法第一步了,@Override 是重写方法的标记,run 是继承 Thead 类需要重写的方法,这里这个 run 方法就是一个线程了。那么我们要如何来启动它呢?

public class Main {

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

        for (int i = 0; i < 5; i++) {
            System.out.println("第" + i + "号菜鸡");
        }

    }

}

很简单,在主线程 main 里面把 MyThread 对象 new 出来,然后跟上个 .start(); 就好啦,这个东西就是这个分线程的启动方法。那么我们来看看结果。
在这里插入图片描述
是不是很简单?接下来我们来看个更简单的。

public class Main {

    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("第" + i + "号菜鸡");
                }

            }
        }.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("第" + i + "号菜鸡");
        }

    }

}

这是匿名内部类的写发,也是我们新手学习期的常用写发。

5. Runnable

和 Thread 差不多,我们也可以这样来用,不过需要注意的是这是个接口。

public class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("第" + i + "号学霸");
        }
    }
}

然后主线程怎么写呢?这就上!

public class Main {

    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();
        Thread t = new Thread(mr);
        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("第" + i + "号菜鸡");
        }

    }

}

这样看起来,似乎 Runnable 用起来更复杂一些啊,那么这个接口有什么有点呢?小的这就为您解答。

Runnable 在实际的使用中其实更为广泛。因为相比于 Thread,Runnable 有更多优点:

  1. 更适合多个线程同时执行相同任务的情况。
  2. 避免了单继承带来的局限性,毕竟 Java 只能单继承,但是可以多实现嘛。
  3. 任务和线程本身是分离的,提高了程序的健壮性。
  4. 线程池技术接受 Runnable 类型的任务,不接受 Thread 类型的任务。

5.线程相关

线程这方面有些有意思的玩法和一点小小的常识,我们来一起看一下。

1)线程休眠

就像名字说的,可以让线程暂停工作的方法,这个方法是 Thread 类下的一个静态方法,不需要创建 Thread对象就可以直接调用。

public class Main {

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 5; i++) {
            System.out.println("第" + i + "号菜鸡");
            Thread.sleep(1000);
        }

    }

}

需要注意的是,sleep 里的 1000 是指毫秒,这个程序可以让电脑每隔一秒钟输出一次。

2)线程阻塞

线程阻塞不是卡住才叫阻塞的,只要有一个操作让程序的执行时间大量消耗,那么就能叫做线程阻塞。

3)线程死锁

比如两个线程在交互时,两个都在等对方发消息。这就好像警察和罪犯一个说:“先放人质我们就放你。”一个说:“先放了我,我就放了人质。” 俩人就卡着事情就毫无进展。线程也是一样的,如果出现了这种情况,就形成了线程死锁,我们应当尽量避免这种情况发生。

6. 线程安全问题

前面已经说了线程存在安全与不安全的问题,那么我们怎么解决它呢?
我们来看看不安全的案例
这是分线程类

public class MyRunnable implements Runnable{

    private Integer num = 10;

    @Override
    public void run() {
        while (num > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(num--);
        }
    }
}

这是主线程

public class Main {

    public static void main(String[] args) throws InterruptedException {

        MyRunnable a = new MyRunnable();

        new Thread(a).start();
        new Thread(a).start();
        new Thread(a).start();
        new Thread(a).start();

    }

}

我们来看看输出结果,看看他为什么不安全。
在这里插入图片描述
可以看到,我们在循环时的条件是 num > 0 才输出,但是居然出现了 -1,-2 的情况。这是为什么呢?
这是因为运行时线程休眠的原因,当 num = 1 的时候,1 线程走到了等待 100 毫秒那里,然后等待的这段时间里 2,3 线程也进来了,这时候1线程把 num 改成0,于是 4 线程进不来了,但是 2 ,3 线程已经进来了鸭,进来就得走完这一程,于是 num 接着连续减了 2 次,变成了 -2 。

1)同步代码块

上面我们看到的这个问题怎么来解决呢?我们可以给它上个锁。

public class MyRunnable implements Runnable{

    private Integer num = 10;
    private Object o = new Object();

    @Override
    public void run() {
        synchronized (o) {
            while (num > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(num--);
            }
        }
    }
}

主线程程序不变,方便阅读我我再给大家展示一下。

public class Main {

    public static void main(String[] args) throws InterruptedException {

        MyRunnable a = new MyRunnable();

        new Thread(a).start();
        new Thread(a).start();
        new Thread(a).start();
        new Thread(a).start();

    }

}

我们来看输出结果
在这里插入图片描述
可以看到现在已经没有输出负数的情况了,那么这是个啥原理呢?看图。
在这里插入图片描述
只要锁内部还有线程在执行,其余线程都得排队等待。注意 Object 对象不能在 run 方法内部创建,否则就是每个线程都有一把锁,大家都看自己的锁有没被锁上,那不是白搞了嘛。

2)同步方法

和同步代码块类似,只是这一次,我们把锁加到了方法上。

public class MyRunnable implements Runnable {

    private Integer num = 10;

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

    public synchronized boolean number() {
        if (num > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(num--);
            return true;
        }
        return false;
    }
}

来看运行结果
在这里插入图片描述
这样也可以避免出现负数。

3)显式锁 Lock

显式锁的用法也非常简单,我们一起来看一下

public class MyRunnable implements Runnable {

    private Integer num = 10;
    private Lock l = new ReentrantLock();

    @Override
    public void run() {
        l.lock();
        while (num > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(num--);
        }
        l.unlock();
    }
}

老规矩,来看看运行结果,看他锁住没。
在这里插入图片描述
锁没锁住还是显而易见的哈。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值