多线程相关

1、sleep()和wait()的区别:

1)前者是Thread类中静态方法,后者是Object中的方法

2)前者不释放对象锁,在指定的时间后恢复。后者释放对象锁,并进入等待池中,只有其他线程调用该同步监视器的notify或notifyAll此线程才恢复到准备状态

3)前者可以在任何地方使用,并会抛出InterruptedException,后者必须放在synchronized块中或者在同步方法中否则抛出IllegalMonitorStateException。

sleep()和yield()的区别:

1)前者会暂停当前线程,把cpu让出给其他线程,不理会其他线程优先级,后者只会让给优先级大于等于当前线程的线程。

2)前者会让线程进入阻塞状态,经过一段时间后才会进入就绪状态,后者直接进入就绪状态,完全可能某个线程调用yield()暂停后,立即再次获得处理器资源被执行。

3)前者抛出InterruptedException,后者没有异常

sleep方法底层调用了操作系统的sleep方法,操作系统把当前线程挂起,把cpu执行权让给其他线程,操作系统也会设置定时器,当定时器到了之后就再次唤醒这个线程,如果调用了Thread.sleep(0),也会触发线程调度的切换,线程会从运行态转为就绪态,然后操作系统根据线程优先级选择一个线程执行。

2、生产者消费者模型

用blockingqueue实现,阻塞队列特点:作为线程同步的工具,当使用offer方法时,如果该队列元素已经满,则阻塞该线程。当使用poll方法,如果该队列已经空了,则阻塞该线程。程序的两个线程通过交替向阻塞对列放入取出元素,即可很好的控制线程的通信。通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率。同时解耦,意味着两者之间的联系少,互相不会制约

class Producer extends Thread {
private Queue<Integer> queue;
private int maxSize;
private String name;
private int i;

public Producer(String name, Queue<Integer> queue, int maxSize) {
super(name);
this.name = name;
this.queue = queue;
this.maxSize = maxSize;
this.i = 0;
}

@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.size() == maxSize) {
try {
System.out.println("queue is full, producer[" + name + "] waiting for consumer to take ");
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
System.out.println("[" + name + "] producing value:" + i);
queue.offer(i++);
queue.notifyAll();
}
try {
Thread.sleep(1000L);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}

 

class Consumer extends Thread {
private Queue<Integer> queue;
private int maxSize;
private String name;

public Consumer(String name, Queue<Integer> queue, int maxSize) {
super(name);
this.name = name;
this.queue = queue;
this.maxSize = maxSize;
}

@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
System.out.println("queue is empty, consumer[" + name + "] waiting for producer ");
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
int x = queue.poll();
System.out.println("[" + name + "] consume value:" + x);
queue.notifyAll();
}
try {
Thread.sleep(1000L);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
class TestProducerConsumer {
public static void main(String[] args) {
int capacity = 5;
Queue<Integer> queue = new LinkedList<>();
Producer producer = new Producer("Producer", queue, capacity);
Consumer consumer = new Consumer("Consumer", queue, capacity);
producer.start();
consumer.start();
}
}

结果:

 注意:在等待线程判断条件是否满足时,应该使用while,而不是if。如果用if产生的问题:

a)此时queue为空,消费者a调用if判断queue为空后,调用wait停止执行,释放锁。

b)消费者b调用if判断queue为空,也调用wait停止执行,释放锁。

c) 生产者a新增数据后,notifyAll唤醒所有等待线程,此时可能消费者a抢占到了对象锁,消费者a正常消费,此时queue没有数据了,然后a消费者也调用notifyAll唤醒等待线程,然后消费者b抢占到了对象锁,而消费者b刚才停止的地方是wait方法,其恢复后会继续执行wait之后的逻辑,造成额外消费。如果用while,消费者b被唤醒后会重新判断循环条件,如果条件不成立再执行while代码块之后的代码块

 

手动实现阻塞队列

