当前位置: 首页 > news >正文

【java】java多线程及线程池面试题

目录

  • 前言
    • 线程是什么?多线程是什么?
    • 多线程的作用和好处以及缺点
    • 守护线程和用户线程
    • 并发和并行的区别
  • 一.线程的状态和常用方法
    • 1.线程各种状态转化图
    • 2.线程相关常用方法有
      • ① wait()
      • ② sleep(long timeout)
      • ③ join()
      • ④ yield()
      • ⑤ notify()和notifyAll()
    • 3.wait()和sleep()的区别?
    • 4.为什么 wait()、notify()、notifyAll()方法定义在 Object 类里面,而不是 Thread 类?
    • 三.实现多线程的方式
    • 1.继承Thread类(只能继承一个类)
    • 2.实现Runnable接口(可以实现多个接口)
    • 3.使用Callable、FutureTask实现有返回结果的多线程
    • 4.使用线程池--下面会详细讲解
    • 5.附加问题:runnable和callable的区别
  • 四.线程池(4大方法、7大参数、4种拒绝策略)
    • 1.线程池的好处
    • 2.七大参数(用银行柜台举例)
    • 3.四大方法
      • ① ExecutorService executor = Executors.newCachedThreadPool()
      • ② ExecutorService executor = Executors.newFixedThreadPool()
      • ③ ExecutorService executor = Executors.newSingleThreadPool()
      • ④ ExecutorService executor = Executors.newScheduleThreadPool()
    • 4.四大拒绝策略
  • 五.线程死锁
    • 1.什么是死锁?
    • 2.进程死锁、饥饿、死循环的区别
    • 3.造成死锁的四个必要条件
    • 3.什么时候会发生死锁?
    • 4.怎么解决死锁?
      • ① 预防死锁
      • ② 避免死锁
      • ③ 死锁的检测和解决
        • 死锁的检测
        • 死锁的解决
        • 如何决定"对谁动手(选择进程解决死锁)"
  • 五.线程安全
    • 1.线程安全主要是三方面
    • 2.保证原子性
    • 3.保证可见性
    • 4.保证有序性
    • 5.volatile和synchronized的区别
    • 6.synchronized和lock的区别

前言

线程是什么?多线程是什么?

线程: 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流(比如一段程序、一个函数等)。
多线程: 多个线程并发执行的技术。

多线程的作用和好处以及缺点

作用: 充分利用CPU的资源,采用多线程的方法去同时完成几件事情而互不干扰。

好处:
使用线程可以把程序中占据时间长的任务放到后台去处理,如图片、视频的下载。
发挥多核处理器的优势,并发执行让系统运行的更快、更流畅,用户体验更好。

坏处
大量的线程降低代码的可读性。
更多的线程需要更多的内存空间。
存在线程不安全问题。

守护线程和用户线程

守护线程: jvm给的线程。比如:GC线程。
用户线程: 用户自己定义的线程。比如:main线程。

扩展:
Thread.setDaemon(false)设置为用户线程
Thread.setDaemon(true)设置为守护线程

并发和并行的区别

**并发:**一个处理器同时处理多个任务。(一个人同时吃两个苹果)
**并行:**多个处理器同时处理多个任务。(两个人同时吃两个苹果)

一.线程的状态和常用方法

1.线程各种状态转化图

在这里插入图片描述

2.线程相关常用方法有

① wait()

  • wait(): 线程等待,会释放锁 (用于同步代码块或同步方法中,不然会报错),然后进入等待状态和notify()和notifyAll()一起使用。
  • wait(long timeout): 等待规定的时间,如果在规定时间内被唤醒就继续执行,如果超过规定时间也会继续向下执行。
public class TestWait implements Runnable {
    private Object lock;
    public TestWait(Object lock) {
        this.lock = lock;
    }
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        // 创建两个线程
        Thread t1 = new Thread(new TestWait(lock),"t1");
        Thread t2 = new Thread(new TestWait(lock),"t2");
        t1.start();
        t2.start();
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
            try {
                //唤醒等待中的线程,进入就绪状态
                lock.notify();
                
                //线程等待,释放锁
                lock.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(Thread.currentThread().getName() + "等待结束,继续执行");
        }
    }
}

运行结果:
t2准备进入等待状态                
t1准备进入等待状态
t2等待结束,本线程继续执行

