Java线程(一)
(一)创建线程的方式及实现
(1)继承 Thread 类创建线程类
- 定义 Thread 类的子类,并重写该类的
run
方法,该run
方法的方法体就代表了线程要完成的任务。因此把run()
方法称为执行体。 - 创建 Thread 子类的实例,即创建了线程对象。
- 调用线程对象的
start()
方法来启动该线程。
(2)通过 Runnable 接口创建线程类
- 定义 Runnable 接口的实现类,并重写该接口的
run()
方法,该run()
方法的方法体同样是该线程的线程执行体。 - 创建 Runnable 实现类的实例,并依此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象。
- 调用线程对象的
start()
方法来启动该线程。
(3)通过 Callable 和 Future 创建线程
- 创建 Callable 接口的实现类,并实现
call()
方法,该call()
方法将作为线程执行体,并且有返回值。 - 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的
call()
方法的返回值。 - 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 FutureTask 对象的
get()
方法来获得子线程执行结束后的返回值
(4)采用实现 Runnable、Callable 接口的方式创见多线程时:
- 优势是:线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
- 劣势是:编程稍微复杂,如果要访问当前线程,则必须使用 Thread.currentThread() 方法。
(5)使用继承 Thread 类的方式创建多线程时:
- 优势是:编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。
- 劣势是:线程类已经继承了 Thread 类,所以不能再继承其他父类。
(二)sleep()、join()、yield()的区别
sleep()
sleep()
方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是 sleep()
方法不会释放“锁标志”,也就是说如果有 synchronized
同步块,其他线程仍然不能访问共享数据。
wait()
wait()
方法需要和 notify()
及 notifyAll()
两个方法一起介绍,这三个方法用于协调多个线程对共享数据的存取,所以必须在 synchronized
语句块内使用,也就是说,调用 wait()
,notify()
和 notifyAll()
的任务在调用这些方法前必须拥有对象的锁。注意,它们都是 Object
类的方法,而不是 Thread
类的方法。
wait()
方法与 sleep()
方法的不同之处在于,wait()
方法会释放对象的“锁标志”。当调用某一对象的 wait()
方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了 notify()
方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的 notifyAll()
方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。
除了使用 notify()
和 notifyAll()
方法,还可以使用带毫秒参数的 wait(long timeout)
方法,效果是在延迟 timeout 毫秒后,被暂停的线程将被恢复到锁标志等待池。
此外,wait()
,notify()
及 notifyAll()
只能在 synchronized
语句中使用,但是如果使用的是 ReenTrantLock
实现同步,该如何达到这三个方法的效果呢?解决方法是使用 ReenTrantLock.newCondition()
获取一个 Condition
类对象,然后 Condition
的 await()
,signal()
以及 signalAll()
分别对应上面的三个方法。
yield()
yield()
方法和 sleep()
方法类似,也不会释放“锁标志”,区别在于,它没有参数,即 yield()
方法只是使当前线程重新回到可执行状态,所以执行 yield()
的线程有可能在进入到可执行状态后马上又被执行,另外 yield()
方法只能使同优先级或者高优先级的线程得到执行机会,这也和 sleep()
方法不同。
join()
join()
方法会使当前线程等待调用 join()
方法的线程结束后才能继续执行
(三)CountDownLatch 原理
CountDownLatch 内部维护了一个整数 n
,n(要大于等于0)在 当前线程 初始化 CountDownLatch
方法指定。当前线程调用 CountDownLatch
的 await()
方法阻塞当前线程,等待其他调用 CountDownLatch
对象的 CountDown()
方法的线程执行完毕。 其他线程调用该 CountDownLatch
的 CountDown()
方法,该方法会把 n-1
,直到所有线程执行完成,n
等于 0
,当前线程 就恢复执行。
(四)CyclicBarrier 原理
CyclicBarrier
是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点(CommonBarrierPoint)。因为该 barrier
在释放等待线程后可以重用,所以称它为循环的 barrier
。
(五)Semaphore 原理
Semaphore
直译为信号。实际上 Semaphore
可以看做是一个信号的集合。不同的线程能够从 Semaphore
中获取若干个信号量。当 Semaphore
对象持有的信号量不足时,尝试从 Semaphore
中获取信号的线程将会阻塞。直到其他线程将信号量释放以后,阻塞的线程会被唤醒,重新尝试获取信号量。
(六)Exchanger 原理
当一个线程到达 exchange
调用点时,如果它的伙伴线程此前已经调用了此方法,那么它的伙伴会被调度唤醒并与之进行对象交换,然后各自返回。如果它的伙伴还没到达交换点,那么当前线程将会被挂起,直至伙伴线程到达——完成交换正常返回;或者当前线程被中断——抛出中断异常;又或者是等候超时——抛出超时异常。
(七)CountDownLatch 与 CyclicBarrier 区别
- CountDownLatch 的作用是允许
1
或N
个线程等待其他线程完成执行;而 CyclicBarrier 则是允许N
个线程相互等待。 - CountDownLatch 的计数器无法被重置; CyclicBarrier 的计数器可以被重置后使用,因此它被称为是循环的 barrier。
(八)ThreadLocal 原理分析
ThreadLocal 提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定。
(九)线程池的几种方式与使用场景
在 Executors
类里面提供了一些静态工厂,生成一些常用的线程池。
1、newFixedThreadPool
:创建固定大小的线程池。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
2、newCachedThreadPool
:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
3、newSingleThreadExecutor
:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
4、newScheduledThreadPool
:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
5、newSingleThreadScheduledExecutor
:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。
6、线程的生命周期:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态
转自:有梦想的咸鱼