创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口
线程自身信息
线程运行的过程会产生很多信息,这些信息都保存在Thread类中的成员变量里面,常见的有:
a.线程的ID是唯一标识getId()
b.线程的名称:getName(),如果不设置线程名称默认为“Thread-xx”
c.线程的优先级:getPriority,线程优先级从1-10,其中数字越大表示优先级别越高,同时获得JVM调度执行的可能性越大,JDK内置了三种常见的状态:
//最小优先级
public final static int MIN_PRIORITY = 1;
//一般优先级
public final static int NORM_PRIORITY = 5;
//最大优先级
public final static int MAX_PRIORITY = 10;
Thread使用start和run方法启动线程区别:
串行执行用run,并行执行用start。 原因:run是Runnable内部方法,该方法只能同步执行 ;start是交给系统函数start0() 执行,是异步执行。
线程六大状态:
1. 新建状态(New):
当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码
2. 就绪状态/可执行状态(Runnable),包括运行状态(Running)
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.
3. 阻塞状态(Blocked)
这个状态,一般是线程等待获取一个锁,来继续执行下一步的操作,比较经典的就是synchronized
关键字,这个关键字修饰的代码块或者方法,均需要获取到对应的锁,在未获取之前,其线程的状态就一直未BLOCKED,如果线程长时间处于这种状态下,我们就是当心看是否出现死锁的问题了。
线程运行过程中,可能由于各种原因进入阻塞状态:
1>线程通过调用sleep方法进入睡眠状态;
2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
3>线程试图得到一个锁,而该锁正被其他线程持有;
4>线程在等待某个触发条件;
......
所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
4. 终止状态(Terminated)
有两个原因会导致线程死亡:
1) run方法正常退出而自然死亡,
2) 一个未捕获的异常终止了run方法而使线程猝死。
为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false。
5. 等待(Waiting) : 一个线程在等待另一个线程执行动作是,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
- Object.wait()
- Thread.join()
- LockSupport.park()
6. 超时等待(TIMED_WAITING)
这个状态和WAITING状态的区别就是,这个状态的等待是有一定时效的,即可以理解为WAITING状态等待的时间是永久的,即必须等到某个条件符合才能继续往下走,否则线程不会被唤醒。但是TIMED_WAITING,等待一段时间之后,会唤醒线程去重新获取锁。当执行如下代码的时候,对应的线程会进入到TIMED_WAITING状态。
- Thread.sleep(long)
- Object.wait(long)
- Thread.join(long)
- LockSupport.parkNanos()
- LockSupport.parkUntil()
8. 几个方法的比较
Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入阻塞,但不释放对象锁,millis后线程自动苏醒进入可运行状态。作用:给其它线程执行机会的最佳方式。
Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。
t.join()/t.join(long millis),当前线程里调用其它线程1的join方法,当前线程阻塞,释放对象锁,直到线程1执行完毕或者millis时间到,当前线程进入可运行状态。
obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。
obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
WAITING、TIMED_WAITING与BLOCKED的区别:
(1)WAITING:进入等待状态,方式:wait/join/park方法进入无限等待,通过notify/notifyAll/unpark唤醒;
(2)TIMED_WAITING:与WAITING类似,方式:a. 给定等待时间的wait/join/park方法;b. sleep方法;
(3)BLOCKED:被动进入等待状态,方式:进入Synchronized块;
sleep(): sleep 过程中线程不会释放锁,只会阻塞线程,让出cpu给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态,可中断,sleep 给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会。
join():一般用于等待异步线程执行完结果之后才能继续运行的场景,会使当前线程等待调用join()方法的线程结束后才能继续执行。在等待的过程中释放对象锁。底层实现用wait()。
主线程创建并启动子线程,如果子线程中需要进行大量的耗时计算,主线程往往早于子线程结束。这时,如果主线程想等待子线程执行结束之后再结束,比如子线程处理一个数据,主线程要取得这个数据,就要用join() 方法。
yield() : 暂停当前正在执行的线程对象,不会释放资源锁,和 sleep 不同的是 yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。还有一点和 sleep 不同的是 yield 方法只能使同优先级或更高优先级的线程有执行的机会。
wait(): wait 方法是属于 Object 类中的,wait 过程中线程会释放对象锁,只有当其他线程调用 notify 才能唤醒此线程。wait 使用时必须先获取对象锁,即必须在 synchronized 修饰的代码块中使用,那么相应的 notify 方法同样必须在 synchronized 修饰的代码块中使用,如果没有在synchronized 修饰的代码块中使用时运行时会抛出IllegalMonitorStateException的异常。
Executor框架的结构
Executor主要由三部分组成:任务产生部分,任务处理部分,结果获取部分。(设计模式:生产者与消费者模式)
1.任务的产生:Runnable接口和Callable接口
这2个对象属于任务对象。工具类Executors可以把一个Runnable对象封装为Callable对象。当我们拥有任务对象之后,就可以将其交给ExecutorService(Executor的一个实现接口)了,这样转入第二部分–任务处理部分。
(1)ThreadPoolExecutor类
构造函数的参数:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
corePoolSize:线程池的核心线程数,默认情况下,核心线程数会一直在线程池中存活,即使它们处理闲置状态。如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的核心线程在等待新任务到来时会执行超时策略,这个时间间隔由keepAliveTime所指定,当等待时间超过keepAliveTime所指定的时长后,核心线程就会被终止。
maximumPoolSize:线程池所能容纳的最大线程数量,当活动线程数到达这个数值后,后续的新任务将会被阻塞。
keepAliveTime:非核心线程闲置时的超时时长,超过这个时长,非核心线程就会被回收。当ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true时,keepAliveTime同样会作用于核心线程。
unit:用于指定keepAliveTime参数的时间单位,这是一个枚举,常用的有TimeUnit.MILLISECONDS(毫秒),TimeUnit.SECONDS(秒)以及TimeUnit.MINUTES(分钟)等。
workQueue:线程池中的任务队列,通过线程池的execute方法提交Runnable对象会存储在这个队列中。
threadFactory:线程工厂,为线程池提供创建新线程的功能。ThreadFactory是一个接口,它只有一个方法:Thread newThread(Runnabler)。
除了上面的参数外还有个不常用的参数,RejectExecutionHandler,这个参数表示当ThreadPoolExecutor已经关闭或者ThreadPoolExecutor已经饱和时(达到了最大线程池大小而且工作队列已经满),execute方法将会调用Handler的rejectExecution方法来通知调用者,默认情况 下是抛出一个RejectExecutionException异常。
ThreadPoolExecutor执行任务时的大致规则:
(1)如果线程池的数量还未达到核心线程的数量,那么会直接启动一个核心线程来执行任务
(2)如果线程池中的线程数量已经达到或者超出核心线程的数量,那么任务会被插入到任务队列中排队等待执行。
(3)如果在步骤(2)中无法将任务插入到任务队列中,这往往是由于任务队列已满,这个时候如果线程数量未达到线程池规定的最大值,那么会立刻启动一个非核心线程来执行任务。
(4)如果在步骤(3)中线程数量已经达到线程池规定的最大值,那么就会拒绝执行此任务,ThreadPoolExecutor会调用RejectExecutionHandler的rejectExecution方法来通知调用者。
工厂类可以创建3种类型的ThreadPoolExecutor类:
1:FixedThreadPool(LinkedBlockingQueue):拥有固定数量线程的线程池,限制了线程的数目,适用于负载比较重的服务器。:
2:CachedThreadPool(SynchronousQueue):大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。
3:SingleThreadExecutor(LinkedBlockingQueue):单个线程的线程池,适用于需要保证顺序的执行各个任务;任意时间点,不会有多个线程活动。
(2):ScheduledThreadPoolExecutor类,继承自ThreadPoolExecutor
1:ScheduledThreadPoolExecutor(DelayQueue):可以执行并行任务也就是多条线程同时执行。
2:SingleThreadScheduledExecutor(DelayQueue):适用于需要单个后台线程执行周期任务,同时需要保证任务顺序执行的应用场景。
Executors 返回线程池对象的弊端:
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
使用Executors类创建线程池与使用ThreadPoolExecutor类的区别就是使用ThreadPoolExecutor类可以自定义传入我们设置的线程池的参数,更加灵活。Executors也是调用ThreadPoolExecutor来新建线程池的。
Callable和Future(接口),通过它们可以在任务执行完毕之后得到任务执行结果。FutureTask是Future唯一实现类。ExcecutorService.submit()方法调用Callable。
对shutdown()方法的调用可以防止新任务被提交给这个Executor,当前线程将继续运行在shutdown()被调用之前提交的所有任务。这个程序将在Executor中所有任务完成之后尽快退出。
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
ReadWriteLock也是一个接口,ReentrantReadWriteLock实现了ReadWriteLock接口,最主要的有两个方法:readLock()和writeLock()用来获取读锁和写锁。
锁的相关概念介绍:
1.可重入锁
如果锁具备可重入性,即某个线程已经获得某个锁,可以再次获取锁而不会出现死锁,则称作为可重入锁。像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。
2.可中断锁:就是可以相应中断的锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。
3.公平锁
公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。
在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。ReentrantLock lock =
new
ReentrantLock(
true
);
4.读写锁
读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。
正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。
ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。
可以通过readLock()获取读锁,通过writeLock()获取写锁。
5. 悲观锁 vs 乐观锁
- synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
- CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。