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) 线程和进程有什 ...
随机推荐
- 通过JTAG对比内核启动后text/rodata段内容
关键词:vmlinux.strip.dump._text.__end_rodata等等. 在日常的调试中,可能会在某些情况下踩到内核重要的数据,比如代码段或者rodata之类. 这种情况下,需要确认这 ...
- Day3 - Python基础3 函数基本、递归函数、内置函数
本节内容 1. 函数基本语法及特性 2. 参数与局部变量 2.2. 函数变量作用域 3. 返回值 嵌套函数 4.递归 5.匿名函数 6.函数式编程介绍 7.高阶函数 8.内置函数 1. 函数基本语法及 ...
- BZOJ1369/LG4395 「BOI2003」Gem 树形DP
问题描述 LG4395 BZOJ1369 题解 发现对于结点 \(x\) ,其父亲,自己,和所有的孩子权值不同,共 \(3\) 类,从贪心的角度考虑,肯定是填 \(1,2,3\) 这三种. 于是套路树 ...
- WPF 字体设置
原文:WPF 字体设置 WPF 主界面 更换字体 可全局 但是有的时候有的窗体 字体还是没变 可以做全局样式 <Window x:Class="CLeopardTestWpf.Main ...
- Codeforces Round #594 (Div. 1) A. Ivan the Fool and the Probability Theory 动态规划
A. Ivan the Fool and the Probability Theory Recently Ivan the Fool decided to become smarter and stu ...
- Java 异常面试问题与解答
Java 提供了一种健壮且面向对象的方法来处理称为 Java异常处理的异常情况. 1. Java中的异常是什么? 异常是在程序执行期间可能发生的错误事件,它会破坏其正常流程.异常可能源于各种情况,例如 ...
- 06-Django视图
什么是视图? 视图就是应用中views.py文件中的函数,视图函数的第一个参数必须是request(HttpRequest)对象.返回的时候必须返回一个HttpResponse对象或子对象(包含Htt ...
- pytest框架之mark标签
对测试用例打标签,在运行测试用例的时候,可根据标签名来过滤要运行的用例. 一.注册标签名 1.创建pytest.ini文件,在文件中按如下方式添加标签名: [pytest] markers = smo ...
- MongoDB for OPS 04:备份恢复
写在前面的话 和 MySQL 一样,mongodb 也是需要将数据进行备份的,毕竟天有不测风云,谁也不知道哪天机器就炸了. 备份恢复 mongodb 提供了两种备份恢复手段:mongoexport / ...
- docker安装完报错:Failed to start docker.service: Unit docker.service is masked
执行 systemctl start docker 报错 Failed to start docker.service: Unit docker.service is masked. 解决 syste ...