玩命加载中 . . .

多线程(面试)


多线程

概念

  • 程序:一段静态的代码
  • 进程:进程作为资源分配的单位,系统在运行时为每个进程划分内存区域
  • 线程:线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器
  • 并行:多个cpu同时执行多个任务
  • 并发:一个cpu同时执行多个任务,比如:秒杀

线程的创建方式

  1. 继承Thread类的子类,重写run方法(和1的匿名子类的方式一样)

    class MyThread extends Thread{
        public void run(){
            System.out.println("Thread创建方法0");
        }
    }
    //调用
    MyThread t1 = new MyThread();
    t1.start();//必须是start
    

    注意:每个线程对象只能启动一次,当需要多次调用该逻辑,可以创建多个线程对象,进行调用

  2. New线程,重写Thread的run方法

    public void test1(){
        Thread thread1 = new Thread(){
            @Override
            public void run() {
                System.out.println("Thread创建方法1");
            }
        };
        thread1.start();
    }
    
  3. New线程,实现Runnable接口

    class RThread implements Runnable(){
        public void run(){
            System.out.println("Thread创建方法2");
        }
    }
    //调用
    RThread mt1 = new RThread();
    Thread t1 = new Thread(mt1);
    t1.start();
    
    public void test2(){
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread创建方法2");
            }
        });
        thread2.start();
    }
    

    此方法创建多个线程时只需要将构造器中的参数更换即可,只需要创建一个runnable实现类对象

    1,2种创建线程方式的比较:

    • 开发中,优先选择实现Runnable接口的方式
    • 原因:
      • 实现的方式没有类的单继承性局限性
      • 实现的方式更适合来处理多个线程有共享数据的情况(因为只创建一个runnable给相同构造器)
    • 相同点:两个方式都需要重写run(),将线程要执行的逻辑声明在run()中
  4. New Callable接口,实现call方法,封装成任务,作为参数加入线程

    callable可以返回结果值,需要在定义的时候指定返回值类型

    public void test3() throws ExecutionException, InterruptedException {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("Thread创建方法3");
                return 10;
            }
        };
        //封装成任务
        FutureTask<Integer> task = new FutureTask<Integer>(callable);
        new Thread(task).start();
        task.isDone();
        System.out.println(task.get());
    }
    

3.1 实现Callable接口

//1.创建Callable的实现类
class CallTest implements Callable{
    @Override
    //重写call方法
        public Object call() throws Exception {
            System.out.println("Thread创建方法3");
            return 10;
        }
}
class test{
    //创建实现类实例
    CallTest callTest = new CallTest();
    //将此实例作为参数传入FutureTask构造器中
    FutureTask task = new FutureTask(callTest);
    //将FutureTask作为参数传入线程中,为了执行线程
    new Thread(task).start();
    //非必选步骤:获取返回值
    System.out.println(task.get());
}

如何理解Callable接口的方式创建多线程比Runnable接口的方式强大?

  1. call()有返回值
  2. call()可以抛出异常,被外边的操作捕获,获取异常信息
  3. Callables是支持泛型的
  1. 使用线程池创建线程

    //提供指定线程数量的线程池
    ExecutorService service = Executors.newFixedThreadPool(10);
    service.execute(Runnable command);//方式1提交:用于runnable
    FutureTask task = service.submit(Callable<T> task);//方式2提交:用于callable
    //非必选步骤:获取返回值
    System.out.println(task.get());
    service.shutdown();//关闭线程池
    

线程的六种状态

  1. NEW:初始态

  2. RUNNABLE:运行态

    a) Ready:准备态(排队中,准备执行)

    b) Running:执行态

  3. BLOCKED:阻塞态(执行锁方法,锁被其他线程拿走,自己成为阻塞态)

  4. WAITING:等待态(执行锁方法,执行了其中的wait()方法,释放锁,当条件满足之后,得到锁就会立刻执行)

  5. TIMED_WAITING:超时等待态(wait(传参2000),进入等待态2秒后,不管条件是否满足,都执行)

  6. TERMINEATED:结束态

线程同步

同步的方式解决了线程安全的问题。

操作同步代码块时,只要有一个线程参会,其他线程等待,相当于单线程过程,效率低

方式一:同步代码块

synchronized(同步监视器){
    //需要被同步的代码
}

说明:

  1. 操作共享数据的代码,即为需要被同步的代码

  2. 共享数据:多个线程共同操作的变量。

  3. 同步监视器,俗称:锁。任何一个对象,都可以当作锁

    要求:多个线程必须要公用同一把锁(static)

    1. 在实现Runnable接口创建多线程的方式,我们可以考虑用this充当同步监视器
    2. 在继承Thread类创建多线程的方式,慎用this充当同步监视器(因为不是一个对象),可以用类.class

方式二:同步方法

如果操作共享数据的代码都声明在一个方法中,不妨将此方法声明同步的。

public void run(){
    while(true){
        show();
    }
}
public synchronized void show(){
    //需要同步的代码
}