class BlockingQueue {
private List<Integer> q;
private int maxSize;

public BlockingQueue(List<Integer> q, int maxSize) {
this.q = q;
this.maxSize = maxSize;
}

public synchronized void offer(int var) {
while (q.size() == maxSize) {
try {
System.out.println("队列已满,等待消费者消费数据");
wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
q.add(var);
System.out.println(Thread.currentThread().getName() + " 加入元素, var = " + var);
notifyAll();
}

public synchronized int poll() {
while (q.size() == 0) {
try {
System.out.println("阻塞队列空了,等待生产者生产数据");
wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
int res = q.remove(q.size() - 1);
System.out.println(Thread.currentThread().getName() + " 取出元素" + res);
notifyAll();
return res;
}

}

class Producer extends Thread {
private BlockingQueue blockingQueue;
public Producer(String name, BlockingQueue blockingQueue) {
super(name);
this.blockingQueue = blockingQueue;
}

int var = 0;
public void run() {
while (true) {
blockingQueue.offer(var++);
try {
Thread.sleep(1000);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}

class Consumer extends Thread {
private BlockingQueue blockingQueue;
public Consumer(String name, BlockingQueue blockingQueue) {
super(name);
this.blockingQueue = blockingQueue;
}

public void run() {
while (true) {
blockingQueue.poll();
try {
Thread.sleep(1000);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}

public class TestSolution {

public static void main(String[] args) {
List<Integer> q = new LinkedList<>();
int maxSize = 3;
BlockingQueue blockingQueue = new BlockingQueue(q, maxSize);
Producer producer1 = new Producer("生产者1", blockingQueue);
Producer producer2 = new Producer("生产者2", blockingQueue);
Consumer consumer1 = new Consumer("消费者1", blockingQueue);
Consumer consumer2 = new Consumer("消费者2", blockingQueue);
producer1.start();
producer2.start();
consumer1.start();
}
}

4、线程的状态:

初始态:创建后尚未启动的线程,即还未调用start方法的线程。

运行态:包括

1)就绪态:线程对象调用了start()方法后,该线程处于就绪状态,该线程何时开始取决于jvm里的调度器。在运行的线程调用yield()方法后会进入就绪状态,当某个线程调用yield()后,只有优先级大于等于当前线程的线程,才会获得执行的机会

2)运行态:获得CPU执行权,正在执行的线程

阻塞态:阻塞态都会导致线程挂起,释放cpu

1)等待阻塞:线程调用object.wait()方法后进入等待阻塞

2)同步阻塞:线程获取synchronized同步锁失败。

3)其他阻塞:线程调用sleep或join,或发出IO请求,当sleep时间过去,join线程执行完,或者IO处理完,线程会回到就绪状态。

终止态:

线程已经结束执行。或执行了stop方法,或线程执行过程中发生异常

线程只能调用一次start方法,如果调用两次start方法,会抛出IllegalThreadStateException,因为start方法内部会检查线程状态,threadStatus必须为0才能执行start方法,只有new出来的线程即初始态,threadStatus才是0

 5、interrupt()解析:

常用的方法:

interrupt方法需要搭配isInterrupted方法一起执行,interrupt方法可以中断线程的执行,isInterrupted可以返回线程的中断状态。如果线程A堵塞在Obejct.wait,Thread.join,Thread.sleep时,此时线程B调用线程Ainterrupt方法(t1.interrupt()意味着给t1线程发送中断信号,使得t1的中断状态为true)那么A线程将抛出一个InterruptedException中断异常(该线程必须事先预备好处理该异常),从而提早的终结被阻塞状态,如果线程没有被阻塞,调用interrupt不起作用。

wait,sleep,join这些方法内部会不断检查中断状态的值。这些方法抛出中断异常后,中断状态其实已经被系统复位了,此时A调用Thread.isInterrupted()返回的是false。如果线程被调用了interrupt方法,此时该线程并不在wait,sleep,join时,下次执行这三个方法,一样会抛出中断异常。所以调用interrupt方法,立刻改变的是中断状态,但如果不是在阻塞,就不会抛出异常,所以中断状态也不会被复位

如果线程阻塞在了nio.channels.InterruptibleChannelIO上Channel将会被关闭,线程被设置为中断状态,并抛出异常。

如果线程阻塞在了nio.channels.Selector上,线程被设置为中断状态,select方法

会马上返回。

6、线程之间的通信机制

java中等待/通知的方法wait()/notify(),两个都是Object中的方法,因为任何对象都可作为锁。而wait()/notify()是由同步监视器调用的。所以这是个巧妙的设计。另外java中提供的是对象锁,调用对象中的wait方法意思就是在等待该对象的锁,而不是某个线程的锁。如果wait方法定义在Thread类中,就等于只有线程才能有锁,而其他对象不可以有锁。

  wait()方法:

1)使当前线程进行等待,该方法会将该线程放入“等待队列,并且在wait所在的代码处停止执行,直到接到通知或被中断为止。

2)在调用wait之前,线程必须获得该对象锁。只能在同步方法或同步快中调用wait方法。

3)wait执行后,当前线程会释放锁。

4)当线程处于wait状态时,调用线程对象的interrupt方法会出现InterruptedException异常。

