面试知识题库

这里写目录标题

1.并发编程(高并发、多线程)

1.1 并发编程的三个必要因素

  • 原子性:原子, 即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功, 要么全部执行失败。
  • 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
  • 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)

1.2 Java程序中怎么保证多线程的运行安全?

  • 线程切换带来的原子性问题解决办法:使用多线程之间同步synchronized或使用锁(lock)。
  • 缓存导致的可见性问题解决办法:synchronized、volatile、LOCK,可以解决可见性问题
  • 编译优化带来的有序性问题解决办法:Happens-Before 规则可以解决有序性问题

1.3 volatile的作用

1.3.1 保证共享变量的可见性

一、共享变量的可见性问题
共享变量存储在主内存中, 各个线程在使用共享变量时都会先将共享变量复制进当前线程的工作内存中, 后续使用到该变量时, 直接从当前线程工作内存中获取变量值,此时如果其他线程更改了该共享变量值, 那么当前线程无法实时更新到该变量的最新值。
在这里插入图片描述

二、volatile如何保证变量的可见性(原理)
将共享变量声明为volatile后, 所有对共享变量的写入都会立即写入主内存, 所有对共享变量的读取都从主内存中读取

1.3.2 禁止指令重排

1.3.3 volatile 变量和 atomic 变量有什么不同?

volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。

而 AtomicInteger 类提供的atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。


1.3.4 volatile 能使得一个非原子操作变成原子操作吗?

关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同
一个实例变量需要加锁进行同步。


1.4 死锁

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

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻
塞,因此程序不可能正常终止。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
请添加图片描述

死锁代码:

public class DeadLockDemo {
	private static String A = "A";
	private static String B = "B";
	
	public static void main(String[] args) {
	
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized (A) {
					// 休眠2秒,等待t2线程锁了B
					try {
						Thread.currentThread().sleep(2000);
					} catch (Exception e) {
						e.printStackTrace();
					}
					// 等待t2线程释放B
					synchronized (B) {
						System.out.println("BBBBBBBBBBBBb");
					}
				}
			}
		});
		
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				synchronized (B) {
					// 等待t1线程释放A
					synchronized (A) {
						System.out.println("AAAAAAAA");
					}
				}
			}
		});
		
		t1.start();
		t2.start();
	}
}

1.4.1 形成死锁的4个必要条件

  • 互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等
    待,直至占有资源的进程用毕释放。
  • 占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
  • 不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在等B,B在等C,C在等A)

1.4.2 如何避免线程死锁

  • 避免一个线程同时获得多个锁
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
  • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制

1.5 线程的状态

请添加图片描述

  1. 新建: 用new关键字创建一个线程时,还没调用start 就是新建状态。
  2. 就绪: 调用了 start 方法之后,线程就进入了就绪阶段。此时,线程不会立即执行run方法,需要等待获取CPU资源。
  3. 运行: 当线程获得CPU时间片后,就会进入运行状态,开始执行run方法。
  4. 阻塞: 当遇到以下几种情况,线程会从运行状态进入到阻塞状态。
    • 调用sleep方法,使线程睡眠
    • 调用wait方法,使线程进入等待
    • 当线程去获取同步锁的时候,锁正在被其他线程持有
    • 调用阻塞式IO方法时会导致线程阻塞。
    • 调用suspend方法,挂起线程,也会造成阻塞。
  5. 死亡: 当run方法正常执行结束时,或者由于某种原因抛出异常都会使线程进入死亡状态。另外,直接调用stop方法也会停止线程。但是,此方法已经被弃用,不推荐使用。

其中需要注意:阻塞状态只能进入就绪状态,不能直接进入运行状态。因为,从就绪状态到运行状态的切换是不受线程自己控制的,而是由线程调度器所决定。只有当线程获得了CPU时间片之后,才会进入运行状态。


1.5.1 sleep和wait的区别

sleep和wait都是用来将线程进入阻塞状态的, 但是两者存在很大的差别

一、所属类不同

  • wait是Object的方法,任何对象实例都能调用
  • sleep是Thread的静态方法

二、释放锁资源不同

  • sleep不会释放锁,它也不需要占用锁
  • wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
