随便谈谈多线程

多线程基础

文章目录

  • 多线程基础
  • 前言
  • 线程的生命周期
    • 线程各个时期的标志行为
  • 多线程的实现
    • 继承Thread类来多线程
    • 实现Runnable接口
    • 实现Callable接口
  • 线程方法
    • 观测线程状态
    • 线程休眠(重点)
    • 线程优先级
    • 线程强制执行
    • 守护线程
  • 进阶篇
  • 线程同步
    • Synchronized锁
    • ReentrantLock锁


前言

线程是进程细分下的更小的一个单位,当今社会的计算机普遍以多核为主,而多核基础上又允许我们进行更为复杂但效率更高的操作:多线程,本篇博客作为个人学习笔记简要巩固多线程学习,作为并发编程的敲门砖,愿与读者共勉且欢迎指正。


提示:以下是本篇文章正文内容,下面案例可供参考

线程的生命周期

  • 创建状态:NEW
  • 就绪状态:RUNNABLE
  • 运行状态:RUNNING
  • 阻塞状态:BLOCKED
  • 消亡状态:DEAD

标准的线程的生命周期一般分为五个阶段,但是当一个线程创建后不是立刻进入运行状态,也不会一直处于执行状态。CPU需要在多条线程间切换,线程状态也会在执行、就绪、阻塞状态中相互切换(设计操作系统,与进程状态相似)

线程各个时期的标志行为

创建:new Thread();
就绪:.start();
执行:调度后进入执行状态
阻塞:调用sleep()和wait()方法进入阻塞状态
消亡:不建议调用stop()或destroy()方法销毁线程,而是使用标志位进行终止变量销毁

多线程的实现

继承Thread类来多线程

步骤:

  1. 继承Thread类,重写run方法
  2. 创建继承类的对象,通过对象调用start方法开启线程

示例代码:

public class thread2 extends Thread{
    @Override
    public void run(){
        //重写run方法
        for (int i = 0; i < 20; i++) {
            System.out.println("这里是子线程:"+i);
        }
    }

    public static void main(String[] args) {
        thread2 t2=new thread2();
//        t2.run();       //顺序执行了

        t2.start();     //并没有顺序执行,而是两线程交叉执行了
        for (int i = 0; i < 200; i++) {
            System.out.println("这里是主线程:"+i);
        }
    }
}

实现Runnable接口

步骤:

  1. 实现Runnable接口,重写run方法
  2. 创建Runnable接口的实现对象
  3. 创建线程对象,通过传参方式将对象注入,调用start方法

示例代码:

public class thread3 implements Runnable{

    @Override
    public void run() {
        //重写run方法
        for (int i = 0; i < 20; i++) {
            System.out.println("这里是子线程:"+i);
        }
    }

    public static void main(String[] args) {
        //1.创建Runnable的实现对象
        thread3 t3=new thread3();

        //使用thread传入对象并启动
        new Thread(t3).start();
        for (int i = 0; i < 200; i++) {
            System.out.println("这里是主线程:"+i);
        }
    }
}

引入Runnable实现线程的主要是因为解决Java无法实现多继承的局限性

实现Callable接口

步骤:

  1. 实现Callable接口,返回所需要返回的类型
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
    ----------------------到这里和实现Runnable几乎一致--------------------------
  4. 创建执行服务
  5. 提交执行
  6. 获取结果
  7. 关闭服务

示例代码:

public class threadCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //3.创建目标对象
        call c=new call();

        //4.创建执行任务
        ExecutorService service= Executors.newFixedThreadPool(10);      //定义线程池的线程数,并创建这个线程池

        //5.提交执行
        Future<String> result=service.submit(c);

        //6.获取结果
        String res=result.get();
        System.out.println(res);
        //7.关闭服务
        service.shutdown();
    }
}

class call implements Callable{

    //1.实现Runnable接口,返回所需要的类型
    //2.重写call方法
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 5; i++) {
            System.out.println("当前线程:"+Thread.currentThread().getName());
        }
        return "Run succeed!";
    }
}

如果你是初学者,那并不需要太过于在意于Callable接口实现多线程,引入Callable接口是因为有(1.可以定义返回值;2.可以抛出异常)的好处,而这往往需要在我们开发时或者进阶阶段才需要深入学习

线程方法

下面收集了我们在学习、开发中常用的线程方法,每一个线程方法在开发及实战中都有其独特的方法,掌握这些方法是极其重要的。
随便谈谈多线程_第1张图片

观测线程状态

前文我们提到过线程的生命周期,生命周期的各个阶段都有其状态,下面我们来讨论研究如何观测线程状态

  • NEW: 创建对象时
  • RUNNABLE: 调用start()方法后
  • BLOCKED: 阻塞状态
  • WAITING: 等待状态
  • TIMED_WAITING: 终止线程的线程状态,线程已完成执行
  • TERMINATED: 消亡状态