  notify()方法:

1) 只能在同步方法或同步快中调用notify方法, 调用notify之前,线程必须获得该对象级别锁。

2)该方法是用来通知那些等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器选出一个wait状态的线程,对其发出notify,并使它等待获取该对象的对象锁。

3)执行notify方法后,等待线程不会立即从wait方法返回,需要等待调用notify的线程释放锁之后,等待线程才从wait返回继续执行。

4) notify只会将等待队列中的一个线程移出,而notifyAll方法会将等待队列中的所有线程移出,并使线程进入就绪状态,重新竞争获得对象锁。优先级最高的线程会最先执行。

  wait和notify总结:两者都必须Synchronized块或Synchronized方法中被调用。理由:当一个线程需要调用对象的wait()方法时,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的notify方法。同样的,一个线程需要调用对象的notify方法时,它会释放这个对象的锁,以便其他线程可以得到这个对象锁。由于所有的这些方法都需要线程持有对象锁,这样就只能通过同步来实现,所以他们只能在同步方法或同步块中被调用。

draw和notify的通信可参考前面的生产者消费者队列。

 7、创建线程的几种方法

1)继承Thread类:

注意Thread类本身就实现了Runnable接口,所以Thread的子类也可以作为Thread(Runnable target, String name)的target参数传入,所以可实现资源共享

2)实现runnable接口

可以实现资源共享,同时线程类还可以继承其他类。推荐

3)通过Callable和FutureTask创建线程

a) 线程类实现Callable接口,并实现call方法。call方法有返回值并可以抛出异常。如果需要任务返回计算结果,或者任务可能抛出需要处理的异常,就使用callable

b) 使用FutureTask类来包装Callable对象,FutureTask类封装了Callable对象call方法的返回值。FutureTask类实现了Runnable接口,所以可以作为Thread类的target。注意:Callable不是Runnable的子接口,所以不能作为Thread的target,而FutureTask是Future接口的实现类,它也是Runnable接口的实现类。所以它可以作为Thread的target。

c) 调用FutureTask对象get方法来获得子线程执行结束后的返回值

 

 

get方法返回call()方法的返回值,调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值。callable接口有泛型限制,其泛型类型call方法的返回值相同

8、CountDownLatch和CyclicBarrier

CountDownLatch是个实用的多线程控制工具类,称之为倒计时,存在util.concurrent包下,它允许一个或多个线程一直等待,直到其他的线程操作执行完后再执行。CountDownLatch是通过一个计数器来实现的,其初始值是线程的数量,每一个线程完成了自己的任务之后,计数器的值就会减一。当计数器到达0,表示所有线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。(例子:火箭发射,发射之前要进行各项准备检查,全完工后才能发射)

可以看到,当两个线程分别执行完之后,才执行latch.await()之后的代码。CountDownLatch的构造函数传入的数字表示需要等待执行完毕的线程数量,在每一个线程执行完毕后,都需要执行CountDownLatch.countDown()方法,不然计数器就不会准确,只有所有线程都执行完毕后,才会执行latch.await()之后的代码。主线程必须在启动其他线程后立即调用await方法,这样主线程的操作就会在这个方法上阻塞。直到其他线程完成各自的任务。其他N个线程必须通知CountDownLatch对象,他们已经完成了各自的任务。这种通知机制是通过CountDownLatch.countDown()方法完成的。每调用一次,在构造函数中初始化的count值就减一。Count的值等于0后,主线程就能通过await方法,恢复执行自己的任务。

  CountDownLatch底层实现使用了AQS,CountDownLatch的构造方法传入的值就是AQS state的值。

CountDown方法会对state的值减一,这个方法会对state值减1,会调用到AQS中releaseShared()方法,目的是为了调用doReleaseShared()方法,这个是AQS定义好的释放资源的方法,而tryReleaseShared()则是子类实现的,可以看到是一个自旋CAS操作,每次都获取state值,如果为0则直接返回,否则就执行减1的操作,失败了就重试,如果减完后值为0就表示要释放所有阻塞住的线程了,也就会执行到AQS中的doReleaseShared()方法。

