原创

Java成神之路笔记之—Java核心并发编程必会知识点4

_________________________________________________________________________________

Java成神之路笔记之—Java核心并发编程必会知识点4

1 并发编程的优缺点
目的:并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度
优点:充分利用多核CPU的计算能力:通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升,方便进行业务拆分,提升系统并发能力和性能。
缺点:并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、线程安全、死锁等问题。

2 并发编程三要素?怎么保证多线程的运行安全?
原子性:指的是一个或多个操作要么全部执行成功要么全部执行失败。
可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
有序性:程序执行的顺序按照代码的先后顺序执行。
出现线程安全问题
1.线程切换带来的原子性问题。解决方法:synchronized、LOCK,可以解决原子性问题。
2.缓存导致的可见性问题。解决方法:synchronized、volatile、LOCK,可以解决可见性问题。
3.编译优化带来的有序性问题。解决方法:Happens-Before 规则可以解决有序性问题。

3 happens-before 规则
规则一:程序的顺序性。规则一个线程中,按照程序的顺序,前面的操作happens-before后续的任何操作。
规则二:volatile规则。对一个volatile变量的写操作,happens-before后续对这个变量的读操作。
规则三:传递性规则如果A happens-before B,B happens-before C,那么A happens-before C。
规则四:管程中的锁规则。对一个锁的解锁操作,happens-before后续对这个锁的加锁操作。规则五:线程start()规则主线程A启动线程B,线程B中可以看到主线程启动B之前的操作。也就是start() happens before 线程B中的操作。规则六:线程join()规则主线程A等待子线程B完成,当子线程B执行完毕后,主线程A可以看到线程B的所有操作。也就是说,子线程B中的任意操作,happens-before join()的返回。

4 线程和进程
进程
一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
线程
进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

5 什么是上下文切换
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。任务从保存到再加载的过程就是一次上下文切换

6 守护线程和用户线程
守护 (Daemon) 线程:运行在后台,为其他前台线程服务。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作
用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程。
main 函数所在的线程就是一个用户线程main 函数启动的同时在 JVM 内部同时还启动了很多守护线程,比如垃圾回收线程。

7 Windows 和 Linux查看线程cpu利用率
windows在任务管理器内查看。
linux下可以用 top 工具查看:终端执行top命令,然后按下shift+p 查找出cpu使用最高的pid。

8 线程死锁
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
死锁的四个必要条件
1.互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放。
2.请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
3.不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
4.循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞。
如何避免死锁
1.加锁顺序(线程按照一定的顺序加锁)
2.加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
3.死锁检测

9 创建线程有四种方式:
  • 继承 Thread 类;
  • 实现 Runnable 接口;
  • 实现 Callable 接口;
  • 使用 Executors 工具类创建线程池
//继承 Thread 类
public class MyThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法正在执行..."); } }
public class TheadTest { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); System.out.println(Thread.currentThread().getName() + " main()方法执行结束"); } }
//实现 Runnable 接口
public class MyRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法执行中..."); } }
public class RunnableTest { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); thread.start(); System.out.println(Thread.currentThread().getName() + " main()方法执行完成"); } }
//实现 Callable 接口
public class MyCallable implements Callable<Integer> { @Override public Integer call() { System.out.println(Thread.currentThread().getName() + " call()方法执行中..."); return 1; } }
public class CallableTest { public static void main(String[] args) { FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable()); Thread thread = new Thread(futureTask); thread.start(); try { Thread.sleep(1000); System.out.println("返回结果 " + futureTask.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " main()方法执行完成"); } }
//Executors 工具类创建线程池
public class MyRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + " run()方法执行中..."); } }
public class SingleThreadExecutorTest { public static void main(String[] args) { ExecutorService executorService = Executors.newSingleThreadExecutor(); MyRunnable runnableTest = new MyRunnable(); for (int i = 0; i < 5; i++) { executorService.execute(runnableTest); } System.out.println("线程任务开始执行"); executorService.shutdown(); } }

10 runnable 和 callable 有什么区别?
相同点:都是接口,可以编写多线程程序,都采用Thread.start()启动线程.
区别:Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值(调用FutureTask.get()得到,方法会阻塞主进程的继续往下执行),可以用Future来获取异步执行的结果;Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息。

11 线程的 run()和 start()有什么区别
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。 调用 start 方法方可启动线程并使线程进入就绪状态可以理解为异步方法,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行,属于同步方法。

12 多线程5种状态
新建(new):新创建了一个线程对象。
可运行(runnable):线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。
运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞(block):处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态。
阻塞的情况分三种:
(一). 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态;
(二). 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态;
(三). 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

13 sleep()和wait()方法的区别?
1.两者都可以暂停线程的执行
2.sleep()是Thread类的静态方法,wait()是Object类的方法。
3.sleep()不释放锁,线程会自动苏醒。wait()释放锁,需要另外的线程调用同对象上的notify()或notifyAll()方法唤醒。

14 interrupt和 isInterrupted 方法的区别?
interrupt:用于中断线程。thread.interrupt();
isInterrupted:查看当前中断信号是true还是false。

线程优先级线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级。如非特别需要,一般无需设置线程优先级。

synchronized
Synchronized是Java中解决并发问题的一种最常用最简单的方法,非公平锁悲观锁,他可以确保线程互斥的访问同步代码。
synchronized可重入的原理
重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁
synchronized 锁升级
锁有4状态:级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

synchronized、volatile、CAS 比较
(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。
(3)CAS 是基于冲突检测的乐观锁(非阻塞)

synchronized 和 ReentrantLock 区别
相同点:两者都是可重入锁
区别如下:
  • ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作,如果使用不当没有 unLock()去释放锁就会造成死锁; synchronized 不需要手动释放和开启锁。
  • ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
  • 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word。

AQS 介绍
AQS(AbstractQueuedSynchronizer)是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

ReentrantLock(重入锁)实现原理与公平锁非公平锁区别
ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。
ReentrantLock重入:state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
ReentrantLock支持两种锁:公平锁非公平锁何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。ReadWriteLock 是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

ConcurrentHashMap
ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。ConcurrentHashMap是线程安全的map,其中利用了锁分段的思想提高了并发度。
JDK1.8后,ConcurrentHashMap抛弃了原有的Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。

ThreadLocal
ThreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个 ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。

ThreadLocal造成内存泄漏及解决方案
在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法。

阻塞队列(BlockingQueue)
常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
BlockingQueue 接口是 Queue 的子接口,特性:当生产者线程试图向 BlockingQueue 放入元素时,如果队列已满,则线程被阻塞,当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞,正是因为它所具有这个特性,所以在程序中多个线程交替向 BlockingQueue 中放入元素,取出元素,它可以很好的控制线程之间的通信。

线程池Executors类创建四种常见线程池
池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
线程池类型:
(1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
(2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能。
(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
(4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

线程池中 submit() 和 execute() 方法
execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。submit()方法可以返回持有计算结果的 Future 对象,而execute()没有。

创建线程池
ThreadPoolExecutor() 是最原始的线程池创建,也是阿里巴巴 Java 开发手册中明确规范的创建线程池的方式。
ThreadPoolExecutor 3 个最重要的参数:
  • corePoolSize :核心线程数,线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize :线程池中允许存在的工作线程的最大数量
  • workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。

CycliBarriar 和 CountdownLatch 有什么区别
CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

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

_________________________________________________________________________________

正文到此结束
本文目录