这一篇博客中,我会列出JAVA多线程编程过程中,容易出现的安全问题(竞态条件、死锁等),以及相应的解决方案,例如 同步机制 等。
究竟什么是线程安全?简单的说, 如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
我们前面已经说过,线程之间共享堆空间,在编程的时候就要格外注意避免竞态条件。危险在于 多个线程同时访问相同的资源并进行读写操作 。当其中一个线程需要根据某个变量的状态来相应执行某个操作的之前,该变量很可能已经被其它线程修改。这里看个简单的例子:
class MyThread extends Thread{ public static int index; public void run(){ for(int i=0;i<10;i++){ System.out.println(getName()+":"+index++); } } } public class Test { public static void main(String[] args){ new MyThread().start(); new MyThread().start(); } }
运行结果是: Thread-0:0 Thread-0:2 Thread-1:1 Thread-0:3 Thread-0:5 Thread-0:6 Thread-0:7 Thread-0:8 Thread-0:9 Thread-0:10 Thread-0:11 Thread-1:4 Thread-1:12 Thread-1:13 Thread-1:14 Thread-1:15 Thread-1:16 Thread-1:17 Thread-1:18 Thread-1:19 在这个例子中,2个线程都会去访问静态变量index,他们获取系统时间片的时刻是不确定的,因此它们对index的访问和修改总是穿插进行的。
我们需要想办法解决上面的竞态条件,当多个线程需要访问同一资源的时候,它们需要以某种顺序来确保该资源在 某一时刻只能被一个线程使用 ,否则程序的运行结果将不可预料。也就是说,当线程A需要使用某个资源,如果该资源正被线程B使用,同步机制就会让线程A一直等待下去,直到线程B结束对该资源的使用,线程A才能使用。
要想实现同步操作,必须获得每一个线程对象的 锁(lock) 。获得锁可以保证同一时刻只有一个线程进入临界区(访问互斥资源的代码块),并且在这个锁被释放之前,其它线程都不能再进入这个临界区。如果还有其他线程想要获得该对象的锁,只能先进入等待队列。当拥有该对象锁的线程退出临界区,锁才会被释放,等待队列中优先级最高的线程才能获得该锁,从而进入共享代码区。
实现同步的方式有以下三种:
其中synchronized块的结构是:
synchronized(syncObject){ //访问syncObject的代码 }
接下来我们看看在使用同步代码块之后,上面提到的竞态条件有没有得到解决。
class MyThread extends Thread{ public static int index; public static Object obj=new Object(); public void run(){ synchronized(obj){ for(int i=0;i<10;i++){ System.out.println(getName()+":"+index++); } } } } public class Test { public static void main(String[] args){ new MyThread().start(); new MyThread().start(); } }
这次的运行结果是: Thread-1:0 Thread-1:1 Thread-1:2 Thread-1:3 Thread-1:4 Thread-1:5 Thread-1:6 Thread-1:7 Thread-1:8 Thread-1:9 Thread-0:10 Thread-0:11 Thread-0:12 Thread-0:13 Thread-0:14 Thread-0:15 Thread-0:16 Thread-0:17 Thread-0:18 Thread-0:19 可以看到,在使用同步之后,线程会按照顺序访问静态变量,也就是说,同步机制通过“锁”解决了竞态条件。
了解同步机制之后,我们需要考虑的就是,到底把哪部分代码放到同步当中?粒度必须足够大,才能将必须视为原子的操作封装在此区域中;然而粒度如果过大,就会导致并发性能降低。因此,应该根据实际业务需求确定锁的粒度大小。
二者都是常用的同步方法,那么,它们有什么区别呢?
下面举一个使用Lock的例子:
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReetrantLock; public class Test{ public static void main(String[] args) throws InterruptedException{ final Lock lock=new ReetrantLock(); lock.lock(); Thread t1=new Thread(new Runnable){ public void run(){ try{ lock.lockInterruptibly(); } catch(InterruptedException e){ System.out.println(" interrupted."); } } }); t1.start(); t1.interrupt(); Thread.sleep(!); } }
程序的运行结果是: interrupted.
死锁是指 两个或两个以上的进程 在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们将一直互相等待而无法推进下去。也就是说,死锁会让你的程序挂起无法完成任务。
另一种与之相近的概念被称为 “活锁” 。活锁与死锁的主要区别是, 活锁进程的状态可以改变(死锁不能改变),但是和死锁一样无法继续执行。 活锁可以理解为在狭小的山道,两辆车相向而行,为了避让而同时往一个方向转头,结果谁都过不去。
究竟什么情况下会发生死锁呢? 死锁必须 同时 满足以下条件
既然死锁的发生必须同时满足这几个条件,我们只需要破坏其中之一就可以避免死锁。通常我们选择阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。
这里我们举个例子说明死锁和避免死锁(程序引用自How to avoid deadlock)。下面的程序会造成死锁,因为如果线程1在执行method1()的时候,获取String对象的锁,而线程2在执行method2()的时候获取Integer对象的锁,双方就会进入无休止的互相等待状态,因为双方都想获取对方已获取的对象锁。
按照上面我们说的,对程序做如下更改,就可以避免死锁的发生。当线程1获取Integer对象的锁的时候,线程2就会等待线程1释放锁之后才会执行,反之亦然。
说明 本人水平有限,不当之处希望各位高手指正。如有转载请注明出处,谢谢。
原文链接:https://blog.csdn.net/antony9118/article/details/51480978