await方法是使调用的线程阻塞住,直到state的值为0就放开所有阻塞的线程。实现会调用到AQS中的acquireSharedInterruptibly()方法,先判断下是否被中断,接着调用了tryAcquireShared()方法,讲AQS那篇文章里提到过这个方法是需要子类实现的,可以看到实现的逻辑就是判断state值是否为0,是就返回1,不是则返回-1。 

  CyclicBarrier(循环屏障)是另一种多线程并发控制使用工具,和CountDownLatch类似,功能更强大些。它做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活CyclicBarrier默认构造方法可以传入个int,表示屏障拦截的线程数量。每个线程调用await()告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。CyclicBarrier强调的是n个线程大家互相等待,只要有一个没完成,所有人都得等着。

可以看出,每个写入线程执行完写数据操作之后,就在等待其他线程写入操作完毕。当所有线程写入完毕后,所有线程就继续进行后续的操作了,如果想所有线程完成写入操作后,进行额外的其他操作,就要提Runnable参数

从结果可以看出,当四个线程都达到barrier状态后,会从四个线程中选择一个去执行Runnable任务,其余线程继续阻塞,直到任务完成,然后全部线程继续往下执行。

CountDownLatch和CyclicBarrier区别:

1)前者计数器只能使用一次,后者计数器可以使用reset方法重置,所以后者可以处理更复杂的业务,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。

2)后者的getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。

3)前者会阻塞主线程,后者不会阻塞主线程,只会阻塞子线程。

4)前者每次调用await方法之前都需要先调用countDown方法,而后者不需要

9、线程池

为了避免系统频繁的创建和销毁线程(频繁切换线程会产生大量性能消耗),我们可以把创建的线程进行复用,即当一个业务用完线程之后,不是立即销毁而是将其放入线程池中,实现复用,避免频繁的创建销毁线程消耗时间。所以创建线程变成了从线程池汇总获取空闲的线程,关闭线程变成了向池子中归还线程。Jdk5新增了Executors工厂类来生产线程池Executor下的接口和类继承关系如下:

  合理估算线程池大小:参考 https://mp.weixin.qq.com/s/JFWjDSQ4HRGbZhj9ei3t6Q

CPU密集型:这种情况下使得CPU使用率很高,尽量使用较小的线程池,一般为CPU+1,若创建太多的线程数,只能增加上下文切换的次数,带来额外开销。

IO密集型:这种情况下使得CPU使用率不高,因此可以让CPU等待IO的时候去处理别的任务,充分利用CPU时间,可以使用稍大的线程池,一般为CPU*2

java官方提供了几种线程池

1) newFixedThreadPool(int nThreads):返回一个固定线程数量的线程池。弊端:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM

2) newSingleThreadExcecutor():创建只有一个线程的线程池,保证所有任务都可以根据先进先出的顺序处理

3) newCachedTheadPool():创建一个可缓存的线程池,可以处理突发流量。特点:a、最大线程数是Integer.MAX_VALUE。b、如果工作线程空闲时间超过60秒,该工作线程会被回收

4)newScheduledThreadPool():创建一个线程池,该线程池可以安排命令在给定延迟后运行,或定期执行

5)newWorkStealingPool:jdk8里面新加的线程池,其内部会构建一个ForkJoinPool,并行处理任务

Excecutors内部创建线程池时,实际创建的是ThreadPoolExcecutor,ThreadPoolExcecutor的构造函数如下:

corePoolSize: 核心线程池大小,核心线程会一直存活,即使没有任务需要处理。在创建了线程池后,线程池中线程数为0,当有任务来之后,如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。即调用execute方法提交任务后,线程池就会创建一个线程去执行任务,当线程池中线程数达到corePoolSize后,就把任务放在任务缓存队列中。为什么当线程池的核心线程满了后,是先加入到阻塞队列,而不是先创建新的线程?线程池创建线程需要获取mainlock这个全局锁,会影响并发效率。所以优先用缓存队列存储任务。

maxPoolSize:线程池最大容量,当线程数大于等于核心线程数,且任务队列已满,线程池就会创建新的线程,直到线程数达到maxPoolSize。如果线程数已经等于maxPoolSize且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。

keepAliveTime: 当线程池中线程数量大于corePoolSize,如果有线程的空闲时间超过了keepAliveTime,就将其移除线程池

ThreadFactory: 线程工厂,用来创建线程

workQueue: 阻塞队列,用来存放等待被执行的任务。任务必须实现Runnable接口。

阻塞队列ArrayBlockingQueue(基于数组,有界的,当此队列满了,线程池就会添加线程的数量),

LinkedBlockingQueue(基于链表,无界队列,默认容量是最大的int值),

SynchronousQueue(是一个没有数据缓冲的阻塞队列,生产者线程对其的插入操作必须等待消费者的移除操作),当线程池中核心线程数已满,如果不想让新来的任务进入队列,就要使用这个阻塞队列