运行结果不唯一,但是总会有一个线程执行不完。
以上运行步骤如下:
1.t2线程运行,调用notify()方法唤醒等待中的线程(现在等待队列中没有线程),然后调用wait()方法进入等待队列。
2.t1线程运行,调用notify()方法唤醒等待中的线程(这时唤醒了t2线程,因为只有等待队列中只有t2,所以一定是t2获取cpu使用权),然后wait()方法进入等待队列。
3.t2线程继续向下运行,然后结束,t1还在等待队列,没有被唤醒,所以t1执行不完。


注意:被唤醒的线程,会从等待队列,进入就绪状态,然后去竞争cpu的使用权。

② sleep(long timeout)

  • sleep(): 线程睡眠,不会释放锁(规定时间到了之后继续执行线程) 。一搬用于模拟网络延时。
public class TestSleep implements Runnable {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        // 创建两个线程
        Thread t1 = new Thread(new TestSleep(),"t1");
        Thread t2 = new Thread(new TestSleep(),"t2");
        t1.start();
        t2.start();
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
        try {
            //线程睡眠两秒之后,继续执行,不会释放锁。
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(Thread.currentThread().getName() + "等待结束,继续执行");
    }
}

运行结果:
t1准备进入睡眠状态
t2准备进入睡眠状态
t1睡眠结束,继续执行
t2睡眠结束,继续执行

以上结果不唯一。

③ join()

  • join(): 指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。底层是利用wait()方法实现。可以应用于必须多个线程完成才能执行主线程,比如:三个人去酒店吃饭,三个都到酒店才能上菜。

正常执行

public class TestJoin {

