[Java]并发

线程之间的通信

在并发编程中,主要考虑两种方式来实现线程之间的通信:
  • 共享内存
  • 消息传递
共享内存的一致性就需要锁来维护了,而在erlang中采用消息传递的方式,每个线程都有自己的消息队列,存放从别的线程拿到的消息。

锁模式

锁的工作模式有两种:
  • 独占模式
  • 共享模式
用读写锁来理解这两种模式很方便:读锁占有时,可以在上面继续加读锁。写锁占有时,任何锁都不能往上加(包括写锁)。

synchronized

synchronized是java关键字,被它修饰的代码块同一时间只能有一个线程进入。常见的两种用法如下:
public class Test {
    public void func1() {
        synchronized (Test.this) {

        }
    }

    public synchronized void func2() {
        
    }
}
在JDK 1.5之前synchronized和concurrent之间的性能差距还是挺大的,但是在JDK 1.6中通过优化,性能已经有比较大的改善。所以有时候还是用JDK自带的东西比较好,他能不断持续优化。被修饰的对象会有四种状态:
  • 无锁
  • 偏向锁
  • 轻量级锁
  • 重锁
具体的实现方式是在对象头中保存信息。可以猜想一段时间内,统一资源的线程数不会很多。进一步说,一个线程很有可能连续的访问同一个资源。如果不停地去获取锁,这样效率就会下降很多。偏向锁是乐观锁,如果发现已经持有锁了,那么就省去了一大堆的操作。

一个线程一直持有锁的时间也不会很长,更常见的情况应该是中间会被短暂打断。此时不需要将当前线程放到阻塞队列里面去,只需要用CAS操作在其自旋几次就可以了,这就是轻量级锁的优势。

如果上面的这些情况都没有出现,那显然是一个悲观的情况,变为重锁

concurrent

大家经常说的并发包就是指java.util.concurrent.*的内容,需要去花时间理解AQS,他对底层的操作(cas、pack、unpack)进行封装,你只需要实现它的几个抽象方法,就能实现不同策略的并发功能(可以把他们看做是关键入口的开关):
protected boolean tryAcquire(int arg) {
	throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
	throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) {
	throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
	throw new UnsupportedOperationException();
}
下面来详细看AQS到底封装了什么。首先,被阻塞的线程会放到一个队列上等待被唤醒,相关的结构如下:
static final class Node {
	volatile int waitStatus;
	volatile Node prev;
	volatile Node next;
	volatile Thread thread;
	Node nextWaiter;
}
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
在并发包中对park、unpark等进行了封装,在LockSupport中实现:
public static void park(Object blocker) {
	Thread t = Thread.currentThread();
	setBlocker(t, blocker);
	unsafe.park(false, 0L);
	setBlocker(t, null);
}
public static void unpark(Thread thread) {
	if (thread != null)
		unsafe.unpark(thread);
}
在park、unpark中需要注意一点,被unpark的线程会在park的地方继续执行,而unpark的线程做完之后就接着干自己的事情。

独占模式加锁

首先来看阻塞模式下加锁的代码:
public final void acquire(int arg) {
	if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
	try {
		boolean interrupted = false;
		for (;;) {
			final Node p = node.predecessor();	// 获取前面的节点
			if (p == head && tryAcquire(arg)) {	// 在阻塞队列的最里面 || 尝试获取锁成功
				setHead(node);
				p.next = null;
				return interrupted;
			}
			// 阻塞操作
			if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
				interrupted = true;
		}
	} catch (RuntimeException ex) {
		cancelAcquire(node);
		throw ex;
	}
}
在acquire中的tryAcquire可以用来实现不同的加锁策略,这个可以看ReentrantLock中的FairSyncNonfairSync。在acquireQueued中实现了真正的逻辑:
不停地尝试获取资源(tryAcquire),如果成功,那么当前线程运行在独占模式,如果失败,则继续阻塞。
整个运行的过程在看完下面解锁的过程就明白了。

独占模式解锁

在独占模式下,要释放锁的时候,只需要将头结点后面的节点唤醒即可:
public final boolean release(int arg) {
	if (tryRelease(arg)) {
		Node h = head;
		if (h != null && h.waitStatus != 0)
		unparkSuccessor(h);
		return true;
	}
	return false;
}
private void unparkSuccessor(Node node) {
	int ws = node.waitStatus;
	if (ws < 0)
		compareAndSetWaitStatus(node, ws, 0); 
	Node s = node.next;
	// 去掉之前CANCLE的节点
	if (s == null || s.waitStatus > 0) {
		s = null;
		for (Node t = tail; t != null && t != node; t = t.prev)
			if (t.waitStatus <= 0)
				s = t;
	}
	// 唤醒后继节点中的线程
	if (s != null)
		LockSupport.unpark(s.thread);
}
独占模式下的解锁时分成两步来进行操作的:
  • 释放掉占用状态
  • 唤醒后面的线程
释放成功之后,后面的线程就工作在独占模式了,对应到加锁代码中的话,就是从parkAndCheckInterrupt中返回了。用一个简单的图来看独占模式:


共享模式加锁

