5.多线程与高并发

1.进程和线程的区别。

  1.  进程是资源分配最小单位
  2.  线程是程序执行的最小单位CPU的基本调度单位 
  3. 一个进程可以包含多个线程。

2.画一个线程的生命周期状态图

答:新建---就绪状态---运行状态--阻塞状态---死亡状态

5.多线程与高并发_第1张图片


3.使用多线程的优缺点?

优点:

(1)多线程技术使程序的响应速度更快

(2)当前没有进行处理的任务可以将处理器时间让给其它任务

(3)占用大量处理时间的任务可以定期将处理器时间让给其它任务

(4)可以随时停止任务

(5)可以分别设置各个任务的优先级以及优化性能

缺点:

(1)等候使用共享资源时造成程序的运行速度变慢

(2)对线程进行管理要求额外的cpu开销

(3)可能出现线程死锁情况。即较长时间的等待或资源竞争以及死锁等症状。


4. start()方法和run()方法简介和区别

     start()方法:

  1. 用start方法来启动线程,真正实现了多线程运行
  2. 通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得CPU时间片,就开始执行run()方法。

    run()方法:

        run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条。

总结:

  1. 调用start方法方可启动线程,
  2. 而run方法只是thread的一个普通方法调用,还是在主线程里执行。
  3. 把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用run()方法,这是由jvm的内存机制规定的。
  4. 并且run()方法必须是public访问权限,返回值类型为void。

5. Runnable接口和Callable接口的相同点和不同点?

相同点:

   Callable和Runnable都是接口

   Callable和Runnable都可以应用于Executors

不同点:

    Callable要实现call方法,Runnable要实现run方法

    call方法可以返回值,run方法不能

    call方法可以抛出checked exception,run方法不能

    Runnable接口在jdk1.1就有了,Callable在Jdk1.5才有


6.多线程的几种实现方式,什么是线程安全

  1. 继承Thread类创建线程
  2. 实现Runnable接口创建线程
  3. 实现Callable接口通过FutureTask包装器来创建Thread线程
  4. 通过线程池创建线程

  线程安全就是说多线程访问同一代码,不会产生不确定的结果 

  https://www.cnblogs.com/zhou-test/p/9811771.html


7.Lock 与 Synchronized 的区别。

两者区别:

  1.首先synchronized是java内置关键字,在jvm层面,Lock是一个java接口;

  2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁 holdsLock(Object obj)方法

 3.synchronized会自动释放锁

     a 线程执行完同步代码会释放锁 ;

     b 线程执行过程中发生异常会释放锁),

     Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;

4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;

5.JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。因此。提倡优先考虑使用synchronized来进行同步

6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

https://www.cnblogs.com/baizhanshi/p/6419268.html

https://www.cnblogs.com/iyyy/p/7993788.html


8.sleep 和 wait 的区别。

  1. sleep方法是Thread的静态方法,wait方法是Object类的普通方法
  2. sleep方法不释放同步锁,wait方法释放同步锁(执行notify方法唤醒wait的线程时是不释放同步锁的)
  3. wait方法用于线程间通信,而sleep方法用于短暂的暂停线程
  4. sleep针对当前线程,而wait针对被同步代码块加锁的对象
  5. sleep方法是当前线程暂停指定时间,将执行机会让给其它线程,时间结束后进入就绪状态等待
  6. 调用wait方法会暂停线程,当前线程释放对象的同步锁,进入等待池(wait pool),只有调用对象的notify或者notifyAll方法唤醒时,线程进入等锁池(lock pool),直到线程再次获得对象的锁才会进入就绪状态
  7. wait方法(notify,notifyAll)只能在同步方法或者同步块中使用(如果在non-synchronized函数或non-synchronizedblock中进行调用,虽然能编译通过,但在运行时会发生illegalMonitorStateException的异常);sleep方法可以在任意位置使用

注:

       如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。

       如果此刻线程B正在wait/sleep/join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束

        https://www.cnblogs.com/renhui/p/6069353.html

        https://www.cnblogs.com/tiancai/p/8855125.html


9.volatile 的原理,作用,能代替锁么。

volatile 关键字的作用:

 保证内存的可见性

  • 修改volatile变量时会强制将修改后的值刷新的主内存中

  • 修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。

  防止指令重排

  • JMM会对volatile变量限制这两种类型的重排序
  • volatile 并不保证复杂原子性,如 i++;这有3步操作

5.多线程与高并发_第2张图片5.多线程与高并发_第3张图片

5.多线程与高并发_第4张图片

5.多线程与高并发_第5张图片

http://www.cnblogs.com/paddix/p/5428507.html


10.volatile 和 synchronized区别

  • volatile 轻量级,只能修饰变量。synchronized重量级,还可修饰普通方法,静态方法,代码块
  • volatile 只能保证数据的可见性,不能用来同步,因为多个线程并发访问 volatile 修饰的变量不会 阻塞。
  • synchronized 不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢 synchronized 锁对象时,会出现阻塞
  • volatile 并不能保证线程安全性。而 synchronized 则可实现线程的安全性

https://www.cnblogs.com/arsense/p/10170062.html


11.用三个线程按顺序循环打印 abc 三个字母,比如 abcabcabc。

参考代码,采用线程池制定创建线程数


		final String str = "abc";
		ExecutorService executorService = Executors.newFixedThreadPool(3);
		executorService.execute(new Runnable() {
			@Override
			public void run() {
				System.out.println("1" + str);
			}
		});
		executorService.execute(new Runnable() {
			@Override
			public void run() {
				System.out.println("2" + str);
			}
		});
		executorService.execute(new Runnable() {
			@Override
			public void run() {
				System.out.println("2" + str);
			}
		});
	

 


12.ThreadLocal 用过么,用途是什么,原理是什么,用的时候要注意什么。

1)ThreadLocal用来解决多线程程序的并发问题

2)ThreadLocal并不是一个Thread,而是Thread的局部变量,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本.

3)从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。

简单说ThreadLocal就是一种以空间换时间的做法,在每个Thread里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了

https://blog.csdn.net/u012834750/article/details/71646700


13.讲讲 java 同步机制的 wait 和 notify。

5.多线程与高并发_第6张图片

    这两个方法只能在同步代码块中调用wait会释放掉对象锁,等待notify,notifyAll唤醒。 
http://blog.csdn.net/ithomer/article/details/7685594


14.多线程如果线程挂住了怎么办。

    根据具体情况(sleep,wait,join等),酌情选择notifyAll,notify进行线程唤醒。 
http://blog.chinaunix.net/uid-122937-id-215913.html


15.使用 synchronized 修饰静态方法和非静态方法有什么区别。

    static的方法属于类方法,它属于这个Class(注意:这里的Class不是指Class的某个具体对象),

那么static获取到的锁,是属于类的锁。而非static方法获取到的锁,是属于当前对象的锁。

所以,他们之间不会产生互斥。

https://www.cnblogs.com/wxgblogs/p/5505368.html


16.导致线程死锁的原因?怎么解除线程死锁。 

    是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

 集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件,那么该组进程是死锁的。

引起死锁的主要原因是:“需要采用互斥方法访问的、不可以被抢占的资源“

预防-避免-检测-解除,对死锁的防范程度依次减弱,但是对应的资源的利用率依次提高,也就是并发程度依次变高。

破坏“请求和保持

破坏“不可抢占”

破坏“循环等待

https://www.cnblogs.com/noteless/p/10354664.html


17.线程的sleep()方法和yield()方法有什么区别?

sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;

   yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性


18.说说你对同步和异步的理解.

Java 中同步与异步,阻塞与非阻塞都是用来形容交互方式,区别在于它们描述的是交互的两个不同层面。

同步与异步

  1. 同步与异步更关注交互双方是否可以同时工作。

       a.以同步的方式完成任务意味着多个任务的完成次序是串行的,假设任务 A 依赖于任务 B,

      那么任务 A 必须等到任务 B 完成之后才能继续,执行流程为 A->B;

      b.以异步的方式完成任务意味着多个任务的完成可以是并行的,这种情况多适用于任务之间没有因果关系,

      假如任务 A 中需要执行任务 B,而任务 A 的完成不依赖于任务 B 的结果,那么任务 A 调用任务 B 后可以继续执行后续步骤        而不需要等待任务 B 完成,也不关心任务 B 是否执行完毕,

     此时任务 A 和任务 B 是并行的。

为了加深对同步和异步的理解,可以使用打电话和发短信的类别同步和异步的交互方式。

打电话时,一方的后续操作必须等到另一方说完才能进行,这种交互方式就是同步的。

发短信则意味着我们不关心对方看到短信后的结果,我们关心自己是否发了短信,发完短信后,我们可以接着手头上的工作,

这种交互方式就是异步的。

阻塞与非阻塞

阻塞与非阻塞关注的是交互双方是否可以弹性工作。

  • 假设对象 A 和对象 B 进行交互,而对象 B 对一个问题需要思考一段时间才能回复 A,那么对象 A 可以选择等待对象 B 回复,这种方式就是一种阻塞式交互,
  • 与此同时,对象 A 可以选择在对象 B 进行思考的时间去完成别的工作,等到对象 B 完成思考后再进行后续交互,这种方式就是一种非阻塞式的交互。

一般来说,阻塞与非阻塞式用来形容 CPU 消耗的。

我们把 CPU 停下来等待慢操作完成以后再接着工作称为阻塞;

把 CPU 在慢操作完成之前去完成其他工作,等慢操作完成后再接着工作称为非阻塞。


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

  1.  一个线程在访问一个对象的同步方法时,另一个线程可以同时访问这个对象的非同步方法
  2. 一个线程在访问一个对象的同步方法时,另一个线程不能同时访问这个同步方法   。
  3.  一个线程在访问一个对象的同步方法时,另一个线程不能同时访问这个对象的其他同步方法                                                

    前提条件:多个线程所持有的对象锁共享且唯一,如果每个线程所持有的对象锁不一样,那么该对象是锁不住的!


20.如何让多个线程顺序执行?

     (1)用线程中的join()方法 ,利用了join()的阻塞特点, 底层调用wait()方法

              唤醒是在jvm中实现调用notifyall()方法

       (2)利用Executors线程池中的 newSingleThreadExecutor()方法, 创建一个单线程的线程池


21.Synchronized及其实现原理 

Synchronized的作用主要有三个:

  1. 确保线程互斥的访问同步代码
  2. 保证共享变量的修改能够及时可见
  3. 有效解决重排序问题。

从语法上讲,Synchronized总共有三种用法:

  1. 修饰普通方法
  2. 修饰静态方法
  3. 修饰代码块

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。


执行monitorexit的线程必须是objectref所对应的monitor的所有者。

指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。 

  通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

https://blog.csdn.net/wangtaomtk/article/details/52264038


22.怎么打出线程栈信息。

jstack主要用来查看某个Java进程内的线程堆栈信息。语法格式如下:

jstack [option] pid
jstack [option] executable core
jstack [option] [server-id@]remote-hostname-or-ip

必点
 https://www.cnblogs.com/zhuqq/p/5938187.html


23.CyclicBarrier和CountDownLatch的区别是什么?

两个看上去有点像的类,都在java.util.concurrent下,都可以用来表示代码运行到某个点上,

二者的区别在于:

(1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运              行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行

(2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务

(3)CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了

5.多线程与高并发_第7张图片


24.怎么唤醒一个阻塞的线程?

    如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;

   如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。


25. Java中如何获取到线程dump文件?

 dump文件的作用:

      死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。因此,线程dump也就是线程堆栈。

  获取到线程堆栈dump文件内容分两步:

(1)第一步:获取到线程的pid,Linux环境下可以使用ps -ef | grep java

(2)第二步:打印线程堆栈,可以通过使用jstack pid命令

另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈,


26. 生产者和消费者模型的作用是什么?

1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用

2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约

https://blog.csdn.net/u011109589/article/details/80519863


27. wait方法和notify/notifyAll方法在放弃对象监视器时有什么区别?

  wait()方法立即释放对象监视器;

  notify() / notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。


28.ConcurrentHashMap的并发度是什么?

  ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据∂?

https://blog.csdn.net/jjc120074203/article/details/78625433


29.ReadWriteLock是什么?

    ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,

实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。


30.FutureTask是什么?

FutureTask表示一个异步运算的任务。

     FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。


31.Java中用到的线程调度算法是什么?

抢占式。

    一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。


32.什么是乐观锁和悲观锁?

(1)乐观锁:对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-设置这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

(2)悲观锁:对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,直接对操作资源上了锁。


33. Java编写一个会导致死锁的程序?

死锁现象描述:

         线程A和线程B相互等待对方持有的锁导致程序无限死循环下去。

死锁的实现步骤:

(1)两个线程里面分别持有两个Object对象:lock1和lock2。这两个lock作为同步代码块的锁;

(2)线程1的run()方法中同步代码块先获取lock1的对象锁,Thread.sleep(xxx),时间不需要太多,100毫秒差不多了,然后接着获取lock2的对象锁。这么做主要是为了防止线程1启动一下子就连续获得了lock1和lock2两个对象的对象锁

(3)线程2的run)(方法中同步代码块先获取lock2的对象锁,接着获取lock1的对象锁,当然这时lock1的对象锁已经被线程1锁持有,线程2肯定是要等待线程1释放lock1的对象锁的

这样,线程1″睡觉”睡完,线程2已经获取了lock2的对象锁了,线程1此时尝试获取lock2的对象锁,便被阻塞,此时一个死锁就形成了。

死锁的实现代码:

https://blog.csdn.net/zhangliangzi/article/details/52729026

输出结果是:

  线程A 锁住资源O1,等待O2

  线程B 锁住资源O2,等待O1


34.一个线程如果出现了运行时异常会怎么样

  如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放


35.如何在两个线程之间共享数据

  通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的

https://www.cnblogs.com/lemingyin/p/8878513.html


36.为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用

这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁,

每个对象都可以被认为是一个"监视器monitor"

先回答问题:

(1)为什么wait()必须在同步(Synchronized)方法/代码块中调用?

    调用wait()就是释放锁,释放锁的前提是必须要先获得锁,先获得锁才能释放锁。

(2)为什么notify(),notifyAll()必须在同步(Synchronized)方法/代码块中调用?

    notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢;(本质是让处于入口队列的线程竞争锁)


37.为什么要使用线程池

避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。


38.怎么检测一个线程是否持有对象监视器

判断某个线程是否持有对象监视器:Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着“某条线程”指的是当前线程。


39.synchronized和ReentrantLock的区别

synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。

既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量

ReentrantLock比synchronized的扩展性体现在几点上:

(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁

(2)ReentrantLock可以获取各种锁的信息

(3)ReentrantLock可以灵活地实现多路通知

另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。


40.Linux环境下如何查找哪个线程使用CPU最长

这是一个比较偏实践的问题,这种问题我觉得挺有意义的。可以这么做:

(1)获取项目的pid,jps或者ps -ef | grep java,这个前面有讲过

(2)top -H -p pid,顺序不能改变

这样就可以打印出当前的项目,每条线程占用CPU时间的百分比。注意这里打出的是LWP,也就是操作系统原生线程的线程号,我笔记本山没有部署Linux环境下的Java工程,因此没有办法截图演示,网友朋友们如果公司是使用Linux环境部署项目的话,可以尝试一下。

使用”top -H -p pid”+”jps pid”可以很容易地找到某条占用CPU高的线程的线程堆栈,从而定位占用CPU高的原因,一般是因为不当的代码操作导致了死循环。

最后提一点,”top -H -p pid”打出来的LWP是十进制的,”jps pid”打出来的本地线程号是十六进制的,转换一下,就能定位到占用CPU高的线程的当前线程堆栈了。


41.不可变对象对多线程有什么帮助

   前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。


42.什么是多线程的上下文切换

多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。


43.如果你提交任务时,线程池队列已满,这时会发生什么

   如果你使用的LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行

因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务;

如果你使用的是有界队列比方说ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,

ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy


44.Thread.sleep(0)的作用是什么

Thread.Sleep(0)的作用,就是“触发操作系统立刻重新进行一次CPU竞争

这个问题和上面那个问题是相关的,我就连在一起了。由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。


45.什么是自旋

   很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。

既然synchronized里面的代码执行地非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。

如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。

https://blog.csdn.net/qq_42882671/article/details/82185265


46.什么是Java内存模型

   Java内存模型定义了一种多线程访问Java内存的规范。Java内存模型要完整讲不是这里几句话能说清楚的,我简单总结一下Java内存模型的几部分内容:

(1)Java内存模型将内存分为了主内存工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次Java线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去

(2)定义了几个原子操作,用于操作主内存和工作内存中的变量

(3)定义了volatile变量的使用规则

(4)happens-before,即先行发生原则,定义了操作A必然先行发生于操作B的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的happens-before规则,则这段代码一定是线程非安全的


47.什么是CAS

CAS,全称为compare And Swap,即比较并交换”,是乐观锁的一种实现方式

假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。

当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。


48.什么是AQS

简单说一下AQS,AQS全称为Abstract Queued Sychronizer,翻译过来应该是抽象队列同步器。

如果说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接所有的Entry,比方说ReentrantLock,所有等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry开始运行。

AQS定义了对双向队列所有的操作,而只开放了tryLock和tryRelease方法给开发者使用,开发者可以根据自己的实现重写tryLock和tryRelease方法,以实现自己的并发功能。


49.Semaphore有什么作用

   Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,可以传入一个int型整数n,表示某段代码最多只有n个线程可以访问,如果超出了n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。


50.Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?

    某个方法中如果有多条语句,并且都在操作同一个类变量,那么在多线程环境下不加锁,势必会引发线程安全问题,这很好理解,但是size()方法明明只有一条语句,为什么还要加锁?

主要原因有两点:

(1)同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程A在执行Hashtable的put方法添加数据,线程B则可以正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A添加了完了数据,但是没有对size++,线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。而给size()方法加了同步之后,意味着线程B调用size()方法只有在线程A调用put方法完毕之后才可以调用,这样就保证了线程安全性

(2)CPU执行代码,执行的不是Java代码,这点很关键,一定得记住。Java代码最终是被翻译成汇编代码执行的,汇编代码才是真正可以和硬件电路交互的代码。即使你看到Java代码只有一行,甚至你看到Java代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句”return count”假设被翻译成了三句汇编语句执行,完全可能执行完第一句,线程就切换了。


51.线程类的构造方法、静态块是被哪个线程调用的

线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。

如果说上面的说法让你感到困惑,那么我举个例子,假设Thread2中new了Thread1,main函数中new了Thread2,那么:

(1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的

(2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的


52.同步方法和同步块,哪个是更好的选择

 同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。

请知道一条原则:同步的范围越少越好。

借着这一条,我额外提一点,虽说同步的范围越少越好,但是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁–>解锁的次数,有效地提升了代码执行的效率。


53.高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

关于这个问题,个人看法是:

(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换

(2)并发不高、任务执行时间长的业务要区分开看:

     a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务

    b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。


54.在Java中Lock接口比synchronized块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?

lock接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像ConcurrentHashMap这样的高性能数据结构和有条件的阻塞。Java线程面试的问题越来越会根据面试者的回答来提问。我强烈建议在你去参加多线程的面试之前认真读一下Locks,因为当前其大量用于构建电子交易终统的客户端缓存和交易连接空间。


55.用Java实现阻塞队列。

阻塞队列与普通队列的不同在于。当队列是空的时候,从队列中获取元素的操作将会被阻塞,或者当队列满时,往队列里面添加元素将会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列,下图展示了如何通过阻塞队列来合作:

线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素

从5.0开始,JDK在Java.util.concurrent包里提供了阻塞队列的官方实现。尽管JDK中已经包含了阻塞队列的官方实现。

阻塞队列的实现

阻塞队列的实现类似于带上限的Semaphore的实现。

废话不多说:

package com.huojg.test;

import java.util.LinkedList;
import java.util.List;

public class BlockingQueue {  
      
      private List queue = new LinkedList();  
      private int  limit = 10;  
      
      public BlockingQueue(int limit){  
        this.limit = limit;  
      }  
      
      public synchronized void enqueue(Object item)  
      throws InterruptedException  {  
        while(this.queue.size() == this.limit) {  
          wait();  
        }  
        if(this.queue.size() == 0) {  
          notifyAll();  
        }  
        this.queue.add(item);  
      }  
      
      public synchronized Object dequeue()  
      throws InterruptedException{  
        while(this.queue.size() == 0){  
          wait();  
        }  
        if(this.queue.size() == this.limit){  
          notifyAll();  
        }  
      
        return this.queue.remove(0);  
      }  
      
    }  
 

必须注意到,在enqueue和dequeue方法内部,只有队列的大小等于上限(limit)或者下限(0)时,才调用notifyAll方法。如果队列的大小既不等于上限,也不等于下限,任何线程调用enqueue或者dequeue方法时,都不会阻塞,都能够正常的往队列中添加或者移除元素。


56.什么是原子操作,Java中的原子操作是什么?

所谓原子操作,就是"不可中断的一个或一系列操作" , 在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能昂贵的锁,甚至借助于原子操作,我们可以实现互斥锁。

https://www.cnblogs.com/hewep/articles/4310623.html


57.什么是竞争条件?你怎样发现和解决竞争?

Java中的多线程,当多个线程对一个数据进行操作时,可能会产生“竞争条件”的现象

这时候需要对线程的操作进行加锁,来解决多线程操作一个数据时可能产生问题。加锁方式有两种,一个是申明Lock对象来对语句快进行加锁,另一种是通过synchronized 关键字来对方法进行加锁。以上两种方法都可以有效解决Java多线程中存在的竞争条件的问题。

https://blog.csdn.net/hourui93/article/details/48596259


58. 在java中绿色线程和本地线程区别?

什么是绿色线程?

绿色线程(Green Thread)是一个相对于操作系统线程(Native Thread)的概念。
操作系统线程(Native Thread)的意思就是,程序里面的线程会真正映射到操作系统的线程,线程的运行和调度都是由操作系统控制的
绿色线程(Green Thread)的意思是,程序里面的线程不会真正映射到操作系统的线程,而是由语言运行平台自身来调度。

绿色线程是由虚拟机调度,而不是本地的操作系统。

通过绿色线程可以模拟出多线程的环境,不需要依赖本地操作系统的支持。

绿色线程被管理在用户空间,而非内核,不需要本地线程支持。

对比:

绿色线程在线程激活和线程同步方面优于本地线程

在I/O和上下文操作方面性能要低于本地线程


59.死锁与活锁的区别,死锁与馅饼的区别?

死锁:线程A或者B需要过独木桥(使用该进程),而C还没有走完(进程还在占用),于是三方僵死; 也可以是没有C 的情况下,A和B互不礼让僵死. A和B都认为自己优先级最高应该使用该进程

5.多线程与高并发_第8张图片

5.多线程与高并发_第9张图片

活锁:线程A和B都需要过桥(都需要使用进程),而都礼让不走(那到的系统优先级相同,都认为不是自己优先级高),就这么僵持下去

 

5.多线程与高并发_第10张图片

饿死锁:这是个独木桥(单进程),桥上只能走一个人,B来到时A在桥上,B等待; 而此时比B年龄小的C来了,B让C现行(A走完后系统把进程分给了C), C上桥后,D又来了,B又让D现行(C走完后系统把进程分个了D) 以此类推B一直是等待状态

5.多线程与高并发_第11张图片

 

5.多线程与高并发_第12张图片

 

https://blog.csdn.net/weixin_34200628/article/details/86856330

https://blog.csdn.net/ZW547138858/article/details/83269342


60.在Java中什么是线程调度?

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度抢占式线程调度

线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。 

同上一个问题,线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。

时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。分配CPU时间可以基于线程优先级或者线程等待的时间。
https://blog.csdn.net/wangdong5678999/article/details/80960161


61. 在线程中你怎么处理不可捕捉异常?

1.CheckException(非运行时异常):对于可恢复条件被检查的异常

2.UnCheckException(运行时异常):已经运行不可恢复的异常。

run()方法不支持throws语句,所以当线程对象的run()方法抛出非运行时异常时,必须捕获并处理他们。当运行时异常从run方法中抛出时,默认行为是在控制台输出堆栈记录并退出程序。

解决方法一般是调用线程的setUncaughtExceptionHandler(UncaughtExceptionHandler x) 在UncaughtExceptionHandler中处理异常。

https://blog.csdn.net/sinat_36265222/article/details/87865973


62. 什么是线程组,为什么在Java中不推荐使用?

ThreadGroup类,可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。

为什么不推荐使用?因为使用有很多的安全隐患吧,

  • 1.线程组ThreadGroup对象中比较有用的方法是stop、resume、suspend等方法,由于这几个方法会导致线程的安全问题(主要是死锁问题),已经被官方废弃掉了,所以线程组本身的应用价值就大打折扣了。
  • 2.线程组ThreadGroup不是线程安全的,这在使用过程中获取的信息并不全是及时有效的,这就降低了它的统计使用价值。

如果需要使用,推荐使用线程池。


63. 为什么使用Executor框架比使用应用创建和管理线程好?

每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗时、耗资源的。

调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。

接使用new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。
https://blog.csdn.net/BinBin_Jun/article/details/84934112 


64. 在Java中Executor和Executors的区别?

  Executor是Java线程池的顶级接口

5.多线程与高并发_第13张图片

Executors是一个

      Executors类提供了若干个静态方法,用于生成不同类型的线程池

5.多线程与高并发_第14张图片


66.什么是线程安全?Vector是一个线程安全类吗?

    如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分成两组,线程安全和非线程安全的。Vector 是用同步方法来实现线程安全的, 而和它相似的ArrayList不是线程安全的。


67.Java中什么是竞态条件? 举个例子说明。

竞态条件会导致程序在并发情况下出现一些bugs。多线程对一些资源的竞争的时候就会产生竞态条件,如果首先要执行的程序竞争失败排到后面执行了,那么整个程序就会出现一些不确定的bugs。这种bugs很难发现而且会重复出现,因为线程间的随机竞争。一个例子就是无序处理,详见答案。


68. Java中如何停止一个线程?

Java提供了很丰富的API但没有为停止线程提供API。JDK 1.0本来有一些像stop(), suspend() 和 resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。点击这里查看示例代码。


69.一个线程运行时发生异常会怎样?

如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。


70.为什么wait, notify 和 notifyAll这些方法不在thread类里面?

说明为什么把这些方法放在Object类里是有意义的,还有不把它放在Thread类里的原因。一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。你也可以查看这篇文章了解更多。


71.Java中interrupted 和 isInterruptedd方法的区别?

    interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除而后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛出InterruptedException异常的方法都会将中断状态清零。无论如何,一个线程的中断状态有有可能被其它线程调用中断来改变。


72.为什么wait和notify方法要在同步块中调用?

   主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件。


73.为什么你应该在循环中检查等待条件?

    处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。因此,当一个等待线程醒来时,不能认为它原来的等待状态仍然是有效的,在notify()方法调用之后和等待线程醒来之前这段时间它可能会改变。这就是在循环中使用wait()方法效果更好的原因,


74.Java中的同步集合与并发集合有什么区别?

    同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在Java1.5之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。更多内容详见答案。


75.Java中堆和栈有什么不同?

   每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。

而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile 变量就可以发挥作用了,它要求线程从主存中读取变量的值。


目录

  1. 常用线程池列表
  2. ThreadPoolExecutor
  3. ThreadFactory线程工厂
  4. RejectedExecutionHandler拒绝策略
  5. Queue任务队列

76.常用线程池列表

1、构造一个固定线程数目的线程池,配置的corePoolSize与maximumPoolSize大小相同,同时使用了一个无界LinkedBlockingQueue存放阻塞任务,因此多余的任务将存在再阻塞队列,不会由RejectedExecutionHandler处理

2、构造一个缓冲功能的线程池,配置corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,keepAliveTime=60s,以及一个无容量的阻塞队列 SynchronousQueue,因此任务提交之后,将会创建新的线程执行;线程空闲超过60s将会销毁

3、构造一个只支持一个线程的线程池,配置corePoolSize=maximumPoolSize=1,无界阻塞队列LinkedBlockingQueue;保证任务由一个线程串行执行

5.多线程与高并发_第15张图片

4、构造有定时功能的线程池,配置corePoolSize,无界延迟阻塞队列DelayedWorkQueue;有意思的是:maximumPoolSize=Integer.MAX_VALUE,由于DelayedWorkQueue是无界队列,所以这个值是没有意义的

 

 


78. ThreadFactory

四. 拒绝策略

拒绝策略就是任务实在是已经执行不了,那么就需要你告诉程序,怎么样去拒绝在执行其他任务

ThreadPoolExecutor类里面是内置了4中拒绝策略,我们一个一个来分析

策略-1

用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;

如果执行程序已关闭,则会丢弃该任务。如下:

5.多线程与高并发_第16张图片

策略-2

直接抛出异常

5.多线程与高并发_第17张图片

策略-3

什么都不做直接丢弃

5.多线程与高并发_第18张图片

策略-4

从任务队列中移除最早的一个,然后将拒绝的任务重新执行

5.多线程与高并发_第19张图片

到这里拒绝策略就说完了,应该都明白了吧,下面我们说下实际中会怎么用。

如果在任务不是特别多特别重要的情境下,可以在执行拒绝策略发送一个通知事件,通知相关的人查看。

比如以下这种

但是当任务特别重要的时候,比如说银行处理用户的转账信息更新事物的任务时候,那么这个任务就比较重要了,不可能用这种的,这种对数据要求高的我们都是用Elastic-Job,这种分布式任务处理框架,任务会首先落数据库,然后从数据库中批量读取任务执行。感兴趣的童鞋可以下去自行了解。


79.任务队列

扩展点知识,队列再次,小编不对队列进行讲解,只提到,感兴趣的童鞋下去在深入研究

Queue:

    分为阻塞队列和非阻塞队里

阻塞队列一共有四套方法分别用来进行insert、remove和examine,当每套方法对应的操作不能马上执行时会有不同的反应,下面这个表格就分类列出了这些方法:

5.多线程与高并发_第20张图片

  1. ThrowsException:如果操作不能马上进行,则抛出异常
  2. SpecialValue:如果操作不能马上进行,将会返回一个特殊的值,一般是true或者false
  3. Blocks:如果操作不能马上进行,操作会被阻塞
  4. TimesOut:如果操作不能马上进行,操作会被阻塞指定的时间,如果指定时间没执行,则返回一个特殊值,一般是true或者false 需要注意的是,我们不能向BlockingQueue中插入null,否则会报NullPointerException。
  5. LinkedBlockingQueue 无边界队里
  6. PriorityBlockingQueue 排序队列,内部会排序但是要实现排序接口
  7. SynchronousQueue 同步队列
  8. ArrayBlockingQueue 阻塞队里,内不是数组,有边界
  9. DelayQueue 延迟队里

80.线程池参数的解释

线程池的了解如果了解ThredPoolExecutor,那么分析构造函数里的参数意义java中作为线程池Executor底层实现类的ThredPoolExecutor的构造函数,我们看下代码

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit, 
                          BlockingQueue workQueue, 
                          ThreadFactory threadFactory, 
                          RejectedExecutionHandler handler) {}

5.多线程与高并发_第21张图片

其中各个参数含义如下:

corePoolSize- 池中所保存的线程数,包括空闲线程。需要注意的是在初创建线程池时线程不会立即启动,直到有任务提交才开始启动线程并逐渐时线程数目达到corePoolSize。若想一开始就创建所有核心线程需调用prestartAllCoreThreads方法。

maximumPoolSize-池中允许的最大线程数。需要注意的是当核心线程满且阻塞队列也满时才会判断当前线程数是否小于最大线程数,并决定是否创建新线程。

keepAliveTime - 当线程数大于核心时,多于的空闲线程最多存活时间 unit - keepAliveTime 参数的时间单位。

workQueue - 当线程数目超过核心线程数时用于保存任务的队列。主要有3种类型的BlockingQueue可供选择:无界队列,有界队列和同步移交。将在下文中详细阐述。从参数中可以看到,此队列仅保存实现Runnable接口的任务。

threadFactory - 执行程序创建新线程时使用的工厂。

 handler - 阻塞队列已满且线程数达到最大值时所采取的饱和策略。java默认提供了4种饱和策略的实现方式:中止、抛弃、抛弃最旧的、调用者运行。将在下文中详细阐述。

6. 可选择的阻塞队列BlockingQueue首先看一下新任务进入时线程池的执行策略:

如果运行的线程少于corePoolSize,则 Executor始终首选添加新的线程,而不进行排队。(如果当前运行的线程小于corePoolSize,则任务根本不会存入queue中,而是直接运行) 如果运行的线程大于等于 corePoolSize,则 Executor始终首选将请求加入队列,而不添加新的线程。 如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。 主要有3种类型的BlockingQueue

6.1无界队列队列大小无限制,常用的为无界的LinkedBlockingQueue,使用该队列做为阻塞队列时要尤其当心,当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM。最近工作中就遇到因为采用LinkedBlockingQueue作为阻塞队列,部分任务耗时80s+且不停有新任务进来,导致cpu和内存飙升服务器挂掉。

6.2 有界队列常用的有两类,一类是遵循FIFO原则的队列如ArrayBlockingQueue与有界的LinkedBlockingQueue,另一类是优先级队列如PriorityBlockingQueue。PriorityBlockingQueue中的优先级由任务的Comparator决定。

使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。

6.3 同步移交如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列。SynchronousQueue不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。


81.java提供的四种常用线程池解析

在JDK帮助文档中,有如此一段话:

强烈建议程序员使用较为方便的Executors工厂方法

Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)、

Executors.newFixedThreadPool(int)(固定大小线程池)

Executors.newSingleThreadExecutor()(单个后台线程)

它们均为大多数使用场景预定义了设置。

7.1 newCachedThreadPool

public static ExecutorService newCachedThreadPool() {

return new ThreadPoolExecutor(0, 
                              Integer.MAX_VALUE,
                              60L, 
                              TimeUnit.SECONDS,
                              new SynchronousQueue());

}

newCachedThreadPool中如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

初看该构造函数时我有这样的疑惑:核心线程池为0,那按照前面所讲的线程池策略新任务来临时无法进入核心线程池,只能进入 SynchronousQueue中进行等待,而SynchronousQueue的大小为1,那岂不是第一个任务到达时只能等待在队列中,直到第二个任务到达发现无法进入队列才能创建第一个线程?

这个问题的答案在上面讲SynchronousQueue时其实已经给出了,要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。因此即便SynchronousQueue一开始为空且大小为1,第一个任务也无法放入其中,因为没有线程在等待从SynchronousQueue中取走元素。因此第一个任务到达时便会创建一个新线程执行该任务。

这里引申出一个小技巧:有时我们可能希望线程池在没有任务的情况下销毁所有的线程,既设置线程池核心大小为0,但又不想使用SynchronousQueue而是想使用有界的等待队列。显然,不进行任何特殊设置的话这样的用法会发生奇怪的行为:直到等待队列被填满才会有新线程被创建,任务才开始执行。这并不是我们希望看到的,此时可通过allowCoreThreadTimeOut使等待队列中的元素出队被调用执行,详细原理和使用将会在后续博客中阐述。


7.2 newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

public static ExecutorService newFixedThreadPool(int nThreads) {

return new ThreadPoolExecutor(
nThreads, 
nThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());

}

看代码一目了然了,使用固定大小的线程池并使用无限大的队列


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

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {

return new ScheduledThreadPoolExecutor(corePoolSize);

}

再来看看ScheduledThreadPoolExecutor()的构造函数

public ScheduledThreadPoolExecutor(int corePoolSize) {

super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,

new DelayedWorkQueue());

}

ScheduledThreadPoolExecutor的父类即ThreadPoolExecutor,因此这里各参数含义和上面一样。值得关心的是DelayedWorkQueue这个阻塞对列,在上面没有介绍,它作为静态内部类就在ScheduledThreadPoolExecutor中进行了实现。具体分析讲会在后续博客中给出,在这里只进行简单说明:DelayedWorkQueue是一个无界队列,它能按一定的顺序对工作队列中的元素进行排列。因此这里设置的最大线程数 Integer.MAX_VALUE没有任何意义。关于ScheduledThreadPoolExecutor的具体使用将会在后续quartz的周期性任务实现原理中进行进一步分析。


7.4 newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {

return new DelegatedScheduledExecutorService

(new ScheduledThreadPoolExecutor(1));

}

//首先new了一个线程数目为1的ScheduledThreadPoolExecutor,再把该对象传入
///DelegatedScheduledExecutorService中,看看DelegatedScheduledExecutorService的实现代码:

DelegatedScheduledExecutorService(ScheduledExecutorService executor) {

super(executor);

e = executor;

}

在看看它的父类

DelegatedExecutorService(ExecutorService executor) { e = executor; }1其实就是使用装饰模式增强了ScheduledExecutorService(1)的功能,不仅确保只有一个线程顺序执行任务,也保证线程意外终止后会重新创建一个线程继续执行任务。


7.5 newWorkStealingPool创建一个拥有多个任务队列(以便减少连接数)的线程池。这是jdk1.8中新增加的一种线程池实现,先看一下它的无参实现

public static ExecutorService newWorkStealingPool() {

return new ForkJoinPool

(Runtime.getRuntime().availableProcessors(),

ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);

}

返回的ForkJoinPool从jdk1.7开始引进,个人感觉类似于mapreduce的思想。这个线程池较为特殊,将在后续博客中给出详细的使用说明和原理。


82.Java--8--新特性--串并行流与ForkJoin框架

并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。穿行流则相反,并行流的底层其实就是ForkJoin框架的一个实现。

那么先了解一下ForkJoin框架吧。

Fork/Join框架:在必要的情况下,将一个大任务,进行拆分(fork) 成若干个子任务(拆到不能再拆,这里就是指我们制定的拆分的临界值),再将一个个小任务的结果进行join汇总。

Fork/Join与传统线程池的区别!

Fork/Join采用“工作窃取模式”,当执行新的任务时他可以将其拆分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随即线程中偷一个并把它加入自己的队列中。

就比如两个CPU上有不同的任务,这时候A已经执行完,B还有任务等待执行,这时候A就会将B队尾的任务偷过来,加入自己的队列中,对于传统的线程,ForkJoin更有效的利用的CPU资源!

Java 8 中将并行流进行了优化,我们可以很容易的对数据进行并行流的操作,Stream API可以声明性的通过parallel()与sequential()在并行流与穿行流中随意切换!


83.Top命令之后有哪些内容,有什么作用?

 当前系统时间,系统进程数量及状态统计,cpu状态,内存状态,swap交换分区,各进程的状态监控

https://blog.csdn.net/piaoslowly/article/details/51759198


84.简述 ConcurrentLinkedQueue LinkedBlockingQueue 的用处和不同之处。

   适用阻塞队列的好处:多线程操作共同的队列时不需要额外的同步,另外就是队列会自动平衡负载,即那边(生产与消费两边)处理快了就会被阻塞掉,从而减少两边的处理速度差距。
当许多线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。
LinkedBlockingQueue 多用于任务队列
ConcurrentLinkedQueue  多用于消息队列

多个生产者,对于LBQ性能还算可以接受;但是多个消费者就不行了mainLoop需要一个timeout的机制,否则空转,cpu会飙升的。LBQ正好提供了timeout的接口,更方便使用
如果CLQ,那么我需要收到处理sleep

单生产者,单消费者  用 LinkedBlockingqueue
多生产者,单消费者   用 LinkedBlockingqueue
单生产者 ,多消费者   用 ConcurrentLinkedQueue
多生产者 ,多消费者   用 ConcurrentLinkedQueue


85.线上CPU爆高,请问你如何找到问题所在?

步骤一、找到最耗CPU的进程

工具:top

方法:

  • 执行top -c ,显示进程运行信息列表

  • 键入P (大写p),进程按照CPU使用率排序

步骤二:找到最耗CPU的线程

  • 工具:top

方法:

  • top -Hp 10765 ,显示一个进程的线程运行信息列表

  • 键入P (大写p),线程按照CPU使用率排序

步骤三:将线程PID转化为16进制

工具:printf

方法:printf “%x\n” 10804

步骤四:查看堆栈,找到线程在干嘛

工具:pstack/jstack/grep

方法:jstack 10765 | grep ‘0x2a34’ -C5 --color

  • 打印进程堆栈

  • 通过线程id,过滤得到线程堆栈

https://blog.csdn.net/jiangzhexi/article/details/77429671


86.介绍下你理解的操作系统中线程切换过程。

   在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换桢”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。 

  上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。


87.线程池的关闭方式有几种,各自的区别是什么。

Java提供的对ExecutorService的关闭方式有两种,

一种是调用其shutdown()方法,

另一种是调用shutdownNow()方法。这两者是有区别的。

shutdown:

  1. 调用之后不允许继续往线程池内继续添加线程;
  2. 线程池的状态变为SHUTDOWN状态;
  3. 所有在调用shutdown()方法之前提交到ExecutorSrvice的任务都会执行;
  4. 一旦所有线程结束执行当前任务,ExecutorService才会真正关闭。

shutdownNow():

  1. 该方法返回尚未执行的 task 的 List;
  2. 线程池的状态变为STOP状态;
  3. 阻止所有正在等待启动的任务, 并且停止当前正在执行的任务;

简单点来说,就是: shutdown()调用后,不可以再 submit 新的 task,已经 submit 的将继续执行 shutdownNow()调用后,试图停止当前正在执行的 task,并返回尚未执行的 task 的 list

https://blog.csdn.net/qq_36691683/article/details/84856516


88.假如有一个第三方接口,有很多个线程去调用获取数据,现在规定每秒钟最多有 10 个线程同时调用它,如何做到。

ScheduledThreadPoolExecutor 设置定时,进行调度。 
public ScheduledThreadPoolExecutor(int corePoolSize, 
ThreadFactory threadFactory) { 
super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS, 
new DelayedWorkQueue(), threadFactory); 
}

http://ifeve.com/java-scheduledthreadpoolexecutor/


89.如果让你实现一个并发安全的链表,你会怎么做。

Collections.synchronizedList() ConcurrentLinkedQueue 

https://blog.csdn.net/xingjiarong/article/details/48046751


90.有哪些无锁数据结构,他们实现的原理是什么。

无锁数据结构

LockFree,CAS 
基于jdk提供的原子类原语实现,例如AtomicReference 

无锁数据结构的实现主要基于两个方面:原子性操作和内存访问控制方法

https://blog.csdn.net/b_h_l/article/details/8704480

https://www.cnblogs.com/woodytu/archive/2015/08/01/4691469.html

https://blog.csdn.net/hellozhxy/article/details/80820257


91.Java分为两种线程:本地线程和守护线程

   守护线程:指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

   用户线程:自己创建的线程。比如:new Thread。这就是自己创建了一个线程。

守护线程和用户线程的没啥本质的区别:唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。


 92.sleep和sleep(0)的区别.

 Sleep 的意思是告诉操作系统自己要休息 n 毫秒,这段时间就让给另一个就绪的线程吧。当 n=0 的时候,意思是要放弃自己剩下的时间片,但是仍然是就绪状态,其实意思和 Yield 有点类似。但是 Sleep(0) 只允许那些优先级相等或更高的线程使用当前的CPU,其它线程只能等着挨饿了。如果没有合适的线程,那当前线程会重新使用 CPU 时间片。

  优势:相比 Yield,可以调度任何处理器的线程使用时间片。

  劣势:只能调度优先级相等或更高的线程,意味着优先级低的线程很难获得时间片,很可能永远都调用不到。当没有符合条件的线程,会一直占用 CPU 时间片,造成 CPU 100%占用率。

Thread.Sleep(1)

   该方法使用 1 作为参数,这会强制当前线程放弃剩下的时间片,并休息 1 毫秒(因为不是实时操作系统,时间无法保证精确,一般可能会滞后几毫秒或一个时间片)。但因此的好处是,所有其它就绪状态的线程都有机会竞争时间片,而不用在乎优先级。

  优势:可以调度任何处理器的线程使用时间片。无论有没有符合的线程,都会放弃 CPU 时间,因此 CPU 占用率较低。

  劣势:相比 Thread.Sleep(0),因为至少会休息一定时间,所以速度要更慢。 

https://blog.csdn.net/xjc200808/article/details/47124629


93.JUC下研究过哪些井发工具,讲讲原理。

常用工具类CountDownLatch、CyclicBarrier、Semaphore;

https://blog.csdn.net/eos2009/article/details/78842040


94.并行和并发有什么区别?

并行:就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。
并发:是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。
  比如我跟两个网友聊天,左手操作一个电脑跟甲聊,同时右手用另一台电脑跟乙聊天,这就叫并行。
如果用一台电脑我先给甲发个消息,然后立刻再给乙发消息,然后再跟甲聊,再跟乙聊。这就叫并发。

  • 5.多线程与高并发_第22张图片

95.线程池中 submit()和 execute()方法有什么区别?

execute():只能执行 Runnable 类型的任务。

submit():可以执行 Runnable 和 Callable 类型的任务。


96.多线程锁的升级原理是什么?

    在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,JVM 让其持有偏向锁,并将threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否尤其线程 id 一致,如果一致则可以直接使用,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,不会堵塞,执行一定次数之后就会升级为重量级锁,进入堵塞,整个过程就是锁升级的原理。

锁的升级的目的:在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,减低了锁带来的性能消耗。

锁升级是为了减低了锁带来的性能消耗。
5.多线程与高并发_第23张图片

 


97. 线程池都有哪些状态?

RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。

SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。

STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。

TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。

TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
 


98.说一下 Atomic 的原理?

Atomic 主要利用 CAS (Compare And Swap) (也有说是Compare And Set)和 volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。


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

  • 方法一:使用安全类,比如 Java. util. concurrent 下的类。
  • 方法二:使用自动锁 synchronized。
  • 方法三:使用手动锁 Lock。5.多线程与高并发_第24张图片

100.创建线程开销,创建线程都有什么开销

多线程中两个必要的开销:线程的创建、上下文切换

上下文切换:

    概念:
        当前任务执行一个时间片后会切换到下一个任务。在切换之前,上一个任务的状态会被保存下来,下次切换回这个任务时,可以再加载这个任务的状态,任务从保存到再加载的过程就是一次上下文切换。

    说明:
        1)时间片是CPU分配给各个线程的时间,时间片一般是几十毫秒。
        2)CPU通过给每个线程分配CPU时间片,并且不停地切换线程来实现多线程。因为时间片非常短,所以感觉多个线程是在同时执行。


    减少上下文切换的方法:
    
        1) 无锁并发编程:
            多线程竞争锁时,会引起上下文切换,所以在使用多线程处理数据时,可以采用一些策略来避免使用锁。
            常见的策略:将数据按照id的哈希值进行切分,不同的线程处理不同段的数据。

        2) 锁分离技术:
            举例:ConcurrentHashMap
            
        3) CAS算法
            java的Atomic包使用CAS算法来更新数据,而不需要加锁。

        4) 使用最少的线程
            避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
            
    举例:

        通过减少大量WAITING的线程,来减少上下文切换次数

            # 转储堆栈信息
            jstack PID > dumpfile

            # 统计所有线程的状态
            grep java.lang.Thread.State dumpfile | awk '{print $2" "$3" "$4" "$5}' | sort | uniq -c

            如果存在大量waiting的线程,则查看dumpfile文件进行分析:
                1)如果是服务器的工作线程大量等待,则修改服务器配置文件中线程池的配置信息,然后重启查看效果。