    public static void main(String[] args) {
      Thread t1 = new Thread(new Runnable() { //线程t1
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                    System.out.println("执行t1");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(new Runnable() { //线程t2
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                    System.out.println("执行t2");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
        System.out.println("执行main");
    }
}

运行结果:
执行main
执行t1
执行t2

让主线程(main)最后执行,让 t1 和 t2 先执行完成。

public class TestJoin {

    public static void main(String[] args) throws InterruptedException {
      Thread t1 = new Thread(new Runnable() { //线程t1
            @Override
            public void run() {
                try {
                    System.out.println("执行t1");
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(new Runnable() { //线程t2
            @Override
            public void run() {
                try {
                    System.out.println("执行t2");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join(); //t1参与到当前线程(main)的执行中,所以主线程需要等待t1执行完成才会继续执行,但是t2不受影响、。
        System.out.println("执行main");
    }
}

运行结果:
执行t2
执行t1
执行main

或者

执行t1
执行t2
执行main

让 t1 和 t2 按顺序执行

public class TestJoin {

    public static void main(String[] args) throws InterruptedException {
      Thread t1 = new Thread(new Runnable() { //线程t1
            @Override
            public void run() {
                try {
                    System.out.println("执行t1");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread t2 = new Thread(new Runnable() { //线程t2
            @Override
            public void run() {
                try {
                    System.out.println("执行t2");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        t1.join(); //t1参与到当前线程的执行中,所以主线程需要等待t1执行完成才会继续执行,这是t2需要等待t1执行完毕之后才能执行。
        t2.start();
    }
}

运行结果:
执行t1
执行t2

④ yield()

  • yield(): 线程让步(正在执行的线程),会使线程让出cpu使用权,进入就绪状态。
public class TestYield {

    public static void main(String[] args) throws InterruptedException {
      Thread t1 = new Thread(new Runnable() { //线程t1
            @Override
            public void run() {
                System.out.println("执行t1");
                Thread.yield();//t1让出cpu使用权限,回到就绪状态
                System.out.println("执行结束t1");
            }
        });

        Thread t2 = new Thread(new Runnable() { //线程t2
            @Override
            public void run() {
                System.out.println("执行t2");
                System.out.println("执行结束t2");
            }
        });
        t1.start();
        t2.start();
    }
}

如果先执行t1,然后t1会让出线程,进入就绪状态,然后t2执行完成,t1再去竞争cpu的使用权,在继续执行。

⑤ notify()和notifyAll()

  • notify(): 随机唤醒一个在等待状态的线程,进入就绪状态。
public class TestNotify implements Runnable{
    static Object lock = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(new TestNotify(), "t1");
        Thread thread2 = new Thread(new TestNotify(), "t2");
        thread1.start();
        thread2.start();
        try {
            Thread.sleep(1000); //主线程睡眠,让线程t1或t2执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " 获得了锁");
            System.out.println("唤醒前" + thread1.getName() + "状态是" + thread1.getState());
            System.out.println("唤醒前" + thread2.getName() + "状态是 " + thread2.getState());
            lock.notify(); // 随机唤醒一个在等待状中的线程
        }
        try {
            Thread.sleep(1000);//主线程睡眠,让线程t1或t2执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("唤醒后" + thread1.getName() + "状态是" + thread1.getState());
        System.out.println("唤醒后" + thread2.getName() + "状态是" + thread2.getState());
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " 开始执行");
            try {
                lock.wait(); // 进入等待状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 线程结束");
        }
    }
}

运行结果:
t1 开始执行
t2 开始执行
main 获得了锁
唤醒前t1状态是WAITING
唤醒前t2状态是 WAITING
t1 线程结束
唤醒后t1状态是TERMINATED
唤醒后t2状态是WAITING

运行结果不唯一,由上面可以看出,notify()随机唤醒了一个线程,唤醒的是t1,也有可能唤醒的是t2。然后没被唤醒的线程一直处于等待状态,这样就会导致程序结束不了。
  • notifyAll(): 唤醒在等待队列的所有线程,进入就绪状态。
package com.navi.vpx;

public class TestNotify implements Runnable{
    static Object lock = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(new TestNotify(), "t1");
        Thread thread2 = new Thread(new TestNotify(), "t2");
        thread1.start();
        thread2.start();
        try {
            Thread.sleep(1000); //主线程睡眠,让线程t1或t2执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " 获得了锁");
            System.out.println("唤醒前" + thread1.getName() + "状态是" + thread1.getState());
            System.out.println("唤醒前" + thread2.getName() + "状态是 " + thread2.getState());
            lock.notifyAll(); // 唤醒全部在等待状态的线程
        }
        try {
            Thread.sleep(1000);//主线程睡眠,让线程t1或t2执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("唤醒后" + thread1.getName() + "状态是" + thread1.getState());
        System.out.println("唤醒后" + thread2.getName() + "状态是" + thread2.getState());
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " 开始执行");
            try {
                lock.wait(); // 进入等待状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 线程结束");
        }
    }
}
运行结果:
t1 开始执行
t2 开始执行
main 获得了锁
唤醒前t1状态是WAITING
唤醒前t2状态是 WAITING
t2 线程结束
t1 线程结束
唤醒后t1状态是TERMINATED
唤醒后t2状态是TERMINATED


运行结果不唯一,有上面可得notifyAll()唤醒的是所有在等待状态的线程,使线程都进入就绪状态,然后竞争锁,最后全部执行完成,程序结束。

3.wait()和sleep()的区别?

① wait() 来自Object,sleep()来自Thread。

② wait()会释放锁,sleep()不会释放锁。

③ wait()只能用在同步方法或代码块中,sleep()可以用在任何地方。

④ wait()不需要捕获异常,sleep()需要捕获异常。

4.为什么 wait()、notify()、notifyAll()方法定义在 Object 类里面,而不是 Thread 类?

锁可以是任何对象,如果在Thread类中,那只能是Thread类的对象才能调用上面的方法了。

② java中进入临界区(同步代码块或同步方法),线程只需要拿到锁就行,而并不关心锁被那个线程持有。

③ 上面方法是java两个线程之间的通信机制,如果不能通过类似synchronized这样的Java关键字来实现这种机制,那么Object类中就是定义它们最好的地方,以此来使任何Java对象都可以拥有实现线程通信机制的能力。

三.实现多线程的方式

1.继承Thread类(只能继承一个类)

public class MyThread extends Thread{
	
    public void run(){
        System.out.println("执行run()方法");
    }

    public static void main(String[] args) {
    	//创建对象
        MyThread myThread = new MyThread();

		//启动线程,执行run()方法
        myThread.start(); 
    }
}

2.实现Runnable接口(可以实现多个接口)

public class MyRunable implements Runnable{
    
    //重写的run()方法
    @Override
    public void run() {
        System.out.println("执行run()方法");
    }

    public static void main(String[] args) {
        //创建对象
        MyRunable myRunable = new MyRunable();
        
        //创建线程对象
        Thread thread = new Thread(myRunable);
		
		//启动线程,执行run()方法
        thread.start(); 
    }
}

3.使用Callable、FutureTask实现有返回结果的多线程

public class MyCallable implements Callable<String> {

    //重写的run()方法
    @Override
    public String call() {
        System.out.println("执行run()方法");
        return "call返回值";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建对象
        MyCallable myCallable = new MyCallable();

        //创建futureTask
        FutureTask<String>  futureTask = new FutureTask(myCallable);

        //创建线程对象
        Thread thread = new Thread(futureTask);

        //开启线程,执行run()方法
        thread.start();

        //接收call()的返回值
        String result = futureTask.get();
        System.out.println(result);

    }
}

4.使用线程池–下面会详细讲解

5.附加问题:runnable和callable的区别

① runnable没有返回值,callable有返回值。

②runnable 只能抛出异常,不能捕获,callable 能抛出异常,也能捕获。

四.线程池(4大方法、7大参数、4种拒绝策略)

1.线程池的好处

① 线程是稀缺资源,使用线程池可以减少线程的创建和销毁,每个线程都可重复使用。

② 可以根据系统的需求,调整线程池里面线程的个数,防止了因为消耗内存过多导致服务器崩溃。

2.七大参数(用银行柜台举例)

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize:核心线程数(最少开放的柜台数)

线程池维护的最小线程数,创建后不会被回收(除非:设置allowCoreThreadTimeout=true后,空闲的核心线程超过存活时间(keepAliveTime)也会被回收)。当线程池使用 executor() 方法执行一个新任务时,会先判断运行的线程是否超过核心线程数,如果不超过,就会创建一个新线程并执行任务。

    /** ThreadPoolExecutor 源码
     * If false (default), core threads stay alive even when idle.
     * If true, core threads use keepAliveTime to time out waiting
     * for work.翻译如下:
     * 如果为false(默认值),则核心线程即使在空闲时也保持活动状态。
     * 如果为true,则核心线程使用keepAliveTime超时等待工作。
     */
    private volatile boolean allowCoreThreadTimeOut;   //核心线程能否被回收
  • maximumPoolSize:最大线程数(最大开方法的柜台数)

线程池允许创建的最大线程数量。

当有有新的任务时,当核心线程满了,还没到达最大线程数,并且没有空闲线程,工作队列已满,那就创建一个新线程去执行任务。

  • keepAliveTime:空闲线程存活时间(当柜台没有人时就关闭的时间)

当一个可被回收的线程,空闲时间超过keepAliveTime就会被回收。

可被回收的线程:
① 设置allowCoreThreadTimeout=true的核心线程。
② 大于核心线程数的线程(非核心线程)。

  • unit:空闲线程存活时间

keepAliveTime的时间单位

可选项含义
NANOSECONDS纳秒(1000纳秒 = 1微秒)
MICROSECONDS微秒(1000微妙 = 1毫秒)
MILLISECONDS毫秒(1000毫秒 = 1秒)
SECONDS
MINUTES分钟
HOURS小时
DAYS
  • workQueue:工作队列(等候区,等候区满了的话,就会开放最大柜台数)

用于存放任务的队列

  • handler:拒绝策略(柜台满了,等候区也满了,不接待的策略)

超出线程范围和队列容量的任务的处理程序

3.四大方法

在这里插入图片描述

注意:为什么不推荐使用jdk自带的executors的方式来创建线程池?

图片内容来自阿里开发手册
在这里插入图片描述

① ExecutorService executor = Executors.newCachedThreadPool()

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
优点:可以灵活的回收线程。
缺点:如果创建任务过多,会导致OOM(内存溢出)。

 	public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

② ExecutorService executor = Executors.newFixedThreadPool()

创建一个指定工作线程数量的线程池。
优点:提高线程池的效率和创建线程时的开销。
缺点:因为可能堆积大量请求(队列中),导致OOM。

	//如果核心线程满了,那就存入队列中,提高了线程池效率和创建的线程时的开销,即使空闲也不会被回收,
	public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    
	public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

③ ExecutorService executor = Executors.newSingleThreadPool()

创建一个单线程
优点:可以保证线程的有序执行,如果出现异常,会在创建一个新的线程。
缺点:因为只能创建一个,可能速度没有那么快。

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

④ ExecutorService executor = Executors.newScheduleThreadPool()

创建一个定长的线程池,支持定时及周期性任务执行

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
             new DelayedWorkQueue());
    }

因为这个线程池比较特殊,所以咱们写一下示例增强理解
第一种:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

 scheduledThreadPool.schedule(new Runnable() {
 
      @Override
      public void run() {
         log.info("延长5秒后执行");
       }
 }, 5, TimeUnit.SECONDS);


第二种:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
 
 scheduledThreadPool.schedule(new Runnable() {
 
      @Override
      public void run() {
         log.info("延迟一秒,每5秒执行一次");
       }
 }, 15, TimeUnit.SECONDS);

4.四大拒绝策略

  • new ThreadPoolExecutor.AbortPolicy()
    添加线程池被拒绝,会抛出异常(默认策略)
  • new ThreadPoolExecutor.CallerRunsPolicy()
    添加线程池被拒绝,不会放弃任务,也不会抛出异常,会让调用者线程去执行这个任务(就是不会使用线程池里的线程去执行任务,会让调用线程池的线程去执行)
  • new ThreadPoolExecutor.DiscardPolicy()
    添加线程池被拒绝,丢掉任务,不抛异常。
  • new ThreadPoolExecutor.DiscardOldestPolicy()
    添加线程池被拒绝,会把线程池队列中等待最久的任务放弃,把拒绝任务放进去。

五.线程死锁

1.什么是死锁?

各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进的现象。死锁会浪费大量系统资源,导致系统崩溃,所以我们一定要避免死锁。

2.进程死锁、饥饿、死循环的区别

死锁: 各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进的现象。死锁会浪费大量系统资源,导致系统崩溃,所以我们一定要避免死锁。
饥饿: 线程一直得不到想要的资源(比如:在短进程优先(SPF)算法中,若有源源不断的短进程到来,则长进程将一直得不到处理机,从而发生长进程“饥饿”。)
死循环: 某进程执行过程中一直跳不出某个循环的现象。

3.造成死锁的四个必要条件

  • 互斥:当资源被一个线程占用时,别的线程不能使用。
  • 不可抢占:进程阻塞时,对占用的资源不释放。
  • 不剥夺:进程获得资源未使用完,不能被强行剥夺。
  • 循环等待:若干进程之间形成头尾相连的循环等待资源关系。

3.什么时候会发生死锁?

① 对系统资源的竞争
对不可剥夺资源(磁带机、打印机)的竞争可能导致死锁。像CPU这种可剥夺的资源就不会形成死锁。
② 进程推进顺序非法
请求和释放资源不当,也会导致死锁。比如:线程A和线程B并行占用了资源1和资源2,然后线程A需要资源2,线程B需要资源1,因为资源被对方所占用而阻塞导致死锁。
③ 信号量不当
如在生产者-消费者问题中,若实现互斥的P操作,在实现同步的P操作之前,就会导致死锁。

信号量: 其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量。
一对原语: wait(S) 原语和 signal(S) 原语,简称为P(S)和V(S),可以把原语理解为我们自己写的函数,函数名分别为 wait 和 signal,括号里的信号量 S 其实就是函数调用时传入的一个参数。

生产者-消费者设置互斥和同步
生产者和消费者共享一个初始为空、大小为n的缓冲区:
二者必须互斥的访问缓冲区(互斥)
只有缓冲区没满的时候,生产者才能把产品放入缓冲区,否则必须等待(同步)
只有缓冲区不空时,消费这才能从中取走产品,否则等待(同步

4.怎么解决死锁?

① 预防死锁

  • 破坏互斥
    只允许互斥使用的资源改造成共享使用; 比如:SPOOLing技术;但是很多时候有些资源改不了互斥,有的还要保护互斥。
  • 破坏不剥夺
    Ⅰ 当某个进程请求新的资源被拒绝,那它就释放自己占有的资源,等以后重新申请。就是资源没用完也得释放,破了不剥夺条件。
    Ⅱ 当某个进程需要的资源被其他进程占用时,可以由操作系统协调,进行资源强行剥夺。这种方式一般要考虑每个进程的优先级。
    缺点:
    (1).实现比较复杂。
    (2).释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源。
    (3).反复地申请和释放资源会增加系统开销,降低系统吞吐量(吞吐量是指对网络、设备、端口、虚电路或其他设施,单位时间内成功地传送数据的数量(以比特、字节、分组等测量))。
    (4).如采用 Ⅰ 有可能一直会得不到自己想要的资源,可能会导致饥饿。
  • 破坏请求和保持
    采用静态分配方法:即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不让它投入运行。一旦投入运行后,这些资源就一直归它所有,该进程就不会再请求别的任何资源了。
    缺点:
    因为会一直占有资源,所以资源利用率极低,严重浪费资源。还可能会导致饥饿。
  • 破坏循环等待
    采用顺序资源分配法:给系统资源编号,然后申请时,只能按编号从小到大申请,同类资源必须一次性申请完。
    缺点
    因为资源编号相对固定,所以添加新资源不方便,因为需要重新分配编号。

② 避免死锁

银行家算法: 每一个新进程进入系统时,必须声明需要每种资源的最大数目,其数目不能超过系统所拥有的的资源总量。当进程请求一组资源时,系统必须首先确定是否有足够的资源分配给该进程,若有,再进一步计算在将这些资源分配给进程后,是否会使系统处于不安全状态如果不会才将资源分配给它,否则让进程等待。
安全状态: 指系统按照这种序列分配资源,则每个进程都能顺利完成。只要能找出一个安全序列系统就是安全状态。当然,安全序列可能有多个。

③ 死锁的检测和解决

死锁的检测

Ⅰ.用某种数据结构(图结构)来保存资源的请求和分配信息;
Ⅱ.提供一种算法,利用上述信息来检测系统是否已进入死锁状态。
在这里插入图片描述
(1)在上图中,找出既不阻塞又不是孤点的进程Pi(即找出一条有向边与它相连,且该有向边对应资源的申请数量小于等于系统中已有空闲资源数量。如下图中,R1没有空闲资源,R2有空闲资源。若所有连接该进程的边均满足上述条件,则这个进程能继续运行直至完成,然后释放它所占有的所有资源)。消去它所有的请求边和分配边,使之成为孤立的结点。在下图中,P1是满足这一条件的进程结点,于是将P1的所有边消去。
(2)进程Pi所释放的资源,可用唤醒某些某些因等待这些资源而阻塞的进程,原来的阻塞进程可能变为非阻塞进程。在下图,P2就满足这样的条件。根据1中的方法进行一系列简化后,若能消区图中所有的边,则称该图是完全简化的。
有向边:若顶点Vi到Vj之间的边有方向,则称这条边为有向边。是图结构的名词。

死锁的解决

Ⅰ资源剥夺法: 挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这样资源分配给其他的死锁进程。但是应该防止被挂起的进程长时间得不到资源而饥饿。
Ⅱ 撤销进程法(或终止进程法):强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。如果有的进程已经运行了很长时间了,被撤销,还得从新执行。
Ⅲ 进程回退法: 让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。

如何决定"对谁动手(选择进程解决死锁)"

  • 进程优先级
  • 已执行多长时间
  • 还要多久能完成
  • 进程已经使用了多少资源
  • 进程是交互式的还是批处理式的

五.线程安全

1.线程安全主要是三方面

  • 原子性: 操作不可分割,要么全部执行,并且不能被打断,否则全部不执行。
  • 可见性: 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性: 程序执行的顺序按照代码的先后顺序执行。

2.保证原子性

  • 使用锁 synchronized和 lock。
  • 使用CAS (compareAndSet:比较并交换),CAS是cpu的并发原语)。

3.保证可见性

  • 使用锁 synchronized和 lock。
  • 使用volatile关键字 。

4.保证有序性

  • 使用 volatile 关键字
  • 使用 synchronized 关键字。

5.volatile和synchronized的区别

① volatile仅能使用在变量级别的,synchronized可以使用在变量、方法、类级别的
② volatile不具备原子性,具备可见性,synchronized有原子性和可见性。
③ volatile不会造成线程阻塞,synchronized会造成线程阻塞。
④ volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好。

6.synchronized和lock的区别

① synchronized是关键字,lock是java类,默认是不公平锁(源码)。
② synchronized适合少量同步代码,lock适合大量同步代码。
③ synchronized会自动释放锁,lock必须放在finally中手工unlock释放锁,不然容易死锁。

线程安全,后面我会具体写一下原因和讲解。

相关文章:

  • 昆明网站设计多少钱/兰州seo
  • 网站建设做哪 个会计科目/企业网络推广方案
  • 网站建设厦门/武汉百度推广多少钱
  • 定制网站开发是什么/网上电商怎么做
  • iis 网站名/5151app是交友软件么
  • 搜狐快站怎么做网站/seo服务优化
  • SAP MM 物料分类账的启用配置
  • 搭建wordpress
  • CSDN网站勋章获取介绍
  • SAP MM 新建移动类型(Movement Type)
  • 大数据技术架构(组件)——Hive:环境准备1
  • freemarker包含字符串操作
  • leetcode 1813. 句子相似性 III【python3双指针的实现思路及过程整理】
  • Android启动流程源码分析(基于Android S)
  • Spring Cloud Gateway(黑马springcloud笔记)
  • 【ROS】—— 机器人导航(仿真)—导航实现(十八)[重要][重要][重要]
  • JAVA会员营销系统源码+数据库,实体店铺会员管理和营销系统源码,采用SpringBoot + Mysql+Mybatis
  • 商业智能 BI 跟业务系统的思维差异,跨越和提升