上篇对线程的一些基础知识作了总结,本篇来对多线程编程中最重要,也是最麻烦的一个部分——同步,来作个总结。java
建立线程并不难,难的是如何让多个线程可以良好的协做运行,大部分须要多线程处理的事情都不是彻底独立的,大都涉及到数据的共享,本篇是对线程同步的一个总结,若有纰漏的地方,欢迎在评论中指出。git
咱们来看一个简单的例子,有两个数 num1,num2,如今用 10 个线程来作这样一件事--每次从 num1 中减去一个随机的数 a,加到 num2 上。github
public class Demo1 {
public static void main(String[] args) {
Bank bank = new Bank();
//建立10个线程,不停的将一个帐号资金转移到另外一个帐号上
for (int i = 0; i < 100; i++) {
new Thread(() -> {
while (true) {
int account1 = ((Double) Math.floor(Math.random() * 10)).intValue();
int account2 = ((Double) Math.floor(Math.random() * 10)).intValue();
int num = ((Long) Math.round(Math.random() * 100)).intValue();
bank.transfer(account1, account2, num);
try {
Thread.sleep(((Double) (Math.random() * 10)).intValue());
} catch (Exception e) {
}
}
}).start();
}
}
}
class Bank {
/** * 10个资金帐户 */
public int[] accounts = new int[10];
public Bank() {
Arrays.fill(accounts, 1000);
}
public void transfer(int from, int to, int num) {
accounts[from] -= num;
accounts[to] += num;
//计算和
int sum = 0;
for (int j = 0; j < 10; j++) {
sum += accounts[j];
}
System.out.println(sum);
}
}
复制代码
正常状况下,不管何时资金帐号的和应该都是 10000.然而真的会这样吗?运行程序一段时间后会发现和不等于 10000 了,可能变大也可能变小了。编程
上面的代码中有多个程序同时更新帐户信息,所以出现了竞争关系。假设两个线程同时执行下面的一句代码:安全
accounts[account1] -= num;
复制代码
该代码不是原子性的,可能会被处理成以下三条指令:微信
这里仅说明单核心状况下的问题(多核同样会有问题),单核心是不能同时运行两个线程的,若是一个线程 A 执行到第三步时,被剥夺了运行权,线程 B 开始执行完成了整个过程,而后线程 A 继续运行第三步,这就产生了错误,线程 A 的结果覆盖了线程 B 的结果,总金额再也不正确。以下图所示: 多线程
为了防止并发致使数据错乱,Java 语言提供了 synchronized 关键字,而且在 Java SE 5 的时候加入了 ReentrantLock 类。synchronized 关键字自动提供了一个锁以及相关的条件,这个后面再说。ReentrantLock 的基本使用以下:并发
myLock.lock()//myLock是一个ReetrantLock对象示例
try{
//要保护的代码块
}finally{
//必定要在finally中释放锁
myLock.unlock();
}
复制代码
上述结构保证任意时刻只有一个线程进入临界区,一旦一个线程调用 lock 方法获取了锁,其余全部线程都会阻塞在 lock 方法处,直到有锁线程调用 unlock 方法。dom
将 ban 类中的 transfer 方法加锁,代码以下:性能
class Bank {
/** * 10个资金帐户 */
public int[] accounts = new int[10];
private ReentrantLock lock = new ReentrantLock();
public Bank() {
Arrays.fill(accounts, 1000);
}
public void transfer(int from, int to, int num) {
try {
lock.lock();
accounts[from] -= num;
accounts[to] += num;
//计算和
int sum = 0;
for (int j = 0; j < 10; j++) {
sum += accounts[j];
}
System.out.println(sum);
} finally {
lock.unlock();
}
}
}
复制代码
通过加锁,不管多少线程同时运行,都不会致使数据错乱。
锁是能够重入的,已经持有锁的线程能够重复获取已经持有的锁。锁有一个持有计数(hold count)来跟踪 lock 方法的嵌套调用。每 lock 一次计数+1,unlock 一次计数-1,当 lock 为 0 时锁释放掉。
能够经过带 boolean 参数构造一个带有公平策略的锁--new ReentrantLock(true)
。公平锁偏心等待时间最长的线程。可是会致使性能大幅下降,并且即便使用公平锁,也不能确保线程调度器是公平的。
一般咱们会遇到这样的问题,当一个线程获取锁后,发现须要知足某个条件才能继续日后执行,这就须要一个条件对象来管理已经获取锁可是却不能作有用工做的线程。
如今来考虑给转帐加一个限制,只有资金充足的帐户才能做为转出帐户,也就是不能出现负值。注意下面的代码是不可行的:
if(bank.accounts[from]>=num){
bank.transfer(from,to,num);
}
复制代码
由于多线程下极有可能 if 判断成功后,恰好数据被其余线程修改了。
能够经过条件对象来这样实现判断:
class Bank {
/** * 10个资金帐户 */
public int[] accounts = new int[10];
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public Bank() {
Arrays.fill(accounts, 1000);
}
public void transfer(int from, int to, int num) {
try {
lock.lock();
while (accounts[from] < num) {
//进入阻塞状态
condition.await();
}
accounts[from] -= num;
accounts[to] += num;
//计算和
int sum = 0;
for (int j = 0; j < 10; j++) {
sum += accounts[j];
}
System.out.println(sum);
//通知解除阻塞
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
复制代码
在 while 循环中判断是否知足,若是不知足条件,调用await
方法进入阻塞状态,同时放弃锁。这样让其余线程有机会给转出帐户转入资金也知足判断条件。
当某一个线程完成转帐工做后,应该调用signalAll
方法让全部阻塞线程接触阻塞状态,由于此时可能会知足判断条件,能够继续转帐操做。
注意:调用signalAll
不会当即激活一个等待线程,仅仅只是接触阻塞状态,以便这些线程能够经过竞争获取锁,继续进行 while 判断。
还有一个方法signal
随机解除一个线程的阻塞状态。这里可能会致使死锁的产生。
上一节中的 Lock 和 Condition 为开发人员提供了强大的同步控制。可是大多数状况并不须要那么复杂的控制。从 java 1.0 版本开始,Java 中的每一个对象都有一个内部锁。若是一个方法用synchronized
声明,那么对象的锁将保护整个方法,也就是调用方法时自动获取内部锁,方法结束时自动解除内部锁。
同 ReentrantLock 锁同样,内部锁也有 wait/notifyAll/notify 方法,对应关系以下:
ReentrantLock
类中方法须要重命名。用 synchronized 实现的 ban 类以下:
class Bank {
/** * 10个资金帐户 */
public int[] accounts = new int[10];
private ReentrantLock lock = new ReentrantLock();
// private Condition condition = lock.newCondition();
public Bank() {
Arrays.fill(accounts, 1000);
}
synchronized public void transfer(int from, int to, int num) {
try {
// lock.lock();
while (accounts[from] < num) {
//进入阻塞状态
// condition.await();
this.wait();
}
accounts[from] -= num;
accounts[to] += num;
//计算和
int sum = 0;
for (int j = 0; j < 10; j++) {
sum += accounts[j];
}
System.out.println(sum);
//通知解除阻塞
// condition.signalAll();
this.notifyAll();
} catch (Exception e) {
e.printStackTrace();
}
// finally {
// lock.unlock();
// }
}
}
复制代码
静态方法也能够声明为 synchronized,调用这中方法,获取到的是对应类的类对象的内部锁。
最好既不使用 Lock/Condition 也不使用 synchronized 关键字,大可能是状况下均可以用 java.util.concurrent 包中的类来完成数据同步,该包中的类都是线程安全的。会在下一篇中讲到。
若是能用 synchronized 的,尽可能用它,这样既能够减小代码数量,减小出错的概率。
若是上面都不能解决问题,那就只能使用 Lock/Condition 了。
本篇所用所有代码:github
本文原创发布于:www.tapme.top/blog/detail…
扫码关注微信公众号:FleyX 学习笔记,获取更多干货