101.销毁一个线程有哪些方法

(1)设置退出标志,使线程正常退出,也就是当run()方法完成后线程终止

(2)使用interrupt()方法中断线程

(3)使用stop方法强行终止线程(不推荐使用,Thread.stop, Thread.suspend, Thread.resume 和

     Runtime.runFinalizersOnExit 这些终止线程运行的方法已经被废弃,使用它们是极端不安全的!)
  stop()方法太过于暴力,会强行把执行一半的线程终止。这样会就不会保证线程的资源正确释放,通常是没有给与线程完成资源释放工作的机会,因此会导致程序工作在不确定的状态下

https://blog.csdn.net/qq_37465368/article/details/80869218


102.每个线程有自己的工作线程,static的变量会被拷贝到工作内存中吗?

    static只是表示这个变量是该类所有实例共享的。volatile是表示变量在使用时直接去共享内存中获取,而不是从当前线程的私有内存区域存取。通常在线程开启时,会将使用到的变量产生一个线程内的副本,与主线程内存中的数据不同步。


103.多线程与高并发的关系和区别

一、什么是高并发

       高并发(High Concurrency)是一种系统运行过程中遇到的一种“短时间内遇到大量操作请求”的情况,主要发生在web系统集中大量访问收到大量请求(例如:12306的抢票情况;天猫双十一活动)。该情况的发生会导致系统在这段时间内执行大量操作,例如对资源的请求、数据库的操作等。

