线程
1、概念:
进程是程序运行资源分配的最小单位。
线程是CPU调度的最小单位,必须依赖于进程而存在。
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。
2、线程生命周期:
线程生命周期的5种状态:
Java线程具有五中基本状态
新建状态(New):当线程对象被创建后,即进入了新建状态,如:Thread t = new MyThread();
就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待队列中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
结束状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
Tip:
当调用线程的yield()方法时,线程从运行状态转换为就绪状态,但接下来CPU调度就绪状态中的哪个线程具有一定的随机性,因此,可能会出现A线程调用了yield()方法后,接下来CPU仍然调度了A线程的情况。
3、线程调度
共同点:
- wait()和sleep()他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
- wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态
不同点 - Thread类的方法:sleep(),yield()等
Object对象的方法:wait()和notify()等 - 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。
sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法 - wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
- sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
4、线程实现
4.1、实现方式
继承Thread覆盖run()方法。
实现Runnable接口,实现run()方法。
实现Callable接口,实现call()方法。
4.2、之间的区别:
4.2.1、Thread类与Runnable接口区别:
Thread:每个线程都独立,不共享资源。
Runnable:存在线程共享概念。
4.2.2、Runnable和Callable的区别:
两者最大的不同点是:
1)是否能返回执行结果:
<font color=red>实现Callable接口的任务线程**能返回执行结果**;而实现Runnable接口的任务线程**不能返回结果**</font>;
2)方法是否能抛出异常:
Callable接口的<font color=red>call()方法**允许抛出异常**</font>;而Runnable接口的<font color=red>run()方法的异常只能在内部消化,**不能继续上抛**</font>;
5、线程安全
线程安全性包括两方面:可见性和原子性。
5.1、volatile与synchronized
5.1.1、volatile
volatile关键字解决的是内存可见性的问题:
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效。这时候会去主内存读取。
5.1.2、volatile与synchronized区别
1) volatile
轻量级,只能修饰变量。synchronized
重量级,可以修饰变量、方法、类。
2) volatile
只能保证数据的内存可见性,不能保证原子性,因为多个线程并发访问volatile修饰的变量不会阻塞。
synchronized
不仅保证内存可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。
一个线程执行临界区代码过程如下:
1 获得同步锁
2 清空工作内存
3 从主存拷贝变量副本到工作内存
4 对这些变量计算
5 将变量从工作内存写回到主存
6 释放锁
所以说:仅仅使用volatile并不能保证线程安全性。而synchronized则可实现线程的安全性。
5.1、synchronized关键字的使用和缺点
1)、获取锁的线程执行完毕,然后线程释放对锁的占有
2)、线程执行发生异常,此时JVM会让线程自动释放锁
缺点:
即使大量读操作也会堵塞,不会并发去读。如果线程中有堵塞超时任务,其他任务也不会往下执行,全部堵塞了。
5.2、Lock
5.2.1、Lock接口中常用方法
lock()
tryLock() :尝试获取锁
tryLock(long time, TimeUnit unit):按照超时时间获取锁
lockInterruptibly():打断锁(注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的)
unLock():释放锁
5.2.2、lock线程间的通信:
Conditon中的await()对应Object的wait();
Condition中的signal()对应Object的notify();
Condition中的signalAll()对应Object的notifyAll()
5.2.3、ReentrantLock(可重入锁)
ReentrantLock支持两种获取锁的方式,一种是公平模型,一种是非公平模型。
公平锁:保证执行顺序,先进先出,FIFO
非公平锁:JVM优化后一种插队模式,后进先出,LIFO(先进的数据可能一直消费不到)
5.2.4、ReentrantReadWriteLock (读写锁)
允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。
相对于排他锁,提高了并发性。
在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量。
5.3、lock和synchronized区别:
Lock和synchronized的选择:
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个接口 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 |
在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。 如果A线程阻塞,B线程会一直等待 |
分情况而定,Lock有多个锁获取的方式,可以尝试获得锁,线程可以不用一直等待 |
锁状态 | 无法判断 | 可以判断 |
锁类型 | 可重入 不可中断 非公平 |
可重入 可判断 可公平(两者皆可) |
性能 | 少量同步 | 大量同步 |
Lock可以提高多个线程进行读操作的效率。
6、守护线程
守护线程,可以理解为后台运行线程。进程结束,守护线程自然而然地就会结束,不需要手动的去关心和通知其状态。Java的垃圾回收也是一个守护线程。
它的好处就是你不需要关心它的结束问题。