共享与独占的模式有些不同,如果一个共享锁获取成功,那么后面正在被阻塞等待共享锁的线程也会被唤醒执行,看代码:
public final void acquireShared(int arg) {
	if (tryAcquireShared(arg) < 0)
		doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
	final Node node = addWaiter(Node.SHARED);
	try {
		boolean interrupted = false;
		for (;;) {
			final Node p = node.predecessor();
			if (p == head) {
				int r = tryAcquireShared(arg);
				if (r >= 0) {				// 获取成功
					setHeadAndPropagate(node, r);	// 设置头结点并唤醒后面的节点
					p.next = null;
					if (interrupted)
						selfInterrupt();
					return;
				}
			}
			// 获取失败则被阻塞
			if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
				interrupted = true;
		}
	} catch (RuntimeException ex) {
		cancelAcquire(node);
		throw ex;
	}
}
主要的不同点在setHeadAndPropagate中,如果节点是共享状态,那么会调用doReleaseShared方法来完成对后面节点唤醒操作。那么被唤醒的节点继续执行,在下一次循环中会继续唤醒后面的节点,所以可以看到这里是一个依次传递的过程。

共享模式解锁

虽然在共享模式下加锁的时候会唤起后面的节点,但是如果后的节点没有继续运行的话(也就是说虽然这个节点没有被继续阻塞,但是它确实没有执行),那么后面的节点就没办法被唤醒了。
public final boolean releaseShared(int arg) {
	if (tryReleaseShared(arg)) {
		doReleaseShared();
		return true;
	}
	return false;
}
private void doReleaseShared() {
	for (;;) {
		Node h = head;
		if (h != null && h != tail) {
			int ws = h.waitStatus;
			if (ws == Node.SIGNAL) {
				if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
					continue;
				unparkSuccessor(h);
			} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
				continue;
		}
		if (h == head)
			break;
	}
}
在共享模式下可能出现下面一种情况:唤醒的线程B比当前线程A执行更快,以至于在A的循环还没执行完head已经变化了:
  • 出现这种情况再次进去循环,将head的状态设置为Node.PROPAGATE。
还有一种情况是B执行的没有那么快,此时head没有发生变化:
  • 在这种情况下当前线程不会沿着链表继续unpark下去。
下面用一张图来看共享模式下的操作:

下面来看concurrent包中的并发工具的用法。

ReentrantLock

可重入锁的含义就是说,一个拿到锁的线程还能再次拿到锁,它工作在独占模式下,用法如下:
lock.lock();
try {
	// ...
} finally {
	lock.unlock();
}

Condition

在很多时候线程会等到某个条件成熟之后才能恢复运行,在此之前它甚至没有机会去被unpark,因为在Condition上等待的线程没有在阻塞队列中:
lock.lock();
try {
	while (count == capacity) {
		notFull.await();
	}
	count++;
	notEmpty.signalAll();
} finally {
	lock.unlock();
}

CountDownLatch

一个计数器:
final CountDownLatch latch = new CountDownLatch(2);
new Thread() {
	public void run() {
		// TODO
		latch.countDown();
	}
}.start();
latch.await();

CyclicBarrier

等到线程数到齐了就一起启动,可以重复使用:
final CyclicBarrier barrier = new CyclicBarrier(2);
new Thread() {
	public void run() { barrier.await();
		// TODO
	}
}.start();

Semaphore

信号量,用来控制访问资源的线程数目:
final Semaphore semaphore = new Semaphore(2);
new Thread() {
	public void run() {
		semaphore.acquire(1);
		try {
			
		} finally {
			semaphore.release(1);
		}
	}
}.start();

ReentrantReadWriteLock

经典的读写锁实现:
final ReadWriteLock rwl = new ReentrantReadWriteLock();
final Lock readLock = rwl.readLock(); // 读锁
final Lock writeLock = rwl.writeLock(); // 写锁
new Thread() {
	public void run() {
		readLock.lock();
		try {

		} finally {
			readLock.unlock();
		}
	}
}.start();
new Thread() {
	public void run() {
		writeLock.lock();
		try {

		} finally {
			writeLock.unlock();
		}
	}
}.start();
----------END----------
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
毕业设计,基于SpringBoot+Vue+MySQL开发的影城管理系统,源码+数据库+论文答辩+毕业论文+视频演示 随着现在网络的快速发展,网上管理系统也逐渐快速发展起来,网上管理模式很快融入到了许多生活之中,随之就产生了“小徐影城管理系统”,这样就让小徐影城管理系统更加方便简单。 对于本小徐影城管理系统的设计来说,系统开发主要是采用java语言技术,在整个系统的设计中应用MySQL数据库来完成数据存储,具体根据小徐影城管理系统的现状来进行开发的,具体根据现实的需求来实现小徐影城管理系统网络化的管理,各类信息有序地进行存储,进入小徐影城管理系统页面之后,方可开始操作主控界面,主要功能包括管理员:首页、个人中心、用户管理、电影类型管理、放映厅管理、电影信息管理、购票统计管理、系统管理、订单管理,用户前台;首页、电影信息、电影资讯、个人中心、后台管理、在线客服等功能。 本论文主要讲述了小徐影城管理系统开发背景,该系统它主要是对需求分析和功能需求做了介绍,并且对系统做了详细的测试和总结。具体从业务流程、数据库设计和系统结构等多方面的问题。望能利用先进的计算机技术和网络技术来改变目前的小徐影城管理系统状况,提高管理效率。 关键词:小徐影城管理系统;Spring Boot框架,MySQL数据库
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值