java基础-多线程

线程安全(重点)

在实际开发中,项目运行的服务器已经将线程的定义,线程对象的创建,线程的启动等都实现了,我们并不需要编写这些代码。我们关注的重点应该是程序在多线程的环境下并发运行,程序数据是否安全。

什么时候会出现线程安全问题

线程安全问题出现的三个条件
条件1:多线程并发。
条件2:有共享数据。
条件3:共享数据有修改的行为。

怎么解决线程安全问题

线程同步,就是让有安全问题的线程排队执行。虽然解决了线程安全问题但此时线程也不能并发了。这种解决线程安全问题的方式叫做“线程同步机制”,该机制让多线程排队执行,自然也就牺牲了一部分的执行效率。没办法,数据安全是第一,只有数据安全了才会去考虑效率的问题。编程模型:异步和同步,异步就是并发,同步就是排队。

模拟两个用户对同一银行卡取款操作

程序结构Account(银行账户实体类)、AccountThread(进行取款的线程)、AccountTest(最终测试的main方法)
Account(银行账户实体类)

//银行账户实体类
public class Account {
    //银行账户
    private String accountNum;
    //账户余额
    private double balance;

    public String getAccountNum() {
        return accountNum;
    }

    public void setAccountNum(String accountNum) {
        this.accountNum = accountNum;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    public Account(String accountNum, double balance) {
        this.accountNum = accountNum;
        this.balance = balance;
    }

    //取钱的方法
    public void withDraw(double money) throws InterruptedException {
        //取钱之前卡里的余额
        double before = this.getBalance();
        //取钱之后余额
        double after = before - money;
        //模拟网络故障,让当前线程休眠1秒
        Thread.sleep(1000);

        /**
         * 多线程开启之后,由于小明和小红同时使用取款的线程AccountThread调用
         * 这里的withDraw(double money)取钱的方法,由于方法存在模拟故障网络延迟,
         * 当一个小明的取款线程遇到此故障,还没来得及修改余额setBalance(after)时,
         * 另一个小红的取款线程又来调用了withDraw(double money)取钱的方法,
         * 此时小红读取到了小明还没来的及修改的余额数据
         * 所以此时就发生了线程安全问题
         * 只有一万块钱的银行卡,两人都取了5千后,卡里还剩5千余额。
         */
        this.setBalance(after);
    }
}

AccountThread(进行取款的线程)

public class AccountThread extends Thread {
    //两个线程共享同一个账户对象
    private Account account;

    //通过构造方法传递账户对象
    public AccountThread(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        //模拟要取钱的数额
        double money = 5000;
        /**
         * 由于withDraw()在方法声明时throws的异常,所以这里调用时必须进行捕获
         * 但由于此处的run()方法是重写的父类Thread的run()方法,而Thread没有抛出任何异常
         * 所以此时AccountThread类的run()里的异常,只能try catch
         */
        try {
            account.withDraw(money);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()
                +"对"+account.getAccountNum()+",取款"+money+"成功,余额"+account.getBalance());

    }
}

AccountTest(最终测试的main方法)

public class AccountTest {
    public static void main(String[] args) {
        //创建小明小红共享的一张银行卡账户
        Account act = new Account("农行001银行卡",10000);
        //创建小明小红各自的取款线程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);

        t1.setName("小明");
        t2.setName("小红");

        //开启小明小红各自的取款线程,此时就是多线程并发了
        t1.start();
        t2.start();
    }
}

执行结果
在这里插入图片描述

多线程概念

现实生活中有很多多线程的例子,比如人可以一边吃饭一边看电视,且吃饭看电视的同时还伴随着呼吸和大脑对电视剧情的思考。这种多个动作步骤同一时间一起执行的机制就称之为多线程。Java将这种思想称之为并发,且将并发完成的每一件事情称为线程

进程和线程的关系

进程就是一个正在进行的程序或者说一个正在执行的任务,而线程就是这个正在执行任务中的一个具体的执行步骤。就好比高铁站就是一个进程,而高铁站里的卖票窗口就是线程,卖票窗口不止一个,那么多个卖票窗口就是多线程。所以一个进程里可以启动多个线程,多个卖票窗口提高了高铁站的卖票速率,提高了高铁站的办事效率。

多个进程之间相互独立不共享内存资源。同一个进程中,多个线程共享方法区和堆,但多个线程之间栈内存相互独立不共享,也就是一个线程一个栈。
堆和方法共享栈独立

单核CPU可以真正的多线程并发吗?

什么是真正的多线程并发?
t1线程执行t1的程序,t2线程执行t2的,t1不影响t2,t2也不影响t1,两个线程之间不相互干扰这才叫做真正的多线程并发。4核CPU的电脑表示同一个时间点上,可以真正的有4个进程并发执行。

单核的CPU表示只有一个大脑:不能真正做到多线程并发,但可以通过分配时间片来给人一种多线程并发的错觉。因为CPU的运行速率太快了,人类的大脑和眼睛根本跟不上CPU的运行速率。所以CPU高速频繁切换多个线程,给人造成了多个线程并发的感觉。

一个栈中的多个方法是多个线程吗

思考下列程序有几个线程

public class ThreadTest01 {
    public static void main(String[] args) {
        System.out.println("main start");
        m1();
        System.out.println("main over");
    }
    public static void m1(){
        System.out.println("m1 start");
        m2();
        System.out.println("m2 over");
    }
    public static void m2(){
        System.out.println("m2 start");
        m3();
        System.out.println("m2 over");
    }
    public static void m3(){
        System.out.println("m3 execute");
    }
}

以上程序只有一个线程,因为程序只有一个栈,m1/m2/m3都在同一个栈里。且此程序是一个栈中,自上而下的顺序依次逐行执行!
执行结果为
在这里插入图片描述

实现线程的三种方式

1.继承Thread类

Java将多线程已经实现了,我们只需要继承就行,继承Thread类,重写run方法。

public class ThreadTest02 {
    /**
     * main方法中的代码属于主线程,在主栈中运行。
     * @param args
     */
    public static void main(String[] args) {
        //新建的myThread对象是一个分支线程对象
        MyThread myThread = new MyThread();
//        myThread.run();

        myThread.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main主线程=>"+i);
        }
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("分支线程=>"+i);
        }
    }
}

