http://blog.csdn.net/yangcheng33/article/details/47708631
线程安全实现方案
线程不安全的原因是:多个线程使用线程共享数据时不能保证更新操作的原子性。
线程安全策略:
一:互斥同步
这是一种悲观同步方案,互斥同步要求使用共享资源的线程必须满足对共享数据的访问具备原子性。如果该线程没有完全结束对共享数据的访问,其他线程不得访问共享数据。
互斥同步的实现方式有synchronized和ReentrantLock两种方式:
前者获取锁的过程中获取不到时线程会被放入队列陷入阻塞等待。后者可以提供非阻塞获取锁的方式,比如trylock和interruptibly。
synchronized既可以修饰实例方法也可以修饰类方法,也可以作为同步块。
1.某个线程获取到对象锁时,这个对象的所有被synchronized修饰的同步方法将被此线程锁住,其他线程只能访问此对象的非synchronized方法。
2.每个对象都有锁,一个对象被一个线程上锁,不影响同属一个类的其他实例对象的锁状态,其他线程可以访问另一个实例对象的非同步方法。
3.synchronized修饰类方法时:同步类方法被执行时所有对象的锁状态变为1,所有对象被上锁。
ReentrantLock跟synchronized相比还包括了中断锁等候和定时锁等候,当线程A先获得了对象锁,线程B在指定时间内无法获取锁时可以自动放弃等待该锁。
Lock lock = new ReentrantLock();
lock.lock();
try{
//可能会出现线程安全的操作
}finally{
//一定在finally中释放锁
//也不能把获取锁在try中进行,因为有可能在获取锁的时候抛出异常
lock.ublock();
}
ReentrantLock是Lock接口一种常见的实现,它是支持重进入的锁即表示该锁能够支持一个线程对资源的重复加锁。该锁还支持获取锁时的公平与非公平的选择。
- void lock() 获取锁,调用该方法当前线程将会获取锁,当锁获取后,该方法将返回。获取不到会一直获取,因此此方法是阻塞的。
- boolean tryLock() 尝试非阻塞的获取锁,调用该方法立即返回,true表示获取到锁
- boolean tryLock(long time,TimeUnit unit) throws InterruptedException 和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false
- void lockInterruptibly() throws InterruptedException 可中断获取锁,与lock()方法不同之处在于该方法会响应中断,即在锁的获取过程中可以中断当前线程
- 当一个线程获取了锁之后,是不会被interrupt()方法中断的。interrupt()方法不能中断正在运行过程中的线程,线程处于阻塞状态才可被中断(如线程调用了sleep,join,wait方法等),但线程获取锁的过程中不可被中断(除了上面新学的方法lockInterruptibly)。线程中断只能。
Thread.interrupt()方法不会中断一个正在运行的线程。它的作用是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。
interrupt方法并不是强制终止线程,它只能设置线程的interrupted状态,被block的线程(sleep() or join())在被调用interrupt时会产生InterruptException(lock是不会的,直到获取到锁才会去处理中断标志),此时是否终止线程由本线程自己决定
- 说白了线程的中断方法的作用并不是中断线程,而是把已经阻塞的线程的中断标志改为true(起作用的基本条件是阻塞),但这个标志不一定立即起作用,lockInterruptibly会立即处理该标志,但lock()方法直到获取到了锁才会处理中断标志。
- void unlock() 释放锁
关于锁的重进入,其实synchronized关键字也支持。如前所述,synchronized关键字也是隐式的支持重进入而对于ReentrantLock而言,对于已经获取到锁的线程,再次调用lock()方法时依然可以获取锁而不被阻塞。
synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
看下面这段代码就明白了:
1
2
3
4
5
6
7
8
9
|
class MyClass { public synchronized void method1() { method2(); } public synchronized void method2() { } } |
上述代码中的两个方法method1和method2都用synchronized修饰了,假如某一时刻,线程A执行到了method1,此时线程A获取了这个对象的锁,而由于method2也是synchronized方法,假如synchronized不具备可重入性,此时线程A需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。
而由于synchronized和Lock都具备可重入性,所以不会发生上述现象。
刚刚提到的公平获取锁与非公平获取锁。如果在绝对时间上,先对于锁进行获取的请求一定先被满足,那么这个锁就是公平的,反之就是非公平的。公平的获取锁也就是等待时间最久的线程优先获取到锁。ReentrantLock的构造函数来控制是否为公平锁。
我在第一次了解到公平获取锁与非公平获取锁的时候,第一反应是公平获取锁的效率高,应该使用公平获取锁。但实际的情况是,非公平获取锁的效率远远大于公平获取锁。
java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程都需要操作系统来帮忙完成,因此需要从用户态切换到内核态。对于代码简单的同步块,状态转换消耗的时间可能比代码执行的时间还要长。因此重量级的synchronized除非有必要否则不使用。API中ReentrantLock和synchronized一样是互斥的方案,一样是可重入的,但多了一些高级功能如可中断,公平锁及锁可以绑定条件等。
二:非阻塞同步
三:可重入代码
如果一段代码的执行结果是可预测的,并且输入相同的数据会得到相同的数据,那么这段代码就是可重入代码,当然也是线程安全的。
用户态与内核态的切换
内核态: CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序
用户态: 只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取
为什么要有用户态和内核态
由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 -- 用户态 和 内核态
所有用户程序都是运行在用户态的, 但是有时候程序确实需要做一些内核态的事情, 例如从硬盘读取数据, 或者从键盘获取输入等. 而唯一可以做这些事情的就是操作系统, 所以此时程序就需要先操作系统请求以程序的名义来执行这些操作.
这时需要一个这样的机制: 用户态程序切换到内核态, 但是不能控制在内核态中执行的指令
这种机制叫系统调用, 在CPU中的实现称之为陷阱指令(Trap Instruction)
他们的工作流程如下:
- 用户态程序将一些数据值放在寄存器中, 或者使用参数创建一个堆栈(stack frame), 以此表明需要操作系统提供的服务.
- 用户态程序执行陷阱指令
- CPU切换到内核态, 并跳到位于内存指定位置的指令, 这些指令是操作系统的一部分, 他们具有内存保护, 不可被用户态程序访问
- 这些指令称之为陷阱(trap)或者系统调用处理器(system call handler). 他们会读取程序放入内存的数据参数, 并执行程序请求的服务
- 系统调用完成后, 操作系统会重置CPU为用户态并返回系统调用的结果