文档章节

Java并发编程之并发代码设计

12叔
 12叔
发布于 2017/09/02 16:07
字数 3487
阅读 1074
收藏 55
点赞 2
评论 5

引子

之前的文章我们探讨了引发线程安全的原因 主要是由于多线程的对共享内存的操作导致的可见性或有序性被破坏,从而导致内存一致性的错误

那么如何设计并发代码解决这个问题呐

我们一般使用这几种方式

  • 线程封闭
  • 不可变对象
  • 同步

发布和逸出

在此之前 我们先来了解一下发布和逸出的概念.

发布是指让对象在当前作用域之外使用,例如将对象的引用传递到其他类的方法,在一个方法中返回其引用等.

在许多情况下我们要保证内部对象不被发布,发布一些内部状态可能会破坏封装性,让使用者可以随意改变其状态,从而破坏线程安全.

而在某些情况下,我们又需要发布某些内部对象,如果需要线程安全的情况下,则需要正确的同步

当一个对象在不应该被发布的时候发布了,这种情况就叫逸出.

public class Escape {
  
   private List<User> users = Lists.newArrayList();

    public List<User> getUsers() {
        return users;
    }

    public void setUsers(List<User> users) {
        this.users = users;
    }
}

getUsers已经逸出了它的作用域,这个私有变量被发布了,因为任何调用者都可能修改数组.

同时发布users的时候也间接发布了User对象的引用.

public class OuterEscape {
    private String str = "Outer's string";


    public class Inner {
        public void write() {
            System.out.println(OuterEscape.this.str);
        }
    }


    public static void main(String[] args) {
        OuterEscape out = new OuterEscape();
        OuterEscape.Inner in = out.new Inner();
        in.write();

    }
}  

在内部类中保存了一个指向创建该内部类的外围类的引用,所以内部类中可以使用创建该内部类的外围类的私有属性、方法

public class ConstructorEscape {
    private Thread t;

    public ConstructorEscape() {

        System.out.println(this);

        t = new Thread() {
            public void run() {
                System.out.println(ConstructorEscape.this);
            }
        };
        t.start();
    }

    public static void main(String[] args) {
        ConstructorEscape a = new ConstructorEscape();
    }
}  

this引用被线程t共享,故线程t的发布将导致ConstructorEscape对象的发布,由于ConstructorEscape对象被发布时还未构造完成,这将导致ConstructorEscape对象逸出

总结一下如何安全发布的步骤

  • 找出构成对象状态的所有变量
  • 找出约束状态变量的不变性条件
  • 建立对象状态的并发访问策略

线程封闭

线程封闭的思想很简单,既然线程安全问题是由于多线程对共享变量的访问造成的,那么 如果我们可以避免操作共享变量,每个线程访问自己的变量,就不会有线程安全的问题,这是实现线程安全最简单的方法

通过 线程控制逃逸规则可以帮助你判断代码中对某些资源的访问是否是线程安全的

如果一个资源的创建,使用,销毁都在同一个线程内完成, 且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。

资源可以是对象,数组,文件,数据库连接,套接字等等。Java中你无需主动销毁对象,所以“销毁”指不再有引用指向对象。 即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了。比如2个线程都创建了各自的数据库连接,每个连接自身是线程安全的,但它们所连接到的同一个数据库也许不是线程安全的

  我们再来看线程封闭的几种实现方式:

栈封闭

栈封闭是线程封闭的一个特例,在栈封闭中只能通过局部变量来访问对象,

局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的

对象的局部引用和基础类型的局部变量不太一样。尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。如果在某个方法中创建的对象不会逸出该方法,那么它就是线程安全的。实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的


public void someMethod(){
  
  LocalObject localObject = new LocalObject();

  localObject.callMethod();
  method2(localObject);
}

public void method2(LocalObject localObject){
  localObject.setValue("value");
}

