上一篇博客
// TODO ThreadPoolExecutor 有待回顾,在项目中踩了坑,需要之后对其源码,以及SynchronousQueue,LinkedBlockingQueue,ArrayBlockingQueue的源码做具体分析。
当多个线程尝试并发读写同一数据,常会发生一下两个问题:
改问题有点像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。
如果是执行两次的情况下,那么就会导致值实际只增加了一次。
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去掉后结果又会不一致,其中还有一些原因是我没有搞清楚的。
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
方法,这样线程便可以获得多把锁。但是要注意避免死锁问题的出现。synchronized
关键字也可以被用在代码块上,但是必须提供该对象的内部锁给synchronized
,如下例代码:public void increment() {
// Acquire Lock
synchronized (this) {
count = count + 1;
}
// Release Lock
}
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";
}
}
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);
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()
方法并获得了写锁,那么所有的读线程都将会暂停执行并等待写线程返回。java.util.concurrent.atomic
包中定义了一系列的Class来支持在单个变量上支持Atomic(原子)操作。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就是compare and swap技术,其由cpu指令实现。
在线程未做CAS判断时,原值被其他线程修改为B后又改为了A。
解决方法:
- 使用版本号机制,如手动增加版本号字段。
- JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题,除了比较预期值外,还增加了一个时间戳,同时去比对时间戳是否一致。
自旋CAS长时间不成功导致。
Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
// TODO关于JAVA并发暂时告一段落,主要是目前缺少应用经验,理解不够透彻。