此仓库包含了图解Java多线程设计模式 的代码和笔记
- 继承Thread类 PrintThread
- 实现runnable接口 PrintRunnable
- ThreadFactory,将runnable传入newThread方法中 PrintThreadFactory Thread类本身就是实现了Runnable接口,其run方法为空
Thread.sleep(1000);
- synchronized方法
- synchronized代码块
- vs synchronized实例方法:两者等效,也就是说
synchronized实例方法
是通过使用this
锁来实现互斥的synchronized void method1() { } void method1() { synchronized(this) { } }
- vs synchronized静态方法:静态方法每次只能由一个线程运行,但是静态方法和实例方法使用的锁是不一样的,
synchronized静态方法
是通过使用类对象
锁,即SomeClass
类对应的java.lang.class
的实例,来实现互斥的class SomeClass { synchronized static void method1() { } static void method1() { synchronized(SomeClass.class) { } } }
- vs synchronized实例方法:两者等效,也就是说
该三个方法都是Object类的方法,与其说是对线程的操作不如说是针对实例的等待队列的操作
Thread.Stat(Enum),可以通过getState()方法获取
- NEW
- RUNNABLE
- TERMINATED
- WAITING
- TIMED_WAITING
- BLOCKED
- 安全性 safety----不损坏对象
- 生存性 liveness----必要的处理能够被执行(不死锁)
- 可重复性 reusability----类可重复利用
- 性能 performance----能快速、大批量的执行
Demo 1的输出结果
*************** Broken *************** Gate{counter=-1682490252, name='Chris', address='Alaska'}
*************** Broken *************** Gate{counter=-1682488874, name='Chris', address='Canada'}
*************** Broken *************** Gate{counter=-1682487562, name='Chris', address='Alaska'}
*************** Broken *************** Gate{counter=-1682491976, name='Chris', address='Canada'}
由此可见:测试也无法证明安全性,调试信息也不可靠。 通常情况下 线程不会考虑其他线程的操作,而是自己一直跑下去,当同时有两个线程要修改name字段,竞争中获胜的一方会先写入值,既data race。此时各字段的值都无法预测。
加入 synchronized 关键词
- SharedResource:可以被多个线程访问的累,包含很多方法,包括:
- safeMethod:多个线程同时调用也不会发生问题的方法
- unsafeMethod:多个线程同时调用会发生问题,因此必须加以保护的方法
- Java中Single Threaded Execution模式使用synchronized进行保护
- 我们把只允许单个线程执行的程序范围成为临界区
- 多线程时
- 多线程同时访问时
- 状态有可能发生变化时
- 需要确保安全性时
Single Threaded Execution模式存在发生死锁的危险,满足下列条件时,死锁就会发生:
- 存在多个SharedResource角色
- 线程在持有某个SharedResource角色的锁的同时还想获取其他SharedResource的锁
- 获取SharedResource角色的锁的顺序不固定
只需要破坏1,2,3中的一个条件,就可以防止死锁发生
假设我们需要编写一个SharedResource角色的子类, 如果子类能够访问SharedResource角色的字段,那么developer就可能会不小心写出无保护的unsafeMethod. 如果不讲包含子类在内的所有unsafeMethod都申明为synchronized方法,就无法确保SharedResource角色的安全性
一般情况下Single Threaded Execution模式会降低程序性能:
- 获取锁花费时间
- 线程冲突引起的等待:如果尽可能的缩小临界区的范围,降低线程冲突的概率,那么就能够抑制性能的下降
- synchronized与Before/After模式: 不管是synchronized方法还是synchronized代码块,都需要用"{"和"}"包起来,可以吧”{“想像成lock,“}”想像成unlock。 但是如果换成lock/unlock,一旦代码中有return和异常那锁就不会被释放;如果使用lock那务必把unlock放在finally中确保锁被释放
- synchronized在保护什么
- 该用什么单位来保护/使用那个锁来保护
- atomic / volatile
- long与double的操作不是原子的(???)
- 计数信号量和Semaphore类
- 如果我们需要一个资源可以被“最多N个线程”执行,就需要计数信号量
- java.util.concurrent提供Semaphore类
- 资源的许可个数(permits),acquire/release方法用于获取/释放资源
Immutable模式中存在着确保实例状态不发生改变(Immutable)的类。在访问这些实例时并不需要执行耗时的互斥处理。
实现方式:将字段声明为final,并且不存在setter方法
-
Immutable:
- 在Immutabl类中字段的值不可以被修改,也不存在修改字段内容的方法。
- Immutable角色实例被创建后,状态将不再发生变化。无需使用synchronized方法
- 实例创建后不再发生变化
- 实例时共享的,且被频繁访问:因为不需要使用synchronize进行保护也就意味着能够在不失去安全性和生存性的前提下提高性能
StringBuilder vs String
- 如果删掉final修饰符并加了setter方法,那么就会破坏Immutable模式
- 如果把字段的实例直接作为getter方法的返回值也会破坏Immutable模式
- 将构造函数的参数直接赋值给字段也会破坏Immutable模式
Single Threaded Execution 模式 在Single Thread Execution模式中以下两种情况会发生conflict:
- write-write conflict
- read-write conflict Immmutable模式中只会发生read-read,不会产生conflict Read-Write Lock 模式 Flyweight 模式
- final 类
- final 方法
- final 字段
- final 变量和参数
- 非线程安全的java.util.ArrayList类
- Collections.synchronizedList
- COpyOnWriteArrayList
Guarded Suspension模式通过让线程等待来保证实例的安全性。在Guarded Suspension模式中,线程会一直等待直到某个条件成立。
Guarded Object: 中包含guardedMethod和stateChangingMethod
wait/notifyall只出现RequestQueue中,而没有出现在ClientThread,ServerThread和Main中. Guarded Suspension模式的实现封装在RequestQueue中。这样使用RequestQueue类的其他类不需要关心wait/notify的细节,只需要调用RequestQueue的方法即可。
Single Threaded Execution 模式 Balking 模式 Producer-Consumer 模式 Future 模式
如果现在不适合执行某个操作,或者没有必要执行这个操作,就停止处理,直接返回。
- 不需要执行时
- 不需要等待守护条件成立时
- 守护条件仅在第一个成立时
Guraded Suspension 模式 Observer 模式
- Balking模式在守护条件不成立时直接返回
- Guarded Suspension模式在守护条件不成立时会一直等待
- 介于这两种极端之间还可以有一种情况是“在守护条件成立之前等一段时间”
- notify/notifyall
- interrupt
- timeout
我们无法区别wait是被notify了还是超时了,为了进行区分,在实现guarded timed时,需要检查当前时间和开始等待的时间的差值是否超过了超时时间。
来看一下以下两种线程状态:
- 想要使用synchronized方法,但是没有获取到锁,线程处于阻塞状态
- 执行wait并进入等待队列的状态
两者情况下线程都是不运行的,但是也存在不同
- 我们无法让(1)状态下的线程超时,因为syncronized方法和syncronized代码块都无法设置超时时间
- 对状态(1)下的线程执行interrupt方法,也不会InterruptedException异常,线程必须获取锁并进入syncronized代码块才会抛出InterruptedException异常
生产者安全的将数据交给消费者。 Producer-Consumer 模式在两者之间建立一个缓冲区,用于消除线程间处理速度的差异。