学习笔记:多线程

学习内容:多线程

1. 线程的概述

1、什么是进程?什么是线程?
进程是一个应用程序。线程是一个进程中的执行场景/执行单元。
一个进程可以启动多个线程。

2、进程和线程是什么关系?
进程可以看做是现实生活当中的公司。
线程可以看做是公司当中的某个员工。
注意:
进程A和进程B的内存独立不共享。
线程A和线程B,堆内存和方法区内存共享。
但是栈内存独立,一个线程一个栈。

在这里插入图片描述

java中之所有多线程机制,目的就是为了提高程序的处理效率。

2. 多线程

1、什么是多线程并发?
t1线程执行t1的,t2线程执行t2的。t1和t2不会相互影响。

单核的CPU表示只有一个“大脑”,但是可以做到给人一种“多线程并发的感觉”。对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,给人的感觉是:多个事情同时在做。

2、java语言中,实现线程有三种方式。
(java支持多线程机制,并且java已经将多线程实现了,我们只需要继承就行了)
第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法。

/*
实现线程的第一种方式:
编写一个类,直接继承`java.lang.Thread`,重写run方法。

怎么创建线程对象? 直接new就行
怎么启动线程?调用线程对象的start()方法
 */
public class Test01 {
    public static void main(String[] args) {
        //这里是main方法,这里的代码属于主线程,在主栈中运行
        //新建一个分支线程对象
        MyThread myThread=new MyThread();
        //启动线程
        //myThread.run();//不会启动线程,不会分配新的分支栈
        //start()方法的作用:启动一个分支线程,在JVM中开辟一个新的栈空间(这段代码瞬间就结束了)
        //启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部
        myThread.start();
        //这里的代码还是运行在主线程中
        for (int i=0;i<1000;i++){
            System.out.println("主线程:--->"+i);
        }
    }
}

//定义线程类
class MyThread extends Thread{
    @Override
    public void run() {
        //编写程序,这段程序运行在分支线程中(分支栈)
        for (int i=0;i<1000;i++){
            System.out.println("分支线程:--->"+i);
        }
    }
}

第二种方式:编写一个类,实现java.lang.Runnable接口,实现run方法。

/*
实现线程的第二种方式:
    编写一个类,实现`java.lang.Runnable`接口
 */
public class Test02 {
    public static void main(String[] args) {
        //创建一个可运行的对象
        //MyRunnable r=new MyRunnable();
        //将可运行的对象封装成一个线程对象
        //Thread t=new Thread(r);
        Thread t=new Thread(new MyRunnable());//合并上面两行代码
        //启动线程
        t.start();

        for (int i=0;i<100;i++){
            System.out.println("主线程:--->"+i);
        }
    }
}

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

也可以采用匿名内部类方式:

public class Test03 {
    public static void main(String[] args) {
        Thread t=new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i=0;i<100;i++){
                    System.out.println("分支线程:--->"+i);
                }
            }
        });

        t.start();

        for (int i=0;i<100;i++){
            System.out.println("主线程:--->"+i);
        }
    }
}

注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其他的类,更灵活。

(第三种方法在后面)

3、线程的生命周期
在这里插入图片描述

3. 常用方法

3.1 void setName(String name) 修改线程对象的名字
3.2 String getName() 获取线程对象的名字
3.3 static Thread currentThread() 获取当前线程对象

public class Test01 {
    public static void main(String[] args) {

        //创建线程对象
        MyThread mt=new MyThread();

        //获取线程的名字(这里的名字是默认的)
        String mtName=mt.getName();
        System.out.println(mtName);//Thread-0

        //设置线程名字
        mt.setName("One");
        String s=mt.getName();
        System.out.println(s);//One
        //启动线程
        mt.start();

        MyThread mt2=new MyThread();
        System.out.println(mt2.getName());//Thread-1

        //获取当前线程
        //currentThread就是当前线程对象
        //这个代码出现在main方法中,使用当前线程就是主线程
        Thread currentThread=Thread.currentThread();
        System.out.println(currentThread.getName());//main
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
            for(int i=0;i<100;i++){
                //currentThread就是当前线程对象
                //当mt执行run方法,当前线程就是mt
                //当mt2执行run方法,当前线程就是mt2
                Thread currentThread=Thread.currentThread();
                System.out.println(currentThread.getName()+"--->"+i);
            }
    }
}