我们常常使用Thread.getState()方法来获取线程所处的状态

示例代码:

public class threadState {
    public static void main(String[] args) {
        Thread thread=new Thread(() ->{
            for (int i = 0; i <5 ; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        System.out.println("Hello");

        //定义方法观测线程状态
        Thread.State state = thread.getState();     //NEW,刚创建出来
        System.out.println(state);

        //观测创建后的状态
        thread.start();
        state=thread.getState();
        System.out.println(state);

        //查看是否阻塞,空转模拟
        while (thread.getState() != Thread.State.TERMINATED){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(thread.getState());
        }
    }
}

线程休眠(重点)

线程休眠(Sleep)是线程中极其重要的操作,它可以实现很多功能或者是有着非常强大的作用,关于sleep,有几个点需要你特别注意:

  1. sleep():方法是毫秒级别的
  2. 存在异常:InterruptedException
  3. 时间达到后线程进入就绪状态
  4. 每一个对象都有一把锁,sleep不会释放锁
  5. 可以模拟网络延迟,倒计时等

示例代码:(模拟倒计时)

public class threadSleep2{
    public static void main(String[] args){
        //线程同步进行模拟
        countDown c=new countDown();
        new Thread(c).start();

        //打印当前时间:
        Date startTime=new Date(System.currentTimeMillis());

        while (true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
            //更新当前时间
            startTime=new Date(System.currentTimeMillis());
        }
    }
}

class countDown implements Runnable {

    @Override
    public void run() {
        int num = 10;
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("倒计时:" + num--);
            if (num <= 0)
                break;
        }
    }
}

这里我曾经存在疑点:为什么将线程子线程实现放在打印时间之后会报错:删除不可达语句,放在打印时间之前就可以实现

解释:因为无论前面程序如何运行,你这一步都无法运行,sleep不释放锁,后面的线程或语句永远无法执行
常见的情况有:
1.return 后的语句
2.throws后的语句
3. break,continue后的语句等

线程优先级

线程和进程一样,也有其优先级,设定线程优先级也是为了让任务程度紧急的线程优先进行处理,但需要注意的是,优先级高并不意味着一定先调度,只是调度的可能性更大

注意:

  • 线程优先级越高,值越大(值范围在1到10间)
  • 常见的线程优先级调用:
    1.Thread.MAX_PRIORITY=10
    2.Thread.MIN_PRIORITY=1
    3.Thread.NORM_PRIORITY=5
    -使用getPriority()获取优先级,使用setPriority(int n)改变优先级

线程强制执行

除了定义线程优先级来进行某些任务的优先调度外,我们还有一种“强硬”的手段来执行线程:join()方法

何为线程的强制执行?

解释:合并线程,待此线程执行完后才能执行其它线程,其他线程阻塞(可以想象成插队

示例代码:

public class threadJoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <1000 ; i++) {
            System.out.println("线程vip来了"+i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        threadJoin tj=new threadJoin();

        Thread thread=new Thread(tj);
        thread.start();

        //主线程
        for (int i = 0; i < 500; i++) {
            if(i==200){
                thread.join();      //插队
            }
            System.out.println("main:"+i);
        }
    }
}

结果分析:在主线程执行到200次之前,主线程和子线程thread交互执行,当i值为200后,因为thread.join()方法强制执行此方法,主线程阻塞,直到thread执行完毕才能继续调度主线程

守护线程

定义:
守护线程也称服务线程,线程分为用户线程和守护线程,守护线程就是为用户线程提供公共服务,在没有用户线程后服务可以自动离开

如何设置守护线程:
通过setDaemon(true) 来设置守护线程:

实例:
Java垃圾回收机制GC就是一个守护线程,当我们的程序中没有任何运行的用户线程时,垃圾回收机制就会自动离开。

注意:

  1. 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务
  2. 在 Daemon 线程中产生的新线程也是 Daemon 的
  3. 线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃的
  4. 虚拟机必须确保用户线程执行完毕,虚拟机不必等待守护线程执行完毕

进阶篇

线程同步

  • 什么是线程同步?
  • 为什么要引入线程同步这个概念?
  • 如何应用线程同步?

或许你会有这些疑问,Normally,我们不妨一个个问题进行解决:

定义:简单理解:线程同步就是实现一种线程的等待机制。

当多个线程对同一数据进行操作时往往会出现不安全的情况,后面会举例说明,而线程同步机制就是让这多个访问相同对象的线程进入对象的线程池形成队列,待前一个线程使用完毕后面的线程方可以使用

听起来似乎很拗口,没关系,我们举两个例子你或许很快就会明白

案例1:
春节抢票时,往往成千上万的用户在同一时刻对仅仅几百几千张票同时进行操作,我们使用代码模拟一下情景并查看抢票是否正常

public class ticketThread implements Runnable{
    private int ticketNums=20;
    @Override
    public void run() {
        while (true){
            if (ticketNums<=0)
                break;
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"--->拿到了第"+ticketNums--+"张票");
        }
    }

    public static void main(String[] args) {
        System.out.println("模拟多线程抢票!");
        ticketThread t1=new ticketThread();

        //多线程操作同一资源下,线程不安全了,数据紊乱,出现数字不是单调减少,或者出现负数和0!
        new Thread(t1,"小明").start();
        new Thread(t1,"小红").start();
        new Thread(t1,"小绿").start();
        new Thread(t1,"小黄").start();
        new Thread(t1,"黄牛").start();
    }
}

结果:
随便谈谈多线程_第2张图片
好家伙,出现抢票的票序不正确,抢到第0张票甚至是负数张票,但依旧显示抢票成功,你想想如果当你拿着厚重的行李进入车站,却告诉你票无效该是多么绝望…

可为什么会出现这种情况呢?
原来我们这里模拟的线程是抢占式调度,当多个线程在看到同一张票时,都会进行判断这张票是否有效能抢,诶,然后就几个线程蜂拥而上,场面混乱不堪,最终酿成你在火车站过夜的悲剧…

案例2:
你是否有过这样的想法,你和你媳妇去银行取钱,一个人人工操作账号,另一个人使用自动取款机取款,然后来个同时取钱(银行卡有5000元,你取5000的同时你媳妇同时也取5000元,诶血赚!),是否能编写程序模拟这个过程

public class unsafeBank {
    public static void main(String[] args) {
        Account account=new Account();
        account.setMoney(5000);
        account.setName("我的账户");

        Bank me=new Bank(account,5000,0);
        me.setName("我");
        Bank girls=new Bank(account,5000,0);
        girls.setName("女朋友");

        me.start();
        girls.start();
    }
}

class Account{
    //余额
    int money;
    String Name;

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public String getName() {
        return Name;
    }

