Java线程和多线程(三)——线程安全和同步
线程安全在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
甚至是常用的StringBuffer
和StringBuilder
,如果开发者查看过对应的源码,就会发现那些线程安全的类只是在方法上加上了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线程和多线程(三)——线程安全和同步的更多相关文章
- Java学习笔记-多线程-创建线程的方式
创建线程 创建线程的方式: 继承java.lang.Thread 实现java.lang.Runnable接口 所有的线程对象都是Thead及其子类的实例 每个线程完成一定的任务,其实就是一段顺序执行 ...
- 0038 Java学习笔记-多线程-传统线程间通信、Condition、阻塞队列、《疯狂Java讲义 第三版》进程间通信示例代码存在的一个问题
调用同步锁的wait().notify().notifyAll()进行线程通信 看这个经典的存取款问题,要求两个线程存款,两个线程取款,账户里有余额的时候只能取款,没余额的时候只能存款,存取款金额相同 ...
- 0036 Java学习笔记-多线程-创建线程的三种方式
创建线程 创建线程的三种方式: 继承java.lang.Thread 实现java.lang.Runnable接口 实现java.util.concurrent.Callable接口 所有的线程对象都 ...
- JAVA多线程(三) 线程池和锁的深度化
github演示代码地址:https://github.com/showkawa/springBoot_2017/tree/master/spb-demo/spb-brian-query-servic ...
- Java基础之多线程篇(线程创建与终止、互斥、通信、本地变量)
线程创建与终止 线程创建 Thread类与Runnable接口的关系 public interface Runnable { public abstract void run(); } public ...
- java 22 - 18 多线程之 线程的状态转换、线程组
线程的状态转换图解:图片 线程的线程组: 线程组: 把多个线程组合到一起. 它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制. 首先创建一个Runnable的实现类 publi ...
- 黑马程序员——JAVA基础之多线程的线程间通讯等
------- android培训.java培训.期待与您交流! ---------- 线程间通讯: 其实就是多个线程在操作同一个资源,但是动作不同. wait(); 在其他线程调用此对象的notif ...
- java基础24 线程、多线程及线程的生命周期(Thread)
1.1.进程 正在执行的程序称作为一个进程.进程负责了内存空间的划分 疑问1:windows电脑称之为多任务的操作系统,那么Windows是同时运行多个应用程序呢? 从宏观的角度:windows确实在 ...
- Java基础学习——多线程之线程池
1.线程池介绍 线程池是一种线程使用模式.线程由于具有空闲(eg:等待返回值)和繁忙这种不同状态,当数量过多时其创建.销毁.调度等都会带来开销.线程池维护了多个线程,当分配可并发执行的任务时, ...
- C#夯实基础之多线程三:线程的优先级
一.为什么需要优先级--线程调度的问题 在现实生活中,优先级是一个很常见的现象:在火车站,如果你是孕妇,你是可以走进站中的专门绿色通道的,可以提前上火车以免拥挤:火警119匪警110出警的时候,都是人 ...
随机推荐
- python 生成器与迭代器
#! /usr/bin/env python# -*- coding:utf-8 -*- def xrange(n): num = 0 while True: if num > n: retur ...
- 094 Binary Tree Inorder Traversal 中序遍历二叉树
给定一个二叉树,返回其中序遍历.例如:给定二叉树 [1,null,2,3], 1 \ 2 / 3返回 [1,3,2].说明: 递归算法很简单,你可以通过迭代算法完成吗?详见 ...
- Ubuntu安装LAMP
1.安装apache2 sudo apt-get install apache2 sudo apt-get install apache2 Apache安装成功后,/var/www/默认作为web的根 ...
- tar打包压缩命令
1. tar命令 用法: tar [选项...] [FILE]... GNU ‘tar’将许多文件一起保存至一个单独的磁带或磁盘归档,并能从归档中单独还原所需文件. 示例 tar -cf archiv ...
- HTTP/1.1 持久连接 persistent connection
首先:HTTP的长连接和短连接本质上是TCP长连接和短连接. 1. 在HTTP1.0中,默认的是短连接,没有正式规定 Connection:Keep-alive 操作:在HTTP1.1中所有连接都是K ...
- CF1149A Prefix Sum Primes
思路: 质数一定是奇数.实现: #include <bits/stdc++.h> using namespace std; int main() { int n, t, x, y; whi ...
- IP-XACT IP IEEE交换格式
1 What is IP-XACT? IP-XACT is an XML format that defines and describes electronic components a ...
- HDU 1729 Stone Game 石头游戏 (Nim, sg函数)
题意: 有n个盒子,每个盒子可以放一定量的石头,盒子中可能已经有了部分石头.假设石头无限,每次可以往任意一个盒子中放石头,可以加的数量不得超过该盒中已有石头数量的平方k^2,即至少放1个,至多放k^2 ...
- (1)Ngixn 编译安装 (版本:1.12.1)
1.创建用户和群组 groupadd nginx 创建一个用户,不允许登陆和不创主目录 useradd -s /sbin/nologin -g nginx -M ngi ...
- (三)maven之一个基本的pom.xml
一个基本项目的pom.xml文件,通常会有以下三部分: 一.项目坐标,信息描述等. <modelVersion>4.0.0</modelVersion> <groupId ...