二、高并发的处理指标

高并发相关常用的一些指标有:响应时间、吞吐量、每秒查询率QPS、并发用户数
1、响应时间(Response Time)

响应时间:系统对请求做出响应的时间。例如系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间

2、吞吐量(Throughput)

吞吐量:单位时间内处理的请求数量。

3、每秒查询率QPS(Query Per Second)

QPS:每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。

4、并发用户数

并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。

三、高并发和多线程的关系和区别

“高并发和多线程”总是被一起提起,给人感觉两者好像相等,实则 高并发 ≠ 多线程

3.1、多线程

       多线程是Java的特性,因为现在cpu都是多核多线程的,可以同时执行几个任务,为了提高jvm的执行效率,Java提供了这种多线程的机制,以增强数据处理效率。多线程对应的是cpu,高并发对应的是访问请求,可以用单线程处理所有访问请求,也可以用多线程同时处理访问请求。
       在过去单CPU时代,单任务在一个时间点只能执行单一程序。之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程。虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个CPU,并交由操作系统来完成多任务间对CPU的运行切换,以使得每个任务都有机会获得一定的时间片运行。
       后来发展到多线程技术,使得在一个程序内部能拥有多个线程并行执行。一个线程的执行可以被认为是一个CPU在执行该程序。当一个程序运行在多线程下,就好像有多个CPU在同时执行该程序。
       总之,多线程即可以这么理解:多线程是处理高并发的一种编程方法,即并发需要用多线程实现。