当线程没有设置名字的时候,默认的名字有什么规律?
Thread-0
Thread-1
Thread-2

3.4 static void sleep(long millis)
1、静态方法
2、参数是毫秒
3、作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其他线程使用

public class Test02 {
    public static void main(String[] args) {
        //让当前线程进入休眠,睡眠5秒
        //当前线程是主线程
        try {
            Thread.sleep(1000*5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Hello World");
    }
}

3.5 void interrupt() 中断线程的睡眠

public class Test02 {
    public static void main(String[] args) {
        Thread t=new Thread(new MyRunnable());
        t.setName("t");
        t.start();

        //中断t线程的睡眠(这种中断睡眠的方式依靠了java的异常处理机制)
        t.interrupt();
    }
}

class MyRunnable implements Runnable{
    //run()方法当中的异常不能throws,只能try catch
    //因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"--->begin");
        try {
            //睡眠1年
            Thread.sleep(1000*60*60*24*365);
        } catch (InterruptedException e) {
            e.printStackTrace();//打印异常信息
        }
        //1年之后才会执行这里
        System.out.println(Thread.currentThread().getName()+"--->end");
    }
}

3.6 void stop() 强行终止线程(缺点:容易丢失数据。因为这种方式是直接将线程杀死了,没有保存的数据将会丢失,不建议使用)

怎么合理的终止一个线程的执行:

public class Test03 {
    public static void main(String[] args) {
        MyRunnable2 r=new MyRunnable2();
        Thread t=new Thread(r);
        t.setName("t");
        t.start();

        //终止线程
        r.run=false;
    }
}

class MyRunnable2 implements Runnable{
    //打一个布尔标记
    boolean run=true;
    @Override
    public void run() {
        for (int i=0;i<10;i++){
            if(run){
                System.out.println(Thread.currentThread().getName()+"--->"+i);
            }else {
                //终止当前线程
                return;
            }
        }
    }
}

4. 线程安全问题

什么时候数据在多线程并发的环境下会存在安全问题?
三个条件:
条件1:多线程并发
条件2:有共享数据
条件3:共享数据有修改的行为
可以使用“线程同步机制”(线程排队执行,不能并发)去解决。但是会牺牲一部分效率。

异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不用等谁,其实就是多线程并发(效率高)

同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。效率较低。线程排队执行。

模拟两个线程对同一个账户取款:
(未使用线程同步机制)

public class Account {
    //账号
    private String actno;
    //余额
    private double balance;

    //取款的方法
    public void withdraw(double money){
        //t1和t2并发这个方法(t1和t2是两个栈。两个栈操作堆中同一个对象)
        //取款之前的余额
        double before=this.getBalance();//10000
        //取款之后的余额
        double after=before-money;
        //在这里模拟一下网络延迟,100%会出问题
        try{
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //更新余额
        //思考:t1执行到这里了,但是还没来得及执行这行代码,t2线程进来withdraw方法了,此时一定出问题。
        this.setBalance(after);
    }

    public Account() {
    }

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

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
}
public class AccountThread extends Thread{
    //两个线程必须共享同步一个账户对象
    private Account act;

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

