本篇来谈谈 Java 并发编程:如何保证对象的线程安全性。

01、前言

先让我吐一句肺腑之言吧,不说出来会憋出内伤的。《Java 并发编程实战》这本书太特么枯燥了,尽管它被奉为并发编程当中的经典之作,但我还是忍不住。因为第四章“对象的组合”我整整啃了两周的时间,才啃出来点肉丝。

读者朋友们见谅啊。要怪只能怪我自己的学习能力有限,真读不了这种生硬无趣的技术书。但是为了学习,为了进步,为了将来(口号喊得有点大了),只能硬着头皮上。

请随我来,我尽量写得有趣点。

02、线程安全类

作者说了啊,设计一个线程安全类需要三个步骤:

1)找出表示对象状态的所有变量

2)对变量进行有效性约束

3)增加类的并发访问策略

我在作者说的基础上做了微调,读起来更加容易理解。怎么和代码对应起来了,先来看一个普通的计数器类 Counter。

public class Counter {
private int value = 0; public int getValue() {
return value;
} public int increment() {
return ++value;
}
}

1)Counter 的状态变量只有一个,就是 value。

2)value 的有效性是什么呢,它最大不能超过 Integer.MAX_VALUE,最小只能为 0(计数嘛,总不能记成负数)。换句话说就是,value 的有效范围是 0 ~ Integer.MAX_VALUE

public int increment() {
if (value == Integer.MAX_VALUE) {
throw new IllegalStateException("counter overflow");
}
return ++value;
}

3)增加类的并发访问策略,直接上 synchronized。

public class Counter {
private int value = 0; public synchronized int getValue() {
return value;
} public synchronized int increment() {
if (value == Integer.MAX_VALUE) {
throw new IllegalStateException("counter overflow");
}
return ++value;
}
}

03、非线程安全的对象

之前我们谈了如何设计一个线程安全的类。如果类是安全的,那么它作为对象使用的时候就是线程安全的。但如果一个类不是线程安全的,它作为对象使用的时候怎么保证是线程安全的呢?

作者提到了一个名词叫做“封闭机制”:

1)把对象作为类的私有成员变量;

2)把对象作为方法内部的局部变量;

3)线程 A 把对象传递到 B 线程,而不是与线程 B 共享这个对象;

大家来看下面这段代码。

class StringList {
private List<String> myList = new ArrayList<>(); public synchronized void addString(String s) {
myList.add(s);
} public synchronized void removeString(String s) {
myList.remove(s);
}
}

本身 ArrayList 不是线程安全的,但 myList 是私有的,访问它的两个方法 addString()removeString() 都加了关键字 synchronized,因此 myList 在使用的时候就变成了线程安全的对象,StringList 类就变成了一个线程安全的类——这种方式被称作 Java 监视器模式:可变的状态被封装在一个类中,访问它们只能通过加上锁的方法。

查看 Vector 的源码,你会发现,它之所以是线程安全的,就是采用的这种监视器模式

04、在已有的线程安全类上追加功能

假如现在有一个线程安全的类,比如之前提到的 StringList,它包含了大多数我们需要的功能,但还不够,那么怎么确保我们追加的功能不破坏原有的线程安全性呢?

最直接的方法当然是修改源码,假如源码掌握在我们自己手里的话。

class StringList {
private List<String> myList = new ArrayList<>(); public synchronized void addString(String s) {
myList.add(s);
} public synchronized void addIfNotExist(String s) {
boolean isExist = myList.contains(s);
if (!isExist) {
myList.add(s);
}
}
}

我们新增了一个 addIfNotExist() 方法:如果字符串 s 还没有添加到 List 当中,就添加一个。

新增的方法没有破坏 StringList 的线程安全性,因为当两个线程同时执行 addIfNotExist() 方法时,需要经过 synchronized 把守的这道大门。

但很多时候,我们无法直接修改源码,这时候就只好在原来的基础上进行改造。大家听过之前的“红芯”浏览器吗?在谷歌浏览器的内核上裹了一层层皇帝的新衣。

class StringList {
protected List<String> myList = new ArrayList<>(); public synchronized void addString(String s) {
myList.add(s);
}
} public class NewStringList extends StringList {
public synchronized void addIfNotExist(String s) {
boolean isExist = myList.contains(s);
if (!isExist) {
myList.add(s);
}
}
}

新建一个类 NewStringList,继承自 StringList,然后在 NewStringList 中新增一个方法 addIfNotExist()。当然了,这样做的前提是父类中的 myList 是 protected 而不是 private 的。因此,这种做法不具有普适性。

05、最后

站在我的角度来看,《Java 并发编程实战》的第四章“对象的组合”写得烂透了。导致我在写这篇文章的时候感觉到万分的痛苦。希望下一章不要写的这么烂。

上一篇:如何保证共享变量的可见性?

上上篇:如何保证共享变量的原子性?

