JAVA多线程知识点
JAVA多线程知识点汇总
1.创建多线程有几种方式?
- 继承Thread类
- 实现Runnable接口
- 应用程序可以使用Executor框架来创建线程池
2.线程状态:
新建-就绪-运行-阻塞-死亡。
3.同步方法和同步代码块的区别是什么?
-
同步方法默认用this或者当前类class对象作为锁;
-
同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生问题的部分代码而不是整个方法;
4.死锁概念:
两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行,结果就是这些线程都陷入了无限的等待中。
多线程产生死锁的四个必要条件:破坏其中一个既可避免死锁或解决死锁。
-
互斥条件:一个资源每次只能被一个进程使用。
-
保持和请求条件:一个进程因请求资源而阻塞时,对已获得资源保持不放。
-
不可剥夺性:进程已获得资源,在未使用完成前,不能被剥夺。
-
循环等待条件(闭环):若干进程之间形成一种头尾相接的循环等待资源关系。
5.volatile关键字
它保证的是有序性和可见性!!!注意没有原子性!!!!只修饰变量,是轻量级实现。这也是和synchronized主要区别。用volatile修饰的变量,每次读取时都会获取修改后(最新)的值进行操作(即可见性)。读取数值时必须重新从主内存加载,并且read load是连续的。修改共享变量后,必须马上同步回主内存,并且存储和写入是连续的。但是没有锁机制无法做到线程安全。更适合用于一个修改者,多个使用者,如状态标识,数据定期发布场景。
代码样例:不加volatile,其他线程读取不到当前线程的最新值
public class VolatileKey extends Thread { volatile boolean flag = false; public void run() { while (!flag) { System.out.println("run"); } System.out.println("stop"); } public static void main(String[] args) throws Exception { VolatileKey vt = new VolatileKey(); vt.start(); Thread.sleep(2000); vt.flag = true; } }
造成这种问题的原因主要是JVM对内存分配的优化,不加volatile时,线程会保存副本,而不是每次都从主内存获取。而volatile限制线程不进行内部缓存和重排,既而解决掉可见性问题。
6、CountDownLatch
CountDownLatch是一个计数器闭锁,通过它可以完成类似于阻塞当前线程的功能,即:一个线程或多个线程一直等待,直到其他线程执行的操作完成。
CountDownLatch用一个给定的计数器来初始化,该计数器的操作是原子操作,即同时只能有一个线程去操作该计数器。当计数器为0时执行await()方法后的代码。
用法:
1.在线程结束时调用.countDown()方法。
2.在主线程合适位置调用.await()方法。
样例代码:
public class CountDownLatchTest { public static void main(String[] args) { final CountDownLatch latch = new CountDownLatch(2); System.out.println("主线程开始执行…… ……"); //第一个子线程执行 ExecutorService es1 = Executors.newSingleThreadExecutor(); es1.execute(new Runnable() { @Override public void run() { try { Thread.sleep(3000); System.out.println("子线程:"+Thread.currentThread().getName()+"执行"); } catch (InterruptedException e) { e.printStackTrace(); } latch.countDown(); } }); es1.shutdown(); //第二个子线程执行 ExecutorService es2 = Executors.newSingleThreadExecutor(); es2.execute(new Runnable() { @Override public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("子线程:"+Thread.currentThread().getName()+"执行"); latch.countDown(); } }); es2.shutdown(); System.out.println("等待两个线程执行完毕…… ……"); try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("两个子线程都执行完毕,继续执行主线程"); } }
7、CyclicBarrier
作用是设置栅栏阻挡所有线程,当所有线程都完成后才进行后续操作。可循环利用,且提供reset方法重置。
用法:
1.在线程内调用.await()方法,CyclicBarrier会在所有线程都将await前的任务完成时,才继续执行后面的代码(本线程内的)。可以在一个方法内多次调用.await()。
2.和countDownLatch的区别:countDownLatch是减法计数器cyclicBarrier是加法计数器。cyclicBarrier可复用,即一个函数可调用多次.await()方法,且提供rest()函数进行主动重新计数。countDownLatch是一次性的。
8、Semaphore
Semaphore用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。还可以用来实现某种资源池限制,或者对容器施加边界。比如限制最大5个人同时访问。
用法:
1.Semaphore semp = new Semaphore(5);//创建通行5个线程
2.semp.acquire();//发放通行证
3.semp.release();//释放资源
当同时访问线程数大于等于5个时,会阻塞,达到限流目的。
9、有关yield()
功能为:暂停当前正在执行的线程对象,并执行其他线程。
注意:yield()让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会,但是不保证一定让其他线程执行,因为有可能会被再次选中。
所以yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
10.简单介绍线程未捕获异常处理
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; /** *异常线程 */ class ExceptionThread implements Runnable { @Override public void run() { System.out.println("UncaughtExceptionHandler " + Thread.currentThread().getUncaughtExceptionHandler()); System.out.println("thread " + Thread.currentThread()); throw new RuntimeException(); } } /** * 未捕获异常处理。 */ class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("catch thread = " + t); System.out.println("catch ex = " + e); } } /** *利用线程池获取线程时使用的工厂类 */ class MyThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { System.out.println("runnable " + r); Thread thread = new Thread(r); //设置未捕获异常处理类 thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler()); return thread; } } public class ExceptionThreadMain { public static void main(String[] args) { ExecutorService ex = Executors.newCachedThreadPool(new MyThreadFactory());//指定工厂 ex.execute(new ExceptionThread()); ex.shutdown(); } }
运行结果可对比查看
runnable java.util.concurrent.ThreadPoolExecutor$Worker@6d6f6e28[State = -1, empty queue] UncaughtExceptionHandler util.threadutil.MyUncaughtExceptionHandler@3986e8ee thread Thread[Thread-0,5,main] catch thread = Thread[Thread-0,5,main] catch ex = java.lang.RuntimeException
JAVA中异常总基类是Throwable,其大部分异常都继承自Exception(如IOException和RuntimeException及其子类)和Error类(如AWTError的StackOverFlowError)
11、Thread、ThreadLocal、ThreadLocalMap
ThreadLocal主要是做到线程间数据隔离,从而达到多线程安全的效果,与Synchronized完全不同。
ThreadLocalMap的代码位置在ThreadLocal里,并且是一个静态内部类。
而每一个Thread里都会有ThreadLocalMap实例,所以TheadLocal里保存变量的集合就是此map。
由于ThreadLocalMap里的entry里的Key部分采取的弱引用,如果ThreadLocal没有强引用指向它,则不会阻止GC回收ThreadLocalMap里Entry的Key部分,但是Value部分却是强引用。就会导致内存泄漏。
所以推荐ThreadLocal采用静态声明或每次用完都调用一次remove()函数。
12、线程杂谈
- 如何在多线程之间共享数据:全局变量,静态变量,或共享对象。
- 共享变量必须放在主内存中,因为每个线程都有自己的内存区,也只能操作自己的内存区,所以解决可见性问题是从主内存读取到工作内存,操作后写回到主内存中。
- 被共享的变量(对象)放到堆、方法区里(主内存)。也就能和jvm的内存分配联系起来。方法区也称"永久代",它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。而堆内存存放大多数对象,因为堆内存只有一个,所以是线程共享的。也正是因为如此(从主内存中读,再写回主内存),带来了线程安全问题。
- java同步交互协议(从主内存读到工作内存协议)有八个原子操作(注意,操作是原子的,但每个原子操作之间没有原子性)
(1)锁定-主内存变量锁定,线程独享。
(2)解锁-解锁变量,其他线程可访问。
(3)读取-从主内存读取到工作内存。
(4)载入-将读取的主内存值保存到工作内存的副本中。
(5)使用-工作内存使用读取的值。
(6)赋值-工作内存操作读取的值。
(7)存储-从工作内存的值传送给主内存。
(8)写入-将存储的值写入到主内存中。
13、常用的几种线程池
1.newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代再次执行它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2.newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,会新分配线程再次执行。
3.newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
4.newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。