Java线程安全

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java线程安全相关的知识,希望对你有一定的参考价值。

  最近做笔试题,遇到了不少关于线程安全的题目,比如:

  synchronized和volatile的区别是什么?

  StringBuilder和StringBuffer的区别是什么?

  HashMap和HashTable的区别是什么?等等......

  这些问题的答案涉及到的,就是关于线程安全问题。首先先要对线程安全有个概念,怎样才叫线程安全。

线程安全和线程不安全:

  线程安全指的是多个线程并发执行的时候,当一个线程访问该类的某个数据的时候,通过加锁的机制,保护数据,直至当前线程读取完,释放锁后,其他线程才能继续使用,我们认为这样是线程安全的。

  有线程安全,自然就有线程不安全,线程不安全指的是,多个线程并发执行的时候,数据没有得到保护,可能会出现多个线程修改或使用某个数据,导致所得到的数据不正确,也就是脏数据。

 

  了解完基本概念后,接下来要引用某大神从Java内存模型和线程同步机制方面来描述线程的安全性。

Java内存模型

  不同的平台,内存模型是不一样的,但是jvm的内存模型规范是统一的。其实java的多线程并发问题最终都会反映在java的内存模型上,所谓线程安全无 非是要控制多个线程对某个资源的有序访问或修改。总结java的内存模型,要解决两个主要的问题:可见性和有序性。 

  可见性: 多个线程之间是不能互相传递数据通信的,它们之间的沟通只能通过共享变量来进行。Java内存模型(JMM)规定了jvm有主内存,主内存是多个线程共享 的。当new一个对象的时候,也是被分配在主内存中,每个线程都有自己的工作内存,工作内存存储了主存的某些对象的副本,当然线程的工作内存大小是有限制 的。当线程操作某个对象时,执行顺序如下:

 (1) 从主存复制变量到当前工作内存 (read and load)
 (2) 执行代码,改变共享变量值 (use and assign)
 (3) 用工作内存数据刷新主存相关内容 (store and write)

  当一个共享变量在多个线程的工作内存中都有副本时,如果一个线程修改了这个共享 变量,那么其他线程应该能够看到这个被修改后的值,这就是多线程的可见性问题。
  有序性:线程在引用变量时不能直接从主内存中引用,如果线程工作内存中没有该变量,则会从主内存中拷贝一个副本到工作内存中,完成后线程会引用该副本。当同一线程再度引用该字段时,有可能重新从主存中获取变量副本(read-load-use),也有可能直接引用原来的副本 (use),也就是说 read,load,use顺序可以由JVM实现系统决定。
        线程不能直接为主存中字段赋值,它会将值指定给工作内存中的变量副本(assign),完成后这个变量副本会同步到主存储区(store- write),至于何时同步过去,根据JVM实现系统决定.有该字段,则会从主内存中将该字段赋值到工作内存中,这个过程为read-load,完成后线 程会引用该变量副本。

  举个例子1,现有一变量x=10,a线程执行x=x+1操作,b线程执行x=x-1操作,俩线程同时运行的时候,x的值是不确定的,有可能为9,也有可能为11,这就是多线程并发执行的顺序是不可预见导致的,所以要使线程安全,要保证a线程和b线程的有序执行,且执行的操作必须为原子操作

  原子操作:在多线程访问共享资源时,能够确保所有其他的进程(线程)都不在同一时间内访问相同的资源。原子操作(atomic operation)是不需要synchronized,这是Java多线程编程的老生常谈了。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。

 

那么如何确保线程执行的有序性和可见性呢?

synchronized关键字

  synchronized关键字可以解决有序性和可见性的问题,它保证了多个线程之间是互斥的,当一段代码会修改共享变量,这一段代码成为互斥区或 临界区,为了保证共享变量的正确性,synchronized标示了临界区。

  常用用法:

Java代码  

synchronized(lock) {
临界区代码
}

//例如:
public synchronized void method() {}

