上一篇博客
// TODO ThreadPoolExecutor 有待回顾,在项目中踩了坑,需要之后对其源码,以及SynchronousQueue,LinkedBlockingQueue,ArrayBlockingQueue的源码做具体分析。
文章目录
四、Thread Synchronization
1、Concurrency issues
当多个线程尝试并发读写同一数据,常会发生一下两个问题:
- 1、线程干扰错误
- 2、内存一致性错误
1.1 线程干扰(竞争危害/竞态条件 Race Conditions)
改问题有点像mysql的脏读,由于未对资源上锁的原因,导致第一个线程修改还未提交时,第二个线程又来读取,即两个线程都读了相同的值,做了重复的事情。
package com;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* @author peng fei
* @since 2020-07-25 23:51
*/
public class SynchronizationApplication {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
Counter counter = new Counter();
for (int i = 0; i < 1000; i++) {
executorService.submit(counter::increment);
}
executorService.shutdown();
executorService.awaitTermination(30, TimeUnit.SECONDS);
System.out.println("final count is : " + counter.getCount());
}
static class Counter {
int count = 0;
public void increment() {
count = count + 1;
}
public int getCount() {
return count;
}
}
}
上述代码期待的结果为1000,实际执行出来可能小于1000。
造成问题的原因如下:
Thread-1:read count = 0
Thread-2:read count = 0
Thread-1: count = count + 1 (= 0 + 1)
Thread-2 :count = count + 1 (= 0 + 1)
导致重复赋值,两个线程操作后值为1。
如果是执行两次的情况下,那么就会导致值实际只增加了一次。
- executorService.shutdown(); 关闭线程池,拒绝新提交的任务,将已提交的任务执行结束。
- executorService.awaitTermination(30, TimeUnit.SECONDS); 等待30秒,如果已提交的任务还未全部执行完成,则引发InterruptedException异常,可以通过shutdownNow()来处理,返回出还未执行的任务。
1.2 内存一致性错误(Memory Consistency Errors)
- 这种错误通常由于不同线程对同一个数据享有不同的观点导致。当一个线程在更新一些共享数据的时候,更改没有及时传播至其他线程,导致其他线程在对旧数据进行操作。
- 发生这种错误的原因有很多,比如:编译器为了优化程序的表现,在对代码进行编译时会重新排列指令。而处理器为了优化事务,很可能会将值读入临时缓存、寄存器等来替代主内存中的值使用,最终导致了这种问题。
package com;
/**
* @author peng fei
* @since 2020-07-25 23:51
*/
public class SynchronizationApplication {
private static String type = "cat";
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (type.equals("cat")) {
}
System.out.println("汪汪汪");
while (type.equals("dog")) {
}
System.out.println("喵喵喵");
});
thread.start();
Thread.sleep(1000);
System.out.println("变狗");
type = "dog";
Thread.sleep(1000);
System.out.println("变猫");
type = "cat";
}
}
上述代码预期的结果应该是:
变狗
汪汪汪
变猫
喵喵喵
但是实际的结果是这样的:
在将上述代码中的sleep去掉后结果又会不一致,其中还有一些原因是我没有搞清楚的。
2、Synchronized
2.1 Synchronized Methods
synchronized
关键字,保证了防止多个线程同时访问该方法。
package com;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* @author peng fei
* @since 2020-07-25 23:51
*/
public class SynchronizationApplication {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
Counter counter = new Counter();
for (int i = 0; i < 1000; i++) {
executorService.submit(counter::increment);
}
executorService.shutdown();
executorService.awaitTermination(30, TimeUnit.SECONDS);
System.out.println("final count is : " + counter.getCount());
}
static class Counter {
int count = 0;
public synchronized void increment() {
count = count + 1;
}
public int getCount() {
return count;
}
}
}
synchronized
关键字保证了一次只有一个线程能够进入increment()
方法。Synchronization
概念是与单个对象有关系,主要针对多线程调用单个对象时,当多线程同时调用多个不同的对象时,那也不会存在竞态条件。
注意点:
synchronized
关键字修饰非静态方法时,锁是加在该方法的对象的上的,多线程竞争的是同一个实例化对象的锁。synchronized
关键字修饰静态方法时,锁则是加在该方法的类上,所有的该类实例化的对象同时竞争一把锁。- 一个类可以同时拥有同步和非同步方法,非同步方法可以被多个线程同时访问而不受限制。
- 线程可以在
synchronized
方法中调用另外一个synchronized
方法,这样线程便可以获得多把锁。但是要注意避免死锁问题的出现。
2.2 Synchronized Blocks
- Java 内部使用一种**内部锁(监控锁)**来管理线程同步,每个对象都有一个内部锁与它关联。
- 每当一个线程调用一个对象的Synchronized方法时,他将自动获取该对象的内部锁,并在退出方法时释放,即便方法是以抛出异常的方式退出的。
- 在针对静态方法时,相乘将会获取Class 对象的内部锁。
synchronized
关键字也可以被用在代码块上,但是必须提供该对象的内部锁给synchronized
,如下例代码:
public void increment() {
// Acquire Lock
synchronized (this) {
count = count + 1;
}
// Release Lock
}
3、Volatile
volatile
关键字被用于在多线程问题中避免线程一致性错误,它会告诉编译器避免对变量的优化工作。- 如果你用
volatile
标记一个变量,编译器将不会优化或重排该变量前后的指令。同时变量的值将总会从主内存中读取来取代临时寄存器。
例子:
package com;
/**
* @author peng fei
* @since 2020-07-25 23:51
*/
public class VolatileApplication {
private static volatile String type = "cat";
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (type.equals("cat")) {
}
System.out.println("汪汪汪");
while (type.equals("dog")) {
}
System.out.println("喵喵喵");
});
thread.start();
Thread.sleep(1000);
System.out.println("变狗");
type = "dog";
Thread.sleep(1000);
System.out.println("变猫");
type = "cat";
}
}
五、Locks
1. ReentrantLock
ReentrantLock
(重入锁)是一个行为与 ”通过synchronized
关键字访问内部锁/隐式锁“ 行为相同的的互斥锁。- 一个目前拥有
ReentrantLock
锁的线程可以不止一次的获取它,而不会有任何问题出现。
package com;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author peng fei
* @since 2020-08-03 14:22
*/
class ReentrantLockMethodsCounter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public int incrementAndGet() {
System.out.println("IsLocked : " + lock.isLocked());
System.out.println("IsHeldByCurrentThread:" + lock.isHeldByCurrentThread());
boolean isAcquired = lock.tryLock();
System.out.println("Lock Acquired : " + isAcquired + "\n");
if (isAcquired) {
try {
Thread.sleep(2000);
count = count + 1;
} catch (InterruptedException e) {
throw new IllegalStateException(e);
} finally {
lock.unlock();
}
}
return count;
}
}
public class ReentrantLockMethodClass {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
ReentrantLockMethodsCounter counter = new ReentrantLockMethodsCounter();
executorService.submit(() -> {
System.out.println("IncrementCount (First Thread)" +
counter.incrementAndGet());
});
executorService.submit(() -> {
System.out.println("IncrementCount (Second Thread)" +
counter.incrementAndGet());
});
executorService.shutdown();
}
}
tryLock()
方法尝试不暂停线程去获得锁,如果线程由于其他线程持有该锁而没能获得锁,那么tryLock()
方法将会立刻return
而不去等待锁的释放。- 你也可以为
tryLock()
方法指定一个延迟时间去等待锁的释放-
lock.tryLock(1, TimeUnit.SECONDS);
2. ReadWriteLock
ReadWriteLock
读写锁包括了一对锁,即读锁和写锁。读锁可能被线程持有的同时,写锁可能没有被任何线程持有。- 读写锁可以提高并发等级,它相对于其他锁,在面对写少于读时能有更好的并发表现。
class ReadWriteCounter {
ReadWriteLock lock = new ReentrantReadWriteLock();
private int count = 0;
public int incrementAndGetCount() {
lock.writeLock().lock();
try {
count = count + 1;
return count;
} finally {
lock.writeLock().unlock();
}
}
public int getCount() {
lock.readLock().lock();
try {
return count;
} finally {
lock.writeLock().unlock();
}
}
}
- 上述例子中,当没有线程调用
incrementAndGetCount()
方法时,多个线程可以同时执行getCount()
方法。 - 如果有任何一个线程调用了
incrementAndGetCount()
方法并获得了写锁,那么所有的读线程都将会暂停执行并等待写线程返回。
六、Atomic
1. Atomic Variables
- Java在并发包
java.util.concurrent.atomic
包中定义了一系列的Class来支持在单个变量上支持Atomic(原子)操作。 - 原子类内部使用了被现代CPU支持的
CAS(compare-and-swap)
指令来实现同步,这些指令通常要比锁更快。
package com;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author peng fei
* @since 2020-08-04 09:43
*/
class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public int incrementAndGet() {
return count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
public class AtomicApplication {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
AtomicCounter counter = new AtomicCounter();
for (int i = 0; i < 1000; i ++) {
executorService.submit(counter::incrementAndGet);
}
executorService.shutdown();
executorService.awaitTermination(60, TimeUnit.SECONDS);
System.out.println("Final Count is :" + counter.getCount());
}
}
上述代码中使用了原子操作,底层使用了CAS:
2. CAS
所谓cas就是compare and swap技术,其由cpu指令实现。
- 由上图可知CAS其实是一种无锁技术,这也是其性能要比synchronized和lock高的原因。CAS也可以认为是一种乐观锁,其乐观认为其他线程在运行过程中不会修改原值。然后通过检测原值变动的方式解决同步问题。
- CAS两个步骤:冲突检测 和 数据更新
- CAS的缺点:
- 1、ABA问题:
在线程未做CAS判断时,原值被其他线程修改为B后又改为了A。
解决方法:
- 使用版本号机制,如手动增加版本号字段。
- JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题,除了比较预期值外,还增加了一个时间戳,同时去比对时间戳是否一致。
- 2、循环时间长开销大
自旋CAS长时间不成功导致。
- 3、只能保证一个共享变量的原子操作
Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
// TODO关于JAVA并发暂时告一段落,主要是目前缺少应用经验,理解不够透彻。