Java 线程中调用wait为啥一定要在同步代码块中?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 线程中调用wait为啥一定要在同步代码块中?相关的知识,希望对你有一定的参考价值。

学习线程,在做个练习

public class Blocked extends Thread
public Blocked()
start();


public void run()
try
wait();
catch (InterruptedException e)
...


public static void main(String[] args)
new Blocked();



跑起来坏掉了

Exception in thread "Thread-0" java.lang.IllegalMonitorStateException: current thread not owner
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:474)
at Blocked.run(Blocked.java:8)

把wait()放到同步代码块中就可以了:
synchronized (this)
wait();


这是为什么呢?

wait是让使用wait方法的对象等待,暂时先把对象锁给让出来,给其它持有该锁的对象用,其它对象用完后再告知(notify)等待的那个对象可以继续执行了,因此,只有在synchronized块中才有意义(否则,如果大家并不遵循同步机制,那还等谁呢?根本没人排队,也就谈不上等待和唤醒了)
以下是一个例子,用以展示这种机制:

    public class ThreadA   
        public static void main(String[] args)   
            ThreadB b = new ThreadB();  
            b.start();//主线程中启动另外一个线程  
            System.out.println("b is start....");  
            //括号里的b是什么意思,应该很好理解吧  
            synchronized(b)   
                try   
                    System.out.println("Waiting for b to complete...");  
                    b.wait();//这一句是什么意思,究竟谁等待?  
                    System.out.println("ThreadB is Completed. Now back to main thread");  
                    catch (InterruptedException e)  
              
            System.out.println("Total is :" + b.total);  
              
      
      
    class ThreadB extends Thread   
            int total;  
            public void run()   
                synchronized(this)   
                    System.out.println("ThreadB is running..");  
                    for (int i=0; i<=100; i++ )   
                        total += i;  
                      
                    System.out.println("total is " + total);  
                    notify();  
                  
              
    


运行结果:
b is start....
Waiting for b to complete...
ThreadB is running..
total is 5050
ThreadB is Completed. Now back to main thread
Total is :5050

参考技术A 楼上的回答不负责任,API中这样描述:
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。换句话说,此方法的行为就好像它仅执行 wait(0) 调用一样。
当前的线程必须拥有此对象监视器。该线程发布对此监视器的所有权并等待,直到其他线程通过调用 notify 方法,或 notifyAll 方法通知在此对象的监视器上等待的线程醒来。然后该线程将等到重新获得对监视器的所有权后才能继续执行。

对于某一个参数的版本,实现中断和虚假唤醒是可能的,而且此方法应始终在循环中使用:

synchronized (obj)
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition

此方法只应由作为此对象监视器的所有者的线程来调用

注意最后一句: 此方法只应由作为此对象监视器的所有者的线程来调用。如果不给当前对象加锁,你无法拥有当前对象。本回答被提问者采纳
参考技术B 因为这个wait就是专门为同步设计的。。。。。。。。。

专门为它设计的,不在它里边用在哪哈儿用呢??

Java:多线程的同步方式和锁

Object.wait(miliSec)/notify()/notifyAll()

线程调用wait()之后可以由notify()唤醒,如果指定了miliSec的话也可超时后自动唤醒。wait方法的调用会让当前线程放弃已经获取的object锁标志位,比如在同步代码块synchronized中调用wait(),则表示当前线程被唤醒之后需要重新获取同步代码块的锁。另外wait/notify由于要操作对象的锁标志位,因此必须在synchronized代码块中调用,否则会抛出运行时异常IllegalMonitorStateException。