    public void setName(String name) {
        Name = name;
    }
}

class Bank extends Thread{
    Account account;    //账户
    //取了多少钱
    int drawing;
    //手上的钱
    int leftMoney;

    public Bank(Account account,int drawing,int leftMoney){
        this.account=account;
        this.drawing=drawing;
        this.leftMoney=leftMoney;
    }

    //取钱操作

    //取钱是银行,修改的是账户
    @Override
    public synchronized void run() {        //加上synchronized后仍然会出现取负的情况,原因是synchronized默认的是this.

            if(account.money<drawing){
                System.out.println(Thread.currentThread().getName()+"取钱失败,余额不足");
                return;
            }

            //模拟延迟
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //账户余额
            account.money-=drawing;
            //手上的钱
            leftMoney+=drawing;

            System.out.println(account.Name+"余额为:"+account.money);
            //Thread.currentThread().getName()=this.getName();  【extend】
            System.out.println(this.getName()+"手里的钱有:"+leftMoney);
    }
}

结果:
随便谈谈多线程_第3张图片
其实抢银行很简单,只需要把我聘过去当程序员…

上面两个案例依旧足够说明为什么要引入线程同步,因为安全!那么我们怎么实现线程同步机制呢,本文浅要谈谈解决方案:

Synchronized锁

上述案例我们或多或少都理解到了,其实就是线程间的“哄乱”导致的不安全,及抢占式调度埋下的隐患,因此,我们需要引入一些限定来制定相应规则,不妨试试Synchronized排它锁吧!

定义:
synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁

★作用范围:

  1. 作用于方法时,锁住的是对象的实例(this);
  2. 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
  3. synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

我的想法:
其实锁的作用很明显,就是只允许同一时间只允许一个线程作用于对象或数据,如公共厕所排队,光排队可不行,万一后面有人直接冲进去咋办,这时加上一把锁很有必要,等里面的人解决完毕后,后面的人才可以依次排队进去

案例解决抢票问题和取钱问题:
取钱问题(修改后代码如下):

public class unsafeBank {
    public static void main(String[] args) {
        Account account=new Account();
        account.setMoney(5000);
        account.setName("我的账户");

        Bank me=new Bank(account,5000,0);
        me.setName("我");
        Bank girls=new Bank(account,5000,0);
        girls.setName("女朋友");

        me.start();
        girls.start();
    }
}

class Account{
    //余额
    int money;
    String Name;

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public String getName() {
        return Name;
    }

