Java 并发(1)——线程安全
我们对并发一词并不陌生,它通常指多个任务同时执行。实际上这不完全对,“并行”才是真正意义上的同时执行,而“并发”则更偏重于多个任务交替执行。有时候我们会看见一些人一边嘴里嚼着东西一边讲话,这是并行;当然,更文明礼貌的方式是讲话前先把嘴里的东西咽下去,这是并发。并发早期被用来提高单处理器的性能,比如I/O阻塞。在多核处理器被广泛应用的今天,并行和并发的概念已经被模糊了,或许我们不必太过纠结二者之间的微妙差别。
Java的并发是通过多线程实现的,如果有多个处理器,线程调度机制会自动向各处理器分派线程。线程不同于进程,它的级别比进程更低,一个进程可以衍生出多个线程。现代操作系统都是多进程的,不同的程序分属于不同的进程,各进程之间不会共享同一块内存空间。因为进程之间没有交集,所以各进程能够相安无事地运行,这就好比同一栋楼里的不同住户,大家关起门来各过各的,别人家夫妻吵架跟你一点关系都没有。计算机中运行的各个程序都分属于不同的进程,你在使用IDE时不必担心播放器会修改你的代码,也不会担心通讯软件会对IDE有什么影响。但是到了多线程,一切都变得复杂了,原来不同的住户现在要搬到一起合租,卫生间、厨房都变成了公用的。每个线程都共享其所属进程的资源,多线程的困难就在于协调不同线程所驱动的任务之间对共享资源的使用。
既然多线程这么困难,为什么不直接使用多进程呢?一个原因是进程及其昂贵,操作系统会限制进程的数量。另一个原因来自遥远的蛮荒年代,当时一些中古系统并不支持多进程,java为了实现可移植的目的,用多线程实现了并发。
Java的多线程无处不在,然而实际情况是,很少有人真正编写过并发代码,实际上有相当多的技术人员从未写过真正意义上的并发。原因是一些诸如Servlets的框架帮助我们处理了并发问题。
任务与线程
Java的线程是通过Runnable接口实现的,可以这样实现一个线程:
class Task implements Runnable {
private int n = 1;
private String tName = ""; public Task(String tName) {
this.tName = tName;
} @Override
public void run() {
while(n <= 10) {
System.out.print(this.tName + "#" + n + " ");
n++;
}
System.out.println(this.tName + " is over.");
}
} public class C_1 {
public static void main(String[] args) {
Task A = new Task("A");
Task B = new Task("B");
A.run();
B.run();
System.out.println("main is over.");
}
}
运行结果与顺序执行没什么不同:
这说明实现了Runnable的类实际上与普通类没什么不同,它充其量只是个任务,想要实现并发,必须把任务附着在一个线程上:
public class C_1 {
public static void main(String[] args) {
Thread t1 = new Thread(new Task("A"));
Thread t2 = new Thread(new Task("B"));
t1.start();
t2.start();
System.out.println("main is over.");
}
}
这次才是真正意义上的并发:
start()会为线程启动做好必要的准备,之后调用任务的run()方法,让任务运行在线程上。在JDK1.5之后加入了线程管理器,可以不必显示地把任务附着在线程上,同时线程管理器还会自动管理线程的生命周期。
public class C_1 {
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
es.execute(new Task("A"));
es.execute(new Task("B"));
es.shutdown();
System.out.println("main is over.");
}
}
shutdown()方法用于阻止向ExecutorService中提交新线程。如果在es.shutdown()时候仍然提交新线程,将会抛出java.util.concurrent.RejectedExecutionException。
JDK8之后加入了lambda表达式,对于一些短小的不需要重用的任务,可以不必单独写成一个类:
public class C_1 {
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
es.execute(new Task("A"));
es.execute(new Task("B"));
es.execute(new Runnable() {
@Override
public void run() {
System.out.println("I am in lambda.");
}
});
es.shutdown();
System.out.println("main is over.");
}
}
由于每个lambda表达式的初始化都会耽误一点时间,因此在执行短小的运行速度很快的多线程程序时,这种方式往往看不出效果,程序更像是顺序的。
线程安全
我们经常说某个方法是线程安全的。我并不觉得“线程安全”是个易于理解的词。简单地说,如果某个方法是“线程安全”的,那么这个方法在多线程环境下的运行结果也将是可预期的。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; class Task2 implements Runnable {
String tName = ""; public Task2(String tName) {
this.tName = tName;
} @Override
public void run() {
for(int i = 1; i <= 10; i++) {
System.out.print(tName + "#" + i + " ");
}
}
} public class C_2 {
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
es.execute(new Task2("A"));
es.execute(new Task2("B"));
es.shutdown();
}
}
运行结果可能是:
作为一个任务,Task2每次运行都会将10个编号依次打印出来,尽管每次打印的顺序可能有所区别,但我们仍然认为它是可预期的,是线程安全的。
Task2之所以安全,是因为它没有共享的状态。如果加入状态,就很容易把一个原本线程安全的方法变成不安全。
class Task2 implements Runnable {
String tName = "";
static int no = 1; public Task2(String tName) {
this.tName = tName;
} @Override
public void run() {
for(int i = 1; i <= 10; i++) {
System.out.print(tName + "#" + i + " ");
no++;
}
}
}
这里仅仅是对Task2稍加修改,让两个任务共享同一个序号,每次执行循环时都会对no加1。我们预期的效果是每次打印出不同的no值,然而实际的运行结果可能是:
出现了A#9和B#9。其原因是两个线程同时对no产生了竞争,而no++并又是通过多条指令完成的。在no=9时,A线程将其打印出来,之后执行++操作,在执行到一半的时候B进来了,由于++操作并未结束,因此B看见的仍是上一状态。
无状态的程序一定是线程安全的。HTTP是无状态的,处理HTTP请求的servlet也是无状态的,因此servlet是线程安全的。尽管如此,你仍需时刻保持警惕,因为没有任何约束阻止你把一个原本无状态的方法变成有状态的。
public class MyServlet extends HttpServlet { private static int no = 1; @Override
protected void service(HttpServletRequest arg0, HttpServletResponse arg1) throws ServletException, IOException {
arg0.setAttribute("no", no++);
}
}
有了共享就有了竞争,此时原本的线程安全也将变成不安全。
单例模式
我曾经面试过很多程序员,问他们知道哪些常用的设计模式,很多人的第一个回答就是单例模式,可见单例模式的深入人心。下面是个典型的单例。
public class Singleton {
private static Singleton sl = null; private Singleton() {
System.out.println("OK");
} public static Singleton getInstance() {
if(sl == null)
sl = new Singleton();
return sl;
} public static void main(String[] args) {
Singleton.getInstance();
Singleton.getInstance();
Singleton.getInstance();
}
Singleton在执行初始化后会打印OK,由于Singleton只会执行一次初始化,因此程序最终仅仅会打印一次OK。然而一切在多线程中变得就不同了。把单例放在线程中:
class Task3 implements Runnable { @Override
public void run() {
Singleton.getInstance();
}
} public class Singleton {
private static Singleton sl = null; private Singleton() {
System.out.println("OK");
} public static Singleton getInstance() {
if(sl == null)
sl = new Singleton();
return sl;
} public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
es.execute(new Task3());
es.execute(new Task3());
es.execute(new Task3());
es.shutdown();
}
}
3个线程同时发现了sl==null,此时可能会执行3次初始化,打印3次OK。这也成为单例模式被人诟病的原因,虽然可以通过双检查锁和volatile关键字解决上述情况,但代码较为复杂,性能也让人捉急。一个好的方式是使用主动初始化代替单例:
public class Singleton_better { private static Singleton_better sl = new Singleton_better(); public static Singleton_better getInstance() {
return sl;
} public Singleton_better() {
System.out.println("OK");
}
}
另一种方式是惰性初始化, 它在解决了线程安全的同时还保留了单例的优点:
public class Single_lazy { private static class Handle {
public static Single_lazy sl = new Single_lazy();
} public static Single_lazy getInstance() {
return Handle.sl;
}
}
作者:我是8位的
出处:http://www.cnblogs.com/bigmonkey
本文以学习、研究和分享为主,如需转载,请联系本人,标明作者和出处,非商业用途!
扫描二维码关注公作者众号“我是8位的”
Java 并发(1)——线程安全的更多相关文章
- Java 并发 中断线程
Java 并发 中断线程 @author ixenos 对Runnable.run()方法的三种处置情况 1.在Runnable.run()方法的中间中断它 2.等待该方法到达对cancel标志的测试 ...
- Java 并发编程 | 线程池详解
原文: https://chenmingyu.top/concurrent-threadpool/ 线程池 线程池用来处理异步任务或者并发执行的任务 优点: 重复利用已创建的线程,减少创建和销毁线程造 ...
- java并发编程 线程基础
java并发编程 线程基础 1. java中的多线程 java是天生多线程的,可以通过启动一个main方法,查看main方法启动的同时有多少线程同时启动 public class OnlyMain { ...
- Java并发1——线程创建、启动、生命周期与线程控制
内容提要: 线程与进程 为什么要使用多线程/进程?线程与进程的区别?线程对比进程的优势?Java中有多进程吗? 线程的创建与启动 线程的创建有哪几种方式?它们之间有什么区别? 线程的生命周期与线程控制 ...
- java并发:线程同步机制之Volatile关键字&原子操作Atomic
volatile关键字 volatile是一个特殊的修饰符,只有成员变量才能使用它,与Synchronized及ReentrantLock等提供的互斥相比,Synchronized保证了Synchro ...
- java并发:线程池、饱和策略、定制、扩展
一.序言 当我们需要使用线程的时候,我们可以新建一个线程,然后显式调用线程的start()方法,这样实现起来非常简便,但在某些场景下存在缺陷:如果需要同时执行多个任务(即并发的线程数量很多),频繁地创 ...
- java并发:线程同步机制之Lock
一.初识Lock Lock是一个接口,提供了无条件的.可轮询的.定时的.可中断的锁获取操作,所有加锁和解锁的方法都是显式的,其包路径是:java.util.concurrent.locks.Lock, ...
- Java并发编程:线程间通信wait、notify
Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...
- Java并发编程:线程和进程的创建(转)
Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...
- Java并发3-多线程面试题
1) 什么是线程? 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速. 2) 线程和进程有什 ...
随机推荐
- SQL Server 查询某一个数据库存储过程、函数是否包含某一个内容或者脚本
SELECT obj.Name 名称, sc.TEXT 内容FROM syscomments scINNER JOIN sysobjects obj ON sc.Id = obj.IDWHERE sc ...
- Attaching an entity of type 'xxx' failed because another entity of the same type already has the same primary key value.
问题的详细描述: Attaching an entity of type 'xxxxx' failed because another entity of the same type already ...
- Docker启动守护式容器
目录 启动守护式容器 查看容器日志 docker后台运行 查看容器内运行的进程 查看容器内部细节 进入正在运行的容器并以命令行交互 重新进入 上述两个区别 从容器内拷贝文件到主机上 启动守护式容器 ...
- [转]java 通过反射获取类的全局变量、方法、构造方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.str; public class ZiFuChuan { ...
- React-router使用
介绍 react-router被分为以下几部分: react-router是浏览器和原生应用中的通用部分. react-router-dom是用于浏览器的. react-router-native是用 ...
- Fiddler修改请求数据
截断方法一: 在菜单中选择“Rules”->“Automatic Breakpoint”->“Before Requests”,这种方式会截断所有Request请求. 2.浏览器打开站点, ...
- C# Stack 集合学习
Stack 集合学习 学习自:博客园相关文章 Stack<T>集合 这个集合的特点为:后进先出,简单来说就是新元素都放到第一位,而且顺序移除元素也是从第一位开始的. 方法一:Push(T ...
- python接口测试:如何将A接口的返回值传递给B接口
在编写接口测试脚本时,要考虑一个问题:参数值从哪里获取 一种方式是可以通过数据库来获取,但是通过这次接口测试,我发现读取数据库有一个缺点:速度慢 可能和我的sql写法有关,有些sql加的约束条件比较少 ...
- thinkphp5.1单模块设置
thinkphp5.1单模块 1. // 是否支持多模块'app_multi_module' => false, // 自动搜索控制器'controller_auto_search' => ...
- app——升级测试点
APP版本升级的测试点 该文章转载于:https://www.cnblogs.com/changpuyi/p/8618755.html 移动端版本更新升级是一个比较重要的功能点,主要分为强制更新和 ...