如上,LocalObject对象没有被方法返回,也没有被传递给someMethod()方法外的对象。每个执行someMethod()的线程都会创建自己的LocalObject对象,并赋值给localObject引用。因此,这里的LocalObject是线程安全的。事实上,整个someMethod()都是线程安全的。即使将LocalObject作为参数传给同一个类的其它方法或其它类的方法时,它仍然是线程安全的。当然,如果LocalObject通过某些方法被传给了别的线程,那它就不再是线程安全的了

程序控制线程封闭

通过程序实现来进行线程封闭,也就是说我们无法利用语言特性将对象封闭到特定的线程上,这一点导致这种方式显得不那么可靠 假设我们保证只有一个线程可以对某个共享的对象进行写入操作,那么这个对象的"读取-修改-写入"在任何情况下都不会出现竟态条件。 如果我们为这个对象加上volatile修饰则可以保证该对象的可见性,任何线程都可以读取该对象,但只有一个线程可以对其进行写入。 这样,仅仅通过volatile修饰就适当地保证了其安全性,相比直接使用synchoronized修饰,虽然更适合,但实现起来稍微复杂。

程序控制线程封闭,这个不是一种具体的技术,而是一种设计思路,从设计上把处理一个对象状态的代码都放到一个线程中去,从而避免线程安全的问题

ThreadLocal

ThreadLocal机制本质上是程序控制线程封闭,只不过是Java本身帮忙处理了 。来看Java的Thread类和ThreadLocal类

  1. Thread线程类维护了一个ThreadLocalMap的实例变量

  2. ThreadLocalMap就是一个Map结构

  3. ThreadLocal的set方法取到当前线程,拿到当前线程的threadLocalMap对象,然后把ThreadLocal对象作为key,把要放入的值作为value,放到Map

  4. ThreadLocal的get方法取到当前线程,拿到当前线程的threadLocalMap对象,然后把ThreadLocal对象作为key,拿到对应的value.

public class Thread implements Runnable {
	 ThreadLocal.ThreadLocalMap threadLocals = null;
}

public class ThreadLocal<T> {
	public T get() {
		Thread t = Thread.currentThread();
		ThreadLocalMap map = getMap(t);
		if (map != null) {
			ThreadLocalMap.Entry e = map.getEntry(this);
			if (e != null)
				return (T)e.value;
		}
		return setInitialValue();
	}

	ThreadLocalMap getMap(Thread t) {
		return t.threadLocals;
	}

	public void set(T value) {
		Thread t = Thread.currentThread();
		ThreadLocalMap map = getMap(t);
		if (map != null)
			map.set(this, value);
		else
			createMap(t, value);
	}
}

ThreadLocal的设计很简单,就是给线程对象设置了一个内部的Map,可以放置一些数据。JVM从底层保证了Thread对象之间不会看到对方的数据。

使用ThreadLocal前提是给每个ThreadLocal保存一个单独的对象,这个对象不能是在多个ThreadLocal共享的,否则这个对象也是线程不安全的

ThreadLocal 内存泄漏

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

所以每次使用完ThreadLocal,都调用它的remove()方法,清除数据就可以避免这个问题

不可变对象

一个对象如果在创建后不能被修改,那么就称为不可变对象。在并发编程中,一种被普遍认可的原则就是:尽可能的使用不可变对象来创建简单、可靠的代码

在并发编程中,不可变对象特别有用。由于创建后不能被修改,所以不会出现操作共享变量导致的内存一致性错误

但是程序员们通常并不热衷于使用不可变对象,因为他们担心每次创建新对象的开销。实际上这种开销常常被过分高估,而且使用不可变对象所带来的一些效率提升也抵消了这种开销

我们先来看一个使用同步来解决线程安全的例子

public class SynchronizedRGB {