Java 并发编程(四):如何保证对象的线程安全性的更多相关文章

  1. Java并发编程学习笔记(一)——线程安全性

    主要概念:线程安全性.原子性.原子变量.原子操作.竟态条件.复合操作.加锁机制.重入.活跃性与性能. 1.当多个线程访问某个状态变量并且其中有一个线程执行写入操作时,必须采用同步机制来协同这些线程对变 ...

  2. 【Java并发编程四】关卡

    一.什么是关卡? 关卡类似于闭锁,它们都能阻塞一组线程,直到某些事件发生. 关卡和闭锁关键的不同在于,所有线程必须同时到达关卡点,才能继续处理.闭锁等待的是事件,关卡等待的是其他线程. 二.Cycli ...

  3. java并发编程JUC第九篇:CountDownLatch线程同步

    在之前的文章中已经为大家介绍了java并发编程的工具:BlockingQueue接口.ArrayBlockingQueue.DelayQueue.LinkedBlockingQueue.Priorit ...

  4. Java并发编程 (四) 线程安全性

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.线程安全性-原子性-atomic-1 1.线程安全性 定义: 当某个线程访问某个类时,不管运行时环境 ...

  5. Java 并发编程(二)对象的不变性和安全的公布对象

    一.不变性 满足同步需求的还有一种方法是使用不可变对象(Immutable Object). 到眼下为止,我们介绍了很多与原子性和可见性相关的问题,比如得到失效数据.丢失更新操作或光查到某个对象处于不 ...

  6. Java 并发编程(二)对象的公布逸出和线程封闭

    对象的公布与逸出 "公布(Publish)"一个对象是指使对象可以在当前作用域之外的代码中使用.可以通过 公有静态变量.非私有方法.构造方法内隐含引用 三种方式. 假设对象构造完毕 ...

  7. Java并发编程原理与实战七:线程带来的风险

    在并发中有两种方式,一是多进程,二是多线程,但是线程相比进程花销更小且能共享资源.但使用多线程同时会带来相应的风险,本文将展开讨论. 一.引言 多线程将会带来几个问题: 1.安全性问题 线程安全性可能 ...

  8. Java 并发编程中的 Executor 框架与线程池

    Java 5 开始引入 Conccurent 软件包,提供完备的并发能力,对线程池有了更好的支持.其中,Executor 框架是最值得称道的. Executor框架是指java 5中引入的一系列并发库 ...

  9. 那些年读过的书《Java并发编程实战》一、构建线程安全类和并发应用程序的基础

    1.线程安全的本质和线程安全的定义 (1)线程安全的本质 并发环境中,当多个线程同时操作对象状态时,如果没有统一的状态访问同步或者协同机制,不同的线程调度方式和不同的线程执行次序就会产生不同的不正确的 ...

随机推荐

  1. pandas.DataFrame的groupby()方法的基本使用

    pandas.DataFrame的groupby()方法是一个特别常用和有用的方法.让我们快速掌握groupby()方法的基础使用,从此数据分析又多一法宝. 首先导入package: import p ...

  2. CSS3 transform属性

    说明: transform 属性向元素应用 2D 或 3D 转换.该属性允许我们对元素进行移动(translate).旋转(rotate).缩放(scale)或倾斜(skew) transition属 ...

  3. ELK 学习笔记之 Kibana入门使用

    Kibana入门使用: 第一次导入索引: 修改展示时间,不然查不到数据: 点Discover,查阅数据: 如果要添加新的index: 点击Visualize, 创建chart: 点击Dashboard ...

  4. 确认自己所用的python版本

    总结: 目前有两个版本的python处于活跃状态:python2,python3 有多种流行的python运行环境:cpython(应用最广泛的python解释器,如无对解释器有要求,一般用这个,默认 ...

  5. 最近学到的Git知识,大厂的Git机制还是很方便的

    本文首发于微信公众号:程序员乔戈里 转载请注明:https://blog.csdn.net/WantFlyDaCheng/article/details/102538508 一.两次的 git com ...

  6. Feign【文件上传】

    话不多说,上代码.... 项目公共依赖配置: <parent> <groupId>org.springframework.boot</groupId> <ar ...

  7. Node.js入门教程 第六篇 (连接使用MySql)

    连接使用MySql 安装MySql模块: npm install mysql 创建连接: const mysql = require('mysql') // 连接 mysql 服务器 const co ...

  8. idea破解版安装、配置jdk以及建立一个简单的maven工程

    idea破解版安装.配置jdk,配置jdk环境变量以及建立一个简单的maven工程 一.idea破解版以及配置文件下载 下载网址:https://pan.baidu.com/s/1yojA51X1RU ...

  9. CentOS 7.6 Telnet服务搭建(Openssh升级之战 第一任务备用运输线搭建)

    (以下内容亲手完成,如果需要搬走记得把写博的小白的名字和邮箱一起搬走) 出来玩(学习),总是要还的! 有不明的问题的时候,都来博客园转转,总能找到答案或者灵感,开博3个月都没发一篇帖(不晓得管理员有何 ...

  10. php代码Xdebug调试使用笔记

    0x01 Xdebug简介 Xdebug是一个开放源代码的PHP程序调试器 运行流程: 0x02  Xdebug配置 日志 xdebug.trace_output_dir: 日志追踪输出目录 xdeb ...