线程安全在Java中是一个很重要的课题。Java提供的多线程环境支持使用Java线程。我们都知道多线程共享一些对象实例的话,可能会在读取和更新共享数据的事后产生数据不一致问题。

线程安全

之所以会产生数据的不一致问题,是因为更新实例变量等类似的行为并非是原子操作。这类操作会有三个步骤:

  • 读取当前的值
  • 做一些必要的操作来获取更新的值
  • 将更新的值写会变量之中

我们来看如下程序中多线程如何更新和共享数据:

package com.sapphire.threads;

public class ThreadSafety {

    public static void main(String[] args) throws InterruptedException {

        ProcessingThread pt = new ProcessingThread();
Thread t1 = new Thread(pt, "t1");
t1.start();
Thread t2 = new Thread(pt, "t2");
t2.start();
//wait for threads to finish processing
t1.join();
t2.join();
System.out.println("Processing count="+pt.getCount());
}
} class ProcessingThread implements Runnable{
private int count; @Override
public void run() {
for(int i=1; i < 5; i++){
processSomething(i);
count++;
}
} public int getCount() {
return this.count;
} private void processSomething(int i) {
// processing some job
try {
Thread.sleep(i*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

在上面的程序中的的for循环里面,count的值每次是自增为1的,执行了四次,因为我们有两个线程,count的值在两个线程执行完毕以后应该是8,但是当你运行上面的程序多次的话,你会发现count的值总是在6,7,8这几个数字之间。这是因为尽管count++操作看起来是一个原子操作,但是实际上它并不是,所以导致了数据的冲突。

Java中的线程安全

线程安全就是指通过对一些处理令我们的程序能够安全的使用多线程的编程模型。以下有一些不同的方式来令我们的程序保证线程安全。

  • 线程同步是最简单和常用的方法来确保线程安全
  • 通过使用java.util.concurrent.atomic包中的原子类,可以确保操作的原子性
  • 通过使用java.util.concurrent.locks包中的锁可以确保线程安全
  • 使用线程安全的并发集合,比如ConcurrentHashMap来确保线程安全
  • 通过volatile关键字来确保每次的变量使用都从内存中访问数据,而非访问线程缓存

Java 同步

同步是我们来获得线程安全的常用方法。JVM会保证同步的代码只会在同一时间仅仅由一个线程来执行。Java的关键字synchronized就是用来创建同步代码的,在内部的执行的时候,synchronized关键字会锁定对象或者类来确保只有一个线程来进入同步的代码块。

  • Java的同步是通过锁定/解锁资源来实现的。在任何线程进入同步代码之前,线程必须请求对象的锁,而在代码执行结束的时候,线程再释放掉该锁,这样其他线程可以再次获取到这个锁。在某个线程执行同步代码的时候,其他的线程只能处于等待状态来等待被锁定的资源。
  • synchronized关键字有两种用法,其一是在方法级别上声明,另一种是创建同步代码块。
  • 当方法被同步的时候,JVM锁定的是对象,如果方法是静态的,那么就会锁定这个。所以,通常最佳的实践是使用同步代码块来锁定需要同步的代码。
  • 当创建同步代码块时,我们需要提供锁定的资源,可以是类本身,也可以是类的成员变量。
  • synchronized(this)会在进入同步代码块之前锁定整个对象。
  • 开发者应该使用最低级别的锁。举例来说,如果类中存在多个需要同步的地方,如果一个方法的访问就锁定了整个对象,那么其他同步代码块就无法被访问了。当我们锁定对象的时候,线程请求的锁是针对对象所有的成员变量的。
  • Java的同步机制提供数据一致性的代价就是性能的损失,所以最好仅仅在最需要的时候使用。
  • Java的同步机制仅仅在同一个JVM中生效的,所以当开发者尝试锁定不同JVM中的多个资源的时候,Java的同步机制是不会有效的。
  • Java的同步机制可能会导致死锁的,需要注意防止产生死锁。
  • Java的synchronized关键字不能同用于变量和构造函数。
  • 在使用Java同步代码块的时候,最好通过创建一个额外的私有对象用来锁定,因为这个引用的对象并不会影响其他的代码。比如,如果开发者针对引用的对象包含一些set方法的调用的话,那么并行的执行可能会导致同步对象的改变。
  • 开发者不应该使用任何常量池中的对象,比如String对象就不应该用来作为同步锁,因为大量的代码可能依赖于相同的字符串,线程就会尝试去请求String pool中的对象锁,这样就会令不同的毫不相关的代码锁定相同的资源。

Java中不少的库也是通过synchronized来实现简单的同步,比如与ArrayList相对应的Vector,和HashMap相对应的HashTable甚至是常用的StringBufferStringBuilder,如果开发者查看过对应的源码,就会发现那些线程安全的类只是在方法上加上了synchronized关键字而已。

下面是一些我们保证线程安全的做法:

//dummy object variable for synchronization
private Object mutex=new Object();
...
//using synchronized block to read, increment and update count value synchronously
synchronized (mutex) {
count++;
}

下面是一些代码帮助我们了解同步的机制:

public class MyObject {

  // Locks on the object's monitor
public synchronized void doSomething() {
// ...
}
} // Hackers code
MyObject myObject = new MyObject();
synchronized (myObject) {
while (true) {
// Indefinitely delay myObject
Thread.sleep(Integer.MAX_VALUE);
}
}

可以看出Hacker的代码是试着锁定myObject的实例,而一旦获得了对应的对象锁,就永远不会释放对象锁,导致doSomething()方法会永远阻塞,一直等待对象锁的释放。这就会导致系统死锁,导致服务拒绝(Denial of Service)。

再参考如下代码:

public class MyObject {
public Object lock = new Object(); public void doSomething() {
synchronized (lock) {
// ...
}
}
} //untrusted code MyObject myObject = new MyObject();
//change the lock Object reference
myObject.lock = new Object();

需要注意的是锁定的对象是一个共有的变量,一旦我们改变原对象所引用的对象,我们就可以任意的并行执行同步代码块中的内容了。如果开发者为私有的锁对象提供setter方法的话,也会导致一样的问题。

再参考如下代码:

public class MyObject {
//locks on the class object's monitor
public static synchronized void doSomething() {
// ...
}
} // hackers code
synchronized (MyObject.class) {
while (true) {
Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
}
}

这段代码与第一段代码很类似,前文已经提到了,静态的static方法会锁定类,所以一旦hacker代码的获得了MyObject的类锁,那么就会形成死锁。

下面是另一个例子:

package com.sapphire.threads;

import java.util.Arrays;

public class SyncronizedMethod {

    public static void main(String[] args) throws InterruptedException {
String[] arr = {"1","2","3","4","5","6"};
HashMapProcessor hmp = new HashMapProcessor(arr);
Thread t1=new Thread(hmp, "t1");
Thread t2=new Thread(hmp, "t2");
Thread t3=new Thread(hmp, "t3");
long start = System.currentTimeMillis();
//start all the threads
t1.start();t2.start();t3.start();
//wait for threads to finish
t1.join();t2.join();t3.join();
System.out.println("Time taken= "+(System.currentTimeMillis()-start));
//check the shared variable value now
System.out.println(Arrays.asList(hmp.getMap()));
} } class HashMapProcessor implements Runnable{ private String[] strArr = null; public HashMapProcessor(String[] m){
this.strArr=m;
} public String[] getMap() {
return strArr;
} @Override
public void run() {
processArr(Thread.currentThread().getName());
} private void processArr(String name) {
for(int i=0; i < strArr.length; i++){
//process data and append thread name
processSomething(i);
addThreadName(i, name);
}
} private void addThreadName(int i, String name) {
strArr[i] = strArr[i] +":"+name;
} private void processSomething(int index) {
// processing some job
try {
Thread.sleep(index*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} }

代码的执行结果如下:

Time taken= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]

可以看出,String的数组出现了不一致问题,因为共享数据以及缺少同步。下面的代码可以改变addThreadName(...)方法来令程序运行正确:

private Object lock = new Object();
private void addThreadName(int i, String name) {
synchronized(lock){
strArr[i] = strArr[i] +":"+name;
}
}

在我们修改了上面的代码以后,程序的输出结果如下:

Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]

Java线程和多线程(三)——线程安全和同步的更多相关文章

  1. Java学习笔记-多线程-创建线程的方式

    创建线程 创建线程的方式: 继承java.lang.Thread 实现java.lang.Runnable接口 所有的线程对象都是Thead及其子类的实例 每个线程完成一定的任务,其实就是一段顺序执行 ...

  2. 0038 Java学习笔记-多线程-传统线程间通信、Condition、阻塞队列、《疯狂Java讲义 第三版》进程间通信示例代码存在的一个问题

    调用同步锁的wait().notify().notifyAll()进行线程通信 看这个经典的存取款问题,要求两个线程存款,两个线程取款,账户里有余额的时候只能取款,没余额的时候只能存款,存取款金额相同 ...

  3. 0036 Java学习笔记-多线程-创建线程的三种方式

    创建线程 创建线程的三种方式: 继承java.lang.Thread 实现java.lang.Runnable接口 实现java.util.concurrent.Callable接口 所有的线程对象都 ...

  4. JAVA多线程(三) 线程池和锁的深度化

    github演示代码地址:https://github.com/showkawa/springBoot_2017/tree/master/spb-demo/spb-brian-query-servic ...

  5. Java基础之多线程篇(线程创建与终止、互斥、通信、本地变量)

    线程创建与终止 线程创建 Thread类与Runnable接口的关系 public interface Runnable { public abstract void run(); } public ...

  6. java 22 - 18 多线程之 线程的状态转换、线程组

    线程的状态转换图解:图片 线程的线程组: 线程组: 把多个线程组合到一起.    它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制. 首先创建一个Runnable的实现类 publi ...

  7. 黑马程序员——JAVA基础之多线程的线程间通讯等

    ------- android培训.java培训.期待与您交流! ---------- 线程间通讯: 其实就是多个线程在操作同一个资源,但是动作不同. wait(); 在其他线程调用此对象的notif ...

  8. java基础24 线程、多线程及线程的生命周期(Thread)

    1.1.进程 正在执行的程序称作为一个进程.进程负责了内存空间的划分 疑问1:windows电脑称之为多任务的操作系统,那么Windows是同时运行多个应用程序呢? 从宏观的角度:windows确实在 ...

  9. Java基础学习——多线程之线程池

    1.线程池介绍     线程池是一种线程使用模式.线程由于具有空闲(eg:等待返回值)和繁忙这种不同状态,当数量过多时其创建.销毁.调度等都会带来开销.线程池维护了多个线程,当分配可并发执行的任务时, ...

  10. C#夯实基础之多线程三:线程的优先级

    一.为什么需要优先级--线程调度的问题 在现实生活中,优先级是一个很常见的现象:在火车站,如果你是孕妇,你是可以走进站中的专门绿色通道的,可以提前上火车以免拥挤:火警119匪警110出警的时候,都是人 ...

随机推荐

  1. Hive_Hive体系结构

     元数据: HQL的执行过程 ORACEL 执行计划,Hive 类似. 无索引时,生成全表扫描执行计划,执行全表扫描.  创建索引后,重新生成SQL语句执行计划,基于索引扫描,提高查询效率.

  2. DDX和DDV——控件与变量之间值的传递

    DoDataExchange由框架调用,作用是交互并且验证对话框数据,主要由(DDX) 和 (DDV)宏实现. 永远不要直接调用这个函数,而是通过UpdateData(TRUE/FALSE)实现控件与 ...

  3. #113. 【UER #2】手机的生产

    链接:http://uoj.ac/problem/113 由于电信技术的发展,人人都可以通过手机互相联系. 有一位电信大佬最近想生产一大批手机,然而从生产线上一台一台地生产实在太慢了,于是他想出了一个 ...

  4. IDEA JavaSE环境配置

    需指定JDK路径: File -> Project Structure -> Project -> Project SDK -> New -> 选择JDK所在的根目录

  5. 自定义orgmode中加粗字体的颜色

    自定义orgmode中加粗字体的颜色 Table of Contents 1. orgmode中加粗字体的默认处理 2. 设置设置加粗字体的颜色 1 orgmode中加粗字体的默认处理 在orgmod ...

  6. 实训随笔:EL表达式JSON应用

    由于之前在学校写的jsp页面都是夹杂着java代码的,所以之前写了个jsp,满满的<%%>和java代码,老师说那样太不美观了啊!!!要全部用EL表达式替代了.本人还是太笨了,弄了一上午才 ...

  7. Android学习总结(二)——Service基本概念和生命周期

    好了,前面我们已经学习了Activity的知识,相信大家也有一定的理解,但是还是不能放松,Android四大组件,我们才学习了一个而已,接下我们继续学习Service.计划总结如下内容: 一.Serv ...

  8. JAVA的程序基本结构和数据类型

    //源程序 Hello.java public class Hello { static String str ="Hello World"; public static void ...

  9. 洛谷 P3183 [HAOI2016]食物链

    题目描述 如图所示为某生态系统的食物网示意图,据图回答第1小题现在给你n个物种和m条能量流动关系,求其中的食物链条数.物种的名称为从1到n编号M条能量流动关系形如a1 b1a2 b2a3 b3.... ...

  10. python 1:列表和字典

    初学Python, 对列表和字典的嵌套使用. phoneBook = [] #列表 list peopleInfo = {} #字典 dict i=0 while i<3: peopleInfo ...