    // Values must be between 0 and 255.
    private int red;
    private int green;
    private int blue;
    private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public SynchronizedRGB(int red,
                           int green,
                           int blue,
                           String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public void set(int red,
                    int green,
                    int blue,
                    String name) {
        check(red, green, blue);
        synchronized (this) {
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }
    }

    public synchronized int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public synchronized String getName() {
        return name;
    }

 
}
SynchronizedRGB color =
    new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB();      // 1
String myColorName = color.getName(); // 2

//如果其他线程在1执行后调用set方法 就会导致 getName 跟getRGB的值不匹配

synchronized (color) {
    int myColorInt = color.getRGB();
    String myColorName = color.getName();
}

//必需使这2个语句同步执行


创建不可变对象的几条原则

  • 不提供修改可变对象的方法。(包括修改字段的方法和修改字段引用对象的方法)
  • 将类的所有字段定义为final、private的。
  • 不允许子类重写方法。简单的办法是将类声明为final,更好的方法是将构造函数声明为私有的,通过工厂方法创建对象。
  • 如果类的字段是对可变对象的引用,不允许修改被引用对象。
  • 不共享可变对象的引用。当一个引用被当做参数传递给构造函数,而这个引用指向的是一个外部的可变对象时,一定不要保存这个引用。如果必须要保存,那么创建可变对象的拷贝,然后保存拷贝对象的引用。同样如果需要返回内部的可变对象时,不要返回可变对象本身,而是返回其拷贝

修改后的例子

final public class ImmutableRGB {

    // Values must be between 0 and 255.
    final private int red;
    final private int green;
    final private int blue;
    final private String name;

    private void check(int red,
                       int green,
                       int blue) {
        if (red < 0 || red > 255
            || green < 0 || green > 255
            || blue < 0 || blue > 255) {
            throw new IllegalArgumentException();
        }
    }

    public ImmutableRGB(int red,
                        int green,
                        int blue,
                        String name) {
        check(red, green, blue);
        this.red = red;
        this.green = green;
        this.blue = blue;
        this.name = name;
    }

    public int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }

    public String getName() {
        return name;
    }

 
}

事实不可变对象

如果对象本事是可变的,但是程序运行过程中,不存在改变的可能,那么就称为事实不可变对象, 这样也不需要额外的线程安全的保护

同步

当我们不得不使用共享变量,而且需要经常修改的时候我们就需要使用同步来实现线程安全了

Java我们可以使用 Synchronized/Lock volatite CAS 来实现同步

synchronized是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁

与锁相比,volatile变量是一和更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换和线程调度等操作,但是volatile变量也存在一些局限:不能用于构建原子的复合操作,因此当一个变量依赖旧值时就不能使用volatile变量

CAS是一种乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

同步解决了三个相互关联的问题

  • 原子性:哪些指令必须是不可分割的
  • 可见性:一个线程执行的结果对另一个线程是可见的
  • 有序性:某个线程的操作结果对其它线程来看是无序的

总结

理解线程安全的概念很重要, 所谓线程安全问题,就是处理对象状态的问题 。如果要处理的对象是无状态的(不变性),或者可以避免多个线程共享的(线程封闭),那么我们可以放心,这个对象可能是线程安全的。当无法避免,必须要共享这个对象状态给多线程访问时,这时候才用到线程同步的一系列技术。

这个理解放大到架构层面,我们来设计业务层代码时,业务层最好做到无状态,这样就业务层就具备了可伸缩性,可以通过横向扩展平滑应对高并发。

所以我们处理线程安全可以有几个层次:

  1. 能否做成无状态的不变对象。无状态是最安全的。

  2. 能否线程封闭

  3. 采用何种同步技术 (Synchronized/Lock volatite CAS)

© 著作权归作者所有

共有 人打赏支持
12叔

12叔

粉丝 146
博文 26
码字总数 54281
作品 3
杭州
程序员
加载中

评论(5)

y
yhroot

引用来自“yhroot”的评论

楼主,最近程序中需要用到多线程,大致业务是这样:需要开通几个进程来执行不同的任务,这几个任务同时调用一个工具类是否会出现线程不安全(工具类没加锁),请解惑,多谢

