精华内容
下载资源
问答
  • Java线程等待唤醒机制(加深理解)

    万次阅读 多人点赞 2019-08-04 16:28:06
    这里就用到了线程等待唤醒机制,下面具体看一下。 等待唤醒机制示例 下面代码是一个简单的线程唤醒机制示例,主要就是在Activity启动的时候初始化并start线程,线程start后会进入等待状态,在onResume方法中执行...

    今天看源码的时候遇到这样一个场景,某线程里面的逻辑需要等待异步处理结果返回后才能继续执行。或者说想要把一个异步的操作封装成一个同步的过程。这里就用到了线程等待唤醒机制,下面具体看一下。

    等待唤醒机制示例

    下面代码是一个简单的线程唤醒机制示例,主要就是在Activity启动的时候初始化并start线程,线程start后会进入等待状态,在onResume方法中执行notify方法唤醒线程。通过这样的方式模拟异步唤醒线程——线程等待唤醒机制。
     

    public class ThreadDemo extends AppCompatActivity {
        private final static String TAG = ThreadDemo.class.getSimpleName();
    
        private Object mLock = new Object();
        private Thread mThread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (mLock) {
                    Log.i(TAG,"state 1 = " + mThread.getState());
                    try {
                        mLock.wait(10 * 1000);
                        Log.i(TAG,"state 2 = " + mThread.getState());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            Log.i(TAG,"state 3 = " + mThread.getState());
            mThread.start();
            Log.i(TAG,"state = 4 " + mThread.getState());
    
        }
    
        @Override
        protected void onStart() {
            super.onStart();
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            synchronized(mLock) {
                Log.i(TAG,"state = 5 " + mThread.getState());
                mLock.notify();
                Log.i(TAG,"state = 6 " + mThread.getState());
            }
        }
    
        @Override
        protected void onPause() {
            super.onPause();
            Log.i(TAG,"state = 7 " + mThread.getState());
        }
    
        @Override
        protected void onStop() {
            super.onStop();
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
        }
    }

    Log如下:

    09-11 17:31:29.577 32658-32658/com.android.peter.threaddemo I/ThreadDemo: state 3 = NEW
    09-11 17:31:29.578 32658-32658/com.android.peter.threaddemo I/ThreadDemo: state = 4 RUNNABLE
    09-11 17:31:29.578 32658-32695/com.android.peter.threaddemo I/ThreadDemo: state 1 = RUNNABLE
    09-11 17:31:29.588 32658-32658/com.android.peter.threaddemo I/ThreadDemo: state = 5 TIMED_WAITING
    09-11 17:31:29.588 32658-32658/com.android.peter.threaddemo I/ThreadDemo: state = 6 BLOCKED
    09-11 17:31:29.588 32658-32695/com.android.peter.threaddemo I/ThreadDemo: state 2 = RUNNABLE
    09-11 17:31:40.276 32658-32658/com.android.peter.threaddemo I/ThreadDemo: state = 7 TERMINATED

    为什么可以这么用


    我当时看完后产生一个疑问,就是run方法的synchronized代码块不是被锁住了处于TIMED_WAITING状态了么,按理说同步锁应该没有被释放啊,为什么onResume中的synchronized代码块可以被执行从而唤醒线程。 

    这是因为wait是Object类的方法,当被调用时即释放资源也释放锁;而sleep是Thread类的方法,当被调用时只释放资源不释放锁。之前都是用的sleep来使线程进入TIMED_WAITING状态的,所以一直的认知是synchronized代码段执行完毕才会释放锁,然后再执行下一个synchronized代码段。经过本次学习对synchronized同步机制有了新的更深刻的认识。

    java线程的状态


    下面是Thread类中枚举的六种线程状态,借此机会顺便学习一下。
     

        libcore/ojluni/src/main/java/java/lang/Thread.java
        public enum State {
            /**
             * Thread state for a thread which has not yet started.
             */
            NEW,
    
            /**
             * Thread state for a runnable thread.  A thread in the runnable
             * state is executing in the Java virtual machine but it may
             * be waiting for other resources from the operating system
             * such as processor.
             */
            RUNNABLE,
    
            /**
             * Thread state for a thread blocked waiting for a monitor lock.
             * A thread in the blocked state is waiting for a monitor lock
             * to enter a synchronized block/method or
             * reenter a synchronized block/method after calling
             * {@link Object#wait() Object.wait}.
             */
            BLOCKED,
    
            /**
             * Thread state for a waiting thread.
             * A thread is in the waiting state due to calling one of the
             * following methods:
             * <ul>
             *   <li>{@link Object#wait() Object.wait} with no timeout</li>
             *   <li>{@link #join() Thread.join} with no timeout</li>
             *   <li>{@link LockSupport#park() LockSupport.park}</li>
             * </ul>
             *
             * <p>A thread in the waiting state is waiting for another thread to
             * perform a particular action.
             *
             * For example, a thread that has called <tt>Object.wait()</tt>
             * on an object is waiting for another thread to call
             * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
             * that object. A thread that has called <tt>Thread.join()</tt>
             * is waiting for a specified thread to terminate.
             */
            WAITING,
    
            /**
             * Thread state for a waiting thread with a specified waiting time.
             * A thread is in the timed waiting state due to calling one of
             * the following methods with a specified positive waiting time:
             * <ul>
             *   <li>{@link #sleep Thread.sleep}</li>
             *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
             *   <li>{@link #join(long) Thread.join} with timeout</li>
             *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
             *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
             * </ul>
             */
            TIMED_WAITING,
    
            /**
             * Thread state for a terminated thread.
             * The thread has completed execution.
             */
            TERMINATED;
        }

    结合上面代码运行的log来理解一下线程的各个状态。

    • NEW(初始化状态) 

    线程通过new初始化完成到调用start方法前都处于等待状态。

    • RUNNABLE(可执行状态) 

    线程执行start方法后就处于可以行状态。

    • BLOCKED(阻塞状态) 

    notify方法被调用后线程被唤醒,但是这时notify的synchronized代码段并没有执行完,同步锁没有被释放,所以线程处于BLOCKED状态。直到notify的synchronized代码段执行完毕锁被释放,线程才回到wait所在的synchronized代码段继续执行。

    • WAITING(等待状态) 

    调用sleep或是wait方法后线程处于WAITING状态,等待被唤醒。

    • TIMED_WAITING(等待超时状态) 

    调用sleep或是wait方法后线程处于TIMED_WAITING状态,等待被唤醒或时间超时自动唤醒。

    • TERMINATED(终止状态) 

    在run方法结束后线程处于结束状态。

    线程的状态图

    1. 初始状态

    实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

    2.1. 就绪状态

    就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
    调用线程的start()方法,此线程进入就绪状态。
    当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
    当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
    锁池里的线程拿到对象锁后,进入就绪状态。
    2.2. 运行中状态

    线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。

    3. 阻塞状态

    阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

    4. 等待

    处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

    5. 超时等待

    处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

    6. 终止状态

    1. 当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
    2. 在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

    等待队列

    • 调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) 代码段内。
    • 与等待队列相关的步骤和图
       

    1. 线程1获取对象A的锁,正在使用对象A。
    2. 线程1调用对象A的wait()方法。
    3. 线程1释放对象A的锁,并马上进入等待队列。
    4. 锁池里面的对象争抢对象A的锁。
    5. 线程5获得对象A的锁,进入synchronized块,使用对象A。
    6. 线程5调用对象A的notifyAll()方法,唤醒所有线程,所有线程进入同步队列。若线程5调用对象A的notify()方法,则唤醒一个线程,不知道会唤醒谁,被唤醒的那个线程进入同步队列。
    7. notifyAll()方法所在synchronized结束,线程5释放对象A的锁。
    8. 同步队列的线程争抢对象锁,但线程1什么时候能抢到就不知道了。 

    同步队列状态

    • 当前线程想调用对象A的同步方法时,发现对象A的锁被别的线程占有,此时当前线程进入同步队列。简言之,同步队列里面放的都是想争夺对象锁的线程。
    • 当一个线程1被另外一个线程2唤醒时,1线程进入同步队列,去争夺对象锁。
    • 同步队列是在同步的环境下才有的概念,一个对象对应一个同步队列。
    • 线程等待时间到了或被notify/notifyAll唤醒后,会进入同步队列竞争锁,如果获得锁,进入RUNNABLE状态,否则进入BLOCKED状态等待获取锁。

    几个方法的比较

    1. Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
    2. Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
    3. thread.join()/thread.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的)。
    4. obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
    5. obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
    6. LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines), 当前线程进入WAITING/TIMED_WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒。

    sleep()方法与yield()方法区别:

    • sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程优先级。yield()方法只会给同优先级或更高优先级线程执行机会。
    • sleep()方法将当前线程转入阻塞状态,而yield()强制将当前线程转入就绪状态,因此完全可能某个线程调用yield()后立即再次获得CPU资源。
    • sleep()方法申明抛出InterruptException异常,要么捕捉要么显示抛出,而yield()没有申明抛出任何异常。
    • sleep()比yield()有更好的移植性,不建议yield()控制并发线程执行。

     

    疑问

    等待队列里许许多多的线程都wait()在一个对象上,此时某一线程调用了对象的notify()方法,那唤醒的到底是哪个线程?随机?队列FIFO?or sth else?Java文档就简单的写了句:选择是任意性的(The choice is arbitrary and occurs at the discretion of the implementation)。
     

    线程的底层实现


    在Window系统和Linux系统上,Java线程的实现是基于一对一的线程模型,所谓的一对一模型,实际上就是通过语言级别层面程序去间接调用系统内核的线程模型,即我们在使用Java线程时,Java虚拟机内部是转而调用当前操作系统的内核线程来完成当前任务。

    这里需要了解一个术语,内核线程(Kernel-Level Thread,KLT),它是由操作系统内核(Kernel)支持的线程,这种线程是由操作系统内核来完成线程切换,内核通过操作调度器(Scheduler)进而对线程执行调度,并将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这也就是操作系统可以同时处理多任务的原因。

    由于我们编写的多线程程序属于语言层面的,程序一般不会直接去调用内核线程,取而代之的是一种轻量级的进程(Light Weight Process),也是通常意义上的线程,由于每个轻量级进程都会映射到一个内核线程,因此我们可以通过轻量级进程调用内核线程,进而由操作系统内核将任务映射到各个处理器,这种轻量级进程与内核线程间1对1的关系就称为一对一的线程模型。如下图 

    如图所示,每个线程最终都会映射到CPU中进行处理,如果CPU存在多核,那么就可以并行执行多个线程任务。


    延伸:Java并发编程——线程同步和等待唤醒机制

    原文:https://blog.csdn.net/huaxun66/article/details/77942389

    线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要原因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据。 

    因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个名称叫互斥锁,即能达到互斥访问目的的锁。

    线程同步

    synchronized

    在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能),这点确实也是很重要的。

    synchronized 修饰方法

    方法可以是实例方法,也可以是静态方法。
     

    public class AccountingSync implements Runnable{
        //共享资源(临界资源)
        static int i=0;
        /**
         * synchronized 修饰实例方法
         */
        public synchronized void increase(){
            i++;
        }
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                increase();
            }
    
        public static void main(String[] args) throws InterruptedException {
            AccountingSync instance=new AccountingSync();
            Thread t1=new Thread(instance);
            Thread t2=new Thread(instance);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(i);
        }
        /**
         * 输出结果:
         * 2000000
         */
    }

    上面synchronized修饰的是实例方法increase(),这样的情况下,当前线程的锁便是实例对象instance,注意Java中的线程同步锁可以是任意对象。

    当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁。但其他线程还是可以访问该实例对象的其他非synchronized方法。

    如果我们改下代码:

    //new新实例
    Thread t1=new Thread(new AccountingSync());
    Thread t2=new Thread(new AccountingSync());

    虽然我们使用synchronized修饰了increase()方法,但却new了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此t1和t2线程使用的是不同的锁,因此线程安全是无法保证的。解决这种困境的方式是将synchronized作用于静态的increase()方法,这样对象锁就是当前类对象,无论创建多少个实例对象,但对于类的对象有且只有一个,所以在这样的情况下对象锁就是唯一的。
     

    public class AccountingSync implements Runnable{
        static int i=0;
        /**
         * 作用于静态方法,锁是当前class对象,也就是
         * AccountingSync类对应的class对象
         */
        public static synchronized void increase(){
            i++;
        }
    
        @Override
        public void run() {
            for(int j=0;j<1000000;j++){
                increase();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            //new新实例
            Thread t1=new Thread(new AccountingSync());
            Thread t2=new Thread(new AccountingSync());
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(i);
        }
    }

    synchronized 同步代码块

    除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块:

    public class AccountingSync implements Runnable{
        static AccountingSync instance=new AccountingSync();
        static int i=0;
        @Override
        public void run() {
            //省略其他耗时操作....
            //使用同步代码块对变量i进行同步操作,锁对象为instance
            synchronized(instance){
                for(int j=0;j<1000000;j++){
                     i++;
                  }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            Thread t1=new Thread(instance);
            Thread t2=new Thread(instance);
            t1.start();t2.start();
            t1.join();t2.join();
            System.out.println(i);
        }
    }

    当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:

    //this,当前实例对象锁
    synchronized(this){
        for(int j=0;j<1000000;j++){
            i++;
        }
    }
    
    //class对象锁
    synchronized(AccountingSync.class){
        for(int j=0;j<1000000;j++){
            i++;
        }
    }

    在Java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。

    Lock

    synchronized属于隐式锁,即锁的持有与释放都是隐式的,我们无需干预,接下来我们讲讲显式锁,即锁的持有和释放都必须由我们手动编写。在Java 1.5中,官方在concurrent并发包中加入了Lock接口,该接口中提供了lock()方法和unLock()方法对显式加锁和显式释放锁操作进行支持,简单了解一下代码编写,如下:
     

    Lock lock = new ReentrantLock();
    lock.lock();
    try{
        //临界区......
    }finally{
        lock.unlock();
    }

    正如代码所示(ReentrantLock是Lock的实现类),当前线程使用lock()与unlock()对临界区进行包围,其他线程由于无法持有锁将无法进入临界区直到当前线程释放锁,注意unlock()操作必须在finally代码块中,这样可以确保即使临界区执行抛出异常,线程最终也能正常释放锁,Lock接口还提供了锁以下相关方法:
     

    public interface Lock {
        //加锁
        void lock();
    
        //解锁
        void unlock();
    
        //可中断获取锁,与lock()不同之处在于可响应中断操作,即在获
        //取锁的过程中可中断,注意synchronized在获取锁时是不可中断的
        void lockInterruptibly() throws InterruptedException;
    
        //尝试非阻塞获取锁,调用该方法后立即返回结果,如果能够获取则返回true,否则返回false
        boolean tryLock();
    
        //根据传入的时间段获取锁,在指定时间内没有获取锁则返回false,如果在指定时间内当前线程未被中并断获取到锁则返回true
        boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    
        //获取等待通知组件,该组件与当前锁绑定,当前线程只有获得了锁
        //才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
        Condition newCondition();
    }

    可见Lock对象锁还提供了synchronized所不具备的其他同步特性,如等待可中断锁的获取(synchronized在等待获取锁时是不可中断的),超时中断锁的获取,可实现公平锁,等待唤醒机制的多条件变量Condition等,这也使得Lock锁在使用上具有更大的灵活性。下面进一步分析Lock的实现类重入锁ReetrantLock。

    重入锁ReetrantLock,JDK 1.5新增的类,实现了Lock接口,作用与synchronized关键字相当,但比synchronized更加灵活。ReetrantLock本身也是一种支持重进入的锁,即该锁可以支持一个线程对资源重复加锁,同时也支持公平锁与非公平锁。所谓的公平与非公平指的是在请求先后顺序上,先对锁进行请求的就一定先获取到锁,那么这就是公平锁,反之,如果对于锁的获取并没有时间上的先后顺序,如后请求的线程可能先获取到锁,这就是非公平锁,一般而言非,非公平锁机制的效率往往会胜过公平锁的机制,但在某些场景下,可能更注重时间先后顺序,那么公平锁自然是很好的选择。
     

    public class ReenterLock implements Runnable{
        public static ReentrantLock lock=new ReentrantLock();
        public static int i=0;
        @Override
        public void run() {
            for(int j=0;j<10000000;j++){
                //支持重入锁
                lock.lock();
                lock.lock();
                try{
                    i++;
                }finally{
                    //执行两次解锁
                    lock.unlock();
                    lock.unlock();
                }
            }
        }
        public static void main(String[] args) throws InterruptedException {
            ReenterLock tl=new ReenterLock();
            Thread t1=new Thread(tl);
            Thread t2=new Thread(tl);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            //输出结果:20000000
            System.out.println(i);
        }
    }

    等待唤醒机制


    进入临界区时,却发现在某一个条件满足之后,它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程,条件对象又称作条件变量。

    synchronized


    synchronized等待唤醒机制主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常。
     

    synchronized (obj) {
           obj.wait();
           obj.notify();
           obj.notifyAll();         
     }

    需要特别理解的一点是,与sleep方法不同的是wait方法调用完成后,线程将被阻塞,wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。

    notify/notifyAll方法只是解除了等待线程的阻塞,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。

    假设一个场景我们需要用银行转账:
     

    public class Bank {
    private double[] accounts;
    
        //我们首先写了银行的类,它的构造函数需要传入账户数量和账户金额
        public Bank(int n,double initialBalance){
            accounts=new double[n];
            for (int i=0;i<accounts.length;i++){
                accounts[i]=initialBalance;
            }
        }
    
        //转账方法,from是转账方,to是接收方,amount转账金额
        public synchronized void transfer(int from,int to,int amount) throws InterruptedException{
        while (accounts[from]<amount) {
            wait();
        }
        //转账的操作
        accounts[from] = accounts[from] - amount;
        accounts[to] = accounts[to] + amount;
        notifyAll();   
        } 
    }

    Lock


    Lock等待唤醒机制与synchronized有点不同,需要用到Condition条件对象。一个锁对象拥有多个相关的条件对象,可以用newCondition方法获得一个条件对象,调用条件对象的signal/signalAll和await方法。同样这三个方法必须被Lock包住。
     

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    lock.lock();
    try{
         condition.await();
         condition.signal();
         condition.signalAll();
    }finally{
        lock.unlock();
    }

    使用Lock等待唤醒机制修改上面的例子:

    public class Bank {
    private double[] accounts;
        private Lock bankLock;
        private Condition condition;
        //构造函数
        public Bank(int n,double initialBalance){
            accounts=new double[n];
            //得到锁
            bankLock=new ReentrantLock();
            //得到条件对象
            condition=bankLock.newCondition();
            for (int i=0;i<accounts.length;i++){
                accounts[i]=initialBalance;
            }
        }
    
        //转账方法
        public void transfer(int from,int to,int amount) throws InterruptedException {
            bankLock.lock();
            try{
                while (accounts[from]<amount){
                    //阻塞当前线程,并放弃锁
                    condition.await();
                }
                //转账的操作
                accounts[from] = accounts[from] - amount;
                accounts[to] = accounts[to] + amount;
                condition.signalAll(); 
            } finally {
                bankLock.unlock();
            }
        }
    }

    一旦一个线程调用了await方法,它就会进入该条件的等待集。当锁可用时,该线程不能马上解锁,相反他处于阻塞状态,直到另一个线程调用了同一个条件上的signalAll方法时为止。当另一个线程转账给我们此前的转账方时,需要调用condition.signalAll();该调用会重新激活因为这一条件而等待的所有线程。

    当一个线程调用了await方法他没法重新激活自身,并寄希望于其他线程来调用signalAll方法来激活自身,如果没有其他线程来激活等待的线程,那么就会产生死锁现象,如果所有的其他线程都被阻塞,最后一个活动线程在解除其他线程阻塞状态前调用await,那么它也被阻塞,就没有任何线程可以解除其他线程的阻塞,程序就被挂起了。

    当调用signalAll方法时并不是立即激活一个等待线程,它仅仅解除了等待线程的阻塞,以便这些线程能够在当前线程释放锁后,通过竞争实现对对象的访问。还有一个方法是signal,它则是随机解除某个线程的阻塞。

    Lock相比synchronized优劣


    优势: 
    Lock对象锁提供了synchronized所不具备的同步特性

    • 等待可中断锁的获取 

    等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。

    • 可实现公平锁 

    公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。

    • 等待唤醒机制的多条件变量Condition 

    锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法只能实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可。

    Lock可通过多个Condition实例对象建立更加精细的线程控制。使用condition.signalAll(); 可以指定解除哪些等待线程的阻塞,是通过condition关联的。而notifyAll只会解除所有等待线程的阻塞。

    如下面例子:银行账户只能有取款和存款操作,每次取款和存款金额必须相同,当账户有钱时只允许取款,不允许存款,账户没钱时只允许存款,不允许取款。
     

    public class Bank {
        private double[] accounts;
        private amount;
        private Lock bankLock;
        private Condition drawCondition; //取款条件
        private Condition depositeCondition; //存款条件
        //构造函数
        public Bank(int n,double initialBalance){
            accounts=new double[n];
            //得到锁
            bankLock=new ReentrantLock();
            //分别得到条件对象
            drawCondition=bankLock.newCondition();
            depositeCondition=bankLock.newCondition();
    
            for (int i=0;i<accounts.length;i++){
                accounts[i]=initialBalance;
            }
            amount=initialBalance;
        }
    
        //取款方法
        public void draw(int from) throws InterruptedException {
            bankLock.lock();
            try{
                while (accounts[from]<amount){
                    //阻塞当前线程,并放弃锁
                    drawCondition.await();
                }
                //取款的操作
                accounts[from] = accounts[from] - amount;
                depositeCondition.signalAll();
            } finally {
                bankLock.unlock();
            }
        }
    
        //存款方法
        public void deposite(int to) throws InterruptedException {
            bankLock.lock();
            try{
                while (accounts[to]>=amount){
                    //阻塞当前线程,并放弃锁
                    depositeCondition.await();
                }
                //存款的操作
                accounts[to] = accounts[to] + amount;
                drawCondition.signalAll();
            } finally {
                bankLock.unlock();
            }
        }
    }

    我们通过两个Condition对象单独控制取款线程与存款线程,这样可以避免取款线程在唤醒线程时唤醒的还是取款线程,如果是通过synchronized的等待唤醒机制实现的话,就可能无法避免这种情况,毕竟同一个锁,对于synchronized关键字来说只能有一组等待唤醒队列,而不能像Condition一样,同一个锁拥有多个等待队列。

    缺点: 
    编程稍复杂。

    synchronized底层实现

    Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步(同步方法)都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。

    下面先来了解一个概念Java对象头,这对深入理解synchronized实现原理非常关键。

    理解Java对象头与Monitor

    在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下: 

    • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
    • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。
    • 对象头:它实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark 

    Word 和 Class Metadata Address 组成,其结构说明如下表:

    由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间: 

    这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

    ObjectMonitor() {
        _header       = NULL;
        _count        = 0; //记录个数
        _waiters      = 0,
        _recursions   = 0;
        _object       = NULL;
        _owner        = NULL; //持有ObjectMonitor对象的线程
        _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
      }

    ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示: 

    由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因,下面我们将进一步分析synchronized在字节码层面的具体语义实现。

    synchronized同步代码块


    现在我们重新定义一个synchronized修饰的同步代码块,在代码块中操作共享变量i,如下:

    public class SyncCodeBlock {
       public int i;
       public void syncTask(){
           //同步代码块
           synchronized (this){
               i++;
           }
       }
    }

    编译上述代码并使用javap反编译后得到字节码如下:

    Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
      Last modified 2017-6-2; size 426 bytes
      MD5 checksum c80bc322c87b312de760942820b4fed5
      Compiled from "SyncCodeBlock.java"
    public class com.zejian.concurrencys.SyncCodeBlock
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
      //........省略常量池中数据
      //构造函数
      public com.zejian.concurrencys.SyncCodeBlock();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 7: 0
      //===========主要看看syncTask方法实现================
      public void syncTask();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=3, args_size=1
             0: aload_0
             1: dup
             2: astore_1
             3: monitorenter  //注意此处,进入同步方法
             4: aload_0
             5: dup
             6: getfield      #2             // Field i:I
             9: iconst_1
            10: iadd
            11: putfield      #2            // Field i:I
            14: aload_1
            15: monitorexit   //注意此处,退出同步方法
            16: goto          24
            19: astore_2
            20: aload_1
            21: monitorexit //注意此处,退出同步方法
            22: aload_2
            23: athrow
            24: return
          Exception table:
          //省略其他字节码.......
    }
    SourceFile: "SyncCodeBlock.java"

    我们主要关注字节码中的如下代码

    3: monitorenter  //进入同步方法
    //..........省略其他  
    15: monitorexit   //退出同步方法
    16: goto          24
    //.........省略其他
    21: monitorexit //退出同步方法

    从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor,重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

    synchronized同步方法


    方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。下面我们看看字节码层面如何实现:

    public class SyncMethod {
       public int i;
       public synchronized void syncTask(){
               i++;
       }
    }

    使用javap反编译后的字节码如下:

    Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class
      Last modified 2017-6-2; size 308 bytes
      MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
      Compiled from "SyncMethod.java"
    public class com.zejian.concurrencys.SyncMethod
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool;
    
       //省略没必要的字节码
      //==================syncTask方法======================
      public synchronized void syncTask();
        descriptor: ()V
        //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
        flags: ACC_PUBLIC, ACC_SYNCHRONIZED
        Code:
          stack=3, locals=1, args_size=1
             0: aload_0
             1: dup
             2: getfield      #2                  // Field i:I
             5: iconst_1
             6: iadd
             7: putfield      #2                  // Field i:I
            10: return
          LineNumberTable:
            line 12: 0
            line 13: 10
    }
    SourceFile: "SyncMethod.java"

    从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。

    Java虚拟机对synchronized的优化


    锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

    偏向锁


    偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

    轻量级锁


    倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

    自旋锁


    轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

    锁消除


    消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。
     

    public class StringBufferRemoveSync {
    
        public void add(String str1, String str2) {
            //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
            //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
            StringBuffer sb = new StringBuffer();
            sb.append(str1).append(str2);
        }
    
        public static void main(String[] args) {
            StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
            for (int i = 0; i < 10000000; i++) {
                rmsync.add("abc", "123");
            }
        }
    }

    无锁同步

    在谈论无锁概念时,总会关联起乐观派与悲观派,对于乐观派而言,他们认为事情总会往好的方向发展,总是认为坏的情况发生的概率特别小,可以无所顾忌地做事,但对于悲观派而已,他们总会认为发展事态如果不及时控制,以后就无法挽回了,即使无法挽回的局面几乎不可能发生。这两种派系映射到并发编程中就如同加锁与无锁的策略,即加锁是一种悲观策略,无锁是一种乐观策略,因为对于加锁的并发程序来说,它们总是认为每次访问共享资源时总会发生冲突,因此必须对每一次数据操作实施加锁策略。而无锁则总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为CAS的技术来保证线程执行的安全性,这项CAS技术就是无锁策略实现的关键,下面我们进一步了解CAS技术的奇妙之处。

    CAS


    CAS的全称是Compare And Swap 即比较交换,CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。其算法核心思想如下 
    执行函数:CAS(V,E,N) 
    其包含3个参数

    • V表示要更新的变量
    • E表示预期值
    • N表示新值


    如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。通俗的理解就是CAS操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作。

    由于Unsafe类不是提供给用户程序调用的类(Unsafe.getUnsafe()的代码中限制了只有启动类加载器(Bootstrap ClassLoader)加载的Class才能访问它),因此,如果不采用反射手段,我们只能通过其他的Java API来间接使用它,如J.U.C包里面的整数原子类AtomicInteger,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作。

    public class Test {
        public AtomicInteger inc = new AtomicInteger();
    
        public void increase() {
            inc.getAndIncrement();
        }
    
        public static void main(String[] args) {
            final Test test = new Test();
            for(int i=0;i<10;i++){
                new Thread(){
                    public void run() {
                        for(int j=0;j<1000;j++)
                            test.increase();
                    };
                }.start();
            }
    
            while(Thread.activeCount()>1)  //保证前面的线程都执行完
                Thread.yield();
            System.out.println(test.inc);
        }
    }
    //输出10000

    ABA


    尽管CAS看起来很美,但显然这种操作无法涵盖互斥同步的所有使用场景,并且CAS从语义上来说并不是完美的,存在这样的一个逻辑漏洞:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那我们就能说它的值没有被其他线程改变过了吗?如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。J.U.C包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。不过目前来说这个类比较“鸡肋”,大部分情况下ABA问题不会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
     

    展开全文
  • java线程等待/通知机制及中断

    千次阅读 2016-10-30 15:21:06
     在线程交互中经常需要对其进行一些控制,希望人为地能够让线程按理想路线发展,在满足某条件时进行执行操作而发生变化时,停止等待。 1、 使用sleep  在 if ( ) { } else { } 中使用sleep 对线程进行停止等待一段...

    一、等待/通知机制

     在线程交互中经常需要对其进行一些控制,希望人为地能够让线程按理想路线发展,在满足某条件时进行执行操作而发生变化时,停止等待。

    1、 使用sleep 

    在 if ( ) { } else { }  中使用sleep 对线程进行停止等待一段时间。   弊端:正常情况下 无法客观预知需要等待的时间,在刻意睡眠一段时间后 很可能发现 依旧不适合由此线程执行之后的操作,或者睡眠过久。

    2、 使用 while + sleep   循环判断条件 使其睡眠    弊端:虽然能加快判断条件的变化,但依旧难以确保及时性,会造成无端浪费。

    3、wait +notify :在某条件发生情况下,线程A调用对象O 的wait() 方法进入等待状态,当线程B调用对象O的notify() 或者notifyAll()方法后,线程A会接受通知,从其wait方法返回,执行后续操作。

    java.lang.Obejct  :

    notify()  通知一个在对象上等待的线程,使其从wait方法返回(前提是该线程获取到对象的锁)

    notifyAll() 通知所有等待在该对象上的线程 (注意,notify等通知时 不会释放当前对象锁)

    wait()  调用该方法的线程进入Waiting状态,只有被中断或者由其他线程通知唤醒才能继续(wait会导致线程释放对象锁)

    wait(long) 超时等待一段时间,等待xx毫秒,若没有收到通知 则超时返回

    wait(long,int)超时精确到纳秒

    例:

    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.concurrent.TimeUnit;
    
    public class WaitAndNotify {
    	static boolean flag=true;
    	static Object lock =new Object();
    	public static void main(String[] args) throws InterruptedException {
    		Thread waitThread=new Thread(new Wait(),"waitThread");
    		waitThread.start();
    		TimeUnit.SECONDS.sleep(2);
    		Thread notifyThread=new Thread(new Notify(),"notifyThread");
    		notifyThread.start();
    	}
    static class Wait implements Runnable{
    	@Override
    	public void run() {
    		synchronized (lock) {
    			//同步代码块
    			while (flag) {
    				try {
    					System.out.println(Thread.currentThread().getName()+" flag=true. wait@"+
    							new SimpleDateFormat("HH:mm:ss").format(new Date()));
    					lock.wait();<span style="white-space:pre">	</span>System.out.println("啊啊?");
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    			}
    			//跳出while时
    			System.out.println(Thread.currentThread().getName()+" flag=false. wait@"+
    					new SimpleDateFormat("HH:mm:ss").format(new Date()));
    		}
    	}
     }
    static class Notify implements Runnable{
    	@Override
    	public void run() {
    		synchronized (lock) {
    			//获取lock对象锁,然后通知唤醒
    			System.out.println(Thread.currentThread().getName()+" hold lock. notify@"+
    					new SimpleDateFormat("HH:mm:ss").format(new Date()));
    			lock.notifyAll();
    			flag=false;
    			try {
    				Thread.sleep(3000);//notify之后线程睡眠3秒,验证Wait类不能马上输出“啊啊?”
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		} //同步代码块结束后 释放锁
    		synchronized (lock) {
    			//再次加锁
    			System.out.println(Thread.currentThread().getName()+" hold lock again. @"+
    					new SimpleDateFormat("HH:mm:ss").format(new Date()));
    			try {
    				Thread.sleep(2000);
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		}
    	}
    }
    }
    
    运行结果:(注:在notifyThread里的两个同步代码块之间 可能会发生lock被Wait获取 而使输出结果的第三、四行互换)

    waitThread flag=true. wait@16:00:44
    notifyThread hold lock. notify@16:00:46
    notifyThread hold lock again. @16:00:49
    啊啊?
    waitThread flag=false. wait@16:00:51

    waitThread 获取到lock对象锁,之后调用wait()方法进入等待队列,而同时会释放掉对象锁,状态为Waiting。  notifyThread获取lock对象锁后调用notify()方法通知一个等待线程(本例中仅一个等待线程),将其移到同步队列,然后继续执行自己的代码,当释放掉lock对象锁后,waitThread线程才有可能重新获取lock并执行先前未完成的代码。   借助《Java并发编程的艺术》中一图:


    注意:

    1)wait() notify() notifyAll() 等使用时 需要先对调用的对象加锁获取。

    2)wait()之后 线程由Running转变为Waiting 将会把当前线程防止到对象的等待队列。

    3)并不是一旦使用notify() notifyAll() 就能实现线程执行wait()方法之后的代码段,需要等发出notify()的线程先释放对象锁然后 等待线程重新获得lock锁 才可以执行原wait()之后的操作!

    4)notify()方法将等待队列中的一个等待线程从其中移到同步队列中,而notifyAll()方法则是唤醒等待队列中的所有线程,全部移到同步队列,将Waiting改为Blocked

    等待方:①.获取对象锁 ②.若条件不满足则调用对象wait(),被通知后要重新判断条件(可能会伪唤醒)③.满足条件后执行后续操作

    通知方:①.获取对象锁 ②.改变条件 ③.通知等待的线程


    二、中断及join()

    interrupt()只是改变中断状态而已. interrupt()不会中断一个正在运行的线程。这一方法实际上完成的是,给受阻塞的线程抛出一个中断信号,这样受阻线程就得以退出阻塞的状态。如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。

    join() 含义:若线程A执行某thread线程的join(),那么当前线程A等待thread线程终止之后才能从join()返回

    例: 每个线程调用前一个线程的join()  按顺序结束操作。

    import java.util.concurrent.TimeUnit;
    
    public class Test2 {
    	public static void main(String[] args) throws InterruptedException {
    		Thread[] myThreads =new Thread[5];
    		Thread previous=Thread.currentThread();
    		for(int i=0;i<5;i++){
    			myThreads[i]=new Thread(new Runner(previous),i+1+" Thread");
    			myThreads[i].start();
    			previous=myThreads[i];
    		}
    //		TimeUnit.SECONDS.sleep(3);//main sleep 3s
    //		myThreads[3].interrupt();
    		TimeUnit.SECONDS.sleep(3);//main sleep 3s
    		System.out.println(Thread.currentThread().getName()+" terminate");
    	}
     static class Runner implements Runnable{
    	 private Thread aThread;
    	 public Runner( Thread aThread) {
    		this.aThread=aThread;
    	}
    	@Override
    	public void run() {
    		try {
    			aThread.join();
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		System.out.println(Thread.currentThread().getName()+" terminate");
    	}
     }
    }
    
    运行结果:

    main terminate
    1 Thread terminate
    2 Thread terminate
    3 Thread terminate
    4 Thread terminate
    5 Thread terminate


    若打开 // myThreads[3].interrupt(); 注释,则会报java.lang.InterruptedException 然后先结束4、5 

    再结束mian 1 2 3   注:由于线程一直运行,在报错的同时,已经在输出4的语句

    java.lang.InterruptedException
    4 Thread terminate
    	at java.lang.Object.wait(Native Method)
    	at java.lang.Thread.join(Thread.java:1249)
    	at java.lang.Thread.join(Thread.java:1323)
    	at Test2$Runner.run(Test2.java:25)
    	at java.lang.Thread.run(Thread.java:745)
    5 Thread terminate
    main terminate
    1 Thread terminate
    2 Thread terminate
    3 Thread terminate


     




    展开全文
  • 专栏原创出处:github-源笔记文件 ,github-源码 ,欢迎 Star,转载请附上原文出处链接和本声明。...《线程等待通知机制(wait、notify)》 《锁相关工具类(LockSupport)》 《锁等待通知机制(Condi...

    专栏原创出处:github-源笔记文件 github-源码 ,欢迎 Star,转载请附上原文出处链接和本声明。

    Java 并发编程专栏系列笔记,系统性学习可访问个人复盘笔记-技术博客 Java 并发编程

    知识回顾

    本节内容需要基础知识点如下,可参考本专栏系统文章温习

    • 《线程等待通知机制(wait、notify)》
    • 《锁相关工具类(LockSupport)》
    • 《锁等待通知机制(Condition)》

    我们可以把线程的等待通知机制看着是线程的通信交流,执行某一块代码时,因为条件不满足进入等待状态,其他线程修改条件后唤醒继续执行。

    本节内容为个人学习理解整理,可能存在偏差,欢迎讨论。

    比较

    • 实现原理(底层)

      • sleep:native 方法,内核定时器触发
      • wait:native 方法,配合 synchronized 的 monitorenter 和 monitorexit 指令
      • park:native 方法,二元信号量
      • Condition:AQS 维护等待队列与同步队列
    • 编码操作

      • sleep:Thread.sleep 静态方法
      • wait:使用 synchronized 加锁的对象,Object 的 wait/notify
      • park:LockSupport.park/unpark
      • Condition:由 Lock 对象 newCondition 方法创建,wait/signal
    • 等待时是否释放锁

      • sleep:不释放
      • wait:释放
      • park:释放
      • Condition:释放
    • 超时等待

      • sleep:支持
      • wait:支持
      • park:支持
      • Condition:支持
    • 等待过程中断

      • sleep:支持
      • wait:支持
      • park:支持
      • Condition:支持
    • 唤醒操作

      • sleep:不支持,定时器唤醒
      • wait:支持,notify/notifyAll
      • park:支持,unpark
      • Condition:支持,signal/signalAll
    • 等待操作精准性

      • sleep:当前线程
      • wait:当前线程
      • park:指定具体的线程
      • Condition:当前线程
    • 唤醒操作精准性

      • wait:notify 随机唤醒一个线程,notifyAll 唤醒所有等待的线程
      • park:unpark 唤醒指定的线程
      • Condition:signal 随机唤醒一个线程,signalAll 唤醒所有等待的线程
    • 执行顺序

      • park:unpark 可以在 park 前执行。可以先调用 unpark 方法释放一个许可证,后面线程调用 park 方法时,发现已经许可证了,就可以直接获取许可证而不用进入休眠状态了
      • wait/notify:保证 wait 方法比 notify 方法先执行。如果 notify 方法比 wait 方法晚执行的话,就会导致因 wait 方法进入休眠的线程接收不到唤醒通知的问题
      • Condition:保证 wait 方法比 signal 方法先执行。
    展开全文
  • Java线程的6种状态及切换(透彻讲解)

    万次阅读 多人点赞 2016-12-24 16:57:03
    2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程...

    Java中线程的状态分为6种。

    1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
    2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
    3. 阻塞(BLOCKED):表示线程阻塞于锁。
    4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
    5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
    6. 终止(TERMINATED):表示该线程已经执行完毕。

    这6种状态定义在Thread类的State枚举中,可查看源码进行一一对应。

    一、线程的状态图     线程状态图

    二、状态详细说明

    1. 初始状态(NEW)

    实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

    2.1. 就绪状态(RUNNABLE之READY)

    1. 就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
    2. 调用线程的start()方法,此线程进入就绪状态。
    3. 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
    4. 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
    5. 锁池里的线程拿到对象锁后,进入就绪状态。

    2.2. 运行中状态(RUNNABLE之RUNNING)

    线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。

    3. 阻塞状态(BLOCKED)

    阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

    4. 等待(WAITING)

    处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

    5. 超时等待(TIMED_WAITING)

    处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

    6. 终止状态(TERMINATED)

    1. 当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
    2. 在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

    三、等待队列

    • 调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) 代码段内。
    • 与等待队列相关的步骤和图

    1. 线程1获取对象A的锁,正在使用对象A。
    2. 线程1调用对象A的wait()方法。
    3. 线程1释放对象A的锁,并马上进入等待队列。
    4. 锁池里面的对象争抢对象A的锁。
    5. 线程5获得对象A的锁,进入synchronized块,使用对象A。
    6. 线程5调用对象A的notifyAll()方法,唤醒所有线程,所有线程进入同步队列。若线程5调用对象A的notify()方法,则唤醒一个线程,不知道会唤醒谁,被唤醒的那个线程进入同步队列。
    7. notifyAll()方法所在synchronized结束,线程5释放对象A的锁。
    8. 同步队列的线程争抢对象锁,但线程1什么时候能抢到就不知道了。 

    四、同步队列状态

    • 当前线程想调用对象A的同步方法时,发现对象A的锁被别的线程占有,此时当前线程进入同步队列。简言之,同步队列里面放的都是想争夺对象锁的线程。
    • 当一个线程1被另外一个线程2唤醒时,1线程进入同步队列,去争夺对象锁。
    • 同步队列是在同步的环境下才有的概念,一个对象对应一个同步队列
    • 线程等待时间到了或被notify/notifyAll唤醒后,会进入同步队列竞争锁,如果获得锁,进入RUNNABLE状态,否则进入BLOCKED状态等待获取锁。

    五、几个方法的比较

    1. Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
    2. Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
    3. thread.join()/thread.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程一般情况下进入RUNNABLE状态,也有可能进入BLOCKED状态(因为join是基于wait实现的)。
    4. obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
    5. obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
    6. LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines), 当前线程进入WAITING/TIMED_WAITING状态。对比wait方法,不需要获得锁就可以让线程进入WAITING/TIMED_WAITING状态,需要通过LockSupport.unpark(Thread thread)唤醒。

    六、疑问

    1. 等待队列里许许多多的线程都wait()在一个对象上,此时某一线程调用了对象的notify()方法,那唤醒的到底是哪个线程?随机?队列FIFO?or sth else?Java文档就简单的写了句:选择是任意性的(The choice is arbitrary and occurs at the discretion of the implementation)。
    展开全文
  • 本篇主要讲java线程的并发和忙等待。 2.内容: java线程最基本的两个内容在这里提一下,那就是线程的创建以及生命周期。 ①java线程的创建:可以通过继承Thread类或实现Runnable接口。 ②线程的生命周期:线程的...
  • JAVA实现线程等待提示框

    千次阅读 2016-07-14 10:09:58
    JAVA实现线程等待提示框 Java语言从其诞生到现在不过短短五年时间,却已经成为全球最热门的语言,Java程序员正成为IT业其它程序员中薪金最高的职员。这一切都应归功于Java良好的特性:简单、面向对象、分布式、...
  • Java多线程--线程等待与唤醒

    千次阅读 2016-06-23 10:24:41
    wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是唤醒单个线程,而notifyAll()
  • 线程等待wait()和通知notify(),主要用于多线程之间的协作,而且这两个方法都是属于Object类,说明任何对象都可以调用这两个方法。 当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。直到另外的...
  • Java——设置线程等待与线程唤醒

    千次阅读 2017-10-10 14:49:24
    //线程间的通信:线程的任务...指明让持有哪个锁的线程等待或被唤醒 */ //还是上次的例子,实现存一个输出一个,而不是输出一大堆//描述数据 class Res{ String name; String sex; //加一个flag标记,false表示没
  • Java等待/通知 机制,举例来说就是,线程A,拿到了对象object的锁,并且调用了object的wait()方法,同时释放了锁,然后进入WAITTING状态。线程B同样前提是拿到了object的锁,然后调用了notify()或notifyAll()方法...
  • 线程对象创建后、调用对象的start()方法,该线程将会处于可运行线程池中,变为可运行后等待获取CPU的使用权 运行状态 就是这就绪状态下的线程获取到了CPU使用权,然后开始执行编写的代码 阻塞状态 1、等待阻塞:运行...
  • 我们知道当我们自己创建一个线程时如果该线程执行完任务后就进入死亡状态,这样如果我们需要在次使用一个线程时得重新创建一个线程,但是线程的创建是要付出一定的代价的,如果在我们的程序中需要频繁使用线程,且每...
  • JAVA线程与多线程

    千次阅读 多人点赞 2016-08-25 19:10:10
    去安卓面试的时候通常会问一些java问题,所以呢你可能觉得答问题时答案很蛋疼,今天来介绍一下线程。先看几个概念:线程:进程中负责程序执行的执行单元。一个进程中至少有一个线程。多线程:解决多任务同时执行的...
  • 万字图解Java线程

    万次阅读 多人点赞 2020-09-06 14:45:07
    java线程我个人觉得是javaSe中最难的一部分,我以前也是感觉学会了,但是真正有多线程的需求却不知道怎么下手,实际上还是对多线程这块知识了解不深刻,不知道多线程api的应用场景,不知道多线程的运行流程等等,...
  • java线程-04-等待线程结束join()

    万次阅读 2021-06-14 10:40:37
    等待线程结束join() jdk 提供三个jion的方法 join从字面的意思就是合并的意思,也就是将几个并行线程的线程合并为一个单线程执行。...//调用方线程线程等待join方法所属的线程终止的时间最长为 millis
  • Java线程(二):线程同步synchronized和volatile

    万次阅读 多人点赞 2012-04-04 10:49:28
    要说明线程同步问题首先要说明Java线程的两个特性,可见性和有序性。多个线程之间是不能直接传递数据交互的,它们之间的交互只能通过共享变量来实现。拿上篇博文中的例子来说明,在多个线程之间共享了Count类的一个...
  • JAVA线程详解

    千次阅读 2019-03-31 22:27:11
    JAVA线程调度 JAVA线程的状态 JAVA新建线程 JAVA线程Thread类 AVA线程属性 线程安全 线程简介 操作系统运行一个程序时,会为其创建一个进程,一个进程里可以创建多个线程,线程为操作系统调度的最小单位,每...
  • Java线程:彻底搞懂线程池

    万次阅读 多人点赞 2019-07-09 19:27:00
    熟悉Java线程编程的同学都知道,当我们线程创建过多时,容易引发内存溢出,因此我们就有必要使用线程池的技术了。 目录 1 线程池的优势 2 线程池的使用 3 线程池的工作原理 4 线程池的参数 4.1 任务队列...
  • Java等待线程执行完毕

    千次阅读 2017-07-18 23:10:27
    前言:前一段时间在做项目的时候,某段代码中用到了多线程,该处代码需要开启多个线程,待这几个线程执行完毕后再接着执行后续的流程...具体如下:一、使用Thread.join()方法该方法在JDK API中的解释为“等待线程终止
  • Java线程03_线程状态、优先级、用户线程和守护线程 线程方法: setPriority() 更改线程优先级 static void sleep() 线程休眠 void join() 插队 static void yield() 礼让 void interrupt() 中断...
  • Java线程之线程概述

    千次阅读 2015-10-14 09:39:09
    本文讨论了线程的概念、类型及多线程模型,并结合上述概念,分析了Java线程
  • Java基础:java线程状态

    万次阅读 2020-04-17 14:45:05
    Java线程具有五中基本状态 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread(); 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就...
  • 线程等待,线程唤醒 “锁“中的主要方法 wait(): 让线程处于冻结状态,被wait的线程会被存储到线程池中。 notify():唤醒线程池中一个线程(任意),没有顺序。 notifyAll():唤醒线程池中的所有线程。 因为...
  • Java-线程Thread等待与唤醒

    千次阅读 2017-03-29 17:13:37
    Java线程等待与唤醒主要包括几个方法:(1)notify():唤醒在此对象监视器上等待的单个线程。(2)notifyAll():唤醒在此对象监视器上等待的所有线程。(3)wait():让当前线程处于阻塞状态,同时释放它所持有的锁...
  • JAVA 线程状态 阻塞和等待 bloked 和 waiting 区别 NEW A thread that has not yet started is in this state. 一个被创建的线程,但是还没有调用start方法 一个正在被执行的线程的状态 RUNNABLE A thread ...
  • java线程 等待多个并发事件的完成

    千次阅读 2016-02-11 19:24:40
    java API中提供了CountDownLatch类,它允许线程一直等待,知道等待操作结束。下面以一个视频会议等待人数为例。 1.视频会议类,实现Runnable接口。import java.util.concurrent.CountDownLatch;public class Video...
  • 原文地址:https://software.intel.com/zh-cn/blogs/2013/10/15/java-countdownlatchcyclicbarrier/?utm_campaign=CSDN&utm_source=intel.csdn.net &utm_medium=Link&utm_content=others-%20Java 本文主要是
  • 创建一批Java线程,然后这批Java线程几乎同时全部跑起来,但是有些开发场景下,开发者需要等到这些Java线程全部执行完毕后,才去执行接下去的业务流程。这个时候就可以CountDownLatch就可以派上用场,CountDownLatch...
  • Java线程

    万次阅读 多人点赞 2021-06-11 16:28:49
    Java线程Java线程线程的创建线程常见方法线程的状态线程的优先级守护线程线程Java线程池线程池的创建线程池的参数线程池的使用线程不安全问题Java中的锁synchronized同步方法synchronized同步语句块...
  • Java线程:概念与原理

    千次阅读 2017-03-29 21:10:42
    Java线程创建与启动 Java线程线程栈模型与线程的变量 Java线程线程状态的转换 Java线程线程的同步与锁 Java线程线程的交互 Java线程线程的调度-休眠 Java线程线程的调度-优先级 Java线程线程的调度-让步 Java线程...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 444,185
精华内容 177,674
关键字:

java线程等待

java 订阅