java多线程(二) 之 线程安全性
来源:互联网 发布:软件系统维护收费标准 编辑:程序博客网 时间:2024/06/05 03:57
如果当多个线程访问同一个可变的状态变量时,没有使用合适的同步,那么程序就会出现错误.有三种方式修复这个错误:
1. 不在线程之间共享该状态变量
2. 将状态变量修改为不可变的变量
3. 在访问状态变量时,使用同步
一. 线程安全性
1.线程安全类:
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的.
无状态的类: 既不包含任何域,也不包含任何对其他类中域的引用.
例如:
public class StatelessFactorizer { public void service(Request req,Response resp){ doing(req,resp); } public void doing(Request req, Response resp){ System.out.print(req); System.out.println(resp); }}
无状态对象一定是线程安全的
2.原子性
假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B.
线程不安全的统计方式:
public class UnsafeCountingFactorizer { private long count; public void service(Request req,Response resp){ ++count; //doing(req,resp); } public long getCount() { return count; }}
当多个线程调用service的时候,由于执行时序问题,导致count的值会有偏差.
1) 竞态条件:在并发编程中,这种由于不恰当的执行时序而出现不正确的结果.
2) 延迟初始化中的竞态条件:
public class LazyInitRace { private Object instance = null; public Object getInstance(){ if(instance!=null) return new Object(); return instance; }}
3) 复合操作
先检查后执行;
读取–修改–写入等操作统称为复合操作
当在无状态的类中添加一个状态的时候,如果该状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的,在实际情况中,应该尽可能的使用现有的线程安全对象,来管理类的状态.
比如下:
public class CountingFactorizer { private final AtomicLong count = new AtomicLong(0); public long getCount(){ return count.get(); } public void service(Request req,Response resp){ count.incrementAndGet(); }}
3.加锁机制:
有了原子操作,为什么还要采用加锁机制?
案例如下:
private final AtomicReference<Integer> lastNumber = new AtomicReference<>();private final AtomicReference<String> lastFactors = new AtomicReference<>(); private final AtomicLong count = new AtomicLong(0); public void service(String s){ int i = (int) count.incrementAndGet(); String s1 = s+i; lastNumber.set(i); lastFactors.set(s1); String s2 = s+lastNumber.get(); if(!lastFactors.get().equals(s2)) System.out.println(s2+"\t"+lastFactors.get()); }
测试代码:
public static void main(String[] args) { final UnsafeCachingFactorizer cachingFactorizer = new UnsafeCachingFactorizer(); new Thread(){ public void run() { for(int i = 0;i<10000;i++){ cachingFactorizer.service("A"); } }; }.start(); for(int i = 0;i<10000;i++){ cachingFactorizer.service("B"); } }
测试结果:
A9 B11A43 B44B50 A50B58 A59A65 B65B70 A70B79 A79A85 B86B92 A93A98 B99B106 A108A116 B117
上面程序企图通过原子引用来实现统计每一次的输入,尽管这些原子引用本身都是线程安全的,但在UnsafeCachingFactorizer 中同样存在竞态条件.无法做到同时保证两个值同时获取和同时修改.
因此,当更新某一个变量的时候,需要在统一院子操作中对其他变量进行同时控制.
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量.
java提供了一种内置锁的机制来支持原子性:同步代码块synchronized block
1) 内置锁synchronized,相当于一种互斥锁
public synchronized void service(Request req,Response resp){ ...}
上面这种做法尽管实现了线程安全,但却非常极端,在同一时刻只有一个线程可以执行service,多个客户无法访问,服务的响应性,非常之低,无法令人接受,这是一个性能问题,而不是线程安全问题.
2) 重入
当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞,然而,由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功.
public class Widge { public synchronized void doSomething(){ System.out.print("widge...doing"); } public static void main(String[] args) { new LoggingWidget().doSomething(); }}class LoggingWidget extends Widge{ public synchronized void doSomething(){ System.out.println("loggingWidget"); super.doSomething(); }}
获取锁的操作粒度是线程,而不是调用
当线程请求一个未被持有的锁时,JVM将记下锁的持有者,并且将获取计数器置为1,如果同一个线程再次获取这个锁的时候,计数值将递增,而当线程退出同步代码块的时候,计数器会相应的递减.当计数值为0时,这个锁将被释放.
4.用锁来保护状态
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下我们称状态变量是由这个锁保护.
每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁.
对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护.
如果同步可以避免竞态条件问题,那么为什么不在每个方法声明时都使用关键字synchronized,事实上,如果不加区别的滥用synchronized,可能会导致程序中出现过多的同步,而且还并不足以保证复合操作都是原子的.
例如:
if(!vector.contains(element)) vector.add(element);
虽然contains和add都是原子方法,但是在上面的操作上仍然存在竞态条件.
5.活跃性与性能:
通常,在简单性与性能之间存在着相互制约因素.当实现某个同步策略时,一定不要盲目的为了性能而牺牲简单性(这有可能会破坏安全性)
当执行时间较长的计算或者无法快速完成的操作时(比如: 网络IO,控制台IO),一定不要持有锁
保证安全下的性能优化:
import java.util.ArrayList;import java.util.List;import java.util.concurrent.atomic.AtomicLong;public class CachedFactorizer { private List<Integer> lastNumber = new ArrayList<>(); private List<String> lastFactors = new ArrayList<>(); private final AtomicLong count = new AtomicLong(-1); //实现了,并非所有的操作都是必须同步,涉及到计算的时候,比如s1 = s+i;是可以不同步计算的. //如果一个程序当中,计算非常复杂耗时的时候,给加上同步,往往是不理智的. //很多时候,需要同步的,一般都会是取数据,和修改数据的时候,而不是计算. public void service(String s){ int i = (int) count.incrementAndGet(); int j; String s1 = s+i; //添加元素的时候,将复合操作变为同步 synchronized (this) { //通过局部变量j来确保获取数据的准确性 j = lastFactors.size(); lastNumber.add(i); lastFactors.add(s1); } String s2 = null; String s3 = null; //获取的时候,将复合操作变为同步 synchronized (this) { s2 = s+lastNumber.get(j); s3 = lastFactors.get(j); } if(!s2.equals(s3)) System.out.println(s2+"\t"+s3); } public static void main(String[] args) { final CachedFactorizer cachingFactorizer = new CachedFactorizer(); new Thread(){ public void run() { for(int i = 0;i<10000;i++){ cachingFactorizer.service("A"); } }; }.start(); for(int i = 0;i<10000;i++){ cachingFactorizer.service("B"); } }}
- java多线程(二) 之 线程安全性
- java多线程之线程的安全性(一)
- 【Java多线程】线程的安全性
- Java并发之线程安全性
- 多线程学习之(一)线程安全性
- Java多线程之线程池(二)
- Java多线程之线程安全二
- 二.java多线程之线程状态转换
- java多线程(二)之线程安全和线程同步
- Java多线程之i++的安全性问题
- 《Java Concurrency in Practice》之线程安全性
- Java 并发编程之线程安全性
- java并发编程实战之线程安全性
- JAVA并发编程之线程安全性
- java 并发编程实战 之 线程安全性
- java并发编程实践之线程安全性
- java多线程与并发之java线程简介(二)
- 对java多线程的线程安全性的一些总结
- Java源码——一个简单的数据库应用程序(通讯录)
- 【IMWeb训练营作业】ToDoList
- imweb训练营作业(一)
- Linux中zip压缩和unzip解压缩命令介绍
- IMWeb训练营作业
- java多线程(二) 之 线程安全性
- 如何实现在Windows下编写的代码,直接在Linux下编译
- IMWeb前端学习笔记——Day5
- tarjan算法应用 割点 桥 双连通分量
- 【IMWeb训练营作业】vue todoList
- “Think Different”是个糟糕的想法
- android studio 64k问题
- 最新配置dagger2,亲身经历,教你少走弯路!
- scala笔记和akka笔记