引用来自“12叔”的评论

关键在于这个工具类 是否存在共享变量, 如果不存在就是简单调用方法,那其实 符合栈封闭的原则 是安全的
多谢,我好像明白了一些:+1:
12叔
12叔

引用来自“18549816712”的评论

楼主可以分析一下threadlocal的内存泄漏问题,或者提醒一下,否则,同学们用起threadlocal来会发生不可预料的事情
很好的建议 已经做了补充
12叔
12叔

引用来自“yhroot”的评论

楼主,最近程序中需要用到多线程,大致业务是这样:需要开通几个进程来执行不同的任务,这几个任务同时调用一个工具类是否会出现线程不安全(工具类没加锁),请解惑,多谢
关键在于这个工具类 是否存在共享变量, 如果不存在就是简单调用方法,那其实 符合栈封闭的原则 是安全的
y
yhroot
楼主,最近程序中需要用到多线程,大致业务是这样:需要开通几个进程来执行不同的任务,这几个任务同时调用一个工具类是否会出现线程不安全(工具类没加锁),请解惑,多谢
1
18549816712
楼主可以分析一下threadlocal的内存泄漏问题,或者提醒一下,否则,同学们用起threadlocal来会发生不可预料的事情
读书笔记之《Java并发编程的艺术》-线程池和Executor的子孙们

读书笔记部分内容来源书出版书,版权归本书作者,如有错误,请指正。 欢迎star、fork,读书笔记系列会同步更新 git https://github.com/xuminwlt/j360-jdk module j360-jdk-thread/me.j360....

Hi徐敏
2015/11/11
0
1
读书笔记之《Java并发编程的艺术》-并发编程容器和框架(重要)

读书笔记部分内容来源书出版书,版权归本书作者,如有错误,请指正。 欢迎star、fork,读书笔记系列会同步更新 git https://github.com/xuminwlt/j360-jdk module j360-jdk-thread/me.j360....

Hi徐敏
2015/11/11
0
1
Scala入门-大数据云计算下的开发语言

Scala编程语言抓住了很多开发者的眼球。如果你粗略浏览Scala的网站,你会觉得Scala是一种纯粹的 面向对象编程语言,而又无缝地结合了命令式编程和 函数式编程风格。Christopher Diggins认为:...

liwei2000
06/30
0
0
读书笔记之《Java并发编程的艺术》-并发编程基础

读书笔记部分内容来源书出版书,版权归本书作者,如有错误,请指正。 欢迎star、fork,读书笔记系列会同步更新 git https://github.com/xuminwlt/j360-jdk module j360-jdk-thread/me.j360....

Hi徐敏
2015/11/11
0
8
Java 编程之美:并发编程高级篇之一

本文来自作者 追梦 在 GitChat 上分享 「Java 编程之美:并发编程高级篇之一」 编辑 | 工藤 前言 借用 Java 并发编程实践中的话:编写正确的程序并不容易,而编写正常的并发程序就更难了。 ...

gitchat
05/24
0
0
阿里,百度,腾讯等一线互联网公司中,Java开发的招聘标准

金三银四的跳槽热潮即将过去,在这两个月的跳槽的旺季中,作为互联网行业的三大巨头,百度、阿里巴巴、腾讯对于互联网人才有很大的吸引力,他们的员工也是众多互联网同行觊觎的资深工程师、管...

javaxuexi123
04/20
0
0
【转】15个顶级Java多线程面试题及回答

Java 线程面试问题   在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分。如果你想获得任何股票投资银行的前台资讯职位,那么你应该准备很多关于多线程的问题。在投资银行业务...

一只死笨死笨的猪
2014/09/30
0
0
15个顶级Java多线程面试题及回答

Java 线程面试问题 在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分。如果你想获得任何股票投资银行的前台资讯职位,那么你应该准备很多关于多线程的问题。在投资银行业务中多...