allowCoreThreadTimeOut:是否允许核心线程空闲退出,默认值false

Handler:拒绝策略。1) 抛出异常RejectedExecutionException,拒绝新提交的任务。Jdk默认的。 2) 丢掉最早缓存在队列中的任务,并尝试执行当前任务。使用场景:发布消息和修改消息,之前较早版本未执行的消息就可以丢弃了。3)直接在提交任务的线程中运行该任务,不往线程池中添加。使用场景:不允许失败,且对性能要求不高的场景,因为是调用者自己执行的,多次提交任务时就会阻塞后续任务执行,性能下降。4) 直接丢弃,使用场景:任务无关紧要。

     核心线程和非核心线程的回收:非核心线程空闲时间超过keepAliveTime后会被回收,核心线程通过allowCoreThreadTimeOut控制是否可以回收,默认是不回收的

  线程池的关闭:调用shutdownshutdownNow两个方法,shutdownNow对正在执行的任务全部发出interrupt(),停止执行,对还没开始的任务全部取消。调用shutdown后,线程池将不再接受新的任务,但会保证已经接受的任务处理完成。shutdown方法是否需要调用?如果是全局公共的线程池,某个应用调用shutdown后,会关闭线程池,影响其他业务执行,而且如果是全局公共的线程池,调用shutdown会频繁创建线程池和销毁线程池,那线程池就失去意义。除非是临时使用和使用频率很低的线程池,可以考虑使用完后调用shutdown关闭。

  启动线程:

1) Executor.execute(new Runnable()),该方式可以提交任务,把任务塞到一个队列中,然后线程池中的线程从这个队列中取任务执行。必须实现Runnable接口,该方式提交的任务不能获取返回值,因此无法判断任务是否执行成功。方法源码

当向线程池中提交一个任务,如果当前线程数小于corePoolSize,则执行addWorker方法创建新的线程执行任务addWorker方法: 获取池中的线程数,如果大于等于corePoolSize的话,则每来一个任务,会尝试将其添加到任务缓存队列中,若添加成功,则该任务会等待空闲线程将其取出执行。若添加失败(一般是由于任务缓存队列已满),则会尝试创建新的线程去执行这个任务。如果当前线程数达到maxPoolSize,则会调用reject拒绝处理。

2)ExecutorService.submit(Callable<T> task),此方法可以返回一个future对象。该方法也可以接收Runnable作为参数。例子如下:

实际业务中,Future和Callable基本是成对出现的,后者负责产生结果,前者负责获取结果。Future.get方法会导致主线程阻塞,直到Callable任务执行完成。submit()方法中,无论参数是Callable还是Runnable,最终都会被封装成RunnalbeFuture,最后调用的还是execute()方法,所以后者是最核心的。

  尽量不要使用Executors去创建线程池,而是通过new ThreadPoolExcecutor的方法,因 为使用newFixedThreadPool会创建无界队列,造成很大的内存消耗,甚至OOM

     这个线程池异常是怎么产生的?

java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@355bffba rejected from java.util.concurrent.ThreadPoolExecutor@7c3d99bf[Running, pool size = 300, active threads = 16, queued tasks = 350, completed tasks = 90358217]

先看各个参数的含义:pool size:核心线程数大小。active threads:正在执行任务的线程数大小,线程执行完任务后状态会变成not active,然后会不停的获取任务直到下次获取新的任务,状态又变成active。queued tasks:队列中任务数量。

为什么pool size会比active threads大很多?当线程完成上一个任务后,它不会立即接收任务,需要先做收尾工作然后为新任务做准备工作,然后线程会从队列中读取任务,这个过程需要花费时间。如果在循环中向提交多个任务,那么到最后可能只有少数工作线程有机会获取任务。

    如何优化ThreadPoolExecutor以提高任务吞吐量?

 1)根据机器资源合理设置核心线程数和最大线程数。2)选择适合业务场景的等待队列,如果任务提交速度较快,可以考虑使用无界队列。3)根据业务需求选择合适的拒绝策略。4)优化任务逻辑,尽量减少单个任务执行时间,做一些异步处理等

  如何知道线程池中的任务是否执行完成?

ThreadPoolExcecutor即ExecutorService,用submit提交任务后,会返回future对象,future的isDone方法可以判断任务是否完成。或者借助CountDownLatch工具,每个线程执行任务完成后,调用latch的countDown方法,在主线程中调用latch的await方法,当CountDownLatch的计数器归零,表示所有任务已完成

    线程池如何实现线程复用?   

