1. 线程常用方法
-
start
这个方法让线程处于Runnable(可运行)状态。 -
yield
使当前正在执行的线程向另一个线程交出运行权。
yield方法属于一种启发式的方法,它会提醒调度器我愿意放弃当前的CPU资源。
如果CPU资源不紧张,则会忽略这种提醒。
调用yield方法会使当前线程从Running状态切换到Runnable状态。
- join
join某个线程A,会使当前线程B进入等待,知道线程A结束生命周期,或者到达给定的时间。
那么在此期间线程B是出于BLOCKED状态的。
2. 锁
在说锁之前,我们先要聊一下竞态条件(race condition)。
什么是竞态条件?
竞态条件是指两个或以上线程需要共享对同意数据的存取。
该共享数据的正确性取决于线程访问数据的次序,线程之间会相互覆盖的情况。
重入锁(ReentrantLock)
有两种机制可以防止并发访问代码块。
一种是synchronized关键字,我们稍后会提到。
另一种是使用锁对象。
Java 5开始引入了ReentrantLock类
使用ReentrantLock高呼代码块的关键代码如下:
myLock.lock();
try
{
...
}
finally
{
myLock.unlock();
}
eg:
Bank.java
public class Bank
{
...
private var bankLock = new ReentrantLock();
...
public void transfer(int from, int to, double amount) throws InterruptedException
{
bankLock.lock();
try
{
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
}
finally
{
bankLock.unlock();
}
}
...
}
3. synchronized关键字
Java中的原子性操作
在说synchronized关键字之前,我们先来了解一下什么是原子性操作?
所谓原子性操作,是指执行一系列操作时,这些操作要么全部执行,要么全部不执行。
比如:
计数器,先读取当前值,然后+1,再更新。
这就是一个典型的读-改-写的原子性操作。
原子性操作如果不能保证,就会出现线程安全的问题。
例如下面的代码:
ThreadNotSafeCount
public class ThreadNotSafeCount {
private Long value;
public Long getCount() {
return value;
}
public void addCount() {
++value;
}
}
这个类明显是线程不安全的,因为如果有多个线程同时操作的时候,会发生竞态条件。
我们把它改成如下:
ThreadSafeCount
public class ThreadSafeCount {
private Long value;
public synchronized Long getCount() {
return value;
}
public synchronized void addCount() {
++value;
}
}
synchronized
如果一个方法声明为synchronized,那么对象的锁将保护整个方法,要调用这个方法,线程必须获得内部对象锁。
synchronized是**独占锁,没有获取锁的线程会被阻塞。
也就是说,同一时间只有一个线程可以调用这个方法。
这显然大大降低了并发性。
那么既然这样做性能会不佳,有没有更好的做法呢?
答案当然是肯定的,使用非阻塞的CAS算法实现的原子性操作类AtomicLong就是一个不错的选择!
4. volatile字段
Java提供了一种弱形式的同步,那就是使用volatile关键字。
如果一个字段声明为volatile,那么编译器和虚拟机就知道该字段可能被另一个线程并发更新。
确保一个变量的更新对其它线程马上可见.
当一个变量声明为volatile时,线程在写入变量时不会把值缓存在它自己的工作内存(寄存器与高速缓存)中,而是会把值刷新回主内存。因此,当其它线程读取该共享变量时,会从主内存重新获取最新值。
例子:
private volatile boolean done;
public boolean isDone() { return done; }
public void setDone() { done = true; }
上面的代码等同于下面的代码:
private boolean done;
public synchronized boolean isDone() { return done; }
public synchronized void setDone() { done = true; }
注: volatile不能完全取代synchronized同步方法,因为它缺乏原子性
5. CAS操作
锁在并发处理中占据了一席之地,但是锁有一个最大的弱点,那就是:
当一个线程没有获取锁的时候会被阻塞挂起。
这样就会导致线程上下文的切换和重新调度的开销。
volatile关键字的出现稍稍弥补了一部分这个弱点,但是volatile关键字虽然保证了共享变量的可见性,
却不能解决类似读-改-写的这种原子性问题。
什么是CAS?
CAS即Compare and Swap。
CAS的思想很简单:三个参数,一个当前内存值V,一个旧的预期值A,一个即将更新的值B。
当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,
否则什么都不做,并返回false。
JVM中的CAS操作正是利用了提到的处理器提供的CMPXCHG指令实现的;循环CAS实现的基本思路就是循环进行CAS操作直到成功为止。
我们来看看CAS在atomic类中的应用
public final native boolean compareAndSwapObject
(Object obj, long valueOffset, Object expect, Object update);
public final native boolean compareAndSwapInt
(Object obj, long valueOffset, int expect, int update);
public final native boolean compareAndSwapLong
(Object obj, long valueOffset, long expect, long update);
atomic类,它们实现了对确认,更改再赋值操作的原子性。
AtomicInteger源码:
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
代码解释:
-
Unsafe是CAS的核心类,Java没有方法能访问底层系统,因此需要本地方法来做,Unsafe就是一个后门,被提供来直接操作内存中的数据。
-
valueOffset:变量在内存中的偏移地址,Unsafe根据偏移地址找到获取数据。
-
value被volatile修饰,保证了内存可见性。
CAS的局限性
CAS存在ABA问题:比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。
AtomicStampedReference来解决ABA问题:这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
网友评论