Java并发(二)——多线程数据共享
Brain的同步规则:如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那些你必须使用同步,并且读写线程都必须用相同的监视器同步。
这一部分可以先参考https://www.cnblogs.com/dolphin0520/p/3920373.html
原子性:对于看似简单的变量操作,在JVM内部可能将其拆分成多个操作比如读与写,那么如果在此过程中被中断或上下文改变,最后将得到错误的结果。
可视性:由于现代计算机通常采用缓存机制,线程并不直接修改内存而是在各自的缓存中进行操作,那么如果一个线程在缓存中修改了某值,在并未更新到内存时被其他线程访问或修改将造成错误。
Synchronized关键字
public class SynchronizedEvenGenerator extends IntGenerator{
private int currentEvenValue = 0;
public synchronized int next(){
++currentEvenValue;
Thread.yield();
++currentEvenValue;
return currentEvenValue;
}
public static void main(String[] args){
EvenChecker.test(new SynchronizedEvenGenerator());
}
}
临界区:通过synchronized关键字指定某段代码只能被单线程访问
synchronized(syncObject){
// This code can be accessed
// by only one task at a time
}
在某些时候程序的执行可能需要一些额外的条件才能正确执行。而当某一线程通过synchronized获得运行权后,此时如果不满足运行条件,可以使用wait()函数来放弃运行权并释放锁,期望其他线程能够改变状态使得该线程能够正确运行;而当其他线程改变状态后,应该调用notify()或notifyAll()来通知任意一个或所有处在wait状态的线程重新校验状态是否满足。最后需要注意的是这三个方法是基类Object的一部分,而不是Thread的一部分。因此你可以把wait()放进任何同步控制方法中,而不考虑这个类是继承自Thread还是实现了Runnable接口。实际上也只能在同步控制方法或同步控制块里调用这三个函数,否者将得到IllegalMonitorStateException异常(当前线程不是拥有者)。
public synchronized void transfer(int from, int to, double amount) throws InterruptedException{
while(accounts[from] < amount)
wait();
System.out.println(Thread.currentThread());
accounts[from] -= amount;
System.out.println("%10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.println("Total Balance: %10.2f%n", getTotalBalance());
notifyAll();
}
上例中通过wait()实现了对是否账户中拥有充足的余额进行了检验。不过synchronized只能指定一个条件,如果需要多个条件变量可以使用Lock类
Lock类
import java.util.concurrent.locks.*;
public class MutexEvenGenerator extends IntGenerator{
private int currentEvenValue = 0;
private Lock lock = new ReentrantLock();
public int next(){
lock.lock();
try{
++currentEvenValue;
Thread.yield();
++currentEvenValue;
return currentEvenValue;
}
finally{
lock.unlock();
}
}
public static void main(String[] args){
EvenChecker.test(new MutexEvenGenerator());
}
}
同理,针对多个条件可以定义多个条件变量,并使用await(),signal()以及signalAll()实现相似的功能。
private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;
public Bank(int n, double initialBalance){
accounts = new double[n];
Arrays.fills(accounts, initialBalance);
bankLock = new ReentrantLock(); // 保证不同的bank之间不会相互影响
sufficientFunds = bankLock.newCondition();
}
public void transfer(int from, int to, double amount) throws InterruptedException{
bankLock.lock();
try{
while(accounts[from] < amount)
sufficientFunds.await();
System.out.println(Thread.currentThread());
accounts[from] -= amount;
System.out.println("%10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.println("Total Balance: %10.2f%n", getTotalBalance());
sufficientFunds.signalAll();
}
finally{
bankLock.unlock();
}
}
mylock.tryLock(100, TimeUnit.MILLISECONDS):在指定时间内尝试获取锁,如果不成功返回false,从而使得线程能够进行判断从而实现流程控制。
同时,Java还允许分别定义读写锁,实现对特定读写操作的控制。
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
volatile关键字
其保证修饰的变量在缓存中的修改将立即被写入到主存中,而读取操作就发生在主存中。但volatile并不保证操作的原子性,即其只能保证简单数据类型的简单操作的并发正确性。
原子类
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.*;
public class AtomicIntegerTest implements Runnable {
private AtomicInteger i = new AtomicInteger(0);
public int getValue() { return i.get(); }
private void evenIncrement() { i.addAndGet(2); }
public void run() {
while(true)
evenIncrement();
}
public static void main(String[] args){
new Timer().schedule(new TimerTask(){
public void run(){
System.err.println("Aborting");
System.exit(0);
}
}, 5000); // Terminate after 5 seconds
ExecutorService exec = Executors.newCachedThreadPool();
AtomicIntegerTest ait = new AtomicIntegerTest();
exec.execute(ait);
while(true){
int val = ait.getValue();
if(val % 2 != 0){
System.out.println(val);
System.exit(0);
}
}
}
}/* Output
Aborting
*///:~
线程局部变量(ThreadLocal)
线程局部变量是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储,实现每个线程都用于相同对象相同域的不同存储块,实现不同线程数据分隔。
import java.util.concurrent.*;
import java.util.*;
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();
}
}
public class ThreadLocalVariableHolder{
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
private Random rand = new Random(47);
protected synchronized Integer initialValue(){
return rand.nextInt(10000);
}
};
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); // Run for a while
exec.shutdownNow(); // All Accessors will quit
}
}/* Output
#0: 9259
#3: 1862
#1: 556
#4: 962
#2: 6694
#4: 963
#1: 557
#3: 1863
#0: 9260
#3: 1864
#1: 558
#4: 964
#2: 6695
...
*///:~
通过ThreadLocal实现了每个线程拥有自己独立的value域。
并发安全容器
除了人工实现正确的线程并发,还可以使用安全的容器——BlockingQueue、LinkedBlockingQueue、PipedReader、PipedWriter等。这样,安全容器能够保证并发线程的安全性。
死锁
死锁的发生必须满足以下四个条件:
- 资源必须互斥。
- 至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有的资源。
- 资源不能被任务抢占,任务必须把资源释放当做普通事件。
- 必须有循环等待。
因此,处理死锁只需要破坏这四个条件中的任何一个就能够解决死锁问题。
虽然Java给出了许多处理并发的机制,但还是有可能出现死锁。因此必须仔细涉及程序,以确保不会出现死锁。