- 对同一个变量 ( 反复 ) 读-改-写 。
- 修改受某个不变性条件约束的多个变量 。
class ThreadSafeArrayList {private final List<Integer> list;public ThreadSafeArrayList(List<Integer> l){list =l;}// 添加新的方法public synchronized boolean putIfAbsent(Integer a){if(list.contains(a)) {list.add(a);return true;}return false;}// 代理 add 方法,其它略public synchronized boolean add(Integer a) {return list.add(a);}// ...}复制代码
事实上,Java 类库已经有了对应的线程安全类 。通常,我们应当优先重用这些已有的类 。在下方的代码块中,我们使用Collection.synchronizedList 工厂方法创建一个线程安全的 list 对象,这样似乎就只需要为新拓展的 putIfAbsent() 方法加锁了 。
class ThreadUnSafeArrayList {private final List<Integer> list = Collections.synchronizedList(new ArrayList<>());// 添加新的方法public synchronized boolean putIfAbsent(Integer a){if(list.contains(a)) {list.add(a);return true;}return false;}public boolean add(Integer a){return list.add(a);}//...}复制代码
但是,上述的代码是错误的 。为什么?问题在于,我们使用了错误的锁进行了同步 。当调用的是 add 方法时,使用的是列表对象的内置锁;但调用 putIfAbsent 方法时,我们使用的却是 ThreadUnsafeArrayList 对象的内置锁 。这意味着 putIfAbsent 方法对于其它的方法来说不是原子的,因此无法确保一个线程执行 putIfAbsent 方法时,其它线程是否会通过调用其它方法修改列表 。因此,想要让这个方法正确执行,我们必须要在正确的地方上锁 。
class ThreadUnSafeArrayList {private final List<Integer> list = Collections.synchronizedList(new ArrayList<>());public boolean putIfAbsent(Integer a){synchronized (list){if(list.contains(a)) {list.add(a);return true;}return false;}}}复制代码
同步容器同步容器是安全的,但在某些情况下仍然需要客户端加锁 。常见的操作如:- 迭代;
- 跳转 ( 比如,寻找下一个元素 );
- 条件运算,如 "若没有则 XX 操作" ( 一种常见的复合操作 );
class DemoOfConcurrentFail {public final List<Integer> list = Collections.synchronizedList(new ArrayList<>());{Collections.addAll(list, 1, 2, 3, 4, 5);}public static void main(String[] args) {var testList = new DemoOfConcurrentFail().list;Runnable t1 = () -> {var last = testList.size() - 1;// x1testList.remove(last);// x2};Runnable t2 = () -> {var last = testList.size() -1;// y1varr = testList.get(last);// y2System.out.println(r);};new Thread(t1).start();new Thread(t2).start();}}复制代码
究其原因,两个线程 T1,T2 执行的复合操作没有受锁保护 ( 实际上就是前文银行转账的例子中犯过的错误 ) 。所以正确的做法是对复合操作整体加锁 。比如:var mutex = new Object();Runnable t1 = () -> {synchronized (mutex){var last = testList.size() - 1;// x1testList.remove(last);// x2}};Runnable t2 = () -> {synchronized (mutex){var last = testList.size() -1;// y1varr = testList.get(last);// y2System.out.println(r);}};// ...复制代码
同步容器的迭代问题在迭代操作中,类似的问题也仍然存在 。无论是直接的 for 循环还是 for-each 循环,对容器的遍历方式是使用 Iterator 。而使用迭代器本身也是先判断 ( hasNext ) 再读取 ( next ) 的复合过程 。Java 对同步容器的迭代处理是:假设某一个线程在迭代的过程中发现容器被修改过了,则立刻失败 ( 也称及时失败 ),并抛出一个ConcurrentModificationException 异常 。
// 可能需要运行多次才能抛出 ConcurrentModificationExceptionRunnable t1 = () -> {// 删除中间的元素int mid =testList.size() / 2;testList.remove(mid);};Runnable t2 = () -> {for(var item : testList){System.out.println(item);}};new Thread(t1).start();new Thread(t2).start();复制代码
推荐阅读
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 使用 JavaScript 实现无限滚动
- 如何自己开发漏洞扫描工具
- Firefox浏览器|火狐浏览器新增实用工具:将支持图片文字识别
- Java|Java:2022年招聘Java开发人员指南
- Java|HR傲慢对待求职者,还“诅咒”对方找不到工作,大学生也太难了
- 什么资源都能搜?这款搜索引擎工具,一键检索各大网盘资源
- 在Java 8及更高版本中使用Java流
- Java实现第三方短信接口发送短信验证码
- 3个超强资源搜索工具 资源搜索工具
- 连裤袜|“颜值经济”盛行,美妆工具受追捧,我国美妆工具市场需求增势明显