wait/notify机制出现之前,生产/消费实现模型的同步一般通过while(true)轮询实现,弊端是极大耗用CPU资源做无用的轮询。在调用wait方法之前,线程需要获取当前实例对象的锁,执行wait方法返回之后,线程释放掉对象锁并进入block状态;其他线程在调用notify方法之前,也需要获取当前实例对象的锁,执行notify方法时,如果有多个线程处理block状态则从中按某规则选择一个唤醒,notify方法调用之后不会立即释放锁,要等线程的同步方法执行完毕之后才释放对象锁,因此一次notify调用只会唤醒一个线程,其他block的线程依旧处理block状态。

 1 public class App1 extends Thread {
 2     private Object lock;
 3     public App1(Object lock) {
 4         super();
 5         this.lock = lock;
 6     }
 7     @Override
 8     public void run() {
 9         try {
10             synchronized (lock) {
11                 System.out.println(Thread.currentThread().getName()
12                                   + " : start to wait.");
13                 lock.wait();
14                 System.out.println(Thread.currentThread().getName() 
15                                                + " : wait ends, execute again.");
16             }
17         } catch (Exception e) {}
18     }
19 }
20 public class App2 extends Thread {
21     private Object lock;
22     public App2(Object lock) {
23         super();
24         this.lock = lock;
25     }
26     @Override
27     public void run() {
28         synchronized (lock) {
29             System.out.println(Thread.currentThread().getName() 
30                                     + " : Start notify.");
31             lock.notify();
32             System.out.println(Thread.currentThread().getName() 
33                                     + " : notify ends, start to execute again.");
34         }
35     }
36     public static void main(String[] args) {
37         try {
38             Object lock = new Object();
39             App1 app1 = new App1(lock);
40             app1.start();
41             Thread.sleep(5000);
42             App2 app2 = new App2(lock);
43             app2.start();
44         } catch (Exception e) {}
45     }
46 }
47          

 

Thread.sleep(miliSec)

线程释放CPU使用权,并进入休眠状态一段时间miliSec,不会放弃线程的锁标志位,比如如果在同步代码块synchronized中调用sleep(),表示线程将一直持有当前同步代码块的锁,其他线程将一直等待。

 

Thread.suspend()/resume()

两个方法配套使用,suspend进入的状态必须有resume调用恢复。跟sleep()方法类似,suspend方法也不会放弃线程已经获取的object锁标志位。这对方法已经不推荐使用,因为容易造成线程自己将自己suspend起来。


Thread.yield()

表示当前线程已经获得了充分的CPU执行时间,释放CPU使用权,并重新进入队列等待执行,yield()调用不会阻塞当前线程,也不会放弃当前线程已经获取的锁标志位 ,因此yield方法仅能让跟当前线程具有同样优先级的线程有限执行。


Thread.join(miliSec)

表示当前线程需要等到join方法的调用线程执行完毕之后才能继续执行,或者是等待join方法的调用线程执行一段时间之后当前线程才能执行,内部由wait方法实现,所以线程等待开始的时候就会释放持有的对象锁。


使用synchronized关键字修饰代码块或者方法

表示这块代码为互斥区或者临界区。有两种类型的锁可以通过synchronized加到代码块或者方法上,一种是实例Object锁,一种是class锁。对于同一个ClassLoader下加载的类而言,一个类只有一把class锁,所有这个类的实例都共享一把锁;同一个类可以实现多个实例对象,也就存在多把Object锁。
synchronized是语言自带的内置独享锁(非公平锁,不管race thread排队的时间先后,通过编排字节码实现,锁为对象或者类的头标记位),而Java语言的ReentraintReadWriteLock机制是基于Abstract Queued Synchronizer的一种实现(公平/非公平锁,state加CLH队列实现),主要的实现类是ReentrantLock;Java的数据主要会在CPU、Register、Cache、Heap和Thread stack之间进行复制操作,而前面四个都是在Java Threads之间共享,因此Java的锁机制主要用于解决Racing Threads的数据一致性;


另外通过synchronized添加的锁具有可重入性,也就是只要一个线程已经获取了锁,这样只要共享同一把锁的其他synchronized修饰的代码块或者方法都可以进入,换句话说其他线程访问对其他synchronized修饰的代码块或者方法也需要等待锁的释放,因此synchronized还支持任意对象的锁,这样同一个类的不同方法可以添加不同的对象锁。

 1 public class App1 {
 2     private Object lock1 = new Object();
 3     private static Object lock2 = new Object();
 4     
 5     synchronized public void funcA() {
 6         //this object lock
 7     }
 8     public void funcB() {
 9         synchronized(this) {
10             //this object lock
11         }
12         //run something without lock
13     }
14     public void funcC(List<String> list) {
15         synchronized(list) {
16             //list object lock
17         }
18     }
19     public void funcD() {
20         synchronized(lock1) {
21             //lock1 object lock
22         }
23     }
24     public void funcE() {
25         synchronized(lock2) {
26             //lock2 static object lock
27         }
28     }
29     public void funcF() {
30         synchronized(App1.class) {
31             //App1 class lock
32         }
33     }
34     synchronized public static void funcG() {
35         //App1 class lock
36     }
37 }

 