    public void run(){
        //run方法的执行表示取款操作
        //假设取款5000
        double money=5000;
        //取款
        act.withdraw(money);
        System.out.println(Thread.currentThread().getName()+"对账户"+act.getActno()+"取款成功,余额"+act.getBalance());
    }
}
public class Test01 {
    public static void main(String[] args) {
        //创建账户对象(只创建一个)
        Account act=new Account("act-001",10000);
        //创建两个线程
        Thread t1=new AccountThread(act);
        Thread t2=new AccountThread(act);
        //设置name
        t1.setName("t1");
        t2.setName("t2");
        //启动线程取款
        t1.start();
        t2.start();
    }
}

使用线程同步机制:

 线程同步机制的语法是:
                synchronized (){
                    //线程同步代码块
                }
 这个小括号里面传入的数据必须是多线程共享的数据,才能达到多线程排队

(其他代码同上,只用修改一下代码)

public void withdraw(double money){
        //以下这几个代码必须是线程排队的,不能并发
        //一个线程把这里的代码全部执行结束之后,另一个线程才能进来
        /*
            线程同步机制的语法是:
                synchronized (){
                    //线程同步代码块
                }

             这个小括号里面传入的数据必须是多线程共享的数据,才能达到多线程排队
        */
        //这里的共享对象是账户
        //不一定是this,只要是多线程共享的对象就行
        synchronized (this){
            double before=this.getBalance();//10000
            double after=before-money;
            try{
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(after);
        }
    }

在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁)

代码执行原理:

1、假设t1和t2线程并发,开始执行以上代码的时候,肯定有一个先一个后。
2、假设t1先执行,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,找到之后,并占有了这把锁,然后执行代码块中的程序,在程序执行过程中一种都是占有这把锁的。直到同步代码块代码执行结束,这把锁才会归还。
3、假设t1已经占有了这把锁,此时t2也遇到了synchronized关键字,也会去占有“后面共享对象”的这把锁,结果锁被t1占有,t2只能在同步代码快外面等待t1的结束,知道t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后 t2占有这把锁之后,进入同步代码块执行程序。

就类似于t1和t2去卫生间洗澡,t1先进去洗,t2就只能在外面等t1洗完了再进去洗。

这样就达到了线程排队执行。
这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队执行的这些线程对象所共享的。

哪些变量有线程安全问题?
实例变量:在堆中
静态变量:在方法区中
局部变量:在栈中
以上三大变量中:局部变量永远都不会存在线程安全问题。因为局部变量不共享(一个线程一个栈),局部变量在栈中。
实例变量在堆中,堆只有一个
静态变量在方法区中,方法区只有一个
堆和方法去都是多线程共享的,使用可能存在线程安全问题。

synchronized有三种写法:
第一种:同步代码块(灵活)

synchronized(线程共享对象){
	同步代码块;
}

第二种:在实例方法上面使用synchronized表示共享对象一定是this,并且同步代码块是整个方法体

第三种:在静态方法上使用synchronized,表示找类锁,类锁永远只有一把。

5. 死锁概述

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

举个例子:
甲拿了A号密室的钥匙进了A号密室,乙拿了B号密室的钥匙进了B号密室。
甲在A号密室里面得打开一个箱子,但是打开箱子需要B号密室的钥匙(在乙身上)。
乙在B号密室里面得打开一个箱子,但是打开箱子需要A号密室的钥匙(在甲身上)。
他们只有打开箱子之后才能拿到出来的信息,但是打开箱子所需要的钥匙都在对方身上,于是就是都只能在密室里面等着,也出不来。

在这里插入图片描述

6. 守护线程概述

java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)
守护线程的特点:
一般守护线程是一个死循环,所以的用户线程只要结束,守护线程自动化结束。
(注意:主线程main方法是一个用户线程)

public class Test02 {
    public static void main(String[] args) {
        Thread t=new BakDataThread();
        t.setName("备份数据的线程");
        //启动线程之前,将线程设置为守护线程
        t.setDaemon(true);

        t.start();

        //主线程:主线程是用户线程
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+"--->"+i);
            try{
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class BakDataThread extends Thread{
    public void run(){
        int i=0;
        //即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止
        while(true){
            System.out.println(Thread.currentThread().getName()+"--->"+(++i));
        try{
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        }
    }
}

7. 定时器概述

定时器作用:间隔特定的时间,执行特定的程序。
java中其实可以采用多种方式实现:
可以使用sleep方法,睡眠,设置睡眠时间,等到整个时间点醒来,执行任务。这种方式是最原始的定时器(比较low)

在java的类库种已经写好了一个定时器,java.util.Timer。可以直接拿来用。

目前使用较多的是Spring框架种提供的SpringTask框架,整个框架只要进行简单的配置,就可以完成定时器的功能

public class Test03 {
    public static void main(String[] args) throws Exception {
        //创建定时器对象
        Timer timer=new Timer();
        //Timer timer=new Timer(true);//守护线程的方式

        //指定定时任务
        //timer.schedule(定时任务,第一次执行时间,间隔多久执行一次);
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date firstTime=sdf.parse("2021-12-09 16:20:00");//这里异常因为繁琐就直接抛了,
        timer.schedule(null,firstTime,1000*10);
    }
}

//编写一个定时任务类
//假设这是一个记录日志是定时任务
class LogTimerTask extends TimerTask {
    @Override
    public void run() {
        //编写你需要执行的任务就行了。
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String strTime=sdf.format(new Date());
        System.out.println(strTime+":成功完成了 一次数据备份");
    }
}

8. 实现线程的第三种方式:实现Callable接口(jdk8新特性)

这种方式实现的线程可以获取线程的返回值。
上面两种方式是无法获取线程返回值的,因为run方法返回void。
例子:
系统委派一个线程去执行一个任务,该线程执行完任务之后,可能会有一个执行结果,我们怎么能拿到整个执行结果呢?

public class Test04 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //第一步:创建一个“未来任务类对象”
        //参数非常重要,需要给一个Callable接口的实现类对象
        FutureTask task=new FutureTask(new Callable() {
            @Override
            //call()方法就相当于run方法。只不过整个有返回值
            public Object call() throws Exception {
                //线程执行一个任务,执行之后可能会有一个执行结果
                //模拟执行
                int a=100;
                int b=200;
                return a+b;//自动装箱
            }
        });

        //创建线程对象
        Thread t=new Thread(task);

        //启动线程
        t.start();

        //这里是main方法,这是在主线程中
        //在主线程中怎么获取t线程的返回结果?
        //get()方法的执行会导致“当前线程阻塞”
        //它会一直等到上面的call方法结束了,获取到返回值了才继续往下执行代码
        Object obj=task.get();
    }
}

了解内容

了解内容:关于线程的调度
1.1、常见的线程调度模型有哪些?
	抢占式调度模型:
			哪个线程的优先级比较高,强盗的CPU时间片的概率就高一些/多一些。
			java采用的就是抢占式调度模型。
	
	均分式调度模型:
			平均分配CPU时间片。每个线程占有的CPU时间片长度一样。平均分配,一切平等。
			有一些编程语言,线程调度模型采用的是这种方式。

1.2、java中提供 哪些方法是和线程调度有关系的呢?
实例方法:
		void setPriority(int newPriority)  设置线程的优先级
		int getPriority()  获取线程优先级
		最低优先级1
		默认优先级是5
		最高优先级10

静态方法:
	static void yield()  让位方法
	暂停当前正在执行的线程对象,并执行其他线程
	yield() 方法不是阻塞方法。让当前线程让位,让给其他线程使用。
	yield() 方法的执行会让当前线程从“运行状态”回到“就绪状态”。

实例方法:
	void join()  合并线程
	
	class MyThread1 extends Thread{
		public void doSome(){
			MyThread2 t=new MyThread2();
			t.join();//当前线程进入阻塞,t线程执行,知道t线程结束。当前线程才可以继续。
		}
	}

class MyThread2 extends Thread{
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值