Java 线程 基础知识总结

线程基础

很不严谨的说,线程是什么?线程就是为了让很多个东西并发执行,大大的提高程序执行的效率啊

三个非常重要的概念:

  • 程序:一组写好了的静态代码块(就我们写的那些代码玩意)
  • 进程:正在进行着的程序,即静态的代码执行起来了
  • 线程:是进程的小单元,或者说多个线程构成一个进程(线程的出现是因为进程满足不了人们的需求,于是进程被细化了)

线程的转换(五个较为简单的转换)可以通过这个图了解一下:
在这里插入图片描述
在Java中,

线程有三类:

  1. 主线程:系统线程,如Java中的Java虚拟机(主线程是最先执行的)
  2. 用户线程:Java中main函数执行的那些玩意
  3. 守护线程(精灵):比如Java中的GC(垃圾回收器,他是为主线程服务的,当系统启动后,GC就随之产生了;Java虚拟机断掉了,GC也就不干活了)

线程是操作系统层次的(又或者说是cpu层次的,进程怎么执行的,顺序是啥。。。这些都是靠cpu分配的)
不过线程怎么去用,Java的JDK已经给我们写好了
在这里插入图片描述
实现线程的过程(有两个方法):

  1. 自己描述一个类
  2. 实现线程的两个前提条件(继承Thread是最方便的,但是Java是单继承的,很可能会和其他的继承冲突;所以还可以通过实现Runnable接口,不过它要额外的通过写一个Thread类来执行start()方法):
    • 方法一:继承一个父类Thread
    • 方法二:实现一个接口Runnable
  3. 我们都必须重写run()方法,
    因为这个方法来源于cpu,是操作系统给JDK提供的接口,JDK将它包装成run(),其实我们执行的线程就是写在run()方法中的代码执行起来了
  4. new一个线程对象(我们是无法调用run()的,它只有cpu才可以调用,cpu分配时间碎片才能开始执行)
    我们只能调用start()方法(这个方法继承自Thread),它会使这个线程除了cpu资源的其它资源都得到满足,让它进入就绪状态,在就绪队列中等待着cpu执行它

(补充:为什么要这么麻烦呢?为什么不直接使用Thread类或者Runnable接口,直接在里面写线程的代码,但是run()方法是不允许有参数的,我们一般要执行的线程都要传参数,所以只能通过继承和实现来解决参数传递的问题)

一个例子】实现线程的两种方式

  • 继承一个父类 Thread,可以看到调用线程时写法较为简单
public class Running extends Thread {
    String name;

    public Running(String name){
        this.name = name;
    }

    @Override
    public void run() {
        for(int i = 1; i <= 10; i++){
            System.out.println(name + "跑到了" + i + "米");
        }
    }

    public static void main(String[] args) {
        Running Lux = new Running("Lux");
        Running Ahri = new Running("Ahri");
        Running Annie = new Running("Annie");

		//从Thread类中继承过来的方法
        Lux.start();
        Ahri.start();
        Annie.start();
    }
}
  • 实现一个接口 Runnable,可以看到调用线程时写法复杂一些
public class Running implements Runnable {
    String name;

    public Running(String name){
        this.name = name;
    }

    @Override
    public void run() {
        for(int i = 1; i <= 10; i++){
            System.out.println(name + "跑到了" + i + "米");
        }
    }

    public static void main(String[] args) {
        Running Lux = new Running("Lux");
        Running Ahri = new Running("Ahri");
        Running Annie = new Running("Annie");

		//Thread类中才有start()方法
        Thread thread1 = new Thread(Lux);
        thread1.start();
        Thread thread2 = new Thread(Ahri);
        thread2.start();
        Thread thread3 = new Thread(Annie);
        thread3.start();
    }
}

多次运行线程代码,发现线程无论执行顺序,还是开始执行时间都是随机的(实际上是操作系统通过算法调度和分配资源,JDK是不能影响它的)

我们只需要类中继承或实现线程,重写run()方法,最后调用start()方法就可以了

我们不要直接调用run()方法,这样就不是多线程了,只是按顺序执行的单线程!!!

模拟一个火车站售票小例子

  • 使用控制台输入输出,仅仅是为了简单玩一下线程,例子很简陋
  • 三个类
    • 车票
    • 售票系统12306
    • 售票窗口
  • 代码github
  • 效果展示:
从广州北站售出:[ 北京11 --> 深圳11 : 150.0 ]
从广州西站售出:[ 北京10 --> 深圳10 : 125.0 ]
从广州南站售出:[ 北京12 --> 深圳12 : 175.0 ]
从广州西站售出:[ 北京14 --> 深圳14 : 225.0 ]
从广州南站售出:[ 北京15 --> 深圳15 : 125.0 ]
...
...
对不起广州南站窗口车票已售完
对不起广州西站窗口车票已售完
对不起广州北站窗口车票已售完