解析start()和run()的区别
start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,只要新的栈空间开辟出来,start()方法就结束了,此时线程就启动成功了。启动成功的线程会自动调用run()方法。所以以上程序中只有MyThread 调用了start(),MyThread 线程才会和main主线程并发。执行效果如下
在这里插入图片描述

run()方法不会启动线程,不会分配新的分支栈,调用它只是一种方法的普通调用,这种方式就是单线程。由于在方法体中的代码永远都是自上而下的顺序依次逐行执行的(这是JAVA亘古不变的道理),所以以上代码中,如果只调用run(),注释掉start(),则程序走到run()调用的那一行时,只会将MyThread的1000次循环全部执行完了,才会去执行main()方法里的1000次循环,所以就是单线程没有并发的效果。执行结果如下图
在这里插入图片描述
start()方法内存图
在这里插入图片描述

2.实现Runnable接口

实现Runnable接口的run()方法(如下代码)。相对于继承Thread类去实现多线程,实现Runnable接口的方法扩展性更强更实用更灵活,因为Java只能单一继承却可以多重实现,所以继承了Thread类之后就没有办法去继承别的类了,而实现了Runnable接口后还可以去继承别的类。

public class ThreadTest03 {
    public static void main(String[] args) {
        // 创建一个可运行的对象
        MyRunnable r = new MyRunnable();
        // 将可运行的对象封装成一个线程对象
        Thread t = new Thread(r);
        //合并以上两行代码的做法
        Thread t2 = new Thread(new MyRunnable());
        //启动线程
        t2.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("main主线程++>"+i);
        }
    }
}

// 这并不是一个线程类。是一个可运行的类,他还不是一个线程。
class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 1000 ; i++) {
            System.out.println("分支线程++>"+i);
        }
    }
}

执行结果(部分截图)
在这里插入图片描述

3.匿名内部类

public class ThreadTest04 {
    public static void main(String[] args) {
         Thread t= new Thread(new Runnable() {
             @Override
             public void run() {
                 for (int i = 0; i < 1000 ; i++) {
                     System.out.println("内部类分支线程"+i);
                 }
             }
         });
         t.start();

        for (int i = 0; i < 1000 ; i++) {
            System.out.println("main主线程"+i);
        }
    }
}

执行结果
在这里插入图片描述

线程的生命周期

在这里插入图片描述

获取当前线程对象

Thread m = Thread.currentThread();获取当前线程对象

public class ThreadTest05 {
    public static void main(String[] args) {
        MyThread02 m = new MyThread02();
        m.start();
        //Thread.currentThread();获取当前线程
        //setName()设置线程名
        //getName()获取线程名
        //m.setName("MyThread02");
        Thread t = Thread.currentThread();
        t.setName("I'm main");
        for (int i = 0; i < 100 ; i++) {
            System.out.println(t.getName()+i);
        }
    }
}

class MyThread02 extends Thread{
    @Override
    public void run() {
        Thread t = new Thread();
        for (int i = 0; i < 100; i++) {
            //系统默认的线程名为:Thread-X,(X代表阿拉伯数字)
            System.out.println(t.getName()+"+线程+"+i);
        }
    }
}

执行结果为
在这里插入图片描述

sleep()方法

static void sleep(long millis)
1.静态方法:Thread.sleep(1000);
2.参数是毫秒
3.作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其他线程使用。如这行代码(Thread.sleep(1000);)出现在A 线程中,A线程就会进入休眠。这行代码出现在B线程中,B线程就会进入休眠。
4.Thread.sleep()方法,实现的效果是:间隔指定的时间,去执行一段特定的代码,每隔多久执行一次。

