上一篇介绍了线程相关的概念,以及一些线程的常规方法,今天学习一下线程的同步的一些知识,这也是多线程中极为重要的一部分!
1. 为什么要同步?
多个线程会同时访问一个公共资源(因为线程之间是共享资源的,而进程之间则是单独拥有一部分资源的),这种情况称为竞争状态。如果一个类的对象在多线程程序中没有导致竞争状态,则称之为线程安全的,否则就是线程不安全!
2. 如何解决同步问题?
Java提供内置支持关键字synchronized,用于保护共享资源。共享资源一般是以对象形式存在的内存片段,也可以是文件、输入/ 输出端口、或者是打印机。要控制对共享资源的访问,得先把它包装进一个对象,然后把所有要访问这个对象的方法标记为synchronized,当有一个线程在调用synchronized方法时,其他调用类中synchronized方法的线程都会被阻塞,普通方法不会受影响!
注意:在使用并发时,将域(指的是共享资源,可以简单理解为成员变量)设置为private非常重要,因为这样一来其他任务就可以直接访问域,从而产生冲突!
3. 什么时候需要同步?
Brian同步规则:如果你正在写一个变量,它可能接下来被另一个线程读取,或者正在读取上一次已经被另一个线程写过的变量,那么必须使用同步!
注意:如果在一个类中有超过一个方法在处理临界(共享资源区,即临界区)数据,那么必须同步所有相关的方法。
一个小示例,启动50个线程,每个线程往一个账户里添加1块钱。那么按照逻辑,当所有的线程执行介绍,账户里应该有50块钱,来看看结果:
package multithreading;
import java.util.concurrent.*;
public class AccountWithSync {
private static Account account = new Account();
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 50; i++) {
executorService.execute(new AddPennyTask());
}
executorService.shutdown();
while (!executorService.isTerminated()) {
}
System.out.println("What is balance? " + account.getBalance());
}
private static class AddPennyTask implements Runnable {
public void run() {
account.deposit(1);
}
}
private static class Account {
private int balance = 0;
public int getBalance() {
return balance;
}
public void deposit(int amount) {
int newBalance = balance + amount;
try {
Thread.sleep(5);
} catch (InterruptedException e) {
}
balance = newBalance;
}
}
}
这段程序的运行结果将是不可预测的,每次的值都会变,就是因为没有同步deposit()方法,下面提供两种添加synchronized的方法:
(1)将23行代码修改成这样(这是同步语句,也称同步块):
synchronized (account) {
<span style="white-space:pre"> </span>account.deposit(1);
}
(2)将32行改成这样(这是同步方法):
public synchronized void deposit(int amount)
提示:
同步块方法会更加有效!
另一种同步的方法(使用Lock对象,显示的加锁同步):
package multithreading;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
public class AccountWithSyncUsingLock {
private static Account account = new Account();
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 50; i++)
executorService.execute(new AddPennyTask());
executorService.shutdown();
//while语句很重要,不写则会出现线程还没执行完就输出结果了!
//因为shutdown()只是告诉执行器不在接受新的线程,但原有的线程还在执行!
while (!executorService.isTerminated()) {
}
System.out.println("What is balance? " + account.getBalance());
}
private static class AddPennyTask implements Runnable {
public void run() {
account.deposit(1);
}
}
private static class Account {
private static Lock lock = new ReentrantLock();
private int balance = 0;
public int getBalance() {
return balance;
}
public void deposit(int amount) {
lock.lock();
try {
int newBalance = balance + amount;
Thread.sleep(5);
balance = newBalance;
} catch (InterruptedException e) {
}
finally {
lock.unlock();
}
}
}
}
提示:在对lock()的调用之后紧随一个try-catch块并且在finally子句中释放这个锁这个习惯很好,确保锁被释放。《Think in Java》中提到:lock()方法必须在try-finally语句中调用,return语句必须在try中出现,以确保unlock()不会过早发生。
synchronized和Lock的对比:《Think in Java》比较赞成使用synchronized,当需要性能优化时就使用Lock。但是给出的理由不是那么有说服力,代码多也成为了一个缺点。确实是,多了几行,对象的创建、锁的创建、try-catch-finally语句,以及释放。查了一些资料,发现Lock的功能强大的多,比如为读写分别提供了锁!所以个人想法是,当能用synchronized解决问题的就用synchronized,不行则用Lock,似乎《Think in Java》讲的是对的,大牛毕竟是大牛!
4. 原子性的讨论
原子操作是不能被线程调度机制中断的操作,一旦开始操作,那么在“上下文”切换(线程切换)之前执行完成。
误区提示:“原子操作不需要进行同步控制”是不正确的!
原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”,JVM会64位的数据分离成两个32位的操作,这就有了一个上下文切换,会造成不安全!在定义long和double时可以加上volatile,那么就会获得原子性(简单的赋值和返回操作)。
提示:不要依赖于原子性。基本类型的读取和赋值操作被认为是安全的原子性操作,但是当对象处于不稳定状态是不安全的,下面是一个示例:
package multithreading;
import java.util.concurrent.*;
public class AtomicityTest implements Runnable {
private int i = 0;
public int getValue() { return i; }
private synchronized void evenIncrement() { i++; i++; }
public void run() {
while(true)
evenIncrement();
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
AtomicityTest at = new AtomicityTest();
exec.execute(at);
while(true) {
int val = at.getValue();
if(val % 2 != 0) {
System.out.println(val);
System.exit(0);
}
}
}
}
按照逻辑,这段代码是死循环,但是现在却有输出!说明
i++递增操作不是原子性!
下面的代码使用了AtomicInteger原子类(还有其他原子类),取消了synchronized的使用,也达到了效果:
private AtomicInteger j = new AtomicInteger(0);
public int getValue() { return j.get(); }
private void evenIncrement() { j.addAndGet(2); }
public void run() {
while(true)
evenIncrement();
}
网络资料:Java 理论与实践: 正确使用 Volatile 变量
提示:volatile声明的变量,只有赋值和读取时,不需要同步对其访问,其他操作依旧需要同步访问!
5. 解决共享资源的另一种方法
在第3节中讨论了加锁的机制来防止在共享资源上产生冲突,第二种方式是根除对变量的共享:线程本地存储。
package zy.thread.demo;
import java.util.Random;
import java.util.concurrent.*;
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
private Random rand = new Random(47);
protected synchronized Integer initialValue() {
return rand.nextInt(100);
}
};
public static void increment() {
value.set(value.get() + 1);
}
public static int get() { return value.get(); }
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
for(int i = 0; i < 5; i++)
exec.execute(new Accessor(i));
TimeUnit.SECONDS.sleep(1);
exec.shutdownNow();
}
}
class Accessor implements Runnable {
private final int id;
public Accessor(int idn) { id = idn; }
public void run() {
while(!Thread.currentThread().isInterrupted()) {
ThreadLocalVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}
public String toString() {
return "#" + id + ": " +
ThreadLocalVariableHolder.get();
}
}