    public void setName(String name) {
        Name = name;
    }
}

class Bank extends Thread{
    Account account;    //账户
    //取了多少钱
    int drawing;
    //手上的钱
    int leftMoney;

    public Bank(Account account,int drawing,int leftMoney){
        this.account=account;
        this.drawing=drawing;
        this.leftMoney=leftMoney;
    }

    //取钱操作

    //取钱是银行,修改的是账户
    @Override
    public synchronized void run() {        //加上synchronized后仍然会出现取负的情况,原因是synchronized默认的是this.

        //使用synchronized块
        synchronized (account){
            if(account.money<drawing){
                System.out.println(Thread.currentThread().getName()+"取钱失败,余额不足");
                return;
            }

            //模拟延迟
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //账户余额
            account.money-=drawing;
            //手上的钱
            leftMoney+=drawing;

            System.out.println(account.Name+"余额为:"+account.money);
            //Thread.currentThread().getName()=this.getName();  【extend】
            System.out.println(this.getName()+"手里的钱有:"+leftMoney);
        }
    }
}

运行结果:
随便谈谈多线程_第4张图片

案例:抢票问题(修改代码如下):

public class unSafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket buy=new BuyTicket();

        new Thread(buy,"小明").start();
        new Thread(buy,"小红").start();
        new Thread(buy,"小黄").start();
    }
}

class BuyTicket implements Runnable{

    int ticketNums=10;
    boolean flag=true;
    @Override
    public void run() {
        //买票
        while (flag){
            try {
                Thread.sleep(20);
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void buy(){     //加上synchronized后锁的是this,同步方法
        if(ticketNums<=0) {
            flag=false;
            return;
        }
        System.out.println(Thread.currentThread().getName()+"买到了第"+ticketNums--+"张票");
    }
}

运行结果可以自己测试

注意事项:

使用synchronized关键字控制对象访问(通过锁)
1.声明后会极大地影响效率,因此方法里需要修改的内容才加锁,锁的太多会浪费资源(synchronized块)
2.加上synchronized后锁的默认是this.
3.同步块:
synchronized(Obj){
}
Obj:同步监视器- - - -你要锁的对象

ReentrantLock锁

ReentrantLock锁继承了Lock接口并实现类接口中定义的方法,它是可重入锁,除了完成Synchronized锁所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程锁死锁的方法

可重入锁:可重入就是说某个线程已经获得某个锁,可以再次获取锁(同一把锁)而不会出现死锁

案例与发现:

仍然模拟抢票,使用lock()锁来对线程显式加锁和释放锁

public class ThreadLock {
    public static void main(String[] args) {
        Lock lock=new Lock();

        new Thread(lock,"1").start();
        new Thread(lock,"2").start();
        new Thread(lock,"3").start();
    }
}

class Lock implements Runnable{

    int ticket=10;

    //定义个lock锁
    private final ReentrantLock reentrantLock=new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try {
                //加锁
                reentrantLock.lock();
                if(ticket<=0)
                    break;
                System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //释放锁
                reentrantLock.unlock();
            }
        }
    }
}

这里结果出乎我的意料:
随便谈谈多线程_第5张图片
1号把所有的票抢光了!带着疑惑我继续进行模拟,于是出现2号全部抢光的尴尬场面,为什么会这样呢?

带着疑惑我猛然想起,关于sleep()的注意事项:
有点懵
简而言之则是:
还行
重构代码后:

public class ThreadLock {
    public static void main(String[] args) {
        Lock lock=new Lock();

        new Thread(lock,"1").start();
        new Thread(lock,"2").start();
        new Thread(lock,"3").start();
    }
}

class Lock implements Runnable{

    int ticket=10;

    //定义个lock锁
    private final ReentrantLock reentrantLock=new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(1000);
                //加锁
                reentrantLock.lock();
                if(ticket<=0)
                    break;
                System.out.println(Thread.currentThread().getName()+"拿到了第"+ticket--+"张票");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                //释放锁
                reentrantLock.unlock();
            }
        }
    }
}

然后发现这个锁不会被一个对象单独占有了,其它线程也可以进行调度了,interesting!

对于ReentrantLock,暂时未能花费过多时间进行研究,进行简单总结:

1.Lock锁是显式锁,手动开启和关闭,别忘记关闭,synchronized是隐式锁,出了作用域自动释放

2.Lock只有代码块锁,而synchronized有代码块锁和方法锁

3.使用Lock锁,JVM将花费较少的时间调度线程,性能更好,并且有更好的拓展性

4.优先调用顺序:
Lock > 同步代码块(已经进入方法体,分配了相应的资源) > 同步方法(在方法体之外)

对于本博客有任何疑问或错漏之处,欢迎读者指正!

你可能感兴趣的:(学习笔记,java,多线程)