Java 线程安全
2016-05-05 15:40
519 查看
I. 线程安全
在使用线程时,如果每个线程所执行的任务中,涉及的变量仅仅是线程内部变量或该变量仅有该线程读写,那么此线程是安全的.但如果多个线程同时读写同一个变量的话,发生的状况往往是变量最后的值和预期不同. 这是因为多个线程在同时执行时,每个线程都会对这个变量进行操作.
这里可以用一个生活中的例子来说明:在订火车票时,某班列车只剩下最后一张票了,但有两人刚好同时看到并预定了这张票,同时因为服务器采用的是多线程,每个用户独享一个单独的线程,那么这两个人将会同时订到这张票,但这显然是不符合生活经验的.
下面的例子便说明了这一点.
public class UnsafeThread implements Runnable { public int count = 0; @Override public void run() { try { TimeUnit.MILLISECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } count++; } public int getCount() { return count; } public static void main(String [] args) { ExecutorService executor = Executors.newCachedThreadPool(); UnsafeThread thread = new UnsafeThread(); for (int i = 0; i < 1000; i++) { executor.execute(thread); } executor.shutdown(); System.out.println("count ==> " + thread.getCount()); } } output:// count ==> 926
在不使用线程的情况下,count 的值应为1000;但因为多个线程同时对count 进行操作,最后的结果只有926.
在顺序编程中,对一个变量执行两次++,在不考虑特殊情况下得到的值一定是2;但如果是用多线程进行操作:线程A 增加了count一次,现在count 等于2;而同时线程B 的任务也是增加count 的值,但线程B 不知道线程A 增加了这个count 值,所以在线程B 执行任务的时候,它得到的count 值为1.
通过上述例子,可以看出,因为线程自身的特殊性,在使用并发时多个线程,如果没有特殊的机制来确保变量的正常操作,那么线程将不会被广泛采用.
II. Synchronised 关键字
对方法使用synchronised 关键字,可以避免上述的情况.这里可以用排队的例子来说明. 在原来的情况中,线程们就像是没有排队的顾客一样,全都挤向了前台;而收银员,也就是被操作的变量,因为顾客太多而被弄得昏头转向,不免出了差错. 现在加上了syncrhonised,相当于在前台加上了栏杆(摆放在前台用来规范队列的东西是什么…),强制顾客们整齐地排成了一条队伍,一次只有一个顾客点餐.
public class SynchronizedThread implements Runnable { private int count = 0; @Override public synchronized void run() { count++; } public int getCount() { return count; } public static void main(String [] args) { ExecutorService executor = Executors.newCachedThreadPool(); SynchronizedThread thread = new SynchronizedThread(); for (int i = 0; i < 1000; i++) { executor.execute(thread); } executor.shutdown(); System.out.println("count ==> " + thread.getCount()); } } output:// count ==> 1000
在
run()方法前加上了synchronised,这样在访问count 变量时,一次只有一个而不是多个线程了.
同时要注意的是,被访问的变量一定要设置为private,不然尽管通过方法操作变量的线程被规范了,直接访问变量的线程却可以任意修改.
III. Lock
Lock 可以理解为显示地使用synchronised 机制.尽管Lock 在语法上没有synchronised 优雅,但在执行一些复杂操作,那么Lock 将具有synchronised 所不具备的灵活性.
public class LockedThread implements Runnable { private int count = 0; private final ReentrantLock lock = new ReentrantLock(); @Override public void run() { boolean captured = lock.tryLock(); try { count++; TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } finally { if (captured) lock.unlock(); } } public int getCount() { return count; } public static void main(String [] args) { ExecutorService executor = Executors.newCachedThreadPool(); SynchronizedThread thread = new SynchronizedThread(); for (int i = 0; i < 1000; i++) { executor.execute(thread); } executor.shutdown(); System.out.println("count ==> " + thread.getCount()); } } output:// count ==> 1000
获得lock 需要调用方法
tryLock(),同时在任务执行完后,需要调用
unlock()来释放锁. 所以推荐方法是在获取锁后,将执行代码放入一个try 模块中,并在finally 模块中释放锁.
IV. Volatile 关键字
原子性(atomic)是除了使用synchronised 关键字和Lock 方法外另一个确保线程安全的方法,它指的是任务操作不可中断性. 但是Java 中的操作大多不是原子性的,并且使用原子性的尝试往往会失败. 因此,不要轻易尝试原子性,除非你已是并发专家.volatile 关键字便是实现原子性的方法之一. 将一个变量设为volatile 后,每一个线程在改变该变量时,所有的线程都可以看到这个改变.
实例为将本章最开始的例子中的count 加上volatile 关键字.
public class UnsafeThread implements Runnable { private volatile int count = 0; @Override public void run() { try { TimeUnit.MILLISECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } count++; } public int getCount() { return count; } public static void main(String [] args) { ExecutorService executor = Executors.newCachedThreadPool(); UnsafeThread thread = new UnsafeThread(); for (int i = 0; i < 1000; i++) { executor.execute(thread); } executor.shutdown(); System.out.println("count ==> " + thread.getCount()); } } output:// count ==> 767
此段解释可能稍有错误
但是从输出可以看出,即使加上了volatile 关键字,count 仍未到达1000. 这和JVM 的运行机制有关. 每个线程在运行时,其实是独自拥有着一个线程栈,并从总栈中复制所需要的变量;而volatile的作用则是强制线程在改变变量后,将这个新的变量推送到总栈中. 但由于这种推送的不及时性,所以导致count的结果依旧不是我们想要的结果.
这个例子也侧面说明了,Java 自身不支持原子性,因此如果除非你是并发专家,不然不要依赖于原子性.
相关文章推荐
- Java中Properties类的使用
- Leetcode第9题Palindrome Number
- SSM整合--增删改查
- 如何在myeclipse中开启两个console?
- Java ConcurrentModificationException异常原因和解决方法
- Java并发编程:同步容器
- Java并发编程:深入剖析ThreadLocal
- Struts2 jsp页面向 action传参(三种)
- Java static块和static方法
- Java并发编程:volatile关键字解析
- SSM框架——详细整合教程(Spring+SpringMVC+MyBatis)
- SpringMVC调度
- java web ajax设置context-type
- myeclipse下安装js提示工具spket
- Struts2后端向前端传参
- Java 多线程编程
- jdk动态代理接口
- Java并发编程:Lock
- Java并发编程:synchronized
- 【第二章】 IoC 之 2.2 IoC 容器基本原理 ——跟我学Spring3