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) 线程和进程有什 ...
随机推荐
- java的异常抛出throws和throw的简单使用
前提: 当在程序测试时,如果你需要定义一个自己的异常,而非现在已经存在的异常,这个时候你需要用到throws和throw,try-catch只是一个简单的捕获异常的过程. 代码如下: package ...
- 用了这么久HTTP, 你是否了解Content-Length?
摘要: 理解HTTP协议... 原文:用了这么久HTTP, 你是否了解Content-Length和Transfer-Encoding ? 作者:朴瑞卿的博客 由Content-Length导致的问题 ...
- & 和 && 的区别,与(&)运算符、位移运算符(<< 、>>、>>>)的含义及使用(Java示例)
& 和 && 的区别,与(&)运算符.位移运算符(<< .>>.>>>)的含义及使用(Java示例) 1. & 和 & ...
- Under what conditions should the 'start_udev' command be run?
环境 Red Hat Enterprise Linux 问题 We run start_udev as part of the storage allocation procedure that we ...
- 201871010113-刘兴瑞《面向对象程序设计(java)》第十一周学习总结
项目 内容 这个作业属于哪个课程 <任课教师博客主页链接> https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 <作业链接地址>htt ...
- poj 3253 Fence Repair 贪心 最小堆 题解《挑战程序设计竞赛》
地址 http://poj.org/problem?id=3253 题解 本题是<挑战程序设计>一书的例题 根据树中描述 所有切割的代价 可以形成一颗二叉树 而最后的代价总和是与子节点和深 ...
- verilog 基础知识
mealy型状态机的下一状态和输出取决于当前状态和当前输入: moore型状态机的下一状态和输出取决于当前状态和当前输入,但其输出仅取决于现在的状态: 基本门原语的输出端口必须写在端口列表的前面,基本 ...
- os 和 sys 的模块使用方法和模块
os 的模块 方法 os.remove()删除文件 os.rename()重命名文件 os.walk()生成目录树下的所有文件名 os.chdir()改变目录 os.mkdir/maked ...
- iOS: 线程中那些常见的锁
一.介绍 在多线程开发中,锁的使用基本必不可少,主要是为了解决资源共享时出现争夺而导致数据不一致的问题,也就是线程安全问题.锁的种类很多,在实际开发中,需要根据情况选择性的选取使用,毕竟使用锁也是消耗 ...
- Python变量与内存管理
Python变量与内存管理 –与C语言中的变量做对比,更好的理解Python的变量. 变量 变量在C语言中 全局变量:其存放在内存的静态变量区中. 局部变量:代码块中存放在内存的代码区当中,当被调 ...