1、创建线程的方式及实现
Java中创建线程有4种方式:
- 继承Thread类,重写run()
- 实现Runnable接口,重写run()
- 实现Callable接口,重写call()
- 通过线程池
1. 继承Thread类
继承Thread
类,重写run()接口。
/*类继承*/
class NewThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开启。。。");
}
}
public static void main(String[] args) {
//类继承调用
NewThread newThread = new NewThread();
newThread.start();
//匿名内部类
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"线程开启。。。");
}).start();
}
2. 实现Runnable接口
实现Runnable接口,重写run()。(Java不可多继承)
//实现Runnable接口
class NewRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开启。。。");
}
}
public static void main(String[] args) {
NewRunnable newRunnable = new NewRunnable();
new Thread(newRunnable).start();
//匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程开启。。。");
}
}).start();
}
3. 实现Callable接口
实现Callable接口,重写call()方法,任务交由FutureTask,由线程处理,能够支持返回值。
//这里Callable<T> 决定返回值类型
class NewCallable implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName()+"线程开启。。。");
return Thread.currentThread().getName();
}
}
public static void main(String[] args) {
NewCallable newCallable = new NewCallable();
FutureTask<String> task = new FutureTask<>(newCallable);
Thread thread = new Thread(task);
thread.start();
try {
System.out.println("获取task返回值:"+task.get());
} catch (Exception e) {
e.printStackTrace();
}
}
4. 通过线程池
Executors.newXXXXXPool()
其底层都是通过ThreadPoolExecutor进行创建,根据《阿里巴巴Java开发手册》,推荐使用自定义ThreadPoolExecutor能够对线程池更深入的把控。
更详细关于线程池内容直接看:ThreadPoolExecutor线程池详解与使用
private static ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(5);
public static void main(String[] args) {
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+"启动了。。。");
});
}
2、sleep()、join()、yield()的区别
-
sleep()
使当前执行的线程休眠,但不会释放锁。先进入阻塞态,待休眠时间结束后,后转入就绪态等待执行机会。
//休眠5秒 Thread.sleep(5000);
-
join()
非静态方法,让一个线程等待另外一个线程完成才继续执行。
public static void main(String[] args) throws InterruptedException { //线程a睡眠2秒 Thread a = new Thread(() -> { System.out.println(Thread.currentThread().getName()+"执行中"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"阻塞结束"); },"a"); //启动线程 a.start(); //main线程阻塞,等待a线程处理完成 a.join(); System.out.println(Thread.currentThread().getName()+"结束"); } //输出结果如下: //a执行中 //a阻塞结束 //main结束
-
yield()
当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行,注意是让自己或者其他线程运行,并不是单纯的让给其他线程。yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!
3、说说CountDownLatch原理
- CountDownLatch应用场景:在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待,直到其他线程操作完成。
- 基于AQS的共享模式。AQS简介
- 常用方法:
countDown()
:计数器减1await()
:直到计数器为0时,线程继续执行,否则是被阻塞状态。await(long timeout, TimeUnit unit)
:直到计数器为0或超过指定时间内,线程继续执行,否则是被阻塞状态。
- 实现原理:
- 内部类
Sync
继承AbstractQueuedSynchronizer
,维护volatile
修饰的全局变量state
。 - final修饰的
Sync
类。 - 创建
CountDownLatch()
对象时,调用new Sync(count)
初始化Sync
类,调用AbstractQueuedSynchronizer
的setState(newState)
方法,赋值全局变量state
- 调用
countDown()
方法时,调用AbstractQueuedSynchronizer
的sync.releaseShared(1)
方法。-
tryReleaseShared(releases)
通过for循环配合CAS尝试进行-1操作,当执行完成返回nextc == 0
;当执行完成结果为0返回true(为0代表计数器结束),否则返回false。- 根据
tryReleaseShared(releases)
结果,若为true,则进入doReleaseShared()
,返回true;否则返回false。
-
- 调用
await()
方法时,调用sync.acquireSharedInterruptibly(1);
- 如果线程中断,抛出
InterruptedException()
异常 - 否则进入
tryAcquireShared()
方法,尝试获取state
值,如果为0则返回1;否则返回-1。- 当返回值小于0时,进入
doAcquireSharedInterruptibly()
方法,阻塞当前线程。
- 当返回值小于0时,进入
- 如果线程中断,抛出
- 内部类
4、说说CyclicBarrier原理
CyclicBarrier
的应用场景为:实现一组线程相互等待,当所有线程都达到某个屏障点后再进行后续操作。
似乎和CountDownLatch差不多?
这个疑问在我一开始理解他的概念时就冒出来了,后来随着深入学习,发现他与CountDownLatch的使用区别在于:CyclicBarrier可以实现循环拦截
。
CyclicBarrier
是基于ReentrantLock
与Condition
组合使用。
CyclicBarrier原理
线程调用await()
,告诉CyclicBarrier
到达屏障,然后线程阻塞;等到所有线程达到屏障count==0
,结束阻塞,继续线程后续逻辑。await()
核心调用dowait()
方法。
dowait()
方法ReentrantLock
加锁- 如果当前屏障被打破,抛出
BrokenBarrierException()
异常 - 如果当前线程被中断,将当前屏障打破(
generation.broken = true;
);当前剩余需要拦截到数量置为拦截的总数量(count = parties;
);唤醒所有等待的线程;抛出InterruptedException()
异常 - 当前线程未被中断,内部计数器-1。
int index = --count;
- 如果内部计数器为0,则说明为最后一个线程到达屏障
- 如有指定执行的任务(构造函数中第二参数所传的任务),则执行指定任务。
- 唤醒所有线程,重置参数(计数器、栅栏下一代),new generation任务。
- 1执行失败,执行:当前屏障打破(
generation.broken = true;
);当前剩余需要拦截到数量置为拦截的总数量(count = parties;
);唤醒所有等待的线程 - 返回0。
- 不为最后一个线程到达屏障,进入代码循环块
- 是否有指定等待时间,如有则继续进入
trip.await()
;否则进入nanos = trip.awaitNanos(nanos);
,返回剩余时间 - 1抛出异常,则判断屏障是否被打破
- 打破:当前屏障打破(
generation.broken = true;
);当前剩余需要拦截到数量置为拦截的总数量(count = parties;
);唤醒所有等待的线程;抛出异常 - 未被打破:当前线程挂起
Thread.currentThread().interrupt();
- 打破:当前屏障打破(
- 当有线程被唤醒,且屏障被打破,抛出
BrokenBarrierException()
异常 - 通过g != generation
判断说明已换代,返回
index= --count` timed && nanos <= 0L
说明线程超时,则:当前屏障打破(generation.broken = true;
);当前剩余需要拦截到数量置为拦截的总数量(count = parties;
);唤醒所有等待的线程;抛出TimeoutException
- 关闭1的Reentrant锁
- 是否有指定等待时间,如有则继续进入
5、说说Semaphore原理
Semaphore可以控制同时访问共享资源的线程个数,线程通过 acquire
方法获取一个信号量,信号量减一,如果没有就等待;通过release
方法释放一个信号量,信号量加一。它通过控制信号量的总数量,以及每个线程所需获取的信号量数量,进而控制多个线程对共享资源访问的并发度,以保证合理的使用共享资源。相比synchronized和独占锁一次只能允许一个线程访问共享资源,功能更加强大
6、说说Exchanger原理
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据, 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
因此使用Exchanger的重点是成对的线程使用exchange()方法,当有一对线程达到了同步点,就会进行交换数据。因此该工具类的线程对象是成对的。 Exchanger类提供了两个方法,String exchange(V x):用于交换,启动交换并等待另一个线程调用exchange;String exchange(V x,long timeout,TimeUnit unit):用于交换,启动交换并等待另一个线程调用exchange,并且设置最大等待时间,当等待时间超过timeout便停止等待。
7、说说CountDownLatch与CyclicBarrier区别
这两个类都可以实现一组线程达到某个条件之前进行等待,内部都有计数器,当计数器为0时被阻塞的线程会被唤醒。
区别:
- 计数器控制权限:
CyclicBarrier
计数器由await()
方法控制,CountDownLatch
计数器由countDown()
方法控制。 - 拦截次数:
CyclicBarrier
可以实现循环拦截,CountDownLatch
则只能拦截一轮。
8、ThreadLocal原理分析
ThreadLocal用于线程间的数据隔离,为每一个线程都提供了数据副本,使得不同线程访问的数据不是同一个对象!!!
每个Thread对象
都有一个ThreadLocalMap,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型。
9、讲讲线程池的实现原理
预先启动一些线程,线程无限循环从任务队列中获取一个任务进行执行,直到线程池被关闭。如果某个线程因为执行某个任务发生异常而终止,那么重新创建一个新的线程而已,如此反复。
10、线程池的几种方式
根据《阿里巴巴Java开发手册》,推荐使用第7种方式。
1、 newSingleThreadExecutor()
:它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
2、 newCachedThreadPool()
:它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;
3、 newFixedThreadPool(int nThreads)
:重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;
4、newSingleThreadScheduledExecutor()
:创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
5、newScheduledThreadPool(int corePoolSize)
:和newSingleThreadScheduledExecutor()类似,创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
6、newWorkStealingPool(int parallelism)
:这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序;
7、ThreadPoolExecutor()
:是最原始的线程池创建,上面创建方式都是对ThreadPoolExecutor的封装。
11、线程的生命周期
线程的生命周期分为5个阶段:
- 新建(New):创建一个线程对象后,该线程就处于新建状态。
- 就绪(Runnable):调用
start()
方法后,该线程就处于就绪状态。 - 运行(Running):获得CPU使用权,执行
run()
方法,系统分配时间内线程都是运行状态。 - 阻塞(Blocked):在某些情况下(如获取同步对象,同步对象被其他线程持有),会让出CPU使用权并暂时中止任务的执行。直到情况消除,进入就绪状态,等待CPU分配时间。
- 死亡(Terminated):线程调用
stop()
或run()
方法执行完成,或抛出异常或错误,线程进入死亡状态,不会再转入其他状态。