public class ThreadTest06 {
    public static void main(String[] args) {
        for (int i = 0; i < 100 ; i++) {
            System.out.println(Thread.currentThread().getName()+i);
            try {
                //每隔5秒输出一个值
                Thread.sleep(1000*5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

思考以下程序,sleep会让哪个线程进入休眠状态,是T线程吗?

public class ThreadTest06 {
    public static void main(String[] args) {
        MyThread03 t = new MyThread03();
        t.start();
        t.setName("T");
        try {
            //sleep会让T线程进入休眠状态吗?
            t.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("hello world");
    }
}

class MyThread03 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+i);
        }
    }
}

sleep()是静态方法,所以在执行t.sleep()时仍然会转换成Thread.sleep();
t.sleep()的作用是让当前线程进入休眠,所以此时这里main线程会进入休眠状态。
在这里插入图片描述

终止线程的方法

public class ThreadTest08 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable2());
        t.setName("T");
        t.start();
        try {
            t.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //5秒钟后唤醒休眠的线程(这种方式依靠了java的异常处理机制)
        t.interrupt();//程序执行到这里时,会让Thread.sleep()抛异常
    }
}

class MyRunnable2 implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"===>start");
        try {
            //这里sleep为什么只能try catch而不能throws?
            //休眠1年
            Thread.sleep(1000*60*60*24*356);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"===>end");
    }
}

interrupt()方法唤醒正在休眠的线程,这种方式依靠了java的异常处理机制,让调用sleep()的地方抛异常。

注意:run()当中的异常不能throws,只能try catch。因为run()方法在父类中没有抛出任何异常,子类重写父类的方法时子类不能抛出比父类更多的异常
执行结果(抛出InterruptedException)
在这里插入图片描述
stop()方法终止线程的方式已经过时,因为其存在很大的缺点。stop()是直接杀死线程,强行终止线程,线程没有保存的数据将会丢失,所以不建议使用。

合理终止线程的方法

打boolean标记。修改flag标记,通过判断真假来做线程的终止,为true就运行,为false就终止。所以在以下程序中,初始化flag为true,当调用start()开启线程之后,模拟5秒钟后修改flag为false,因为分支线程每隔一秒打印一次i值,所以在5秒钟后flag被修改为flase,此时因为if else判断分支线程就终止了打印i=5的操作。然而在else语句块里,return后就意味着当前线程的终止。那么在return之前,如果有需要保存数据的操作,就可在return之前做保存数据的逻辑代码。如此便是一个合理终止线程的实现。

public class ThreadTest09 {
    public static void main(String[] args) {
        MyRunnable4 m = new MyRunnable4();
        Thread t = new Thread(m);
        t.start();
        try {
            t.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m.flag = false;
    }
}

class MyRunnable4 implements Runnable{

    Boolean flag = true;

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if(flag){
                System.out.println(Thread.currentThread().getName()+"->"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else{
                //save(保存数据的逻辑)
                //return意味着当前线程就终止了,所以在此之前可以做保存数据的逻辑
                return;
            }
        }
    }
}

执行结果
在这里插入图片描述

常见的线程调度模型

抢占式调度模型
哪个线程的优先级较高,抢到的cpu时间片的概率就高一些多一些。java采用的就是这种模型。
均分式调度模型
平均分配cpu时间片,每个线程占有的cpu时间片时间长度一样。平均分配,一切平等。别的语言采用的模型。
java中与线程调度有关的方法
实例方法:
void setPriority(int newPriority)设置线程的优先级
int getPriority() 获取线程优先级,最低优先级是1,默认是5,最高是10,优先级高的获取CPU时间片可能会多一些(大概率,也不完全是)。
静态方法:static void yield()让位方法,暂停当前正在执行的线程对象,并执行其他线程。它不是阻塞方法。该方法的执行会让当前线程从“运行状态”回到“就绪状态”。注意,回到就绪之后,有可能还会再次抢到执行权。

线程优先级、让位、合并

设置优先级setPriority()
优先级高的线程,只是抢到cpu时间片可能相对来说会多一些,也存在的大概率事件。
Thread.MAX_PRIORITY(最大10)、Thread.MIN_PRIORITY(最小1)、Thread.NORM_PRIORITY(默认5)
让位yield():
大概率时间,大概率会让位,也有不让位的情况

public class ThreadTest10 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable7());
        t.setName("t");
        t.start();
        for (int i = 1; i <= 10000 ; i++) {
            System.out.println(Thread.currentThread().getName()+"-->"+i);
        }
    }
}

class MyRunnable7 implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i <= 10000; i++) {
            if(i % 100 ==0){
                Thread.yield(); //当前线程暂停一下,让给主线程。
            }
            System.out.println(Thread.currentThread().getName()+"-->"+i);
        }
    }
}

执行结果:t线程大部分到了100的倍数都后都让了main线程,小部分没有让。
在这里插入图片描述
合并join():
当前线程停止,join进来的线程执行直到结束,再执行当前线程。

public class ThreadTest11 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable8());
        t.setName("t");
        t.start();
        //合并线程
        try {
            //t合并当前线程中,当期线程受阻,t线程执行直到结束。
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main over");
    }
}

class MyRunnable8 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100 ; i++) {
            System.out.println(Thread.currentThread().getName()+"-->"+i);
        }
    }
}

执行结果:当没有调用join()时,main over总是最先输出,当调用了join()后,main over总是在t线程循环结束后才输出。
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值