使用ReentraintLock和ReentraintReadWriteLock实现线程的同步

java的lock机制基于Abstract Queued Synchronizer (AQS)的实现,AQS定义了多线程访问共享资源的同步器框架,常见的如ReentraintLock/Semaphore/CountDownLatch等都依赖于AQS的实现;

AQS通过维护一个FIFO队列,并且通过一个由volatile修饰的int状态值来实现锁的获取。FIFO队列中每一个Node表示一个排队线程,其保存着线程的引用和状态,然后通过三个方法分别对获取或者设置状态。

 1 private volatile int state;
 2 static final class Node {
 3     int waitStatus;
 4     Node prev;
 5     Node next;
 6     Node nextWaiter;
 7     Thread thread;
 8 }
 9 protected final int getState() {
10         return state;
11     }
12 protected final void setState(int newState) {
13         state = newState;
14     
15 protected final boolean compareAndSetState(int expect, int update) {
16         // See below for intrinsics setup to support this
17         return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
18 }
19 protected boolean tryAcquire(int arg) 
20 protected boolean tryRelease(int arg)
21 protected int tryAcquireShared(int arg)
22 protected boolean tryReleaseShared(int arg) 
23 protected boolean isHeldExclusively() 

 

通过对getState,setState和compareAndSetState的封装,AQS的继承类需要试下如下几个方法,前面两个表示获取和释放独占锁(如ReentraintLock),后面两个表示获取和释放共享锁(如Semaphore和CountDownLatch)。ReentrantLock初始化状态state=0,线程A访问同步代码的时候使用ReentrantLock.lock(),内部会调用tryAcquire尝试获取独占锁,状态变成state+1;其他线程调用ReentrantLock.lock()的时候就会失败,直到线程A调用unlock(内部为tryRelease)将状态编程state=0;如果线程A在持有独占锁的同时访问其他同步代码块,这时候state的值就会累加,需要调用unlock(内部为tryRelease)减少state的值。ReentrantLock也提供了类似wait/notify的方法,await/signal,同样的线程在调用这两个方法之前需要获得对象锁监视,也就是执行lock.lock()方法。


ReentrantLock是纯粹的独占锁,为了提升效率引入了ReentrantReadWriteLock.readLock/writeLock,读读共享,读写互斥,写写互斥。CLH队列中的节点模式分为shared和exclusive两种,当一个线程修改了state状态则表示成功获取了锁,如果线程的模式是shared则会执行一个传递读锁的过程,策略是从CLH队列的头到尾依次传递读锁,直到遇到一个模式为exclusive的写锁模式的节点,这个exclusive模式的节点需要等之前所有shared模式的节点对应的操作都执行完毕之后才会获取到锁,这就是读写锁的模式。

 1 public class App1 extends Thread {
 2     private Lock lock = new ReentrantLock();
 3     private Condition condition = lock.newCondition();
 4     public App1() {
 5         super();
 6     }
 7     @Override
 8     public void run() {
 9         try {
10             lock.lock();
11             System.out.println(Thread.currentThread().getName() + " : start to wait.");
12             condition.await();//condition.signal();
13             System.out.println(Thread.currentThread().getName()
14                    + " : wait ends, execute again.");
15         } catch (Exception e) {
16             
17         } finally {
18             lock.unlock();
19         }
20     }
21 }

 










以上是关于Java 线程中调用wait为啥一定要在同步代码块中?的主要内容,如果未能解决你的问题,请参考以下文章

为啥wait,notify和notifyAll必须在同步块或同步方法中调

为什么wait和notify方法要在同步块中调用?

java同步中,为啥要wait,又notify谁?

Java:多线程的同步方式和锁

Java学习---面试基础知识点总结

为什么wait(),notify(),notifyAll()必须在同步方法/代码块中调用?