volatile和synchronized关键字
volatile
Java支持多线程同时访问一个对象或者对象的成员变量,尤其是读操作时。
虽然对象以及成员变量是分配在共享内存中,但是为了加速程序的执行,每个线程会对目标进行拷贝。因此内存中会存在实例针对不同线程的多份拷贝。
例如针对一个实例A,线程1进行读操作,它会拷贝出和实例A完全一模一样的实例B;线程2进行读写操作,它会拷贝出和实例A完全一模一样的实例C。此时如果线程2执行写操作改变了实例C,那么原实例A和拷贝实例B也应该做相应的调整,否则会导致在程序执行中线程看到的变量并不一定是最新的。
关键字volatile修饰的字段或者成员变量,任何线程对其访问均需从共享内存中获取,而对它的改变必须同步刷新到共享内存中,并且设置所有拷贝为失效状态。这就是volatile的可见性。
注:过多的使用volatile也是没有必要的,这样会极大的降低程序执行效率。
synchronized
关键字synchronized可以修饰对象、方法或者同步块,它确保了多个线程在同一时刻,只能有一个线程处于方法或同步块中。也就是具有可见性和排他性。
监视器
任何对象都拥有自己的监视器,当一个对象被调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法中,而没有获取监视器的线程将会被阻塞在同步块或者同步方法的入口处,进入BLOCKED状态。
关键词synchronized的本质是使一个对象的监视器获取具有排他性的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
当线程获取监视器成功时,也就是获得了对象的访问权限,这就是我们常说的锁。当线程获取对象的监视器失败时,也就是该对象的锁已经被占用了,线程会进入同步队列,线程的状态变为BLOCKED。
当获得锁的线程释放锁时,该释放操作会唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的竞争获取,至于同步队列中的哪一个线程获得锁,这个是由CPU的调度策略决定的。
wait()
线程释放锁主要有两种方式,一般情况是线程中同步块或者同步方法执行完,程序自动释放锁;另外是调用wait()方法。
线程调用对象的wait()方法,释放该对象的锁,线程状态由RUNNING变成WAITING,并将线程放置到对象的等待队列。此处需要注意几点:
- 调用的是对象的wait()方法,所以仅仅释放的是该对象的锁,这样该对象的同步队列中的线程可以竞争该对象的锁了。
- 线程释放锁之后,会进入等待队列。不同于同步队列中的线程正在竞争对象的锁,等待队列中的线程暂时并不竞争锁,前者属于BLOCKED状态,后者属于WAITING状态。
- 等待队列的线程只有在收到对象的notify()或者notifyAll()才能一个或者全部转化到同步队列中,也就是状态由WAITING转化为BLOCKED,这样才能参与对象锁的竞争,否则将永远处于等待状态。
- 对象的wait()方法会释放锁,同步列队中BLOCKED的线程就会去竞争该锁,每个线程获取锁的概率是随机的,取决于CPU调度。
- notify()/notifyAll()并不释放锁,它只是告诉等待队列中的线程可以参与竞争锁了,但不是马上获得锁,也许此时锁还在自己或者其他线程手里还没释放。
public class ThreadCommunicate {
static boolean flag = false;
static Object lock = new Object();
static class Wait implements Runnable{
@Override
public void run() {
synchronized (lock){//竞争lock对象锁,获得锁就执行代码块内容,否则进入同步队列,处于阻塞状态
while (!flag){//判断条件,条件成熟则跳过while,否则调用wait()释放锁,并进入等待队列。
try {
System.out.println(Thread.currentThread() + " flag is false, wait at " + new Date());
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//条件满足时执行代码逻辑
System.out.println(Thread.currentThread() + " flag is true, running at " + new Date());
}
/*代码块结束,释放lock对象锁*/
}
}
static class Notify implements Runnable{
@Override
public void run() {
synchronized (lock){//竞争lock对象锁,获得锁就执行代码块内容,否则进入同步队列,处于阻塞状态
System.out.println(Thread.currentThread() + " hold lock, notify at " + new Date());
lock.notifyAll();//通知lock对象的等待队列,使其线程进入同步队列
flag = true;//改变条件
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/*代码块结束,释放lock对象锁*/
synchronized (lock){//竞争lock对象锁
System.out.println(Thread.currentThread() + " hold lock again at " + new Date());
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/*代码块结束,释放lock对象锁*/
}
}
public static void main(String[] args) throws InterruptedException {
Thread waitThread = new Thread(new Wait(), "WaitThread");
Thread notifyThread = new Thread(new Notify(), "NotifyThread");
waitThread.start();
TimeUnit.SECONDS.sleep(1);
notifyThread.start();
}
}
输出如下:
Thread[WaitThread,5,main] flag is false, wait at Mon Nov 11 14:37:26 CST 2019
Thread[NotifyThread,5,main] hold lock, notify at Mon Nov 11 14:37:27 CST 2019
Thread[NotifyThread,5,main] hold lock again at Mon Nov 11 14:37:32 CST 2019
Thread[WaitThread,5,main] flag is true, running at Mon Nov 11 14:37:37 CST 2019
由于同步队列中获得锁是由CPU调度控制的,NotifyThread再次获得锁和WaitThread重新获得锁顺序可能会不同。
join()
当线程终止时,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的线程。
如果一个线程执行了otherThread.join(),也就是该线程等待otherThread线程终止之后才从join()返回。注意不同于wait()方法,join()是针对线程的方法。
下面我们讨论线程A中调用线程B的join()方法过程中,线程A是否持续持有锁,还是会进入等待队列?
public class ThreadJoin {
static Thread thread = new Thread(new ThreadB(), "threadB");
static Object key = new Object();
public static void main(String[] args){
Thread threadA = new Thread(new ThreadA(), "threadA");
Thread threadC = new Thread(new ThreadC(), "threadC");
Thread threadD = new Thread(new ThreadD(), "threadD");
threadA.start();
thread.start();
threadC.start();
threadD.start();
}
static class ThreadA implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread() + "start @" + new Date());
synchronized (key){//获得key的锁
System.out.println(Thread.currentThread() + "get key @" + new Date());
try {
thread.join();//等待threadB执行完成
//此时释放threadB的锁,同时线程持有key的锁进入等待队列。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "threadB join return @" + new Date());
}
System.out.println(Thread.currentThread() + "release key @" + new Date());
}
}
static class ThreadB implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread() + "start @" + new Date());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "end @" + new Date());
}
}
static class ThreadC implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread() + "start @" + new Date());
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "try to get key @" + new Date());
synchronized (key){//尝试获取key的锁,如果失败进入同步队列
System.out.println(Thread.currentThread() + "get key success @" + new Date());
try {
TimeUnit.SECONDS.sleep(15);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread() + "release key @" + new Date());
}
}
static class ThreadD implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread() + "start @" + new Date());
try {
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "try to get threadB lock @" + new Date());
synchronized (thread){//尝试获取threadB的锁,如果失败进入同步队列
System.out.println(Thread.currentThread() + "get thread key @" + new Date());
try {
thread.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "wait return @" + new Date());
}
System.out.println(Thread.currentThread() + "release threadB lock @" + new Date());
}
}
}
执行结果如下:
Thread[threadD,5,main]start @Mon Nov 11 15:42:10 CST 2019
Thread[threadC,5,main]start @Mon Nov 11 15:42:10 CST 2019
Thread[threadB,5,main]start @Mon Nov 11 15:42:10 CST 2019
Thread[threadA,5,main]start @Mon Nov 11 15:42:10 CST 2019
Thread[threadA,5,main]get key @Mon Nov 11 15:42:10 CST 2019
Thread[threadC,5,main]try to get key @Mon Nov 11 15:42:13 CST 2019
Thread[threadD,5,main]try to get threadB lock @Mon Nov 11 15:42:16 CST 2019
Thread[threadD,5,main]get thread key @Mon Nov 11 15:42:16 CST 2019
Thread[threadD,5,main]wait return @Mon Nov 11 15:42:18 CST 2019
Thread[threadD,5,main]release threadB lock @Mon Nov 11 15:42:18 CST 2019
Thread[threadB,5,main]end @Mon Nov 11 15:42:20 CST 2019
Thread[threadA,5,main]threadB join return @Mon Nov 11 15:42:20 CST 2019
Thread[threadA,5,main]release key @Mon Nov 11 15:42:20 CST 2019
Thread[threadC,5,main]get key success @Mon Nov 11 15:42:20 CST 2019
Thread[threadC,5,main]release key @Mon Nov 11 15:42:35 CST 2019
线程A在获得key锁的情况下执行线程B的join(),在等待线程B执行完过程中线程C也会竞争key锁,同时线程D也会竞争线程B的锁。从结果上看,线程A执行线程B的join()时,释放了线程B的锁(让线程D获得锁继续执行),同时带着key的锁进入同步队列(线程B结束后线程A执行完线程B的join(),然后释放key锁,从而线程C才能获得key锁)。
JDK中Thread.join()的方法源码:
public final synchronized void join() throws InterruptedException {
while (isAlive()) {//isAlive是 join方法的本意和目标。即使中间被唤醒(虚拟唤醒),他仍然会再次调用wait(0)来等待下一次通知。
wait(0);//如果 timeout 为零,则不考虑实际时间,在获得通知前该线程将一直等待。
}
}
因此,分析清楚到底获取或者释放了哪个对象的监视器(锁)是至关重要的。