线程可以理解为一个轻量化进程。是进程的最小部分,可以与进程的其他部分(线程)并发执行。
多线程是多个线程并发执行。Java支持多线程,因此它允许应用程序并发执行两个或多个任务。
有两种方式创建线程:
继承Thread类
实现Runnable接口
但是本质上最终都是创建Thread类。
实现Runnable接口被认为是比继承Thread类更好的方法,原因如下:
在说区别之前,需要先了解sleep和wait的作用。
sleep
monitors
,因此如果从同步代码中调用它,其他线程不能进入该块或方法;synchronized(lockedObject) {
Thread.sleep(1000); // 不会释放锁
// 只有当1000ms之后,或者调用interrupt方法,才会唤醒
}
wait
synchronized(lockedObject) {
lockedObject.wait(); // 会释放lockedObject上的锁
//因此,只有其他线程调用notify()或notifyAll(),它才会被唤醒
}
Parameter | wait | sleep |
---|---|---|
是否同步 | 如果不在synchronized中调用wait,它会抛出IllegalMonitorStateException |
It 不需要在同步代码中调用。 |
调用方式 | wait方法是对Object的操作,定义在Object类中 | sleep方法操作的是线程,所以定义在Thread类中 |
释放锁 | 调用wait方法会释放锁 | sleep方法不会释放锁 |
唤醒条件 | 调用notify()或notifyAll() | 睡眠时间到达或者调用interrupt() |
是否静态 | wait方法不是静态方法 | sleep是静态方法 |
线程wait和notify是基于同一个对象锁进行的等待和通知机制。
如果wait(), notify()和notifyAll()定义在线程类中,那么每个线程都必须知道另一个线程的状态,这是没有意义的,因为每个线程都是独立于其他线程运行的,并且对其他线程没有特定的标识。
调用wait()
线程就可以等待,线程必须放弃锁。
要放弃锁,线程必须首先拥有它。而获得锁是进入同步上下文的前提。
如果在同步上下文之外调用wait方法,那么它将抛出IllegalMonitorStateException
。
在java中有5种线程状态:
New:当创建一个线程对象还没有激活时的状态。
Runnable:当调用线程的start方法时,线程进入runnable状态。它是立即执行还是经过一段时间后执行,取决于线程调度程序。
Run:当线程正在执行时,它进入运行状态。
Block:当线程等待某些资源或其他线程完成时,它将进入阻塞状态。
Dead:当线程的run方法执行完毕后时,线程进入dead状态。
不能直接调用run方法来启动线程。你需要调用start方法来启动一个新线程。
直接调用run方法,不会创建一个新线程,会在主线程中直接执行。
不可以,一旦启动了一个线程,它就不能再启动了。 如果尝试再次启动线程,它将抛出IllegalThreadStateException
。
可以使用join()方法来实现。
守护线程是为用户线程提供服务的低优先级后台线程。 它的生命取决于用户线程。 如果没有用户线程在运行,那么即使守护进程线程在运行,JVM也可以退出。 JVM不会等待守护线程完成。
setDaemon(true)
方法可以将用户线程改为守护进程。
同步是将对共享资源的访问限制在一个线程的能力。 当两个或多个线程需要访问共享资源时,必须有某种机制使共享资源只被一个线程使用。 这种机制称为同步。
可以通过一个例子来更好的理解同步。
假设我们要对一个url的请求次数进行统计,我们通过下面代码来实现:
非同步方式
public class RequestCounter {
private int count;
public int incrementCount()
{
count++;
return count;
}
}
这是没有同步机制的实现方式,如果有两个请求同时到达时,如线程T1在count=10时加1,同时线程T2也对count=10加一,最后的结果会是11,但是实际上的请求数是12,造成数据不一致。
同步方式
同步可以通过synchronized代码块或或者synchronized方法。
public class RequestCounter {
private int count;
public synchronized int incrementCount()
{
count++;
return count;
}
}
这种方式下,同一时间只能有一个线程执行incrementCount()
,另一个线程只能等获得锁的线程执行完成释放锁之后执行。
public class RequestCounter {
private int count;
public int incrementCount() {
synchronized (this) {
count++;
return count;
}
}
}
对象锁
对象锁定意味着需要同步非静态方法或块,以便它一次只能被该实例的一个线程访问。如果您保护非静态数据,则使用它。
同步方法和同步代码块使用的是对象锁。
public synchronized int incrementCount(){
}
同步代码块
public int incrementCount() {
synchronized (this) {
count++;
return count;
}
}
或者在其他对象上使用synchronized
块:
private final Object lock=new Object();
public int incrementCount() {
synchronized (lock) {
count++;
return count;
}
}
类锁
类级锁定意味着需要同步静态方法或静态方法中的块,以便整个类中只有一个线程可以访问它。如果你有10个类的实例,那么只有一个线程一次只能访问一个实例的一个方法或块。如果想保护静态数据,则使用它。
静态同步方法
public static synchronized int incrementCount(){
}
在class上使用同步代码块:
public int incrementCount() {
synchronized (RequestCounter.class) {
count++;
return count;
}
}
可以,因为两个线程获得不同对象上的锁,所以它们可以并发执行而不会有任何问题。
可以,因为一个线程需要锁才能进入同步块,而第二个线程执行的是非同步方法不需要任何锁,所以它可以并发执行。
是的,从一个同步方法调用另一个同步方法是安全的,因为当你调用synchronized
方法时,你会得到这个对象的锁,当你调用同一个类的另一个同步方法时,它是安全的,因为它已经有了这个对象的锁。同步方法使用的是同一个对象锁。
死锁是两个或多个线程相互等待对方释放资源的情况。
线程1对对象1进行了锁定,并等待对对象2进行锁定。线程2对对象2有锁定,并等待对对象1获得锁定。在这个场景中,两个线程将无限期地等待对方。
notify
当调用对象上的notify方法时,它会唤醒一个等待该对象的线程。因此,如果多个线程正在等待一个对象,它将唤醒其中一个。具体唤醒哪一个取决于操作系统。
notifyAll
notifyAll将唤醒所有等待该对象的线程,而notify只唤醒一个线程。
如果将任何变量设置为volatile,那么该变量将从主内存中读取,而不是从CPU缓存中读取,这样每个线程都将获得更新后的变量值。
volatile可以保证变量的可见性和有序性,但是不保证原子性。
两个线程,T1和T2。您需要使用一个线程打印奇数,使用另一个线程打印偶数。
可以通过wait和notify解决这个问题。
class PrintOddEven implements Runnable{
public int MAX_NUMBER =10;
static int number=1;
int rem;
static Object lock=new Object();
PrintOddEven(int remainder)
{
this.rem =remainder;
}
@Override
public void run() {
while (number < MAX_NUMBER) {
synchronized (lock) {
while (number % 2 != rem) { // wait
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " " + number);
number++;
lock.notifyAll();
}
}
}
}
public class OddEvenMain {
public static void main(String[] args) {
PrintOddEven oddRunnable=new PrintOddEven(1);
PrintOddEven evenRunnable=new PrintOddEven(0);
Thread t1=new Thread(oddRunnable,"T1");
Thread t2=new Thread(evenRunnable,"T2");
t1.start();
t2.start();
}
}
TheadLocal可以理解为线程的本地变量。用于存放只能由当前线程读写的变量。两个线程不能看到彼此的ThreadLocal变量,因此即使它们正在执行相同的代码,也不会存在任何竞争条件,代码将是线程安全的。
比如下面代码中的ThreadLocalRunnable中有一个用于存放本地变量的ThreadLocal tl
,在run方法中将变量存放到tl中:
public class ThreadLocalRunnable implements Runnable {
// ThreadLocal of Integer type
private ThreadLocal<Integer> tl = new ThreadLocal<Integer>();
@Override
public void run() {
tl.set( (int) (Math.random() * 10) );
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName()+":"+tl.get());
}
}
创建一个主类:
public class ThreadLocalMain {
public static void main(String[] args) throws InterruptedException {
ThreadLocalRunnable tl = new ThreadLocalRunnable();
Thread t1 = new Thread(tl,"Thread1");
Thread t2 = new Thread(tl,"Thread2");
t1.start();
t2.start();
t1.join();
t2.join();
}
}
输出内容为:
Thread2:6
Thread1:3
t1和t2共享相同的ThreadLocalRunnable实例,并且都为ThreadLocal变量设置了不同的值。如果我们使用synchronized
而不是ThreadLocal
,那么厚执行的线程将覆盖由第一个线程设置的值。
thread dump是进程中所有活动线程的快照。它包含了大量关于线程及其当前状态的信息。
当出现死锁问题时时,thread dump非常有用。
可以使用jdk工具,如jvisualVM,jstack和Java Mission control打印出thread dump信息。
Java 5引入了executor框架,用于管理线程。
太多的手动创建线程对于线程的管理非常不便,如果在应用中创建数千个线程,应用程序的性能也会受到影响,而且每个线程的维护也会带来开销。线程的创建和销毁也会有很大的系统开销。
Executor框架用来解决限制线程数量和线程重复使用的问题。
线程池表示一组正在等待作业的工作线程,这些工作线程可以被多次重用。
每当一个任务需要执行时,将从线程池中取出一个线程来执行该任务。如果没有可用的工作线程,则任务必须等待执行。
java.util.concurrent.Executors
类提供创建线程池的工厂方法。
newFixedThreadPool:此方法返回最大大小固定的线程池。如果所有线程都在忙着执行任务,并且提交了额外的任务,那么它们必须在队列中等待,直到有线程可用来执行这些任务。
newCachedThreadPool:此方法返回未绑定的线程池。如果线程在确定的时间(keepAliveTime)内没有被使用,那么它将杀死的线程。
newSingleThreadedExecutor:这个方法返回一个带有单线程的Executor。
newScheduledThreadPool:此方法返回大小固定的线程池,该线程池可以定期或以给定的延迟调度任务。
BlockingQueue是一种特殊的队列类型,用于生产者线程生产对象和消费者线程消费对象。
生产者线程一直往队列中插入对象,一旦它满了,线程将被阻塞,除非消费线程开始消费。
类似地,消费者线程会一直消费对象,直到它为空为止。一旦它为空,它将被阻塞,除非生成线程开始生成。
在java 1.0版本中引入,而Callable是Runnable的扩展版本,是在java 1.5中引入的,以解决Runnable的限制。
Runnable的run()
不返回任何值;它的返回类型是void,而Callable有一个返回类型。
因此,在Callable任务完成后,可以使用Future类的get()方法获取结果。Future类有各种方法,如get()、cancel()和isDone(),通过这些方法,您可以获得或执行与任务相关的各种操作。
//Runnable 接口的run方法没有返回值
public void run();
//Callable 接口的call方法返回值类型是泛型V
V call() throws Exception;
Callable是一个泛型接口,这意味着实现类将决定它将返回的值的类型。
Runnable不抛出受检异常,而Callable抛出异常,因此,在编译时我们可以识别错误。
在Runnable中,我们覆盖run()方法,而在Callable中,我们需要覆盖call()方法。
对于Callable,我们不能将Callable传递给Thread执行,所以我们需要使用ExecutorService来执行Callable对象。
java.util.concurrent.lock.Lock
在Java 1.5中引入,它为线程同步提供了一系列重要的操作。比标准的同步处理方式更灵活方便。
Lock的有点:
Condition实例本质上是绑定到Lock上的。可以使用Lock接口的newcondition()方法获取Condition实例。
Condition用于对一个锁创建多个排队条件。例如:
在生产者消费者问题中,如果缓冲区已满,生产者可以在Condition实例notEmpty条件下等待;如果缓冲区为空,消费者可以z在Condition实例notFull条件下等待,知道被唤醒。
如果缓冲区中的空间可用,消费者可以使用条件实例notEmpty向生产者发送信号。类似地,当生产者开始向缓冲区添加元素时,它可以在条件实例notFull情况下通知消费者。
创建一个名为CustomBlockingQueue的类表示我们的自定义队列:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class CustomBlockingQueue {
final Lock lock = new ReentrantLock();
// 表示队列未满和未空的条件
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
// 用来存放元素
final Object[] arr = new Object[3];
int putIndex, takeIndex;
int count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == arr.length){
// 队列满了,生产者等待
notFull.await();
}
arr[putIndex] = x;
System.out.println("Putting in Queue - " + x);
putIndex++;
if (putIndex == arr.length){
putIndex = 0;
}
// 增加计数
++count;
// 通知消费者可以消费
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0){
// 队列空了,消费者等待
notEmpty.await();
}
Object x = arr[takeIndex];
System.out.println("Taking from queue - " + x);
takeIndex++;
if (takeIndex == arr.length){
takeIndex = 0;
}
// 减少计数
--count;
// 通知生产者可以生产
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
创建一个主类使用自定义队列:
public class CustomBlockingQueueMain {
public static void main(String[] args) {
CustomBlockingQueue customBlockingQueue = new CustomBlockingQueue();
// Creating producer and consumer threads
Thread producer = new Thread(new Producer(customBlockingQueue));
Thread consumer = new Thread(new Consumer(customBlockingQueue));
producer.start();
consumer.start();
}
}
class Producer implements Runnable {
private CustomBlockingQueue cbq;
public Producer(CustomBlockingQueue cbq){
this.cbq = cbq;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
cbq.put(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private CustomBlockingQueue cbq;
public Consumer(CustomBlockingQueue cbq){
this.cbq = cbq;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
cbq.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
输出结果:
Putting in Queue – 1
Putting in Queue – 2
Putting in Queue – 3
Taking from queue – 1
Taking from queue – 2
Taking from queue – 3
Putting in Queue – 4
Putting in Queue – 5
Taking from queue – 4
Taking from queue – 5
当数组大小为3时,在CustomBlockingQueue中放入3个元素后,Producer线程被阻塞了。
CountDownLatch是一种同步工具,允许一个或多个线程等待其他线程中的操作完成。
CountDownLatch初始化时指定一个count。每当一个线程调用latch.await()
,将会一直等待,直到count变为0或线程被另一个线程中断。
当其他线程调用latch.countDown()
时,count会减少1。一旦count达到0,调用了latch.await()
的线程将被被唤醒。
CyclicBarrier与CountDownLatch类似,但是当count减到0时可以重用。特殊的地方是可以循环使用,而CountDownLatch只能使用一次。
使用CyclicBarrier还可以在计数达到0时触发公共事件。
当我们想要限制访问资源的并发线程的数量时,我们可以使用Semaphore。
Semaphore维护一组许可,如果许可不可用,线程必须等待。
信号量可以用于实现资源池或有界集合。
以上是关于java中的多线程并发相关的面试问题。