马哥7期高端Go语言百万并发高薪班2022最新end无秘
Java并发编程--多线程间的同步控制和通讯
马哥7期高端Go语言百万并发高薪班2022最新end无秘
download:https://www.zxit666.com/5066/
运用多线程并发处置,目的是为了让程序更充沛天时用CPU ,好能加快程序的处置速度和用户体验。假如每个线程各自处置的局部互不相干,那真是极好的,我们在程序主线程要做的同步控制最多也就是等候几个工作线程的执行终了,假如不 Care 结果的话,连同步等候都能省去,主线程撒开手让这些线程干就行了。
不过,理想还是很严酷的,大局部状况下,多个线程是会有竞争操作同一个对象的状况的,这个时分就会招致并发常见的一个问题--数据竞争(Data Racing)。
这篇文章我们就来讨论一下这个并发招致的问题,以及多线程间停止同步控制和通讯的学问,本文大纲如下:
并发招致的Data Racing问题
怎样了解这个问题呢,拿一个多个线程同时对累加器对象停止累加的例子来解释吧。
package com.learnthread;
public class DataRacingTest {
public static void main(String[] args) throws InterruptedException {
final DataRacingTest test = new DataRacingTest();
// 创立两个线程,执行 add100000() 操作
// 创立Thread 实例时的 Runnable 接口完成,这里直接运用了 Lambda
Thread th1 = new Thread(()-> test.add100000());
Thread th2 = new Thread(()-> test.add100000());
// 启动两个线程
th1.start();
th2.start();
// 等候两个线程执行完毕
th1.join();
th2.join();
System.out.println(test.count);
}
private long count = 0;
// 想复现 Data Racing,去掉这里的 synchronized
private void add100000() {
int idx = 0;
while(idx++ < 100000) {
count += 1;
}
}
}
复制代码
上面这个例程,假如我们不启动 th2 线程,只用 th1 一个线程停止累加操作的话结果是 100000。依照这个思想,假如我们启动两个线程那么最后累加的结果就应该是 200000。 但实践上并不是,我们运转一下上面的例程,得到的结果是:
168404
Process finished with exit code 0
复制代码
当然这个在每个人的机器上的结果是不一样的,而且也是有可能恰恰等于 200000,需求多运转几次,或者是多开几个线程执行累加,呈现 Data Racing 的几率才高。
程序呈现 Data Racing 的现象,就意味着最终拿到的数据是不正确的。那么为了防止这个问题就需求经过加锁来处理了,让同一时间只要持有锁的线程才干对数据对象停止操作。当然针对简单的运算、赋值等操作我们也能直接运用原子操作完成无锁处理 Data Racing, 我们为了示例足够简单易懂才举了一个累加的例子,实践上假如是一段业务逻辑操作的话,就只能运用加锁来保证不会呈现 Data Racing了。
加锁,只是线程并发同步控制的一种,还有释放锁、唤醒线程、同步等候线程执行终了等操作,下面我们会逐一停止学习。
同步控制--synchronized
开头的那个例程,假如想防止 Data Racing,那么就需求加上同步锁,让同一个时间只能有一个线程操作数据对象。 针对我们的例程,我们只需求在 add100000 办法的声明中加上 synchronized 即可。
// 想复现 Data Racing,去掉这里的 synchronized
private synchronized void add100000() {
int idx = 0;
while(idx++ < 100000) {
count += 1;
}
}
复制代码
是不是很简单,当然 synchronized 的用法远不止这个,它能够加在实例办法、静态办法、代码块上,假如运用的不对,就不能正确地给需求同步锁维护的对象加上锁。
synchronized 是 Java 中的关键字,是应用锁的机制来完成互斥同步的。
synchronized 能够保证在同一个时辰,只要一个线程能够执行某个办法或者某个代码块。
假如不需求 Lock 、读写锁ReadWriteLock 所提供的高级同步特性,应该优先思索运用synchronized 这种方式加锁,主要缘由如下:
Java 自 1.6 版本以后,对 synchronized 做了大量的优化,其性能曾经与 JUC 包中的 Lock 、ReadWriteLock 根本上持平。从趋向来看,Java 将来仍将继续优化 synchronized ,而不是 ReentrantLock 。
ReentrantLock 是 Oracle JDK 的 API,在其他版本的 JDK 中不一定支持;而 synchronized 是 JVM 的内置特性,一切 JDK 版本都提供支持。
synchronized 能够应用在实例办法、静态办法和代码块上:
用 synchronized 关键字修饰实例办法,即为同步实例办法,锁是当前的实例对象。
用 synchronized 关键字修饰类的静态办法,即为同步静态办法,锁是当前的类的 Class 对象。
假如把 synchronized 应用在代码块上,锁是 synchronized 括号里配置的对象,synchronized(this) {..} 锁就是代码块所在实例的对象,synchronized(类名.class) {...} ,锁就是类的 Class 对象。
同步实例办法和代码块
上面我们曾经看过怎样给实例办法加 synchronized 让它变成同步办法了。下面我们看一下,synchronized 给实例办法加锁时,不能保证资源被同步锁维护的例子。
class Account {
private int balance;
// 转账
synchronized void transfer(Account target, int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
复制代码
在这段代码中,临界区内有两个资源,分别是转出账户的余额 this.balance 和转入账户的余额 target.balance,并且用的是一把实例对象的锁。问题就出在 this 这把锁上,this 这把锁能够维护本人的余额 this.balance,却维护不了他人的余额 target.balance,就像你不能用自家的锁来维护他人家的资产一个道理。
应该保证运用的锁能维护一切应受维护资源。我们能够运用Account.class 作为加锁的对象。Account.class 是一切 Account 类的对象共享的,而且是 Java 虚拟机在加载 Account 类的时分创立的,保证了它的全局独一性。
class Account {
private int balance;
// 转账
void transfer(Account target, int amt){
synchronized(Account.class) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
复制代码
用 synchronized 给 Account.class 加锁,这样就保证出账、入账两个 Account 对象在同步代码块里都能收到维护。
当然我们也能够运用这笔转账的买卖对象作为加锁的对象,保证只要这比买卖的两个 Account 对象受维护,这样就不会影响到其他转账买卖里的出账、入账 Account 对象了。
class Account {
private Trans trans;
private int balance;
private Account();
// 创立 Account 时传入同一个 买卖对象作为 lock 对象
public Account(Trans trans) {
this.trans = trans;
}
// 转账
void transfer(Account target, int amt){
// 此处检查一切对象共享的锁
synchronized(trans) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
经过处理上面这个问题我们顺道就把 synchronized 修饰同步代码块的学问点学了, 如今我们来看 synchronized 的最后一个用法--修饰同步静态办法。
同步静态办法
静态办法的同步是指,用 synchronized 修饰的静态办法,与运用所在类的 Class 对象完成的同步代码块,效果相似。由于在 JVM 中一个类只能对应一个类的 Class 对象,所以同时只允许一个线程执行同一个类中的静态同步办法。
关于同一个类中的多个静态同步办法,持有锁的线程能够执行每个类中的静态同步办法而无需等候。不论类中的哪个静态同步办法被调用,一个类只能由一个线程同时执行。
package com.learnthread;
public class SynchronizedStatic implements Runnable {
private static final int MAX = 100000;
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
SynchronizedStatic instance = new SynchronizedStatic();
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
// 等候工作线程执行完毕
t1.join();
t2.join();
System.out.println(count);
}
@Override
public void run() {
for (int i = 0; i < MAX; i++) {
increase();
}
}
/**
* synchronized 修饰静态办法
*/
public synchronized static void increase() {
count++;
}
}
复制代码
线程挂起和唤醒
上面我们看了运用 synchronized 给对象加同步锁,让同一时间只要一个线程能操作临界区的控制。接下来,我们看一下线程的挂起和唤醒,这两个操作运用被线程胜利加锁的对象的 wait 和 notify 办法来完成,唤醒除了notify 外还有 notifyAll办法用来唤醒一切线程。下面我们先看一下这几个办法的解释。
wait - wait 会自动释放当前线程占有的对象锁,并恳求操作系统挂起当前线程,让线程从 Running 状态转入 Waiting 状态,等候被 notify / notifyAll 来唤醒。假如没有释放锁,那么其它线程就无法进入对象的同步办法或者同步代码块中,那么就无法执行 notify 或者 notifyAll 来唤醒挂起的线程,会形成死锁。
notify - 唤醒一个正在 Waiting 状态的线程,并让它拿到对象锁,详细唤醒哪一个线程由 JVM 控制 。
notifyAll - 唤醒一切正在 Waiting 状态的线程,接下来它们需求竞争对象锁。
这里有两点需求各位留意的中央, 第一个是 wait、notify、notifyAll 都是 Object 类中的办法,而不是 Thread 类的。
由于 Object 是始祖类,是不是意味着一切类的对象都能调用这几个办法呢?是,也不是... 由于 wait、notify、notifyAll 只能用在 synchronized 办法或者 synchronized 代码块中运用,否则会在运转时抛出 IllegalMonitorStateException。换句话说,只要被 synchronized 加上锁的对象,才干调用这三个办法。
为什么 wait、notify、notifyAll 不定义在 Thread 类中?为什么 wait、notify、notifyAll 要配合 synchronized 运用?
了解为什么这么设计,需求理解几个根本学问点:
每一个 Java 对象都有一个与之对应的监视器(monitor)
每一个监视器里面都有一个 对象锁 、一个 等候队列、一个 同步队列
理解了以上概念,我们回过头来了解前面两个问题。
为什么这几个办法不定义在 Thread 中?
由于每个对象都具有对象锁,让当前线程等候某个对象锁,自然应该基于这个对象(Object)来操作,而非运用当前线程(Thread)来操作。由于当前线程可能会等候多个线程释放锁,假如基于线程(Thread)来操作,就十分复杂了。
为什么 wait、notify、notifyAll 要配合 synchronized 运用?
假如调用某个对象的 wait 办法,当前线程必需具有这个对象的对象锁,因而调用 wait 办法必需在 synchronized 办法和 synchronized 代码块中。
下面看一个 wait、notify、notifyAll 的一个经典运用案例,完成一个消费者、消费者形式:
package com.learnthread;
import java.util.PriorityQueue;
public class ThreadWaitNotifyDemo {
private static final int QUEUE_SIZE = 10;
private static final PriorityQueue queue = new PriorityQueue<>(QUEUE_SIZE);
public static void main(String[] args) {
new Producer("消费者A").start();
new Producer("消费者B").start();
new Consumer("消费者A").start();
new Consumer("消费者B").start();
}
static class Consumer extends Thread {
Consumer(String name) {
super(name);
}
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.size() == 0) {
try {
System.out.println("队列空,等候数据");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
queue.notifyAll();
}
}
queue.poll(); // 每次移走队首元素
queue.notifyAll();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 从队列取走一个元素,队列当前有:" + queue.size() + "个元素");
}
}
}
}
static class Producer extends Thread {
Producer(String name) {
super(name);
}
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.size() == QUEUE_SIZE) {
try {
System.out.println("队列满,等候有空余空间");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
queue.notifyAll();
}
}
queue.offer(1); // 每次插入一个元素
queue.notifyAll();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 向队列取中插入一个元素,队列当前有:" + queue.size() + "个元素");
}
}
}
}
}
复制代码
上面的例程有两个消费者和两个消费者。消费者向队列中放数据,每次向队列中放入数据后运用 notifyAll 唤醒消费者线程,当队列满后消费者会 wait 让出线程,等候消费者取走数据后再被唤醒 (消费者取数据后也会调用 notifyAll )。同理消费者在队列空后也会运用 wait 让出线程,等候消费者向队列中放入数据后被唤醒。
线程等候--join
与 wait 和 notify 办法一样,join 是另一种线程间同步机制。当我们调用线程对象 join 办法时,调用线程会进入等候状态,它会不断处于等候状态,直到被援用的线程执行完毕。在上面的几个例子中,我们曾经运用过了 join 办法
...
public static void main(String[] args) throws InterruptedException {
SynchronizedStatic instance = new SynchronizedStatic();
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
// 等候工作线程执行完毕
t1.join();
t2.join();
System.out.println(count);
}
复制代码
这个例子里,主线程调用 t1 和 t2 的 join 办法后,就会不断等候,直到他们两个执行完毕。假如 t1 或者 t2 线程处置时间过长,调用它们 join 办法的主线程将不断等候,程序阻塞住。为了防止这些状况,能够运用能指定超时时间的重载版本的 join 办法。
t2.join(1000); // 最长等候1s
复制代码
假如援用的线程被中缀,join办法也会返回。在这种状况下,还会触发 InterruptedException。所以上面的main办法为了演示便当,直接选择抛出了 InterruptedException。
总结
同步控制的一大思绪就是加锁,除了本问学习到的 sychronized 同步控制,Java 里还有 JUC 的可重入锁、读写锁这种加锁方式,这个我们后续引见 JUC 的时分会给大家解说。
另外一种思绪是不加锁,让线程和线程之间尽量不要运用共享数据,ThreadLocal 就是这种思绪,下篇我们引见 Java 的线程本地存储 -- ThreadLocal。