线程池使用了生产者消费者模式,实现了线程复用,线程池通过中间容器即阻塞队列完成生产者消费者解耦。生产者不断生产任务保存到队列中,消费者不断从队列中消费任务去执行。

   线程池中的线程抛出异常怎么办?

如果使用excute方式提交任务,抛出异常后,线程不会吃掉异常,能看到异常的堆栈信息。

如果使用submit方式提交任务,抛出异常后,线程会吃掉异常,没有异常的堆栈信息,处理异常方式:

1)自己捕获

 2)使用future获取异常结果

 

10、多线程的CAS

SynchronizedReentrantlock等悲观锁会引起线程的挂起恢复,非常浪费时间。CAS是比较并替换(compare and swap),是乐观的并发策略,其思想很简单:三个参数,一个变量的内存当前v,旧的预期值A,即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。 

下面以AtomicInteger的addAndGet实现为例子,分析下CAS的实现机制

1)AtomicInteger里面value原始值为3,即主内存中AtomicInteger的value为3,根据java内存模型,线程1和线程2各自持有一份value的副本,值为3.

 2) 线程1利用get方法获取当前value为3,此时线程1被挂起。

 3) 线程2开始运行,利用get方法获取到value为3,运气好没有被挂起,并利用CAS对比内存中的值也是3,比较成功,修改内存,此时内存中的value改变比方说是4,线程切换。

 4) 此时线程1恢复,利用CAS比较,发现自己手里的值(3)和内存的值(4)不一致,说明该值已经被其他线程提前修改过,只能重新再来一遍。

5) 重新获取value值,因为变量value被volatile修饰,所以其他线程对它的修改,线程1总是能看到,只要线程1发现当前获取的value是4,内存中value也是4,说明线程2对value的修改已经完毕并且线程1可以尝试去修改它。

 CAS的缺点:

1)ABA问题,如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过吗?

如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过,针对这种情况,可以通过引入版本号,即每次操作都记录一次操作的版本号。每次变量更新的时候都把版本号加一。来保证CAS的正确。

2)一个CAS操作只针对一个变量,如果有多个变量,乐观锁cas将变得力不从心。

3)长时间循环可能导致开销较大,假如cas长时间不成功而一直自旋,会给cpu带来很大的开销。

11、死锁的demo

 避免死锁:使用定时锁,使用lock.tryLock(timeOut),当超时等待时当前线程不会阻塞。

12、Semaphore

 信号量,控制的是线程并发的数量。Public Semaphore(int permits),permits就是一个信号量,如果permits1,代表单线程

 

我们创建的semaphore对象信号量是5,所以有5个线程能同时执行,如图每次执行都会输出五行字。也可以用acqiure动态的添加permits的数量,它表示一次性获取许可的数量。

Semaphore semaphore = new Semaphore(10);
try {
semaphore.acquire(2);
} catch (Exception ex) {

}

上述代码中总的信号量除以每次获取的许可数,10/2=5,即可以允许5个线程一起运行。

 13、如何确保main方法所在的线程是java程序最后结束的线程?

我们可以使用Thread的join()方法来确保所有程序创建的线程在main方法退出前结束

 14、现在T1,T2,T3三个线程,怎么保证T2在T1执行完后执行,T3在T2执行完后执行?即实现线程接力

可以join实现,join的作用是让当前线程等待另一个线程执行完毕后再继续执行

15、守护线程

守护线程是区别于用户线程,用户线程即我们手动创建的线程,而守护线程是程序运行的时候在后台提供一种通用服务的线程。垃圾回收线程就是典型的守护线程。

守护线程和非守护线程的区别:

 执行结果:

可以发现标记为守护线程后,主线程销毁停止,守护线程一起销毁。

去掉 t1.setDaemon(true)的结果:

所以,当主线程退出时,守护线程同时也会结束,即使是死循环。当主线程退出时,如果t1不是守护线程,它会一直停在死循环跑。这就是守护线程和非守护线程的区别。所以垃圾回收是守护线程,jvm退出时守护线程结束

16、线程交替打印

多线程交替打印。那就是线程A负责打印A ,线程B负责打印B ,线程C负责打印C。需要A线程打印完, B线程打印,B线程打印完, C线程打印

public class DoTest {

//控制三个线程 ABC,保证同一时刻只有一个线程工作
private static Lock lock = new ReentrantLock(true);

// Condition ,控制等待或是 通知
private static Condition conditionA = lock.newCondition();
private static Condition conditionB = lock.newCondition();
private static Condition conditionC = lock.newCondition();

//默认的通知 暗号 A
private static String CODE= "A";

public static void main(String[] args) {

Thread a = new Thread(() -> {
while (true) {
try {
//等待
lock.lock(); // 必须要在循环里加锁,如果在循环外加锁,可能没获取到锁就调用了conditiA.await()方法
//核对
while (!CODE.equals("A")) {
//暗号不对,就进入等待
conditionA.await();
}
System.out.println("A");
//改暗号,通知B
CODE = "B";
conditionB.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});

Thread b = new Thread(() -> {
while (true) {
try {
//等待
lock.lock();
//核对
while (!CODE.equals("B")) {
//暗号不对,就进入等待
conditionB.await();
}
System.out.println("B");
//改暗号,通知B
CODE = "C";
conditionC.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});

Thread c = new Thread(() -> {
while (true) {
try {
//等待
lock.lock();
//核对
while (!CODE.equals("C")) {
//暗号不对,就进入等待
conditionC.await();
}
System.out.println("C");
//改暗号,通知B
CODE = "A";
conditionA.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});
a.start();
b.start();
c.start();
}
}

如果要加一些判断,打印100行之后停止,需要怎么做?

public class DoTest {

//控制三个线程 ABC,保证同一时刻只有一个线程工作
private static Lock lock = new ReentrantLock(true);

// Condition ,控制等待或是 通知
private static Condition conditionA = lock.newCondition();
private static Condition conditionB = lock.newCondition();
private static Condition conditionC = lock.newCondition();

//默认的通知 暗号 A
private static String CODE= "A";
private static int COUNT_NUM = 1;
private static volatile boolean IS_INTERRUPT = false;

public static void main(String[] args) {

Thread a = new Thread(() -> {
while (!IS_INTERRUPT) {
try {
//等待
lock.lock();
if (COUNT_NUM >= 100) {
IS_INTERRUPT = true;
return;
}
//核对
while (!CODE.equals("A")) {
//暗号不对,就进入等待
conditionA.await();
}
System.out.println("A:" + COUNT_NUM);
//改暗号,通知B
CODE = "B";
COUNT_NUM = COUNT_NUM + 1;
conditionB.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});

Thread b = new Thread(() -> {
while (!IS_INTERRUPT) {
try {
//等待
lock.lock();
if (COUNT_NUM >= 100) {
IS_INTERRUPT = true;
return;
}
//核对
while (!CODE.equals("B")) {
//暗号不对,就进入等待
conditionB.await();
}
System.out.println("B:" + COUNT_NUM);
//改暗号,通知B
CODE = "C";
COUNT_NUM = COUNT_NUM + 1;
conditionC.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});

Thread c = new Thread(() -> {
while (!IS_INTERRUPT) {
try {
//等待
lock.lock();
if (COUNT_NUM >= 100) {
IS_INTERRUPT = true;
return;
}
//核对
while (!CODE.equals("C")) {
//暗号不对,就进入等待
conditionC.await();
}
System.out.println("C:" + COUNT_NUM);
//改暗号,通知B
CODE = "A";
COUNT_NUM = COUNT_NUM + 1;
conditionA.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});
a.start();
b.start();
c.start();
}
}

用synchronized怎么实现?

public class DoTest1 {
private volatile int COUNT_NUM = 1;
private volatile String CODE = "A";
private static int oneTimes = 34;
private static int othersTimes = 33;

void onePrint() {
synchronized (this) {
while(!CODE.equals("A")) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": " + COUNT_NUM);
COUNT_NUM++;
CODE = "B";
notifyAll();
}
}
void twoPrint() {
synchronized (this) {
while(!CODE.equals("B")) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": " + COUNT_NUM);
COUNT_NUM++;
CODE = "C";
notifyAll();
}
}
void threePrint() {
synchronized (this) {
while(!CODE.equals("C")) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": " + COUNT_NUM);
COUNT_NUM++;
CODE = "A";
notifyAll();
}
}


public static void main(String[] args) {
DoTest1 printNumber = new DoTest1();
new Thread(() -> {
for (int i = 0; i < oneTimes; i++) {
printNumber.onePrint();
}
},"线程A").start();

new Thread(() -> {
for (int i = 0; i < othersTimes; i++) {
printNumber.twoPrint();
}
},"线程B").start();

new Thread(() -> {
for (int i = 0; i < othersTimes; i++) {
printNumber.threePrint();
}
},"线程C").start();
}
}

17、Fork/Join线程池

Java 7开始引入了一种新的Fork/Join线程池,它可以执行一种特殊的任务:把一个大任务拆成多个小任务并行执行。Fork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果

18、一个线程OOM后,其他线程还能运行不?

当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行。如果是栈溢出,结论也是一样的。线程发生OOM后,如果线程没有处理异常,线程会被终结掉,该线程持有的对象占用的内存都会被gc了,释放内存。因为发生OOM之前要进行gc,就算其他线程能够正常工作,也会因为频繁gc产生较大的影响。

发生OOM时JVM进程就退出吗?
不一定,要看是否还有存活的非守护线程,如果有则jvm进程不会退出,如果没有存活的非守护线程,jvm才会退出。

18、CompletableFuture: 

https://mp.weixin.qq.com/s/j1BYnPwUMztajesjmPjqag

java8中引入的组件,可以异步执行任务和处理异步任务返回的结果,在此组件出现之前只能用Callable或future获取线程的执行结果,future是通过阻塞等待的方式实现的,而且需要和线程池搭配使用。
CompletableFuture可以将任务异步执行,有supplyAsync和runAsync两种方式创建异步任务,前者的任务有返回值,后者的任务无返回值。使用supplyAsync和runAsync都可以传入线程池或者不传入,如果不传入的话, CompletableFuture内部使用了ForkJoinPool来处理异步任务。ForkJoinPool的核心线程数,是机器cpu数量-1。意味着如果是4核机器,那最多也只有3个核心线程。对于IO密集型的任务(rpc调用等)来说,远远不够用,会导致大量的IO任务在等待。所以建议使用自定义的线程池

CompletableFuture有get,join和getNow方法获取返回值,

get会阻塞调用线程,直到计算完成或抛出异常。
join和get类似,也会阻塞调用线程直到计算完成,但它不会抛出InterruptedException
getNow是非阻塞的,它立即返回结果或默认值,而不等待计算完成

CompletableFuture提供thenRun,thenAccept,thenApply,whenComplete等方法链式处理异步任务的执行结果。
thenRun:做完第一个任务后,再做第二个任务。前后两个任务没有参数传递,第二个任务也没有返回值。
thenAccept:第一个任务执行完成后,把第一个任务的执行结果作为入参,传递到第二个任务中,第二个任务没有返回值。
thenApply:第一个任务执行完成后,把CompletableFuture里的元素,转换成其他元素,例如thenApply方法可以把A转换为B,CompletableFuture<A>调用thenApply方法后,变为了CompletableFuture<B>,thenApply不会创建新的CompletableFuture。

thenCompose:用于连接两个异步操作,例如method1返回CompletableFuture<A>,method2入参是A,返回CompletableFuture<B>
CompletableFuture<A> fa = method1();
CompletableFuture<B> fb = fa.thenCompose(a -> method2(a));

 

19、如何在不加锁的情况下解决线程安全问题

1)自旋锁 CAS
2)乐观锁,数据库加版本号字段
3)尽量减少共享资源的使用,业务层面实现隔离避免并发

20、ArrayBlockingQueue实现原理

基于数组的队列,队列为空的时候消费元素的线程会阻塞等待,直到队列为非空。队列满了的时候,生产元素的线程会阻塞等待,直到队列不是满载。ArrayBlockingQueue一般用在生产者消费者模型中,底层需要用到线程的阻塞和唤醒机制,用的是ReetrantLock和condition

21、阻塞队列被异步消费怎么保持顺序呢

阻塞队列使用condition维护了两个等待队列,一个是队列为空时存储被阻塞的消费者,另一个是队列满了的时候存储被阻塞的生产者,condition是个FIFO的链表,如果阻塞队列里面有很多任务的话,消费者从阻塞队列获取任务的时候,需要先获得排它锁(阻塞队列ArrayBlockingQueue底层使用了ReetrantLock锁),如果阻塞队列没有任务,消费者也是按照FIFO顺序在condition的等待队列中等待,当阻塞队列中有任务后,等待的消费者会按照FIFO的顺序被唤醒,保证了消费者对于任务的处理顺序

22、并发和并行区别

1)前者是一个cpu同时处理多个任务,本质上是把cpu分配给各个线程分微小时间段执行。后者是多个cpu同时处理多个任务
2)前者线程之间会去抢占cpu,轮流使用。后者线程间不会抢占cpu

posted @ 2023-02-08 17:43  MarkLeeBYR  阅读(22)  评论(0)    收藏  举报