线程1——javaEE 附面题

   

目录

         

引入

进程

进程和线程的关系

线程

创建线程

1.通过创建 Thread 子类

2.通过实现 Runnable接口

3.子类匿名表达式

4.匿名内部类(Runnable)

启动线程  

Thread 类的常用属性(方法)

线程的状态

线程休眠

打断线程

线程等待 join

线程安全问题

锁synchronized()

先从使用入手

特性

wait/notify

使用

wait使用

两者搭配

面经:


     

引入

         线程的概念离不开cpu(中央处理器),CPU 作为电脑的“脑”,其算力是非常夸张的但它是如何工作的呢,在认识线程前我们先来了解一下 CPU。


         计算机祖师爷冯诺依曼提出的冯诺依曼体系结构(运算器,控制器,存储设备,输入设备,输出设备)奠定了现代计算机的硬件基础。运算器,控制器便是CPU最基础,最核心的功能。

cpu 执行是很复杂的 可以简化成

1. 读取指令        -------------------------------------------------            指令(机器语言 )

2. 解析指令        -------------------------------------------------            从指令表中对应查找指令是什么意思

3. 执行指令        -------------------------------------------------            运算

现代 多核 cpu 下诞生了进程

进程

进程是操作系统中资源分配的基本单位。

操作系统是一个描述系统资源,通过一定数据结构组织资源的管理软件。     、


                                                           系统通过PCB 来描述进程

PID同一台机器,同一时间,不重复
内容指针内存资源分为数据/指令 操作系统可通过其找到数据/指令
文件描述符表硬盘资源  打开文件可以得到一个文件描述符,打开多个就可以用数组/顺序表表示
状态就绪状态  /  阻塞状态
优先级依据重要性给进程分配资源
上下文进程调度出 cpu 时保存一个中间状态,保证进程再调度回来时可以恢复之前的工作
记账信息PCB 会记录下进程在CPU上执行的次数,分配写资源给使用资源少的进程

进程的创建/销毁开销(销毁时间和系统资源)非常大,在创建时申请资源(大开销操作),为了提高效率降低开销引入了线程。

进程和线程的关系

1.线程是更轻量的进程。(进程太重了大开销,低效率)
2.进程包含线程,一个线程有大于等于一个线程,不能没有。
3.同一个进程上的线程共享进程的资源。
4.每个线程都可以执行独立的逻辑,并在cpu上独立调度
5.当进程已经有了,在进程上创建线程可以省去申请资源的开销。

线程

线程是系统调度执行的基本单位。       
线程满足了“并发编程” 使一个服务器可以同时处理多个客户端的访问。          


线程虽更轻量,多线程可以提升效率,但过犹不及。
线程过多带来的问题:
1.线程过多时,线程的创建和销毁时的开销就不可忽视了。
2.多线程环境下,多个线程对同一个变量同时进行操作。
     多对一 可读不可取
     一对一 可读又可取

3.线程中断会抛出异常,如果没有被捕获到,进程就会崩溃,线程会全挂掉。
 

创建线程

1.通过创建 Thread 子类

重写 run(); 方法,创建Thread 对象,调用start();

代码实现:

class myThread extends Thread{
    public void run (){
        System.out.println("hello thread");
    }
}
public class demo1 {


    public static void main(String[] args) {
         Thread thread = new myThread();
         thread.start();
        System.out.println("hello main");


    }
}

👀输出:

通过写入无限循环观察下:

代码:

