monitor
监视器锁对象完成,被synchronized
修饰的代码段当它的monitor
被占用时候,会处于锁定状态加锁的过程就是获取monitor
锁权限的过程,0
代表monitor
没有枷锁 1
代表monitor
加锁/**
* 经典售票问题
* Runnable 方式创建线程
*
* @author: Lidong
* @time: 2020/8/6 20:55
**/
public class CreateThreadByRunnableTest {
private static final Logger logger = LoggerFactory.getLogger(CreateThreadByRunnableTest.class);
private static final int TICKET_NUM = 11;
/**
* 经典售票问题 20个线程抢 11 张票
* 使用 Runnable 的方式创建代码可以达到相同代码公用共同的资源
* @throws InterruptedException InterruptedException
*/
@Test
public void test() throws InterruptedException {
BuyTicketsRunnable buyTicketTask = new BuyTicketsRunnable(TICKET_NUM);
int threadNum = 20;
for (int i = 0; i < threadNum; i++) {
Thread thread = new Thread(buyTicketTask);
thread.start();
if (Thread.holdsLock(Thread.currentThread())) {
logger.info("当前线程持有对象监视器!");
}
}
Thread.sleep(10000);
}
private class BuyTicketsRunnable implements Runnable {
private final Logger logger = LoggerFactory.getLogger(BuyTicketsRunnable.class);
private int ticketNum;
public BuyTicketsRunnable(int aTicketNum) {
this.ticketNum = aTicketNum;
}
@Override
public void run() {
synchronized (this) {
if (ticketNum > 0) {
ticketNum--;
logger.info("Thread {} 买到一张票 还剩:{} 张票", Thread.currentThread().getId(), ticketNum);
} else {
logger.info("Thread {} 没有抢到票 还剩:{} 张票", Thread.currentThread().getId(), ticketNum);
}
}
}
}
}
ReentrantLock
是可重入锁,意味着同一个线程可以多次获取同一把锁而不会导致死锁。而synchronized
也是可重入的,同一个线程可以多次获取同一把锁。ReentrantLock
提供了两种获取锁的方式,分别是公平锁和非公平锁,可以根据需要选择。synchronized
是非公平锁,即先尝试获取锁的线程有更高的优先级。ReentrantLock
时,需要手动释放锁,即在finally
块中调用unlock()
方法。而synchronized
在代码块执行完毕或者异常时会自动释放锁。ReentrantLock
的粒度更细,可以通过使用多个锁来实现更细粒度的同步控制,而synchronized
只能使用一个锁。ReentrantLock
提供了一些synchronized
不具备的功能,比如可中断、可定时、可轮询等。/**
* 三个窗口总共买 100 张票
*
* @throws InterruptedException interrupted exception
*/
@Test
public void test2() throws InterruptedException {
TicketTask ticketTask = new TicketTask();
new Thread(ticketTask, "1号窗口").start();
new Thread(ticketTask, "2号窗口").start();
new Thread(ticketTask, "3号窗口").start();
Thread.sleep(10000);
}
private class TicketTask implements Runnable {
private final Logger logger = LoggerFactory.getLogger(TicketTask.class);
private int tickets = 100;
private final Lock lock = new ReentrantLock(false);
@Override
public void run() {
while (true) {
// 上Lock锁
lock.lock();
try {
if (tickets > 0) {
--tickets;
logger.info("{} ======完成售票,余票为{}", Thread.currentThread().getName(), tickets);
} else {
logger.info("{} ======余票为{}", Thread.currentThread().getName(), tickets);
break;
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
// 释放 Lock 锁避免发生死锁
lock.unlock();
}
}
}
}
lock()
:获取锁。如果锁可用,则当前线程会立即获取锁并继续执行,否则当前线程会被阻塞直到获取到锁。lockInterruptibly()
:获取锁,但允许响应中断。如果锁可用,则当前线程会立即获取锁并继续执行,否则当前线程会被阻塞,直到获取到锁或者被其他线程中断。tryLock()
:尝试获取锁。如果锁可用,则当前线程会立即获取锁并返回true
,否则立即返回false
,不会阻塞。tryLock(long timeout, TimeUnit unit)
:在指定的时间内尝试获取锁。如果锁可用,则当前线程会立即获取锁并返回true
,否则会等待指定的时间,如果在等待时间内获取到锁则返回true
,否则返回false
。unlock()
:释放锁。当前持有锁的线程可以调用此方法来释放锁。isHeldByCurrentThread()
:判断当前线程是否持有该锁。如果当前线程是获取锁的线程,则返回true
,否则返回false
。ReentrantReadWriteLock
是 Java
中提供的一种同步机制,用于管理共享资源的并发读写访问。public class ReadWriteLockTest {
@Test
public void test() throws InterruptedException {
// 创建读写锁
ReadWriteLock lock = new ReentrantReadWriteLock();
// 读锁
Lock readLock = lock.readLock();
// 写锁
Lock writeLock = lock.writeLock();
Map<String, Object> map = new HashMap<>();
for (int i = 10; i > 0; i--) {
String key = String.valueOf(System.currentTimeMillis());
WriteTask writeTask = new WriteTask(writeLock, map, key);
ReadTask readTask = new ReadTask(readLock, map, key);
ThreadPoolUtils.executor(writeTask);
ThreadPoolUtils.executor(readTask);
}
Thread.sleep(10000);
}
/**
* 读任务
*/
private class ReadTask implements Runnable {
private final Logger logger = LoggerFactory.getLogger(ReadTask.class);
private Lock readLock;
private Map<String, Object> map;
private String key;
private ReadTask(Lock readLock, Map<String, Object> map, String key) {
this.readLock = readLock;
this.map = map;
this.key = key;
}
@Override
public void run() {
readLock.lock();
try {
if (Objects.nonNull(map)) {
Object object = map.get(key);
logger.info("ReadTask read value:{}", object);
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
// 释放锁
readLock.unlock();
}
}
}
/**
* 写任务
*/
private class WriteTask implements Runnable {
private final Logger logger = LoggerFactory.getLogger(WriteTask.class);
private Lock writeLock;
private Map<String, Object> map;
private String key;
private WriteTask(Lock writeLock, Map<String, Object> map, String key) {
this.writeLock = writeLock;
this.map = map;
this.key = key;
}
@Override
public void run() {
writeLock.lock();
try {
if (Objects.isNull(map)) {
map = new HashMap<>();
}
long l = System.currentTimeMillis();
map.put(key, l);
logger.info("WriteTask write value:{}", l);
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
// 释放锁
writeLock.unlock();
}
}
}
}
StampedLock
时,乐观读锁是一种特殊的读锁,它不会阻塞其他线程的写入操作。StampedLock
并不支持重入锁,如果在同一线程中重复获取同一个锁,则会导致死锁。StampedLock
还提供了其他方法,如 tryReadLock()
tryWriteLock()
等,用于尝试非阻塞地获取读锁或写锁。public class StampedLockTest {
private static final Logger logger = LoggerFactory.getLogger(StampedLockTest.class);
@Test
public void test1() throws InterruptedException {
StampedLock lock = new StampedLock();
List<String> list = new ArrayList<>();
ReadTask readTask = new ReadTask(lock, list);
WriteTask writeTask = new WriteTask(lock, list);
for (int i = 0; i < 20; i++) {
new Thread(readTask).start();
new Thread(writeTask).start();
}
Thread.sleep(10000);
}
/**
* 读任务
*/
private class ReadTask implements Runnable {
private StampedLock lock;
List<String> list;
public ReadTask(StampedLock lock, List<String> list) {
this.lock = lock;
this.list = list;
}
@Override
public void run() {
// 尝试获取乐观读锁
long stamp = lock.tryOptimisticRead();
// 检查乐观读锁是否有效
if (!lock.validate(stamp)) {
// 获取悲观读锁
stamp = lock.readLock();
try {
logger.info("读取到值:{}", list.toString());
} finally {
// 释放读锁
lock.unlockRead(stamp);
}
}
}
}
private class WriteTask implements Runnable {
private StampedLock lock;
List<String> list;
public WriteTask(StampedLock lock, List<String> list) {
this.lock = lock;
this.list = list;
}
@Override
public void run() {
// 获取写锁
long stamp = lock.writeLock();
try {
// 修改共享资源
list.add(UUID.randomUUID().toString());
logger.info("写入值后 list 的大小:{}", list.size());
} finally {
// 释放写锁
lock.unlockWrite(stamp);
}
}
}
}
wait()
和 notify()
方法用于线程之间的通信和同步。它们通常在多个线程需要协调它们的操作时使用。
wait()
是在 Object
类中定义的方法,它允许一个线程释放它持有的锁,并等待直到另一个线程通知它恢复执行。
当一个线程调用 wait()
时,它进入等待状态,直到另一个线程在相同的对象上调用 notify()
或 notifyAll()
。
public class WaitNotifyTest {
private static final Logger logger = LoggerFactory.getLogger(WaitNotifyTest.class);
@Test
public void test1() throws InterruptedException {
for (int i = 0; i < 10; i++) {
Message message = new Message();
int finalI = i;
ThreadPoolUtils.executor(new Thread(() -> {
logger.info(message.getMessage());
}));
ThreadPoolUtils.executor(new Thread(() -> {
message.setMessage("消息" + finalI);
}));
}
Thread.sleep(10000);
}
private class Message {
private String content;
private boolean isMessageReady;
public synchronized void setMessage(String content) {
while (isMessageReady) {
try {
// 等待,直到消息被消费
logger.info("消息没有被消费,等待被消费:{}", content);
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
this.content = content;
isMessageReady = true;
// 唤醒等待的线程
notify();
}
public synchronized String getMessage() {
while (!isMessageReady) {
try {
// 等待,直到消息被设置
logger.info("消息为空等待有消息", content);
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
isMessageReady = false;
// 唤醒等待的线程
notify();
return content;
}
}
}
在单核CPU
上,由于只有一个核心在执行指令,不存在多个线程同时执行的情况,因此可见性问题在理论上是不存在的。每个线程的操作都是按照顺序执行的,不会出现多个线程同时对共享变量进行读写的情况。
在单核CPU
上,如果多个线程同时访问共享变量,并且没有适当的同步机制来保证可见性,仍然可能出现可见性问题
CPU
上,每个CPU
的内核都有自己的缓存。当多个不同的线程运行在不同的CPU
内核上时,这些线程操作的是不同的CPU
缓存。一个线程对其绑定的CPU
的缓存的写操作,对于另外一个线程来说,不一定是可见的,这就造成了线程的可见性问题。Java
并发程序运行在多核CPU
上时,线程的私有内存,也就是工作内存就相当于多核CPU
中每个CPU
内核的缓存了。CPU
的缓存导致的,而缓存导致的可见性问题是导致诸多诡异的并发编程问题的幕后黑手之一。CPU
中执行的过程不被中断的特性。原子性操作一旦开始运行,就会一直到运行结束为止,中间不会有中断的情况发生。Java
提供了一些机制来解决原子性问题,包括以下几种方式:
synchronized
关键字:使用synchronized
关键字可以将代码块或方法标记为同步代码,确保同一时间只有一个线程可以执行该代码块或方法。这样可以保证对共享变量的操作是原子的。volatile
关键字:使用volatile
关键字可以保证共享变量的可见性,即一个线程对共享变量的修改对其他线程是立即可见的。虽然volatile
不能解决原子性问题,但可以用来保证对共享变量的读写操作是原子的。Atomic Classes
):Java
提供了一系列原子类,如AtomicInteger
、AtomicLong
等,它们提供了一些原子操作方法,可以保证对共享变量的操作是原子的。Lock
):Java
中的锁机制可以用来保护临界区,确保同一时间只有一个线程可以进入临界区。通过使用锁,可以保证对共享变量的操作是原子的。Java
提供了一些线程安全的容器类,如ConcurrentHashMap
、ConcurrentLinkedQueue
等,这些容器类内部使用了一些并发技术来保证对容器的操作是线程安全的,从而避免原子性问题。需要注意的是,并发编程中的原子性问题不仅仅限于对共享变量的操作,还可能涉及到多个操作的组合,这时候就需要考虑使用更高级的并发编程技术,如原子操作的组合、事务等。
volatile
关键字、synchronized
关键字、显式的同步机制或使用并发工具类中提供的有序性保证。CPU
为了对程序进行优化,会对程序的指令进行重排序,此时程序的执行顺序和代码的编写顺序不一定一致,这就可能会引起有序性问题。现代处理器为了提高指令的执行效率,可能会对指令进行乱序执行或重排序。处理器重排序是在单线程环境下的,同样不会影响单线程程序的执行结果。
volatile
可以确保对该变量的读写操作具有可见性。当一个线程修改了 volatile
变量的值,该值会立即被写回主内存,并且其他线程可以立即看到最新的值,从而解决了可见性问题。private static volatile ThreadPoolExecutor threadPool;
synchronized
关键字可以确保多个线程对共享变量的访问具有原子性和有序性。synchronized
关键字可以用来修饰方法或代码块,当一个线程获取了对象的锁时,其他线程必须等待该线程释放锁才能继续执行。这样可以保证同一时刻只有一个线程能够访问共享变量,从而解决了原子性和有序性问题。public class LazySingleton {
private static final Logger logger = LoggerFactory.getLogger(LazySingleton.class);
/**
* 保证lazySingleton在线程中同步
*/
private static volatile LazySingleton lazySingleton;
/**
* 保证类不在别的地方被实例化
*/
private LazySingleton() {
}
/**
* synchronize保证线程安全
*/
public static synchronized LazySingleton getInstance() {
if (null == lazySingleton) {
logger.info("懒汉式单例创建对象!");
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
Java
提供了显式的锁机制,如 ReentrantLock
,可以使用 lock()
和 unlock()
方法来手动控制线程的加锁和解锁操作。显式的锁机制提供了更灵活的同步方式,并且可以使用条件变量来实现更复杂的线程间通信,从而解决有序性问题。ReentrantLock
锁的使用/**
* Lock对象
*/
private static Lock LOCK = new ReentrantLock();
/**
* simpleDateFormat1 定义为类变量 对 simpleDateFormat1 在使用的时候同步处理
* 使用 Lock 锁
*/
@Test
public void test2() {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
LOCK.lock();
try {
String dateString = simpleDateFormat.format(new Date());
Date parse = simpleDateFormat.parse(dateString);
logger.info(simpleDateFormat.format(parse));
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
LOCK.unlock();
}
}).start();
}
}
Java
并发包中提供了一些并发工具类,如 CountDownLatch
、CyclicBarrier
、Semaphore
等,它们可以用来控制多个线程的执行顺序和并发访问的数量。这些工具类提供了更高级的同步机制,可以解决复杂的有序性问题。public class SemaphoreTest {
private static final Logger logger = LoggerFactory.getLogger(SemaphoreTest.class);
private static final Semaphore semaphore1 = new Semaphore(0);
private static final Semaphore semaphore2 = new Semaphore(0);
private class One extends Thread {
@Override
public void run() {
logger.info("=====》One线程执行完成...");
semaphore1.release();
}
}
private class Two extends Thread {
@Override
public void run() {
try {
semaphore1.acquire();
logger.info("=====》Two线程执行完成...");
semaphore2.release();
} catch (InterruptedException e) {
logger.error(e.getMessage(), e);
}
}
}
private class Three extends Thread {
@Override
public void run() {
try {
semaphore2.acquire();
logger.info("======》Three线程执行完成...");
semaphore2.release();
} catch (InterruptedException e) {
logger.error(e.getMessage(), e);
}
}
}
@Test
public void test1() throws InterruptedException {
Thread one = new One();
one.start();
Thread two = new Two();
two.start();
Thread three = new Three();
three.start();
Thread.sleep(5000);
logger.info("=====>三个子线程结束...");
}
}
Java
内存屏障(Memory Barriers
)是一种同步机制,用于控制编译器和处理器对内存操作的重排序和可见性。Load Barrier
(读屏障):确保在读操作之前,所有之前的读写操作都已经完成,防止读取到过期的数据。Store Barrier
(写屏障):确保在写操作之前,所有之前的读写操作都已经完成,防止写入的数据被重排序到后面的操作之前。