LCZ777
2014/05/27
0
0
书单丨5本Java后端技术书指引你快速进阶

一名Java开发工程师 不仅要对Java语言及特性有深层次的理解 而且需要掌握与Java相关的 框架、生态及后端开发知识 本文涉及多种后端开发需要掌握的技能 对于帮助提高开发能力非常有帮助 NO.1...

Java高级架构
05/30
0
0
Java语言标准(第10版)第一章(节选)翻译与评注

英文原文链接:https://docs.oracle.com/javase/specs/jls/se10/html/jls-1.html 评注是括在鱼尾号之间的文字,其余均为翻译 Java编程语言是一种通用目的的【有别于VBA、Matlab这些专用型语言...

Jelif
06/02
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

pbgo: 基于Protobuf的迷你RPC/REST框架

https://www.oschina.net/p/pbgo

chai2010
34分钟前
0
0
rsync工具介绍、常用选项以及通过ssh同步

linux下的文件同步工具 rsync rsync是非常实用的一个同步工具,可以从a机器到b机器传输一个文件,也可以备份数据,系统默认没有这个工具,要使用命令 yum install -y rsync 安装。 rsync的命...

黄昏残影
49分钟前
0
0
OSChina 周四乱弹 —— 表妹要嫁人 舅妈叮嘱……

Osc乱弹歌单(2018)请戳(这里) 【今日歌曲】 @哈哈哈哈哈嗝:一定要听——The Pancakes的单曲《咁咁咁》 《咁咁咁》- The Pancakes 手机党少年们想听歌,请使劲儿戳(这里) @clouddyy :...

小小编辑
今天
245
4
流利阅读笔记30-20180719待学习

重磅:让人类得老年痴呆的竟是它? Lala 2018-07-19 1.今日导读 去年奥斯卡最佳动画长片《寻梦环游记》里有一句经典台词:“比死亡更可怕的,是遗忘”。在电影中,年迈的曾祖母会重复说一样的...

aibinxiao
今天
3
0
1.16 Linux机器相互登录

Linux机器之间以密码方式互相登录 运行命令#ssh [ip address],标准命令:#ssh [username]@ip, 如果没有写用户名,则默认为系统当前登录的用户 命令#w查看系统负载,可查看到连接到该主机的...

小丑鱼00
今天
0
0
about git flow

  昨天元芳做了git分支管理规范的分享,为了拓展大家关于git分支的认知,这里我特意再分享这两个关于git flow的链接,大家可以看一下。 Git 工作流程 Git分支管理策略   git flow本质上是...

qwfys
今天
2
0
Linux系统日志文件

/var/log/messages linux系统总日志 /etc/logrotate.conf 日志切割配置文件 参考https://my.oschina.net/u/2000675/blog/908189 dmesg命令 dmesg’命令显示linux内核的环形缓冲区信息,我们可...

chencheng-linux
今天
1
0
MacOS下给树莓派安装Raspbian系统

下载镜像 前往 树莓派官网 下载镜像。 点击 最新版Raspbian 下载最新版镜像。 下载后请,通过 访达 双击解压,或通过 unzip 命令解压。 检查下载的文件 ls -lh -rw-r--r-- 1 dingdayu s...

dingdayu
今天
1
0
spring boot使用通用mapper(tk.mapper) ,id自增和回显等问题

最近项目使用到tk.mapper设置id自增,数据库是mysql。在使用通用mapper主键生成过程中有一些问题,在总结一下。 1、UUID生成方式-字符串主键 在主键上增加注解 @Id @GeneratedValue...

北岩
今天
2
0
告警系统邮件引擎、运行告警系统

告警系统邮件引擎 cd mail vim mail.py #!/usr/bin/env python#-*- coding: UTF-8 -*-import os,sysreload(sys)sys.setdefaultencoding('utf8')import getoptimport smtplibfr......

Zhouliang6
今天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部