class myThread extends Thread{
        @Override
        public void run() {
            while(true){
                try {
                    System.out.println("hello thread");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }



public class demo1 {

    public static void main(String[] args) {
        Thread thread = new myThread();
        thread.start();
        
        while(true){
            try {
                System.out.println("hello main");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

👀输出

观察结果可以发现 main 主线程 和 我们自己创建的 thread 线程是并行执行的,顺序由cpu调度决定,随机出现。

2.通过实现 Runnable接口

再将 runnable 实例作为参数传给Thread 构造方法

代码:

class myRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("hello thread");

    }
}    
public class demo2 {
    public static void main(String[] args) {
        Runnable runnable = new myRunnable(); 
        Thread thread = new Thread(runnable);
        thread.start();
        System.out.println("hello main");
    }
}

👀输出:

这样的写法分离了任务逻辑和线程管理,不依赖于具体的类,使得后续可以轻松替换任务实现,降低程序的耦合度。

3.子类匿名表达式

代码: Thread t = new Thread () {

                   public void run(){

                   }

};

public class demo3 {

    public static void main(String[] args) {
        // 匿名内部类 是Thread的子类 重写了run方法
        Thread t = new Thread(){
            public void run(){
                while(true){
                    try {
                        System.out.println("hello thread");
                        Thread.sleep(1000);  //  休眠1000ms  降低打印速度方便观察
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
        System.out.println("hello main");
    }

}

👀输出:

4.匿名内部类(Runnable)

代码:

Runnable runnable = new Runnable({

});

Thread t  = new Thread(runnable);

public class demo4 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable(){
            @Override
            public void run() {
                while(true){
                    try {
                        System.out.println("hello thread");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }; 
        Thread t = new Thread(runnable);
        t.start();

        while(true){
            try {
                System.out.println("hello main");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

👀输出:

启动线程  

Thread.start();            start() 是 Thread 类的一个静态方法,可以直接用类名调用。

1.调用 start 会真正调用系统中创建线程的 api   start 执行不会产生阻塞,按代码顺序立刻向下执行。

2.一个线程只能 start 一次。  start 后 线程要么是就绪,要么是阻塞 不能重新 启动 了。

3.start 执行会自动执行 run() 方法。

Thread 类的常用属性(方法)

方法类别方法名功能描述补充说明
IDgetId()获取线程唯一标识符每个线程都有一个唯一的标识符,由 JVM 分配,从 1 开始递增
名称getName()获取线程名称线程创建时可以指定名称
状态getState()获取线程状态返回线程当前状态(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED)
优先级getPriority()获取线程优先级线程优先级设置效果受操作系统调度机制影响,"改了不一定有用",
守护线程isDaemon()判断是否为守护线程守护线程会在所有非守护线程结束后自动终止,主要用于后台支持任务
存活状态isAlive()判断线程是否存活返回 false 表示线程未启动(NEW 状态)或已结束(TERMINATED 状态)
中断状态isInterrupted()判断线程是否被中断不会清除中断标记,需要通过 Thread.interrupted () 静态方法清除中断标记

注:isDaemon()    是否为后台线程
后台线程: 当线程没执行完时,进程要结束,线程无法阻止当前进程结束。
前台线程: 当前线程没执行完,进程要结束要等线程执行完,这样的线程成为前台线程。

线程的状态

New创建了线程对象但还没start   isAlive  false
TERMINED执行完成了(run完了)但对象还在  isAlive  false
WAITING死等  join 无参未设置超时时间
TIME_WAITING有时间的等  join 设置了超时时间
BLOCKED锁竞争产生的阻塞

线程休眠

sleep(时间 ms)  Thread 类的静态方法 让线程休眠多少毫秒后(进入Time_WAITING)
sleep 线程进入阻塞,调度出CPU ,唤醒后变为就绪状态,但不会立即执行,等待CPU调度。

打断线程

希望线程提前结束(sleep 时提前唤醒)

1.通过变量修改

2.通过 isInterrupted 标志位

查看当前中断状态:Thread.currentThread().isInterrupted()  不清除中断标志
检查中断状态:Thread.interrupted()  清除中断标志

若线程处于休眠sleep,会抛出InterruptedException 异常,需要 catch

try{
  Thread.sleep(1000);
}catch (InterruptedException e) {
  Thread.currentThread.interrupt();
}

线程等待 join

控制线程之间的执行顺序。

有两个版本的join
1.join();                  死等
2.join(超时时间)       等待到超时时间后就不等了

线程 t1      线程t2
t1.join();          t2等t1执行完
t1.join(1000)   t2等待t1执行完,等了1000ms t1还没结束就不等了   

⭐谁调用谁被等

线程安全问题

why:❓❓❓
1. 【根本原因】操作系统的随即调度,抢占式执行。
2. 操作不是原子的。  (原子的:不可再分的最小操作)
3. 多个变量同时操作同一个变量。
4. 内容不可见
5. 指令重排序

        面对非原子的操作,多线程就会出现多线程做同一个操作但做的是这个操作的不同部分
怎么理解呢? 就好像把一个大象放进冰箱需要几步。(这就是非原子操作是可拆分的)
                       1.打开冰箱门  
                       2.把大象放进去
                       3.关上冰箱门
        这时候如果有多个线程同时进行把同一只大象放进冰箱的操作。就有可能线程一打开了冰箱门被调度出CPU,线程2也执行了打开冰箱门,重复开门冰箱们受不了,线程2把大象放进冰箱并关上了冰箱门然后被调度出CPU,线程一被调度回CPU读取了中间状态继续之前的操作,把大象放进去,关上冰箱门。

        把一只大象放进去了两遍也就是BUG出现了,有人要问最后不还是大象在冰箱里面吗,但无效的操作消耗了资源,在这虽然没造成什么严重后果但这要是转账操作呢,同时扣了两次款呢?

how:
那怎么保证安全呢?
        既然是非原子操作造成的那可以把操作打包成原子的,java中提供了synchronized 可以给操作加锁保证线程一次把该执行完的逻辑执行完,这时有其他线程来执行这个操作就会触发锁竞争,产生阻塞等待上一个执行此操作的线程执行完解锁才能拿到锁,开始执行该操作。

锁synchronized()

先从使用入手

synchronized 有两个大括号
进入第一个大括号表示锁已经加上了,
从第二个大括号出来就表示解锁了。

通过加锁操作可以把操作变成原子的。

原理:加同一把锁的线程(锁对象是相同的)会竞争同一把锁(锁竞争)没抢到的阻塞等待抢到的解锁再抢。     

锁对象  Object locker = new Object(); 

1.锁

synchronized(锁对象){
      // 操作

}

2.修饰普通方法

synchronized public void 方法(){

//     this是锁对象

}

3.修饰静态方法

synchronized public static void 方法(){
          // static 没有this ,所以锁对象是类对象
          // 类对象 .class
}

特性

1.互斥

当一个线程已经拥有锁的时候,该线程的锁不能被抢占。

2.可重入

当一个线程已经拥有一把锁的情况下,对于已有的这把锁可以重复加锁多次(连续加同一把锁)且不会触发死锁。

wait/notify

Object类的方法

协调线程之间执行的顺序  区别 join 控制线程间结束顺序

wait 会使线程释放锁主动阻塞等待,直到被notify 唤醒
eg:  
希望t1先执行再让t2执行
使用wait主动让t2阻塞让t1先参与调度,等t1执行完用notify唤醒t2.

使用

Object object = new Object ();
object.wait();   //   wait() 会阻塞 可能会抛出 InterruptedException  当执行wait时其内部会第一时间把锁放了

放锁的前提得先有一把锁,wait放锁后当前线程就会进入阻塞状态  WAITING  ,等待被唤醒,被唤醒后会再重新尝试去获取之前的锁,就会引发锁竞争 BLOCKED (被唤醒了,等拿到锁就会继续执行)

所以wait 应该搭配synchronized使用

wait使用
Object object = new Object();
try{
    synchronized(object){
    object.wait();
    }
}catch(InterruptedExecption e){
     
两者搭配

创建两个线程,线程t1等待,线程t2来唤醒t1

import java.util.Scanner;

public class demo_notify {

    public static Object locker = new Object();
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("t1 等待");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 等待后");
        });


        Thread t2 = new Thread(() -> {
            System.out.println("请输入任意内容唤醒t1");
            Scanner scanner = new Scanner(System.in);
            scanner.next();
            synchronized (locker) { 
                locker.notify();
            }
        });


        t1.start();
        t2.start(); 

    }

}

👀输出

当多个线程都在 wait 时(同一个对象),此时 notify 会随机唤醒一个。使用 notifyAll 可以唤醒所有的。

import java.util.Scanner;

public class demo_notifyAll {

    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            System.out.println("t1 等待前");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 等待后");
        });

    Thread t2 = new Thread(() -> {
        System.out.println("t2 等待前");
        synchronized (locker) {
            try {
                locker.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("t2 等待后");
    });

    Thread t3 = new Thread(() -> {
        System.out.println("请输入任意内容唤醒t1或t2");
        Scanner scanner = new Scanner(System.in);
        scanner.next();
        synchronized (locker) {
            locker.notify();
        }
    });

    t1.start();
    t2.start();
    t3.start();

    }

}

输出:此时t3只会唤醒t1或t2其中一个。我这里运行唤醒了t2

想同时唤醒t1,t2就需要用notifyAll
 

import java.util.Scanner;

public class demo_notifyAll {

    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            System.out.println("t1 等待前");
            synchronized (locker) {
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1 等待后");
        });

    Thread t2 = new Thread(() -> {
        System.out.println("t2 等待前");
        synchronized (locker) {
            try {
                locker.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("t2 等待后");
    });

    Thread t3 = new Thread(() -> {
        System.out.println("请输入任意内容唤醒t1或t2");
        Scanner scanner = new Scanner(System.in);
        scanner.next();
        synchronized (locker) {
            locker.notifyAll();
        }
    });

    t1.start();
    t2.start();
    t3.start();

    }

}

输出:

面经:

谈谈sleep 和 wait 的区别。
答:
1.wait 的设计是为了提前唤醒,超时时间只是作为Plan B。
   sleep 的设计就是为了到时间唤醒,虽可用 interrupt 提前唤醒但这样的唤醒会产生异常。

2.wait 需要搭配锁使用,因为执行时会先释放锁。(避免其他线程一直拿不到锁)
   sleep 就不需要搭配锁使用,当sleep 被放到synchronized中时,不会释放锁而是抱着锁睡。
 

多线程实用但充满陷阱未完待续。

                爱是个什么东西,它太理想主义,爱有什么了不起,我充满许多怀疑   
                                                                                                                    爱是个什么东西  DT
                                                                   ⭐❤️👍

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值