一、简述线程线程是cpu可执行的最小单位而进程是操作系统可分配的最小资源单位。一个进程中可以有多个线程。线程的五个状态新建new Thread()就绪 thread.start()运行cpu开始执行该线程阻塞线程在等待获得锁销毁线程执行完毕或出现异常。创建线程1.继承Thread类并重写run方法2.实现runnable接口并重写run方法3.实现callable接口并重写call方法call方法和run方法不同之处在于run方法时无返回值的call方法有返回值是个泛型。4.使用线程池二、多线程并发执行所谓的并发执行就是多个线程交替执行。并发执行的优点1.多个线程共享资源可以减少内存的加载和释放从而提高执行效率2.多个线程因为操作系统的调度可能会被分配到不同的cpu核心上执行更好的利用了多核处理器的资源从而提升了程序的可扩展性。并发执行遇到的问题1.不可见性由于Java内存模型JMM的原因JMM分为主内存区和本地内存区每个线程都会有自己的本地内存区。多核处理器允许多个线程并行执行在不同的核心上如果多个线程同时对主内存区中的数据进行操作就需要现将其加载到该线程的本地内存区中当第一个线程对数据修改完写回到本地内存后第二个线程不知道本地内存中的数据已经被修改此时就会导致堆数据的操作出现错误。2.乱序性为了优化性能有时候cpu会将后面的指令提前执行这样指令的运行顺序就被打乱了。3.非原子性cpu执行指令指令是原子性的但是高级语言的语句却是非原子性的一条高级语句往往可以拆成多条指令。非原子性就会导致线程交叉操作使得结果错误。总结java缓存模型导致了不可见性。编译器优化导致了乱序性。线程交换导致了非原子性。4.解决方法1.volatile关键字volatile关键字对共享变量修饰后1.该变量一旦被线程修改对其他线程来说是立即可见的。2.禁止了指令的重排序3.仍不能解决非原子性2.如何保证原子性2.1synchronized锁synchronized锁是一种独占锁因此只有持有锁的线程才能被执行虽然不能阻止线程交换但是当其他线程想要执行时会因为没有锁而阻塞仍需要等待当前线程执行完毕才能尝试获得 锁去执行。如此变相的保证了原子性。2.2原子类该方式是以volatileCAS来实现的volatile保证了主内存数据的可见性而CAS即比较和交换在具体实现中当多个线程对同一数据进行操作当一个线程加载主内存的数据到对本地内存时此时会对该数据记录此时的值为预期值当对数据进行修改后写回到主内存区之前会对预期值和当前主内存区的值进行比较如果已被更改就重新进行修改操作。CAS缺点CAS使用自旋锁的方式由于该锁会不断循环判断因此不会类似synchronize 线程阻塞导致线程切换。但是不断的自旋会导致CPU的消耗在并发量大的时候容易导致CPU跑满。5.Java中的锁分类1.乐观锁/悲观锁乐观锁和悲观锁并非是真实存在的锁而是一种思想。乐观锁认为多线程开发中不需要加锁例如使用原子类采用不加锁的方式解决问题。悲观锁认为多线程开发中一定要加锁否则会出现问题。2.可重入锁可重入锁又名递归锁是指在外层方法获得锁后进入内层方法后会再次获得锁。reentrantlock就是可重入锁。reentrantlock锁可以避免死锁。3.读写锁读写锁有以下特点多个线程同时读取数据时不会互斥但是一旦有线程进行写操作就会互斥阻止该操作。4.共享锁/独占锁共享锁是允许多个获得该锁的线程在不发生写操作的前提下可以同时对数据进行读取操作。而独占锁一次只允许一个线程进入锁代码块中。5.分段锁分段锁也是一种思想主张将数据分段在每个分段上都加锁以提高并发效率。如ConcurrentHashMap,ConcurrentHashMap底层哈希表有16个空间,可以用每一个位置上的第一个节点当做锁,这样可以同时由不同的线程操作不同的位置,只是同一个位置多个线程不能同时操作。6.自旋锁自旋锁是指在线程进行抢锁的过程中如果没抢到锁会多次进行抢锁操作实在抢不到锁才会将该线程阻塞。但是自旋过程中不会释放cpu资源因此比较耗费cpu但是在低并发情况下会有较高的效率。7.公平锁/非公平锁公平锁是指按照请求锁的顺序分配,拥有稳定获得锁的机会。非公平锁是指不按照请求锁的顺序分配,不一定拥有获得锁的机会。8.偏向锁/ 轻量级锁/重量级锁锁的状态无锁偏向锁轻量级锁、重量级锁无锁状态:没有任何线程获取锁偏向锁状态:偏向锁是指一段同步代码一直被一个线程所访问那么该线程会自动获取锁。 降低获取锁的代价。轻量级锁状态:当锁状态为偏向锁时, 继续有其他线程过来获取锁,锁状态升级为轻量级锁,线程不会进入到阻塞状态,一直自旋获得锁。重量级锁状态:当锁状态为轻量级锁时, 线程数量持续增多,且线程自旋次数到一定数量时,锁状态升级为重量级锁,线程会进入到阻塞状态,等待操作系统调度执行。6.synchronized锁实现synchronized锁相当于一个监视器当线程进入锁修饰的代码块或方法中时就会自动获得锁其他线程再访问该方法/代码块时就会阻塞直到有锁的线程运行完毕或被wait释放了锁。1.原子性synchronized锁不能同时加到多个线程上因此其保证了操作的原子性。2.可见性synchronized锁会在线程释放锁后才会将本地内存中的数据刷新到主内存中。等到其他线程从主内存中读取数据时已经被最新的值了。3.有序性synchronized锁会阻止操作系统对线程中的指令进行重排序以确保操作的有序性synchronized控制同步,是依靠底层的指令实现的.如果是同步方法,在指令中会为方法添加ACC_SYNCHRONIZED标志如果是同步代码块,在进入到同步代码块时,会执行monitorenter, 离开同步代码块时或者出异常时,执行monitorexit7.AQSAbstractQueuedSynchronizer抽象同步队列并发包中很多类的底层都用到了AQS在该队列中将线程放到Node类中的thread变量上。class AbstractQueuedSynchronizer { private transient volatile Node head; private transient volatile Node tail; private volatile int state; //表示有没有线程访问共享数据 默认是0 表示没有线程访问 //修改状态的方法CAS protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); } static final class Node { volatile Node prev; volatile Node next; volatile Thread thread; } }ReentrantlockReentrantlock类公平锁和非公平锁都可以实现可以对资源进行共享同步和synchronized一样是支持可重入的。其内部有三个类Sync、FairSync、NonfairSyncclass ReentrantLock{ abstract static class Sync extends AbstractQueuedSynchronizer { abstract void lock(); } //非公平锁 static final class NonfairSync extends Sync { void lock(){ } } //公平锁 static final class FairSync extends Sync { void lock(){ } } }7.1获取锁的时机acquire调用时机1.线程被唤醒2.调用lock方法3.线程在同步队列中等待直到被前驱节点唤醒或者轮到自己尝试获取锁。7.2公平锁和非公平锁线程进队列时如果是公平锁则会默默等待前驱结点被唤醒或轮到自己尝试获取锁。而在非公平锁中线程进来会调用方法比较当前锁的状态是否为0如果是为其设置状态为1在ReentrantLock中acquire方法的作用是尝试获取锁。如果获取锁成功则当前线程获得锁并继续执行如果获取锁失败则当前线程会被加入到同步队列中等待。8.JUC常用类8.1ConcurrentHashMap该类相比HashMap的优点在于相对于HashMap而言是线程安全的相对于HashTable是高效的原因在于他加synchronized锁方式是分段锁JDK5-7之前在jdk8以后是桶链表/红黑树的数据结构采用volatilesynchronized来保证安全性其中synchronized锁只加在发生哈希冲突的桶上执行增改操作时如果桶的这个位置为空则不加锁如果已经有元素存在了就需要加锁。ConcurrentHashMap中的元素键和键值都不可以为null这是为了防止产生歧义如map.put(b,b) System.out.println(map1.get(a));//null 值是null 还是键不存在返回null map.put(a,null)8.2CopyOnWriteArrayListCopyOnWriteArrayList是对写方法加了Reentrantlock锁他的效率比Vector高一是因为读取数据没有了锁多个线程就可同时对数据进行读操作。而写操作不会直接在原数组上修改是先复制一个数组然后对复制出来的数组进行修改最后将底层数组切换为新的数组。因此CopyOnWriteArrayList写的效率较低适合高并发读多写少的情况。但是因写方法的特殊性CopyOnWriteArrayList可以在迭代过程中直接对集合的底层数组进行修改而不需要使用迭代器的对象对数组修改。普通数组如果在迭代过程中对集合底层数组进行修改java为了保证迭代操作的一致性和安全性会禁止该操作抛出ConcurrentModificationException异常。CopyOnWriteArrayList在迭代上的优势ArrayList在迭代时会通过修改次数来判断是否在此过程中原集合被修改如果被修改则会抛出异常ConcurrentModificationException而CopyOnWriteArrayList在迭代时是对快照进行迭代因此可以很好的避免此问题。// ArrayList 迭代时修改会抛异常 ListString arrayList new ArrayList(); arrayList.add(a); IteratorString it arrayList.iterator(); arrayList.add(b); // 迭代时修改集合 it.next(); // 抛出 ConcurrentModificationException // CopyOnWriteArrayList 迭代时修改不会抛异常 ListString cowList new CopyOnWriteArrayList(); cowList.add(a); IteratorString cowIt cowList.iterator(); cowList.add(b); // 迭代时修改集合复制新数组 cowIt.next(); // 正常返回 a遍历的是旧快照8.3CountDownLacthCountDownLatch在其内部维护了一个计数器初始为其设置一个大小通常一个线程执行完毕后会调用类中的countdown方法来使实例中的大小减一。当实例的大小减至0时会唤醒所有因调用了await方法而沉睡的线程。import java.util.concurrent.CountDownLatch; public class CountDownLatchExample { public static void main(String[] args) throws InterruptedException { // 创建一个 CountDownLatch 对象初始计数器值为 3 CountDownLatch latch new CountDownLatch(3); // 创建并启动三个工作线程 for (int i 0; i 3; i) { new Worker(latch).start(); } // 主线程等待所有工作线程完成 System.out.println(主线程等待工作线程完成...); latch.await(); System.out.println(所有工作线程已完成主线程继续执行。); } static class Worker extends Thread { private final CountDownLatch latch; public Worker(CountDownLatch latch) { this.latch latch; } Override public void run() { try { System.out.println(Thread.currentThread().getName() 开始工作...); // 模拟工作耗时 Thread.sleep(2000); System.out.println(Thread.currentThread().getName() 工作完成。); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 工作完成后计数器减 1 latch.countDown(); } } } }输出结果主线程等待工作线程完成...Thread-0 开始工作...Thread-1 开始工作...Thread-2 开始工作...等待约 2 秒后Thread-0 工作完成。Thread-1 工作完成。Thread-2 工作完成。所有工作线程已完成主线程继续执行。在主线程调用thread0、1、2后在子线程执行完毕之前又调用了await方法进入等待状态待子线程因调用countdown方法将countdownlatch的大小减至0后主线程被唤醒。使用场景1.一些场景中需要将主线程分解成多个子线程2.在某些微服务的情况下一些服务需要其他服务先启动也可以使用countdownlatchcountdownlatch在大小减至0后不能被重置所以是无法被重复使用的如果想要重复使用可以采用CyclicBarrier。CyclicBarrier在减至0后会重置重置后的值就是上次设置的值如上次设置大小为5在减至0后会再次重置为5。8.4CyclicBarrierimport java.util.concurrent.CyclicBarrier; public class CyclicBarrierDemo { // 定义屏障5个线程到达后执行“会议开始”的回调任务 private static final CyclicBarrier barrier new CyclicBarrier(5, () - { System.out.println(Thread.currentThread().getName() 所有员工到齐会议开始); }); public static void main(String[] args) { // 第一轮会议5个员工线程 for (int i 1; i 5; i) { int employeeId i; new Thread(() - { try { System.out.println(Thread.currentThread().getName() 员工 employeeId 到达会议室); // 等待其他员工到达屏障点 barrier.await(); // 所有员工到齐后执行后续逻辑 System.out.println(Thread.currentThread().getName() 员工 employeeId 开始开会); } catch (Exception e) { e.printStackTrace(); } }, 线程 i).start(); } // 等待第一轮会议结束模拟时间 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println( 第一轮会议结束重置屏障准备第二轮 ); // 重置屏障复用 barrier.reset(); // 第二轮会议再次启动5个员工线程 for (int i 6; i 10; i) { int employeeId i; new Thread(() - { try { System.out.println(Thread.currentThread().getName() 员工 employeeId 到达会议室); barrier.await(); System.out.println(Thread.currentThread().getName() 员工 employeeId 开始第二轮开会); } catch (Exception e) { e.printStackTrace(); } }, 线程 i).start(); } } } 线程1员工1到达会议室 线程2员工2到达会议室 线程4员工4到达会议室 线程3员工3到达会议室 线程5员工5到达会议室 线程5所有员工到齐会议开始 线程5员工5开始开会 线程1员工1开始开会 线程2员工2开始开会 线程3员工3开始开会 线程4员工4开始开会 第一轮会议结束重置屏障准备第二轮 线程6员工6到达会议室 线程7员工7到达会议室 线程8员工8到达会议室 线程9员工9到达会议室 线程10员工10到达会议室 线程10所有员工到齐会议开始 线程10员工10开始第二轮开会 线程6员工6开始第二轮开会 线程7员工7开始第二轮开会 线程8员工8开始第二轮开会 线程9员工9开始第二轮开会9.线程池在高并发过程中频繁地创建线程和销毁线程都会大大降低运行的效率因此在jdk5引入了线程池一般使用ThreadPoolExecutor来创建线程池。public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)9.1线程池中的参数corePollSize:记录核心线程池的线程最大数量maximumPollSize:记录线程池中的线程最大数量包含核心线程池中的线程数keepAliveTime记录非核心线程池中的空闲线程的空闲生命如果非核心线程池中的线程长时间未被调用就会被销毁。unit时间单位workQueue等待队列当核心线程池中的线程都在被使用时如果再进来新的任务就会存放在等待队列中如果等待队列也满了就会在非核心线程池中创建新的线程。threadFactory线程工厂负责创建新的线程。handler拒绝策略当核心线程池和非核心线程池中的线程都在使用中且等待队列也已经满了此时会执行拒绝策略。9.2线程池工作流程当有任务进入线程池如果核心线程池中有空闲的线程就交给核心线程池中的线程执行。否则判断等待队列是否有空如果有则放进等待队列否则判断非核心线程池是否还有容量如果有就使用线程工厂创建新的线程来执行该任务否则执行拒绝策略。9.3四种拒绝策略AbortPolicy: 抛异常CallerRunsPolicy: 将新来的线程交给提交任务的线程去执行DiscardOldestPolicy: 丢弃等待时间最长的任务DiscardPolicy: 丢弃最后的任务9.4提交任务的方法执行任务除了可以使用execute方法和submit方法。它们的主要区别 是execute没有返回值而submit会有返回值。9.5关闭线程池shutdown(),执行该方法后会停止接收新的任务会将线程池中的所有任务执行完毕再彻底关闭线程池。shutdownNow(),执行该方法后会立刻关闭线程池正在执行的任务也会停止。10.ThreadLocal10.1介绍ThreadLocal是本地线程变量他会为每个线程设置一个本地变量初始值由ThreadLocal构造器生成。本地线程变量只在当前线程中使用每个线程之间的本地变量是没有关系的。ThreadLocal会为每个线程创建ThreadLocalMap对象来储存值ThreadLocalMap的默认值便是初始化ThreadLocal对象时的值。static ThreadLocalInteger threadLocal new ThreadLocalInteger(){ Override protected Integer initialValue() { return 123; 将每个线程中的ThreadLocalMap的值设置为123 } }; public static void main(String[] args) { new Thread(()-{ System.out.println(1: threadLocal.get()); //123 threadLocal.set(10); System.out.println(2: threadLocal.get()); //10 }).start(); new Thread(()-{ System.out.println(3: threadLocal.get()); //123 threadLocal.set(20); System.out.println(4: threadLocal.get()); //20 }).start(); new Thread(()-{ System.out.println(5: threadLocal.get()); //123 threadLocal.set(30); System.out.println(6: threadLocal.get()); //30 }).start(); System.out.println(7: threadLocal.get()); //123 threadLocal.remove();//回收threadlocal变量防止内存泄漏 }10.2threadLocal的内存泄漏问题ThreadLocalMap变量中键是弱引用而值是强引用当GC时一定会将其键回收掉就会导致有值无键的结果从而产生内存泄漏。对于该问题我们需要在使用完ThreadLocal后调用其remove方法将其回收。对象引用分为四种:强引用直接指向对象的引用Object obj new Object(); 强引用obj.hashCode();objnull; 没有引用指向对象此时不属于任何引用类型。对象如果有强引用关联,那么肯定是不能被回收的软 / 弱 / 虚引用必须通过对应的包装类SoftReference/WeakReference/PhantomReference主动创建软引用被SoftReference类包裹的对象, 当内存充足时,不会被回收,当内存不足时,即使有引用指向,也会被回收Object o1 new Object(); SoftReferenceObject softReference new SoftReferenceObject(o1);弱引用被WeakReference类包裹的对象,只要发送垃圾回收,该类对象都会被回收掉,不管内存是否充足Object o1 new Object(); WeakReferenceObject weakReference new WeakReferenceObject(o1);ThreadLocal 被弱引用管理static class Entry extends WeakReferenceThreadLocal? {}当发生垃圾回收时,被回收掉,但是value还与外界保持引用关系,不能被回收. 造成内存泄漏threadLocal.remove();//不再使用时,调用remove方法,删除键值对,可以避免内存泄漏问题虚引用被PhantomReference类包裹的对象,随时都可以被回收,通过虚引用对象跟踪对象回收的状态