3.2、高并发

       高并发不是JAVA的专有的东西,是语言无关的广义的,为提供更好互联网服务而提出的概念。典型的场景,例如:12306抢火车票,天猫双十一秒杀活动等。该情况的发生会导致系统在这段时间内执行大量操作,例如对资源的请求,数据库的操作等。如果高并发处理不好,不仅仅降低了用户的体验度(请求响应时间过长),同时可能导致系统宕机,严重的甚至导致OOM异常,系统停止工作等。
       如果要想系统能够适应高并发状态,则需要从各个方面进行系统优化,包括,硬件、网络、系统架构、开发语言的选取、数据结构的运用、算法优化、数据库优化等……而多线程只是其中解决方法之一。

四、多线程并发技术

Java多线程编程将会涉及到如下技术点:
1、并发编程三要素
        原子性:即一个不可再被分割的颗粒。在Java中原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
        有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
        可见性:当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值。
2、 线程的五大状态
        创建状态:当用 new 操作符创建一个线程的时候
        就绪状态:调用 start 方法,处于就绪状态的线程并不一定马上就会执行 run 方法,还需要等待CPU的调度
        运行状态:CPU 开始调度线程,并开始执行 run 方法
        阻塞状态:线程的执行过程中由于一些原因进入阻塞状态比如:调用 sleep 方法、尝试去得到一个锁等等
        死亡状态:run 方法执行完 或者 执行过程中遇到了一个异常