public static synchronized void method() {}

  无论synchronized关键字是加在方法还是对象中,都是取对象当作锁,理论上每个对象都可以是一个锁。

  对于public synchronized void method()这种情况,锁就是这个方法所在的对象。同理,如果方法是public  static synchronized void method(),那么锁就是这个方法所在的class。

  synchronized关键字有两种锁对象,一种是对象加锁,另一种是对类加锁,对类加锁,则类锁对类里的所有对象都起作用,而对象锁只是针对该类的一个指定的对象加锁,这个类的其它对象仍然可以使用已经对前一个对象加锁的synchronized方法。

  例如:车站只剩一张票,A业务员和B业务员同时出票,执行public synchronized void sell()方法,那么结果就会出现,剩余票数为(-1)的情况,这就是对象锁导致的问题,其他对象仍然可以使用这个方法;

  当把sell方法加上static后,锁的对象就是这个类,对于A业务员和B业务员来说,都要按顺序来操作业务,这样就不会出现剩余票数为负数的轻卡UN个了。

  当一个对象是锁的时候,应该要被多个线程共享才是有意义的。这也得出一个结论:非线程安全!=不安全。

  比如ArrayList就是线程不安全的,但是并非说多线程情况下就不用ArrayList,如果你的每一个线程都new了一个ArrayList对象,也就是对象是非共享的,线程之间不存在资源竞争,那么多线程执行的时候也是没有安全问题的。

  每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列存储了将要获得锁的线程,阻塞队列存储了被阻塞的线程,当一个线程被唤醒 后,才会进入到就绪队列,等待cpu的调度。

  一个线程执行临界区代码过程如下:
  1 获得同步锁
  2 清空工作内存
  3 从主存拷贝变量副本到工作内存
  4 对这些变量计算
  5 将变量从工作内存写回到主存
  6 释放锁
  可见,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性。

  生产者/消费者模式就是典型的同步锁问题,多线程执行过程除了操作是互斥的以外,往往也会出现线程之间互相协作的情况。

  比方说,A机器人负责造车子,造好了车子,就放在仓库里,仓库每次只能放一辆车。

  (1)起初,A机器人被运行,执行造车子make方法,造好了车子放在仓库后,没有马上关机(释放锁),而是唤醒了(notify)准备上班的(阻塞队列)B机器人,自己再下班(进入阻塞队列)

  (2)B机器人获得同步锁,上班,开始把造好的车子运出仓库拿去出售,车子运出去后,它也没有马上溜了,而是唤醒了刚下班的A机器人继续造车子(赤裸裸的剥削,我也要卖车子)。

  (3)A机器人发现仓库的车子被运走了,接到任务就只能继续开始造车子,造好车子把B机器人叫回来。

  (4)B机器人卖车子,叫醒A机器人造车子。

  。。。。。。

  可以看出,在同步锁的作用下,造车子,卖车子,造车子,卖车子可以有序的进行,很愉快。

 

接下来要说的是另外一个关键字:volatile

volatile关键字

  volatile同样也是Java同步的一种方法,只不过和synchronized锁相比,稍弱了点,它只能保证多线程执行的可见性,可不能保证有序性。

  它的原理是不需要从主内存中复制一份副本到工作内存,而是直接对主内存的数据进行修改,这样,其他线程都能立马看到数据的修改。因此,volatile的使用范围要臂synchronized小,常用于直接赋值的情况,诸如例子1的情况就是不适用的。

 

最后需要注意的是,synchronized锁虽好,但是不能多用,会影响执行效率,阻塞队列的线程也会不断地尝试获取锁,消耗性能。更多关于synchronized和volatile的用法这里就不展开来说了,可以baidu一下详细的用法。

 

本文参考了http://www.iteye.com/topic/806990,比较容易理解,感谢大神。

 












以上是关于Java线程安全的主要内容,如果未能解决你的问题,请参考以下文章

怎样去写线程安全的代码(Java)

怎样去写线程安全的代码(Java)

Java线程安全问题代码实现

java是线程安全的吗

HashMap 和 ConcurrentHashMap 的区别

Java线程 — 线程同步及安全问题