我们对并发一词并不陌生,它通常指多个任务同时执行。实际上这不完全对,“并行”才是真正意义上的同时执行,而“并发”则更偏重于多个任务交替执行。有时候我们会看见一些人一边嘴里嚼着东西一边讲话,这是并行;当然,更文明礼貌的方式是讲话前先把嘴里的东西咽下去,这是并发。并发早期被用来提高单处理器的性能,比如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)——线程安全的更多相关文章

  1. Java 并发 中断线程

    Java 并发 中断线程 @author ixenos 对Runnable.run()方法的三种处置情况 1.在Runnable.run()方法的中间中断它 2.等待该方法到达对cancel标志的测试 ...

  2. Java 并发编程 | 线程池详解

    原文: https://chenmingyu.top/concurrent-threadpool/ 线程池 线程池用来处理异步任务或者并发执行的任务 优点: 重复利用已创建的线程,减少创建和销毁线程造 ...

  3. java并发编程 线程基础

    java并发编程 线程基础 1. java中的多线程 java是天生多线程的,可以通过启动一个main方法,查看main方法启动的同时有多少线程同时启动 public class OnlyMain { ...

  4. Java并发1——线程创建、启动、生命周期与线程控制

    内容提要: 线程与进程 为什么要使用多线程/进程?线程与进程的区别?线程对比进程的优势?Java中有多进程吗? 线程的创建与启动 线程的创建有哪几种方式?它们之间有什么区别? 线程的生命周期与线程控制 ...

  5. java并发:线程同步机制之Volatile关键字&原子操作Atomic

    volatile关键字 volatile是一个特殊的修饰符,只有成员变量才能使用它,与Synchronized及ReentrantLock等提供的互斥相比,Synchronized保证了Synchro ...

  6. java并发:线程池、饱和策略、定制、扩展

    一.序言 当我们需要使用线程的时候,我们可以新建一个线程,然后显式调用线程的start()方法,这样实现起来非常简便,但在某些场景下存在缺陷:如果需要同时执行多个任务(即并发的线程数量很多),频繁地创 ...

  7. java并发:线程同步机制之Lock

    一.初识Lock Lock是一个接口,提供了无条件的.可轮询的.定时的.可中断的锁获取操作,所有加锁和解锁的方法都是显式的,其包路径是:java.util.concurrent.locks.Lock, ...

  8. Java并发编程:线程间通信wait、notify

    Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...

  9. Java并发编程:线程和进程的创建(转)

    Java并发编程:如何创建线程? 在前面一篇文章中已经讲述了在进程和线程的由来,今天就来讲一下在Java中如何创建线程,让线程去执行一个子任务.下面先讲述一下Java中的应用程序和进程相关的概念知识, ...

  10. Java并发3-多线程面试题

    1) 什么是线程? 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速. 2) 线程和进程有什 ...

随机推荐

  1. Linux:LAMP环境的搭建

    LAMP环境的搭建 安装DNS服务器 安装DNS服务 yum install bind -y DNS的配置 创建正向解析 以创建一个名为"lsy.com"的正向查找区域为例: 第一 ...

  2. AcWing 33. 链表中倒数第k个节点

    习题地址 https://www.acwing.com/solution/acwing/content/2997/ 题目描述输入一个链表,输出该链表中倒数第k个结点. 注意: k >= 0;如果 ...

  3. angular路由事件

    Angular 4检测路由变化,可以使用router.events来监听: 支持的事件类型: NavigationStart:导航开始 NavigationEnd:导航结束 NavigationCan ...

  4. 用nodejs快速实现websocket服务端(带SSL证书生成)

    有不少公司将nodejs的socket.io作为websocket的解决方案,很遗憾的是socket.io是对websocket的封装,并不支持html5原始的websocket协议,微信小程序使用的 ...

  5. asp.net的原理

    Asp.net的作业流是指什么?很多人都不是很了解,也不知道是用来干什么的有怎样运用,其实能够从简略的基础了解. 作业流(Workflow),便是“事务进程的部分或全体在核算机使用环境下的主动化”(i ...

  6. cartographer 3D scan matching 理解

    cartographer 3D scan matching没有论文和其它资料,因此尝试通过源码理解其处理方法,理解不当之处还请指正. 目录: 0.2D 匹配方法简介 1.real time corre ...

  7. ssm-restful风格

    码云 https://gitee.com/MarkPolaris/ssm-test02

  8. 阿里Nacos-配置-多环境

    多环境的配置隔离是配置中心最基础的一个功能之一.不同的环境配置的值不一样,比如数据库的信息,业务的配置等. Spring Boot 多环境配置 首先我们来回顾下在Spring Boot中用配置文件的方 ...

  9. flask--数据库迁移之连环踩坑记

    flask数据库迁移命令: python manage.py db init python manage.py db migrate python manage.py db upgrade 1.报错: ...

  10. 【转】linux下使用sqlplus执行包含语句块的sql文件,运行时会不断显示行号,而在plsqldev中能执行

    一.数据库:Oracle数据库 二.sql文件内容: --创建函数 CREATE OR REPLACE function fun_createuid1 return varchar2 is Resul ...