3、悲观锁与乐观锁
        悲观锁:每次操作都会加锁,会造成线程阻塞。
        乐观锁:每次操作不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞。
4、线程之间的协作
        线程间的协作有:wait/notify/notifyAll等
5、synchronized 关键字
        synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
        1)、修饰一个代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
        2)、修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
        3)、修改一个静态的方法:其作用的范围是整个静态方法,作用的对象是这个类的所有对象
        4)、修改一个类:其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
6、CAS
        CAS全称是Compare And Swap,即比较替换,是实现并发应用到的一种技术。操作包含三个操作数—内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
        CAS存在三大问题:ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作。
7、线程池
        如果我们使用线程的时候就去创建一个线程,虽然简单,但是存在很大的问题。如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池通过复用可以大大减少线程频繁创建与销毁带来的性能上的损耗。

五、高并发技术解决方案

5.1、怎样提高系统的高并发能力?

1、静态资源结合CDN来解决图片文件等访问
2、分布式缓存:redis、memcached等。
3、消息队列中间件:activeMQ等,解决大量消息的异步处理能力。
4、应用拆分:一个工程被拆分为多个工程部署,利用dubbo解决多工程之间的通信。
5、数据库垂直拆分和水平拆分(分库分表)等。
6、数据库读写分离,解决大数据的查询问题。
7、利用nosql ,例如mongoDB配合mysql组合使用。
8、建立大数据访问情况下的服务降级以及限流机制等。


