线程和进程的关系
进程是系统的执行单位,一般一个应用程序即是一个进程,程序启动时系统默认有一个主线程,即是UI线程,我们知道不能做耗时任务,否则ANR程序无响应。这时需要借助子线程实现,即多线程。由于线程是系统CPU的最小单位,用多线程其实就是为了更好的利用cpu的资源。
多线程创建方式
1、继承Thread类,重写run函数方法
class xx extends Thread{
public void run(){
Thread.sleep(1000); //线程休眠1000毫秒,sleep使线程进入Block状态,并释放资源
}
}
xx.start(); //启动线程,run函数运行
2、实现Runnable接口,重写run函数方法
Runnable run =new Runnable() {
@Override
public void run() {
}
}
3、实现Callable接口,重写call函数方法
Callable call =new Callable() {
@Override
public Object call() throws Exception {
return null;
}
}
小结:Callable 与 Runnable 对比,它可返回值、可抛出异常
4、HandlerThread
通过handlerThread.quit()或者quitSafely()使线程结束自己的生命周期。
5、AsyncTask线程池
AsyncTask默认是开启一个线程队列,即含有5个新线程的线程池。详情可参考。
6、IntentService
它是Service的子类,用法跟Service也差不多。耗时逻辑写在onHandleIntent(Intent intent)的方法体里。当任务执行完后会自动停止,无须手动去终止它。例如在APP里我们要实现一个下载功能,当退出页面后下载不会被中断,那么这时候IntentService就是一个不错的选择了。
线程管理
线程有六种状态:
- NEW (新建):线程尚未启动的线程状态。
- RUNNABLE (可运行):可运行线程的线程状态。
- BLOCKED (阻塞):一个线程的线程状态阻塞等待监视器锁定。
- TIMED_WAITING (计时等待):具有指定等待时间的等待线程的线程状态。
- WAITING (等待):等待线程的线程状态
- TERMINATED (结束):终止线程的线程状态。
1、wait():使一个线程处于等待状态,属于Object类中的。会释放持有的对象锁。
2、sleep():使一个线程处于休眠状态,属于Thread类中的。不会释放对象锁。让出cpu给其他线程,但是它仍然保持对Thread的监控状态,直到时间结束后恢复可运行状态。
3、notify():唤醒等待状态的线程。但是由JVM决定且不按优先级。
4、allnotify():唤醒所有等待状态的线程。并不是给所有线程上锁,而是让它们竞争。
5、join():挂起(插队)等待该线程结束,才能执行其他线程,属于Thread类中的。
6、yield():礼让,Thread类中的yield方法可以让一个running状态的线程转入runnable。
基础概念
1、并行。多个cpu实例或多台机器同时执行一段代码,是真正的同时。
2、并发。让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。
3、线程安全。多线程并发情况下,线程的调度顺序不影响最终结果。线程不安全就意味着线程的调度顺序会影响最终结果,比如某段代码不加事务去并发访问。
4、线程同步。通过控制和调度保证共享资源的多线程访问成为线程安全,来保证结果的准确。如某段代码加入@synchronized关键字。
5、原子性。一个操作或者一系列操作,要么全部执行要么全部不执行。数据库中的“事物”就是个典型的原子操作。
6、可见性。当一个线程修改了共享属性的值,其它线程能立刻看到共享属性值的更改。如JMM分为主存和工作内存,共享属性的修改过程是在主存中读取并复制到工作内存中,在工作内存中修改完成之后,再刷新主存中的值。若线程A在工作内存中修改完成但还没来得及刷新主存中的值,这时线程B访问该属性的值仍是旧值。这样可见性就没法保证。
7、有序性。程序的执行顺序会按照代码的先后顺序执行。
Synchronized 同步锁
synchronized可以修饰
:普通方法、代码块、静态方法(锁类)。 它可以保证只有一个线程拿到锁访问共享资源。
- 悲观锁:每次访问共享资源时都会上锁。
- 非公平锁:线程获取锁的顺序并不一定是按照线程阻塞的顺序。
- 可重入锁:已经获取锁的线程可以再次获取锁。(底层有count变量计数)
- 互斥/排他锁:该锁只能被一个线程所持有,其他线程均被阻塞。
Synchronized 锁升级理论
偏向锁:只有一个线程争抢锁资源时,将线程拥有者标识为当前线程;(默认开启)
轻量级锁(自旋锁):多个线程数竞争锁时开启,即多线程通过CAS去争抢锁,抢不到则自旋。
轻量级锁就适用于耗时任务较短线程,少量自旋,就能够轻松获取到锁。若A持用锁迟迟不释放,其他线程则循环尝试获取锁,获取不到就一直自旋,这个过程对CPU资源是有损耗的。所以,就将轻量级锁锁升级为重量级锁。
重量级锁:多个线程竞争锁时,将未争抢成功的锁放到队列中阻塞;
轻量级锁升级重量级锁的时机:当自旋的线程循环超过10次,或者线程等待的数量超过cpu的1/2,就会将轻量级锁升级为重量级锁。
Synchronized 原理
在jdk1.6之前synchronized
只能实现重量级锁,之后优化自旋锁/轻量级锁、偏向锁等。
Java虚拟机是基于Monitor 对象监视器来实现锁的。
Monitor是由ObjectMonitor实现的,其源码是用C++语言编写的,Hotspot的源码下载链接:http://hg.openjdk.java.net/jdk8/jdk8/hotspot。找到ObjectMonitor.hpp文件,路径是src/share/vm/runtime/objectMonitor.hpp,
其数据结构:
ObjectMonitor() {
_header = NULL;
_count = 0; //锁的计数器,获取锁时值加1,释放锁值减1
_waiters = 0, //等待线程数
_recursions = 0; // 线程重入次数
_object = NULL; // 存储Monitor对象
_owner = NULL; // 持有当前线程的owner
_WaitSet = NULL; // wait状态的线程列表
_EntryList = NULL ; // 处于等待锁状态block状态的线程列表
.....}
其中 _owner、_WaitSet和_EntryList 字段比较重要,它们之间的转换关系如下图
同步机制
- 新线程先尝试获取锁,失败后进入到等待列表进行block;
- 阻塞列表中线程通过CAS的方式,尝试获取owner;(互斥锁)
- 若获取锁成功, count +1;且 owner == 当前线程,则 recursions+1;(重入锁)
- 若获取锁失败,则返回到EntryList中。
- 线程释放锁,count -1,recursions-1;当 recursions = 0时则已经释放了锁。
_WaitSet
持有锁的线程调用wait()
方法,owner = null,count -1,recursions-1,当前线程加入到WaitSet中,等待被唤醒。
Lock 同步锁
Lock是个接口,实现类ReentrantLock重入锁也叫做递归锁。
所谓重入锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码。如果线程可以继续持有这个锁,那么就是可重入的。否则就是不可重入锁。
重入锁,可以解决避免死锁问题,Synchronized也可重入锁。
使用示例:
class Bank {
private int account = 100;
private Lock lock = new ReentrantLock(); //需要声明这个锁
public int getAccount() {
return account;
}
public void save(int money) {
lock.lock();
try{
account += money;
}finally{
lock.unlock();
}
}
}
用ReentrantLock时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁。
Lock 支持公平锁 与非公平锁
- 公平锁:在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列。如果是等待队列的第一个就占有锁,否则就加入等待队列中。以后会按照FIFO的规则从队列中取到自己。
- 非公平锁:上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
非公平锁性能高于公平锁。非公平锁能更充分的利用cpu的时间片,减少cpu空闲的状态时间。
底层实现,通过AQS。详情可查看:JAVA 并发编程之AQS排队同步框架
Lock 与 Syncronized
- Lock 显式的加锁lock() 和unlock()解锁。不同的是 synchronized 的加解锁是隐式的,尤其是抛异常的时候也能保证释放锁。
- Lock 可以反向解锁。比如先Lock1锁再Lock2 锁,可以先解锁 Lock1,再解锁 Lock2。Synchronized 只能先对 obj2 解锁再 解锁 obj1。因为 synchronized 加解锁是由 JVM 实现的,在执行完代码后会自动解锁。所以会按照 synchronized 的嵌套顺序加解锁,不能自行控制。
- Synchronized 重量级,若每个线程持有锁很长时间,那么整个程序的运行效率就会降低。Lock 类的线程在等锁的过程中,可以中断退出,也可以用 tryLock() 方法主动尝试获取锁。
- Lock支持并发读锁, Synchronized 做不到。
- Lock支持公平锁与非公平锁 ,Synchronized只支持非公平锁。
性能区别
在 Java 5 以及之前,synchronized 的性能比较低,但是到了 Java 6 以后,发生了变化,因为 JDK 对 synchronized 进行了很多优化,比如自适应自旋、锁消除、锁粗化、轻量级锁、偏向锁等,所以后期的 Java 版本里的 synchronized 的性能并不比 Lock 差。
如何选择
如果 synchronized 关键字适合你的程序, 那么请尽量使用它。
synchronized 加解锁是由 JVM 实现,可以减少编写代码的数量,减少出错的概率。
若干lock 忘记在 finally 里 unlock,就会出现死锁。程序就会出问题,synchronized 更安全。
如果特别需要 Lock 的特殊功能,比如尝试获取锁、可中断、超时功能等,才使用 Lock。
推荐文章: