Java多线程 线程同步
如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你需要使用同步,并且,读写线程都必须用相同的监视器锁同步。--Brain同步规则
synchronized
- 所有对象都自动含有单一的锁,当在调用一个对象的任意synchronized方法时,此对象将被加锁。
- 对于某个特定对象来说,所有的synchronized方法共享同一个锁。所以某个线程在访问对象的一个synchronized方法时,其他线程访问该对象的任何synchronized方法都将被阻塞。
- 一个任务可以多次获得对象锁,如在调用对象的一个方法时,该方法又调用了其他的方法。JVM负责跟踪对象被加锁的次数,这也是锁的可重入性,不可重入的锁可能造成死锁。
- 每个类也有一个锁,每个类都是一个class对象,synchronized static方法可以防止对static数据的并发访问
- 在使用并发时,最好将字段设置为private,防止线程直接访问字段。
可重入锁ReentrantLock
在java.util.concurrent.locks包中定义了显式的Lock,该Lock锁需要显式的创建、锁定和释放。比起synchronized关键字,显式的Lock锁使用起来更繁琐,在使用时也更有可能出错,但有更强大的功能。locks包中包含了两种锁ReentrantLock和ReentrantReadWriteLock。
- ReentrantReadWriteLock,看了些博文了解了下。线程进入读锁的前提条件,(a)没有其他线程的写锁,(b)没有写请求或者有写请求,但调用线程和持有锁的线程是同一个;线程进入写锁的前提条件,(a)没有其他线程的读锁,(b)没有其他线程的写锁。
- ReentrantLock有两种构造,ReentrantLock(boolean fair) fair为true告诉创建的锁为公平锁,不传fair参数创建非公平锁。非公平锁是直接获取锁,没有维护等待队列;公平锁依然需要检查当前线程是否是等待队列的第一个。
public class LockTest { private int current = 0; private Lock lock = new ReentrantLock(); public int next(){ //lock.lock(); boolean flag = lock.tryLock(); if(flag){ try{ ++current; Thread.yield(); ++current; return current; }finally{ lock.unlock(); } }else{ System.out.println("locked"); return -1; } } }
显示锁可以去尝试获取锁,这是比synchronized关键字更为灵活的地方。tryLock(long timeout, TimeUnit unit)还可以设置你等待获取锁的时间。由于Lock锁更有可能出错,除非解决特殊问题,否则使用synchronized关键字即可。
volatile
使用volatile关键字可以使你定义的基本类型变量的赋值和返回操作为原子操作。
volatile可以是声明的字段具有可视性。可视性指只要你对字段进行了写操作,那么所有的读操作就都可以看到这个修改。因为线程对某一字段的修改,可能不会直接体现在内存中,其他线程也就获取不到最新的值。
volatile关键字应该慎重使用,需要进行线程同步时,首选还是synchronized关键字。
原子类
Java SE5提供了AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类。这些类以boolean compareAndSet(int expect, int update)的形式进行原子性条件的更新。这些原子类都有基本的get、set操作,用来获取和设置值。
Atomic类的设计主要用来构建java.util.concurrent中的类,常规编程还是使用锁要更安全些,但是在涉及性能调优时,这些原子类就大有用武之地了。
public class AtomicityTest implements Runnable { public static void main(String[] args) { // TODO Auto-generated method stub new Timer().schedule(new TimerTask() { @Override public void run() { // TODO Auto-generated method stub System.err.println("aborting"); System.exit(0); } }, 5000); 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); } } } private AtomicInteger i = new AtomicInteger(0); public int getValue(){ return i.get(); } private void evenIncrement(){ i.addAndGet(2); System.out.println(i); } @Override public void run(){ while(true) evenIncrement(); } }
临界区和其他对象上同步
有时我们只希望某部分代码不被多个线程同时访问,这类被同步的代码块叫临界区。临界区可以使用synchronized关键字来建立。
synchronized(syncObject) { //临界区代码 }
某个线程在进入临界区代码块时,必须获得syncObject对象上的锁。需要注意的是,syncObject既可以是对象本身this,也可以使用其他的对象。在介绍synchronized关键字时说过,所有对象都含有一个锁,临界区就是通过对象锁来实现的。这样多个线程可以同时进入同一个对象,只要该对象被访问的代码块使用了不同的对象锁。
ThreadLocal
ThreadLocal通过根除对变量的共享来防止多线程中共享资源的冲突。ThreadLocal为使用相同的变量的每个不同的线程都创建不同存储。ThreadLocal对象通常当做静态字段来存储。下面的代码,相当于每一个线程都为其设置了一个Integer id。
public class ThreadLocalTest { public static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){ protected synchronized Integer initialValue() { return 100; } }; public static void main(String[] args) { // TODO Auto-generated method stub new Timer().schedule(new TimerTask() { @Override public void run() { // TODO Auto-generated method stub System.exit(0); } }, 5000); ExecutorService exec = Executors.newCachedThreadPool(); for(int i=0; i<10; ++i){ exec.execute(new Accesor(value.get())); } exec.shutdown(); } } class Accesor implements Runnable { private int id; public Accesor(Integer id){ this.id = id.intValue(); System.out.println("init value: " + this.id); } public void run(){ while(true) id++; } }