总结:

  1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明
  2. 非静态的同步方法,同步方法是:this(不适用继承Thread类的多线程)
  3. 静态的同步方法,同步监视器是:当前类本身(适用于所有情况)

Lock锁

Private ReentrantLock lock = new ReentrantLock();
lock.lock();
lock.unlock();

注意,如果是继承Thread的方式,需要将Lock加上static

synchronized与Lock的异同

  • 相同:二者都可以解决线程安全问题
  • 不同:
    • synchronized机制在执行完相应的同步代码之后,自动的释放同步监视器
    • Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())

优先使用顺序:

Lock–>同步代码块(已经进入了方法体,分类了相应资源)–>同步方法(在方法体之外)

如何解决线程安全问题?有几种

3种,Lock锁方式,同步代码快,同步方法

死锁

不同线程分别占用了对方需要的同步资源不放弃,都在等待对方放弃自己需要的资源

如何解决死锁

  1. A调B,B调A的情况注意同步问题
  2. 减少同步资源的定义
  3. 避免嵌套同步

线程的通信

  1. wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
  2. notify():一旦执行此方法,就会唤醒一个线程,如果有多个线程被wait,就唤醒优先级高
  3. notifyAll():一旦执行此方法,就会唤醒所有wait线程

注意:

  1. 这三个方法必须使用在同步代码块或同步方法中
  2. 这三个方法的调用者,必须是同步监视器,否则会出现IllegalMonitorStateException(this)
  3. 这三个方法都是定义在object中

Sleep和Wait的区别

a) 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态

a) 不同点:

  1. 声明的位置不同:Thread类中的声明sleep(),Object类中声明wait()
  2. 调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须在同步代码快,同步方法中调用
  3. 关于是否释放同步监视器:如果两个方法都使用在同步代码快或同步方法中,sleep不会释放,wait会
  4. 唤醒时机不同:sleep()会在读秒之后自己唤醒,wait()不会自动唤醒,需要手动调用notify/notifyall来进行唤醒

Sleep

  1. 暂停当前线程执行,但是不释放锁(在厕所里睡着了,但厕所其他人使用不了)

  2. 它是Thread中的方法

  3. 可以在任何场景中使用

  4. 只有睡够时间之后才能醒来

Wait

  1. 暂停当前线程执行,但是释放锁

  2. 它是Object对象的方法,可以直接使用

  3. 只能在同步块,同步方法中使用

  4. 可以随时被唤醒,(notify()也可以在其他线程使用来唤醒该线程)

生产者消费者问题(线程通信应用)

class Clerk{
    private int i =0;
    /*
    此处用到同步,在生产时不能进行消费,消费时不能生产
    解决线程安全问题
    需要注意:
    1.wait(),notify()方法必须放在同步代码块/同步方法中
    2.唤醒的时机
     */
    public synchronized void produce() {
        if(i<20){
            i++;
            System.out.println(Thread.currentThread().getName()+":开始生产第"+i+"个产品");
            notify();//在生产完提醒消费
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    public synchronized void consume() {
        if(i>0){
            System.out.println(Thread.currentThread().getName()+":开始消费第"+i+"个产品");
            i--;//先消费再减少
            notify();//在消费完产品提醒生产
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Producer extends Thread{
    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produce();
        }
    }
}

class Consumer extends Thread{
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consume();
        }

    }
}

public class test {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer producer = new Producer(clerk);
        Consumer consumer = new Consumer(clerk);
        producer.setName("1号生产者");
        consumer.setName("1号消费者");
        producer.start();
        consumer.start();
    }
}

线程池

线程资源必须通过线程池提供,不允许在应用中自行显示创建线程

好处:

  1. 减少创建/销毁线程上的系统资源开销。
  2. 提高响应速度(减少创建线程的时间)
  3. 便于线程管理

创建ExecutorService线程池:

//提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);

设置线程池属性:

service.setCorePoolSize(20);//核心池的大小
maximumPoolSize//最大线程熟
keepAliveTime//线程没有任务时最多保持多长时间会终止

ExecutorService:线程池接口

  • void execute(Runnable command):执行任务,没有返回值,一般用于执行runnable接
  • submit(Callable task):执行任务,有返回值(FutureTask),一般用于执行Callable
  • void shutdown():关闭连接池

Executors:线程池工具类,用于创建不同类型的线程池

线程的常用方法

start() 启动当前线程,调用当前线程的run()
run() 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
currentThread() 返回执行当前代码的线程
getName() 获取当前线程的名字
setName() 设置当前线程的名字
yield() 释放当前cpu的执行权
join() 在线程a中调用线程b的join(),线程a就会进入阻塞状态
sleep(long millitime) 睡眠,让当前线程“睡眠”指定毫秒值,让其在规定时间内是阻塞状态
isAlive() 判断当前线程是否存活

线程优先级

优先级:

  1. MAX_PRIORITY:10
  2. MIN_PRIORITY:1
  3. NORM_PRIORITY:5

涉及的方法

  1. getPriority():返回当前线程优先值,Thread.currentThread().getPriority()
  2. setPriority(int newPriority):改变线程的优先级

文章作者: 小苏
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小苏 !
评论
  目录