多线程同时读取一个文件

  • 题目:有一个26字节的文件 test.txt,里面按顺序存储着26个小写英文字母,使用3个线程同时读取
  • 使用控制台输入输出,仅仅是为了简单玩一下线程与IO,例子很简陋
  • 分析:
    • 使用字节型输入流,使用skip()方法
    • 1号线程:读取1-10字节
    • 2号线程:读取11-20字节
    • 3号线程:读取21-26字节
  • 代码github
  • 效果展示:
Thread-0读取:a
Thread-2读取:u
Thread-1读取:k
Thread-2读取:v
Thread-0读取:b
Thread-2读取:w
...
...

生产消费者模型

生产消费者模型

我创建一个生产者向仓库添加物品
两个消费者向仓库中拿走物品

在多线程并发进行中会出现这么一种问题:
在这里插入图片描述
如果仓库只剩一个物品时,出现了这么一个顺序

  1. 消费者1进行判断(还剩一个)
  2. 消费者2进行判断(还剩一个)
  3. 消费者1执行get方法
    • get方法作用是:获取物品
  4. 消费者2执行get方法(这时候就出现问题了,虽然判断时仓库还有一个可以拿;但是到执行get方法时,仓库是空的,那么问题出现了:多线程并发抢夺资源
  • 生产者消费者模型
    • 模拟上述描述的问题,并且代码中不涉及其余线程操作,只会出现生产者消费者模型的问题
    • 三个类:Producer、Consumer、WareHouse
    • 分别代表:生产者、消费者、仓库
  • 代码github
  • 不出所料,在执行的过程中出现问题了
    在这里插入图片描述两个消费者,成功演示出了线程安全问题;两个消费者并发访问,可能产生抢夺资源的问题;所以多个线程并发执行的时候,有安全隐患

synchronized

  • 生产者消费者模型 解决办法
    • 让仓库被线程访问的时候,仓库对象被锁定(即仓库对象只能被一个线程访问,其它的线程处于等待状态)
    • 使用一个特征修饰符即可synchronized,又称为线程安全锁,表示同步的意思,作用:一个时间点只有一个线程访问
    • 给仓库提供的获取物品的方法(get)加上线程安全锁,可以避免多线程并发抢夺资源

synchronized 有两种写法:

  1. synchronized关键字,放在方法的结构上(特征修饰符),其实它锁定的是synchronized修饰方法所在的对象,由调用该方法的线程锁定
public synchronized void test(){代码}
  1. synchronized关键字,放在方法(/构造方法/块)内部
public void test(){
	很多代码
	synchronized(对象){   //只有方法执行到这里的时候才被锁定
						 //锁定哪个对象呢?可以由synchronized()的参数决定
		某些代码
	}
	很多代码
}

第二种写法性能更加棒;而且更为灵活,给内部的synchronized(){}的参数传对象,不仅可以由调用方法的线程锁定,还可由其他线程对象来锁定

线程对象改变引起的异常

  • 如果线程调用的方法中有wait()notify()/notifyAll()方法,但是没有加synchronized线程安全锁时会出现一种异常:java.lang.IllegalMonitorStateExceptionr

  • 注意:wait和notify需要成对出现,或者wait和notifyAll成对出现,否则可能出现假死锁

    • wait() 线程等待
    • notify() 唤醒某个线程
    • notifyAll() 唤醒所有线程
    • wait() notify() notifyAll()都是Object的方法,但和synchronized关键字一样,也是哪个线程调用则对哪个线程起作用

在生产者消费者模型代码基础上进行修改,仓库的获取物品与存放物品的方法不加线程安全锁,并且使用线程等待与线程唤醒方法
在这里插入图片描述
这就是张冠李戴的字面解释了,当访问仓库的生产者线程等待,告知生产者的这一刹那,对象变成了另一个线程:该等待的没有等待,不该等待的等待了

join

  • join()方法也是Thread类中的方法,挺有用的
  • 可以让两个并行的线程变成单线程

线程创建好之后,我们不能控制线程执行的先后顺序,我们只能让线程进入就绪状态

例子】首先来回顾一下线程并发执行的情况,以线程ThreadOne与ThreadTwo为例:

ThreadOne one = new ThreadOne();
ThreadTwo two = new ThreadTwo();
one.start();
two.start();
  • one执行,one结束,two执行,two结束
  • one执行,two执行,one结束,two结束
  • one执行,two执行,two结束,one结束

例子】如果有两个线程ThreadOne和ThreadTwo,我想让ThreadTwo加入到ThreadOne中,可以这么写:

ThreadOne

public class ThreadOne extends Thread {
    public void run() {
        System.out.println("thread-one start");
        ThreadTwo two = new ThreadTwo();
        two.start();
        try {
            two.join();//线程2加入到线程1中
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("thread-one end");
    }
}

ThreadTwo

public class ThreadTwo extends Thread{
    public void run() {
        System.out.println("thread-two start");
        try {
            Thread.sleep(5000);//睡眠5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("thread-two end");
    }
}

在main中,调用ThreadOne线程的start()方法:
在这里插入图片描述

  • 这里用的是join()的无参方法,thread-one启动后,必须要等着thread-two执行完,才能往后面执行

  • join(millis)还有一个有参重载方法

将ThreadOne类中的 two.join() 改为 two.join(4000)

thread-one启动后,不是必须等待着thread-two执行完,而是等待4000毫秒如果thread-two还没执行完,thread-one就继续往后执行
在这里插入图片描述

join源码结合例子分析

【join源码】

  • 无参的 join() 方法:
public final void join() throws InterruptedException {
    join(0);//还是会调用有参的join方法
}
  • 有参的 join() 方法:
public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) { //这就是调用无参join时的执行代码
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
  • ThreadOne类中调用的无参 two.join()
    • 只要 ThreaTwo 活着,那么 ThreadOne 就一直是等待状态
while (two.isAlive()) {
    two.wait(0);
}
  • ThreadOne类中调用的有参且参数大于0 two.join(4000)
    • ThreaTwo 活着的期间,ThreadOne 等待millis毫秒后打破循环,即不再等待了
while (two.isAlive()) {
    long delay = millis - now;
    if (delay <= 0) {
        break;
    }
    two.wait(delay);
    now = System.currentTimeMillis() - base;
}

join遇上线程安全锁

  • 感受一下 synchronized 的强大吧
    • 三个线程类:ThreadOne、ThreadTwo、ThreadThree
    • ThreadOne 中调用 ThreadTwo,并 two.join(2000);,不过 ThreadTwo 需要执行5秒
    • ThreadTwo 中调用 ThreadThree,并将自身通过参数传给 ThreadThree
    • ThreadThree 通过 synchronized 将 ThreadTwo 锁起来10秒
  • 代码github
  • 执行效果
    在这里插入图片描述
  1. one启动
  2. two启动
  3. three启动
  4. two就join进one中了(two要执行5000毫秒,但是one只给了two2000毫秒)
  5. 2000毫秒之后,one想要把two从自己的线程中剔除掉;但是发现,two已经不在自己的手中,two已经被three锁定(要被锁定10000毫秒)
  6. one只能等待three将two释放后才能剔除掉two

所以,锁是非常强大滴

死锁

  • synchronized非常厉害,一旦线程被锁定,不释放的情况,其它的线程都需要等待
  • 如果锁没用好,有可能产生死锁的问题(互相都想要对方的资源,却都得不到满足)

这么说吧,
Ahri手里有a资源,然后需要b资源
Lux手里有b资源,然后需要a资源
而且,a资源被Ahri独占了,其它进程拿不到;b资源被Lux独占了,其它进程都拿不到
那么:
Ahri得不到Lux的b资源
Lux得不到Ahri的a资源
就会这样:
Ahri和Lux一直等着对方释放资源

这就是死锁

哲学家进餐问题

哲学家进餐问题 是一个著名的死锁问题:
在这里插入图片描述
四个人在餐桌上吃饭,但是只有四只筷子,
四个人同时拿筷子,
而且要求,每一个人先拿左手边的筷子,然后拿右手边的筷子

所以嘛,有可能他们四个人拿筷子的速度恰好一样,都拿了左手边的筷子,于是僵持住了

避免死锁的方法:

  • 礼让----产生时间差
  • 不要产生共用的问题

有趣的Timer类

  • 计时器/定时器是JDK已经写好了的线程类
  • java.util包,Timer

小例子】用Timer类模拟一个短信轰炸的效果(每隔一段时间发送信息)

Timer有这么一个方法schedule(),有四个重载方法
在这里插入图片描述
对上面出现的参数的解释

  1. TimerTask task表示一个任务,以字符串形式表示
    TimerTask是一个抽象类,new不了的,不过可以使用匿名内部类对象
  2. Date timeDate firstTime表示起始时间(到了起始时间,任务开始执行)
  3. long delay表示延迟时间(及延迟给的时间后,任务开始执行)
  4. long period周期,表示多长时间后再干一次,是一个循环

短信轰炸

//导包
//...
public class TestTimer {

	private int count = 1;//记录轰炸次数
    ArrayList<String> userBox = new ArrayList<>();//存储众多个人信息
    
    {
        userBox.add("a"); userBox.add("b"); userBox.add("c"); userBox.add("d");
    }

	启动一个小线程,记录时间,可以每隔一段时间去做一件事情
    public void test() throws ParseException {
        System.out.println("开始啦");
        Timer timer = new Timer();

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date firstTime = sdf.parse("2020-07-27 16:30:00");

        timer.schedule(new TimerTask() {
            public void run() {
            	System.out.println("第" + count++ + "次执行");
                for(int i = 0; i < userBox.size(); i++){
                    System.out.println("给" + userBox.get(i) + "发送了一条消息:陌生人祝你幸福");
                }
                System.out.println("做了点坏事儿 真开心~~~");
            }
        },firstTime,3000);
    }

    public static void main(String[] args) {
        TestTimer demo = new TestTimer();
        try{
            demo.test();
        }catch (ParseException e){
            e.getErrorOffset();
        }
    }
    
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值