104.什么是线程安全

我给出一个个人认为解释地最好的:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。

这个问题有值得一提的地方,就是线程安全也是有几个级别的:

(1)不可变

像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用

(2)绝对线程安全

不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet

(3)相对线程安全

相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。

(4)线程非安全

这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类多。


线程池补充:

ThreadPoolExecutor

Executor框架最核心的类是ThreadPoolExecutor,它是线程池的实现类,Executors提供了几种常用线程池,都是是实现该类,下面简单看下该类的构造方法。

5.多线程与高并发_第25张图片

 

5.多线程与高并发_第26张图片

 

通过Executor框架的工具类Executors,可以创建3种类型的ThreadPoolExecutor。

  • FixedThreadPool。
  • SingleThreadExecutor。
  • CachedThreadPool。

下面一一介绍


FixedThreadPool

该线程池,被称为可重用固定线程数的线程池。下面看他源码实现。

public static ExecutorService newFixedThreadPool(int nThreads) { 
return new ThreadPoolExecutor(nThreads, nThreads, 
0L, TimeUnit.MILLISECONDS, 
new LinkedBlockingQueue()); 
} 

从源码实现上看,的corePoolSize和maximumPoolSize都被设置为创建FixedThreadPool时指 定的参数nThreads。 意味着,可自由设置其线程池中的线程多少,且最大线程数=核心线程数。

这里把keepAliveTime设置为0L,意味着多余的空闲线程会被立即终止。

FixedThreadPool的execute()方法如下图:

5.多线程与高并发_第27张图片

 

通过上图分析:

  1. 如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务。
  2. 当运行的线程数等于corePoolSize,将新任务加入LinkedBlockingQueue。
  3. 线程执行完1中的任务后,会在循环中反复从LinkedBlockingQueue获取任务来执行。

FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为 Integer.MAX_VALUE)。使用无界队列作为工作队列会对线程池带来如下影响。

  1. 当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中 的线程数不会超过corePoolSize,则maximumPoolSize将是一个无效的参数。
  2. 由于使用无界队列,运行中的FixedThreadPool(未执行方法shutdown()或 shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)。

SingleThreadExecutor

该线程池,被称为单个worker线程。下面看他源码实现。

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

SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1。其他参数与 FixedThreadPool相同。SingleThreadExecutor使用的也是LinkedBlockingQueuedu队列,对线程池带来的影响与FixedThreadPool相同。

SingleThreadExecutor的execute()方法如下图:

5.多线程与高并发_第28张图片

 

从上图中可看出:

  1. 当线程池中无运行线程时,新任务将会建立一个线程去执行。
  2. 预热之后(线程池中已经有个线程运行),将新任务加入队列。
  3. 线程执行完,反复从队列中获取来执行。

可以看出他是按照一定顺序依次执行任务

CachedThreadPool

该线程被称为,根据即需即建的线程池。下面看下他的源码。

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

它的corePoolSize被设置为0,maximumPoolSize被设置为 Integer.MAX_VALUE,即maximumPool是无界的。keepAliveTime设置为60L,空闲线程超过60秒后将会被终止。 它使用没有容量的SynchronousQueue作为线程池的工作队列,但它maximumPool无界,意味着如果主线程提交任务的速度高于 maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。

极端情况下, 它会因为创建过多线程而耗尽CPU和内存资源,所以需谨慎使用。

CachedThreadPool的execute()方法如下图:

5.多线程与高并发_第29张图片

 

从上图看出:

  1. 首先执行压队操作SynchronousQueue.offer(Runnable task)。如果当前maximumPool中有空闲线程 正在执行取队操作SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方 法执行完成;否则执行下面的步骤2)。
  2. 当maximumPool中当前没有空闲线程时,将没有线程执行poll。这种情况下,步骤1将失败。此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成。
  3. 步骤2创建新线程将任务执行完后,会执行poll,这个poll会让空闲线程最多在SynchronousQueue中等待60秒钟,如果60秒内有新的任务加入,则这个空闲线程将直接执行该任务,如果60秒内没有新任务,则终止该线程,所以长时间空闲的线程不会使用任何资源

前面提到SynchronousQueue是没有容量的队列,那么他每次的压队操作,必须等待另一个线程对应的取队操作。反之亦然。

5.多线程与高并发_第30张图片

 

各自适用的场景

  • FixedThreadPool 适用:执行固定大小的短任务,避免长时间执行,造成无界队列长期阻塞,浪费内存。
  • CachedThreadPool适用:执行很多短期异步的小程序或者负载较轻的服务器,处理完之后,线程会自行回收
  • SingleThreadExecutor适用:一个任务一个任务顺序执行的场景。

自定义线程池注意的几点

根据需求,建立自定义线程步骤

  1. 核心线程池大小,及最大线程数和线程空闲时间;
  2. 其次是考虑队列,队列分三种:直接提交(如SynchronousQueue无界队列(如LinkedBlockingQueue),有界队列(如 ArrayBlockingQueue);
  3. 然后是拒绝策略:AbortPolicy(这种策略直接抛出异常,丢弃任务),DiscardPolicy(这种策略和AbortPolicy几乎一样,也是丢弃任务,只不过他不抛出异常),DiscardOldestPolicy(首先丢掉缓存在队列中的最早的任务,然后重新尝试运行该任务),CallerRunsPolicy(反馈控制机制,能够减缓新任务的提交速度)。

你可能感兴趣的:(5.多线程与高并发)