-------------------------- 测试 sleep 不释放锁 ----------------------------
public static void main(String[] args) throws InterruptedException {
    Object lock = new Object();
    new Thread(() -> {
        synchronized (lock) {
            System.out.println("新线程获取到锁:" + LocalDateTime.now());
            try {
                // 休眠 2s
                Thread.sleep(2000);
                System.out.println("新线程获释放锁:" + LocalDateTime.now());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
    // 等新线程先获得锁
    Thread.sleep(200);
    System.out.println("主线程尝试获取锁:" + LocalDateTime.now());
    // 在新线程休眠之后,尝试获取锁
    synchronized (lock) {
        System.out.println("主线程获取到锁:" + LocalDateTime.now());
    }
}

-------------------------- 测试 wait 释放锁 ----------------------------
public static void main(String[] args) throws InterruptedException {
    Object lock = new Object();
    new Thread(() -> {
        synchronized (lock) {
            System.out.println("新线程获取到锁:" + LocalDateTime.now());
            try {
                // 休眠 2s
                lock.wait(2000);
                System.out.println("新线程获释放锁:" + LocalDateTime.now());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }).start();
    // 等新线程先获得锁
    Thread.sleep(200);
    System.out.println("主线程尝试获取锁:" + LocalDateTime.now());
    // 在新线程休眠之后,尝试获取锁
    synchronized (lock) {
        System.out.println("主线程获取到锁:" + LocalDateTime.now());
    }
}

  Thread.Sleep(2000) 意思是在未来的2000毫秒内本线程不参与CPU竞争,2000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去。另外值得一提的是Thread.Sleep(0)的作用,就是触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。

  wait(2000)表示将锁释放2000毫秒,到时间后如果锁没有被其他线程占用,则再次得到锁,然后wait方法结束,执行后面的代码,如果锁被其他线程占用,则等待其他线程释放锁。注意,设置了超时时间的wait方法一旦过了超时时间,并不需要其他线程执行notify也能自动解除阻塞,但是如果没设置超时时间的wait方法必须等待其他线程执行notify。

三、唤醒方式不同

  1. sleep 方法具有主动唤醒功能
  2. 而不传递任何参数的 wait 方法只能被动的被唤醒。

wait与sleep的讲解(wait有参及无参区别)


1.5.2 wait的使用

一、wait() 与wait( long timeout ) 区别

  • wait( long timeout) :当线程超过了设置时间之后,自动恢复执行;
  • wait( long timeout): 线程会一直等待, 直到被notify或ontifyAll进行唤醒
public class WaitDemo4 {
    public static void main(String[] args) {
        Object lock = new Object();
        Object lock2 = new Object();
        new Thread(() -> {
            System.out.println("线程1: 开始执行" + LocalDateTime.now());
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程1: 执行完成" + LocalDateTime.now());
            }
        },"无参wait线程").start();
 
        new Thread(() -> {
            System.out.println("线程2: 开始执行" + LocalDateTime.now());
            synchronized (lock2) {
                try {
                    lock2.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程2: 执行完成" + LocalDateTime.now());
            }
        },"有参wait线程").start();
    }
}

输出:
线程1: 开始执行2023-03-16T19:59:31.246995600
线程2: 开始执行2023-03-16T19:59:31.246995600
线程2: 执行完成2023-03-16T19:59:32.249342200

二、notify、notifyAll唤醒wait的线程

  1. 无论是有参的wait方法还是无参的wait方法,它都可以使用当前线程进入休眠状态。
  2. 无论是有参的wait方法还是无参的wait方法,它都可以使用notify / ontifyAll进行唤醒。

测试notify唤醒线程

public class WaitDemo5 {
    public static void main(String[] args) {
        Object lock = new Object();
        Object lock2 = new Object();
        new Thread(() -> {
            synchronized (lock2) {
                System.out.println("线程2: 开始执行" + LocalDateTime.now());
                try {
                    lock2.wait(60 * 60 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程2: 执行完成" + LocalDateTime.now());
            }
        },"有参wait线程").start();
 
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2) {
                System.out.println("唤醒线程2");
                lock2.notify();
            }
        }).start();
    }
}

输出:
线程2: 开始执行2022-04-12T12:28:23.200
唤醒线程2
线程2: 执行完成2022-04-12T12:28:24.169

测试notifyAll唤醒线程

public class WaitDemo6 {
    public static void main(String[] args) {
        Object lock = new Object();
 
        new Thread(() -> {
            System.out.println("线程1: 开始执行" + LocalDateTime.now());
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程1: 执行完成" + LocalDateTime.now());
            }
        },"无参wait线程").start();
 
        new Thread(() -> {
            System.out.println("线程2: 开始执行" + LocalDateTime.now());
            synchronized (lock) {
                try {
                    lock.wait(60 * 60 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程2: 执行完成" + LocalDateTime.now());
            }
        },"有参wait线程").start();
 
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock) {
                System.out.println("唤醒所有线程");
                lock.notifyAll();
            }
        }).start();
    }
}
 
输出:
线程1: 开始执行2022-04-12T12:34:34.317
线程2: 开始执行2022-04-12T12:34:34.317
唤醒所有线程
线程2: 执行完成2022-04-12T12:34:35.295
线程1: 执行完成2022-04-12T12:34:35.295

1.5.3 为什么wait、notify、notifyAll跟synchronized一起使用?

  • Object.wait():释放当前对象锁,并进入阻塞队列
  • Object.notify():唤醒当前对象阻塞队列里的任一线程(并不保证唤醒哪一个)
  • Object.notifyAll():唤醒当前对象阻塞队列里的所有线程

为什么这三个方法要与synchronized一起使用呢?解释这个问题之前,我们先要了解几个知识点
JVM 在运行时会强制检查 wait 和 notify 有没有在 synchronized 代码中,如果没有的话就会报非法监视器状态异常(IllegalMonitorStateException),但这也仅仅是运行时的程序表象,那为什么 Java 要这样设计呢?其实这样设计的原因就是为了防止多线程并发运行时,程序的执行混乱问题。

wait和notify问题复现
我们假设 wait 和 notify 可以不加锁,我们用它们来实现一个自定义阻塞队列。这里的阻塞队列是指读操作阻塞,也就是当读取数据时,如果有数据就返回数据,如果没有数据则阻塞等待数据,实现代码如下:

class MyBlockingQueue {
	// 用来保存数据的集合
	Queue<String> queue = new LinkedList<>();

	/**
	 * 添加方法
	 */
	public void put(String data) {
	    // 队列加入数据
	    queue.add(data); 
	    // 唤醒线程继续执行(这里的线程指的是执行 take 方法的线程)
	    notify(); // 步骤三
	}

	/**
	 * 获取方法(阻塞式执行)
	 * 如果队列里面有数据则返回数据,如果没有数据就阻塞等待数据
	 * @return
	 */
	public String take() throws InterruptedException {
	    // 使用 while 判断是否有数据(这里使用 while 而非 if 是为了防止虚假唤醒)
	    while (queue.isEmpty()) { // 步骤一 
	        // 没有任务,先阻塞等待
	        wait(); // 步骤二
	    }
    return queue.remove(); // 返回数据
}

注意上述代码,我们在代码中标识了三个关键执行步骤:①:判断队列中是否有数据;②:执行 wait 休眠操作;③:给队列中添加数据并唤醒阻塞线程。如果不强制要求添加 synchronized,那么就会出现如下问题:

步骤线程1线程2
1执行步骤一判断当前队列中没有数据
2执行步骤三将数据添加到队列,并唤醒线程1继续执行
3执行步骤二线程1进入休眠状态

如果 wait 和 notify 不强制要求加锁,那么在线程 1 执行完判断之后,尚未执行休眠之前,此时另一个线程添加数据到队列中。然而这时线程 1 已经执行过判断了,所以就会直接进入休眠状态,从而导致队列中的那条数据永久性不能被读取,这就是程序并发运行时“执行结果混乱”的问题。

然而如果配合 synchronized 一起使用的话,代码就会变成以下这样:

class MyBlockingQueue {
    // 用来保存任务的集合
    Queue<String> queue = new LinkedList<>();
 
    /**
     * 添加方法
     */
    public void put(String data) {
        synchronized (MyBlockingQueue.class) {
            // 队列加入数据
            queue.add(data);
            // 为了防止 take 方法阻塞休眠,这里需要调用唤醒方法 notify
            notify(); // 步骤三
        }
    }
 
    /**
     * 获取方法(阻塞式执行)
     * 如果队列里面有数据则返回数据,如果没有数据就阻塞等待数据
     * @return
     */
    public String take() throws InterruptedException {
        synchronized (MyBlockingQueue.class) {
            // 使用 while 判断是否有数据(这里使用 while 而非 if 是为了防止虚假唤醒)
            while (queue.isEmpty()) {  // 步骤一
                // 没有任务,先阻塞等待
                wait(); // 步骤二
            }
        }
        return queue.remove(); // 返回数据
    }
}

这样改造之后,关键步骤 ① 和关键步骤 ② 就可以一起执行了,从而当线程执行了步骤 ③ 之后,线程 1 就可以读取到队列中的那条数据了,它们的执行流程如下:

步骤线程1线程2
1执行步骤一判断当前队列没有数据
2执行步骤二线程进入休眠状态
3执行步骤三将数据添加到队列,并执行唤醒操作
4线程被唤醒,继续执行
5判断队列中有数据,返回数据

总结
本文介绍了 wait 和 notify 的基础使用,以及为什么 wait 和 notify/notifyAll 一定要配合 synchronized 使用的原因。如果 wait 和 notify/notifyAll 不强制和 synchronized 一起使用,那么在多线程执行时,就会出现 wait 执行了一半,然后又执行了添加数据和 notify 的操作,从而导致线程一直休眠的缺陷。


1.5.4 yield的作用

和 sleep 一样都是 Thread 类的方法,都是暂停当前正在执行的线程对象,不会释放资源锁,和 sleep 不同的是 yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。还有一点和 sleep 不同的是 yield 方法只能使同优先级或更高优先级的线程有执行的机会
浅谈sleep、wait、yield、join区别


1.5.5 为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?

因为Java所有类的都继承了Object,Java想让任何对象都可以作为锁

并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。


1.6 创建线程的四种方式

  • 继承 Thread 类;
  • 实现 Runnable 接口
  • 实现 Callable 接口
  • 使用匿名内部类方式
// 继承 Thread 类
public class MyThread extends Thread {
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + " run()方法正在执行..."
	);
}

// 实现 Runnable 接口
public class MyRunnable implements Runnable {
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + " run()方法执行中..."
	);
}

// 实现 Callable 接口
public class MyCallable implements Callable<Integer> {
	@Override
	public Integer call() {
		System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
		return 1;
	}
}

// 使用匿名内部类方式
public class CreateRunnable {
	public static void main(String[] args) {
		//创建多线程创建开始
		Thread thread = new Thread(new Runnable() {
			public void run() {
				for (int i = 0; i < 10; i++) {
					System.out.println("i:" + i);
				}
			}
		});
		thread.start();
	}
}

1.6.1 runnable和callable的区别

  • 相同点:
    • 两者都需要调用Thread.start()启动线程
  • 不同点
    • Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、
      FutureTask配合可以用来获取异步执行的结果
    • Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;

一、获取Callable的call()方法的返回值
如果我们想要拿到线程的执行结果(返回值), 就需要等待这个线程执行结束, 需要使用FutureTask的get()方法等待子线程结束后返回结果, 在这个阶段主线程是阻塞的

public class CallableTest implements Callable<String> {

    private String str;

    public CallableTest(String str) {
        this.str = str;
    }

    @Override
    public String call() throws Exception {
        //任务阻塞5秒,异常向上抛出
        Thread.sleep(5000);
        return this.str;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> callable = new CallableTest("my callable is ok!");
        FutureTask<String> task = new FutureTask<String>(callable);

        // 创建线程
        new Thread(task).start();
        // 调用get()方法阻塞主线程
        String str = task.get();
        System.out.println("hello :" + str);
    }
}
  • 使用了FutureTask中的get方法就可以实现线程的同步等待
  • Callable的call()可以throws Exception, 需要配置FutureTask中的get()方法使用, 才能在外部线程中捕获到异常, 不然外部线程都执行结束了, 你再抛异常就没有线程能捕获到这个异常了
  • 而runnable的run()方法中是不能抛异常的, 所以只能在run方法中内部处理异常

1.6.2 为什么Callable需要FutureTask包装一下?

因为Thread类的构造方法需要的参数,是Runnable

public Thread(Runnable target) {
   init(null, target, "Thread-" + nextThreadNum(), 0);
}
  • 首先可以通过FutureTask的构造方法,传入Callable接口的实例,构造FutureTask对象
  • 由于FutureTask间接实现了Runnable接口,同时Thread类的构造方法要求放入一个Runnable,这时候就可以放入当前构造的FutureTask对象

1.6.3 在主线程中等待子线程结束的7种方式

一、while循环
不断的轮巡子线程是否存活

public static void main(String[] args)
	// 启动线程
	Thread t = new Thread(() -> {
		......
	};
	t.start();
	
	// 当线程结束后, 就会跳出while循环, 也代表了子线程执行结束
	while(t.isAlive() == true){
		System.out.println("子线程还在执行");
		
		// 加入sleep, 减少对cpu性能的消耗
		try {
	        Thread.sleep(10);
	    }catch (InterruptedException e){
	        e.printStackTrace();
	    }
	}
	System.out.println("子线程执行结束");
}

二、Thread的join方法
作用:join方法会挂起调用线程(当前线程)的执行, 等待被调用的对象完成它的执行

public static void main(String[] args)
	// 启动线程
	Thread t = new Thread(() -> {
		......
	};
	t.start();
	
	// 等待t线程执行结束
	t.join();
	System.out.println("子线程执行结束");
}

1.7 线程的 run()和 start()有什么区别?

  • 用 start方法来启动线程, 是真正实现了多线程, 这时此线程处于就绪(可运行)状态, 接着会尝试去获取cpu时间片, 当线程获得CPU时间片后,就会进入运行状态,开始执行run方法。
  • run方法,只是类的一个普通方法

1.8 Future是什么?

Future表示一个可能还没有完成的异步任务的结果, 通过实现Callback接口,并用Future可以来接收多线程的执行结果。请添加图片描述
《Java线程详解》这篇文章中,介绍了创建一个Java线程的三种方式,其中继承Thread类或实现Runnable接口都可以创建线程,但这两种方法都有一个问题就是:没有返回值,不能获取执行完的结果。因此后面在JDK1.5才新增了一个Callable接口来解决上面的问题,而Future和FutureTask就可以与Callable配合起来使用。

Callable只能在线程池中提交任务使用,且只能在submit()invokeAnay()以及invokeAll()这三个任务提交的方法中使用,如果需要直接使用Thread的方式启动线程,则需要使用FutureTask对象作为Thread的构造参数,而FutureTask的构造参数又是Callable的对象

Future的局限性:

  • 并发执行多任务:Future只提供了get()方法来获取结果,并且是阻塞的。所以,除了等待你别无他法;
  • 无法对多个任务进行链式调用:如果你希望在计算任务完成后执行特定动作,比如发邮件,但Future却没有提供这样的能力;
  • 无法组合多个任务:如果你运行了10个任务,并期望在它们全部执行结束后执行特定动作,那么在Future中这是无能为力的;
  • 没有异常处理:Future接口中没有关于异常处理的方法;

CompletionService使用与源码分析


1.8.1 CompletableFuture

CompletableFuture使用详解


1.9 java中用到的线程调度算法是什么?

计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务。在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权。(Java是由JVM中的线程计数器来实现线程调度)

有两种调度模型:

  • 分时调度模型
    分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU的时间片这个也比较好理解
  • 抢占式调度模型。
    是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线
    程会一直运行,直至它不得不放弃 CPU。

1.10 如何停止一个正在运行的线程?

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  2. 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期
    作废的方法。
  3. 使用interrupt方法中断线程。

1.11 java中垃圾回收的作用?什么时候进行垃圾回收?

作用:识别并丢弃应用中不再使用的对象, 用来释放和重用资源

哪些对象会被回收?

  • 内存中没有引用的对象
  • 超过作用域的对象

1.12 线程之间如何通信及线程之间如何同步

通信是指线程之间以如何来交换信息。一般线程之间的通信机制有两种:

  • 共享内存(Java的并发采用的是共享内存模型)
  • 消息传递

1.13 什么是指令重排

程序执行的顺序按照代码的先后顺序执行。一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序)

  • 不保证程序中各个语句的执行先后顺序同代码中的顺序一致
  • 保证程序最终执行结果和代码顺序执行的结果是一致的。
int a = 5; //语句1
int r = 3; //语句2
a = a + 2; //语句3
r = a*a; //语句4

则因为重排序,他还可能执行顺序为(这里标注的是语句的执行顺序) 2-1-3-4,1-3-2-4 但绝不
可能 2-1-4-3,因为这打破了依赖关系。

显然重排序对单线程运行是不会有任何问题,但是多线程就不一定了,所以我们在多线程编程时就
得考虑这个问题了


1.14 当一个线程进入一个对象的synchronized方法A之后, 其它线程是否可进入此对象的synchronized方法B

不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A 方法说明对象锁已经被取走,那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁


1.15 synchronized 和 Lock 有什么区别?

  • 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
  • synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
  • synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有unLock()去释放锁就会造成死锁。
  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

1.16 Lock 接口和synchronized 对比同步它有什么优势?

Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。

优势有:

  • 可以使锁更公平
  • 可以使线程在等待锁的时候响应中断
  • 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
  • 可以在不同的范围,以不同的顺序获取和释放锁

整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。


1.17 乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

  • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。(拿数据的时候上锁)
  • 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是
    在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。(更新时判断)

1.18 什么是线程池?

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来许多好处。

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降
    低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用

1.19 线程池作用?

线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。

如果一个线程所需要执行的时间非常长的话,就没必要用线程池了(不是不能作长时间操作,而是不宜。本来降低线程创建和销毁,结果你那么久我还不好控制还不如直接创建线程),况且我们还不能控制线程池中线程的开始、挂起、和中止。


1.20 线程池有什么优点?

  • 降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
  • 提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值