精华内容
下载资源
问答
  • Java线程等待唤醒机制
    2021-12-10 21:10:16

    前言

    当多个线程处理同一资源时,如果这些线程完成的任务不同,我们可能会需要让这些线程以一定规律来处理这一资源。对于这个需求,Java提供了等待唤醒机制来让多个线程协调运行。

    wait(),notify()和notifyAll()

    等待唤醒机制顾名思义就是使线程进入等待状态或唤醒等待状态下的线程。使用wait()方法可以让所属锁的当前线程进入wait set中,变为等待状态,并且释放所属锁,使得锁对象对应的其他线程获得此锁来执行相应的任务;而notify()方法可以唤醒锁对象对应的等待状态下的某个线程,让它重新回到调度队列中(ready queue),与其他线程共同竞争锁,获取锁后会在当初wait()方法后恢复执行。

    这两个方法需要在同步下才能生效,而且需要在当前获取的锁对象上调用,来表明这两个方法操作的是哪个锁对象对应的线程。

    notifyAll()方法可以唤醒所有等待状态下的线程。

    参考资料

    1. 使用wait和notify
    更多相关内容
  • 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 线程状态和等待唤醒机制和线程池的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
  • Java线程等待唤醒机制

    2018-09-11 21:27:29
    这里就用到了线程等待唤醒机制,下面具体看一下。 等待唤醒机制示例 下面代码是一个简单的线程唤醒机制示例,主要就是在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方法结束后线程处于结束状态。

    参考文献

    深入理解Java并发之synchronized实现原理
    java多线程状态详解

    展开全文
  • 典型实例有生产者和消费者,本文也通过实例来分析线程等待唤醒机制。  1、相关API介绍  public final void notify()  唤醒在此对象监视器上等待的单个线程,如果所有线程都在此对象上等待,则会任意选择唤醒...
  • 主要介绍了Java等待唤醒机制线程通信原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
  • 1.线程间的等待唤醒机制 Object 类中 void wait () 在其他线程调用此对象的 notify () 方法或 notifyAll () 方法前,导致当前线程等待。 void wait (long timeout) 在其他线程调用此对象的 notify () 方法或 ...

    1.线程间的等待唤醒机制

    Object 类中
     void wait ()  在其他线程调用此对象的 notify () 方法或 notifyAll () 方法前,导致当前线程等待。
           
     void wait (long timeout) 在其他线程调用此对象的 notify () 方法或 notifyAll () 方法,或者超过指定的时间量前,导致当前线程等待。
     
     void notify () 唤醒在此对象监视器上等待的单个线程,随机唤醒。
            
     void notifyAll ()  唤醒在此对象监视器上等待的所有线程。 
        
    notify()需要在同步方法或同步块中调用,即在调用前,线程必须获得该对象的对象级别锁
    在调用wait()方法之前,线程必须获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait方法。
    

    wait()和sleep()的区别
    Wait和Sleep的区别:
    1.它们最大本质的区别是,Sleep()不释放同步锁,Wait()释放同步锁。
    2.还有用法的上的不同是:Sleep(milliseconds)可以用时间指定来使他自动醒过来,
    如果时间不到你只能调用Interreput()来强行打断;Wait()可以用Notify()直接唤起。
    4.这两个方法来自不同的类分别是Thread和Object
    5.最主要是Sleep方法没有释放锁,而Wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。

    2.内存可见性问题 volatile

    volatile 解决内存可见性问题
     想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的。
    	 Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,
    	 线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。
    	 线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。
    	 不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
    	 
    Java中的可见性:
    	 对于可见性,Java提供了volatile关键字来保证可见性。
    	 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
    	 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,
    	 当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
    	 另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,
    	 并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
    	 
    volatile 关键字:当多个线程进行操作共享数据时,可以保证内存中的数据可见。
     	           相较于 synchronized 是一种较为轻量级的同步策略。
    volatile 变量,用来确保将变量的更新操作通知到其他线程。
    可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:
    对于多线程,不是一种互斥关系 
    不能保证变量状态的“原子性操作”              
    

    3.CAS 算法

     CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器
    操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。
    CAS 是一种无锁的非阻塞算法的实现。
    CAS 包含了 3 个操作数:
    需要读写的内存值 V 内存
    进行比较的值 A 预估值
    拟写入的新值 B 
    当且仅当 V 的值等于 A 时, CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作。
     jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。
    
    java.util.concurrent.atomic 包下提供了一些原子操作的常用类:
    AtomicBoolean 、 AtomicInteger 、 AtomicLong 、 AtomicReference
    AtomicIntegerArray 、 AtomicLongArray
    AtomicMarkableReference
    AtomicReferenceArray 
    

    4.线程的状态

    新建:线程被创建出来
    就绪:具有CPU的执行资格,但是不具有CPU的执行权
    运行:具有CPU的执行资格,也具有CPU的执行权
    阻塞:不具有CPU的执行资格,也不具有CPU的执行权
    死亡:不具有CPU的执行资格,也不具有CPU的执行权
    线程的状态图
    Java中线程的状态分为6种:

    1. 初始(New):

    新创建了一个线程对象,但还没有调用start()方法。

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

    就绪状态的线程在获得CPU时间片后变为运行中状态(running)。这也是线程进入运行状态的唯一的一种方式。

    1. 阻塞(Blocked):

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

    1. 等待(Waiting) 跟 超时等待(Timed_Waiting):
    1. 处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒(通知或中断),否则会处于无限期等待的状态。
    2. 处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
    1. 终止(Terminated):

    当线程正常运行结束或者被异常中断后就会被终止。线程一旦终止了,就不能复生。

    注意:

    1. 调用 obj.wait 的线程需要先获取 objmonitorwait会释放 objmonitor 并进入等待态。所以 wait()/notify() 都要与 synchronized 联用。
    2. 其实线程从阻塞/等待状态 到 可运行状态都涉及到同步队列等待队列的

    阻塞与等待的区别
    阻塞

    当一个线程试图获取对象锁(非JUC库中的锁,即synchronized),而该锁被其他线程持有,则该线程进入阻塞状态。它的特点是使用简单,由JVM调度器来决定唤醒自己,而不需要由另一个线程来显式唤醒自己,不响应中断。

    等待

    当一个线程等待另一个线程通知调度器一个条件时,该线程进入等待状态。它的特点是需要等待另一个线程显式地唤醒自己,实现灵活,语义更丰富,可响应中断。例如调用:Object.wait()、**Thread.join()**以及等待 LockCondition

    虽然 synchronizedJUC 里的 Lock 都实现锁的功能,但线程进入的状态是不一样的。synchronized 会让线程进入阻塞态,而 JUC 里的 Lock是用park()/unpark() 来实现阻塞/唤醒 的,会让线程进入等待状态。虽然等锁时进入的状态不一样,但被唤醒后又都进入Runnable状态,从行为效果来看又是一样的。

    展开全文
  • 等待唤醒案例(线程之间的通信) 实现: 等待唤醒案例:线程之间的通信 创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待) 创建一个老板线程(生产...
  • Java 线程 等待唤醒机制 代码优化 适合初学者学习参考
  • 主要介绍了Java线程线程通信生产者消费者模式及等待唤醒机制代码详解,具有一定参考价值,需要的朋友可以了解下。
  • 线程等待唤醒机制

    千次阅读 2022-03-15 14:31:53
    三种让线程等待唤醒的方法: LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程: 第一种方式:synchronized + wait + notify: 即使用Object中的wait方法让线程等待,使用Object中的notify方法唤醒线程: ...
  • 27_多线程_第2天(线程安全、线程同步、等待唤醒机制、单例设计模式)_讲义
  • 等待唤醒机制 线程之间的通信可以有效的利用资源,让线程之间的活动尽然有序,在线程之间的通信常用的方法有: 对象锁.wait();//让线程进入等待Block状态 对象锁.notify();//唤醒Block中的线程 对象锁.notifyAll()...
  • Java等待唤醒机制wait/notify深入解析

    千次阅读 2020-11-25 16:44:58
    本文将帮助你彻底弄明白Java的wait/notify等待唤醒机制。为了弄明白wait/notify机制,我们需要清楚线程通信、volatile和synchronized关键字、wait/notify方法、Object的monitor机制。本文将会从这几个方面详细讲解...
  • Java线程 等待/通知机制 文章目录Java线程 等待/通知机制1、什么是等待通知机制2、底层实现原理2.1 对象模型2.2 Moniter(对象监视者)2.3 原理3、实例:生产者消费者问题4、总结 1、什么是等待通知机制 (1)...
  • 线程等待唤醒机制

    千次阅读 2017-05-09 11:03:26
    为了避免此类问题,我们会用到线程间的通信,而等待唤醒机制,就是线程间通信的一种形式。 等待唤醒机制用到的方法主要有: public final void wait() throws InterruptedException : 当前线程
  • Java中的等待唤醒机制—至少50%的工程师还没掌握!发布时间:2019-12-14 01:53,浏览次数:222, 标签:Java这是一篇走心的填坑笔记,自学Java的几年总是在不断学习新的技术,一路走来发现自己踩坑无数,而填上的坑却...
  • 五、等待唤醒机制 一、线程安全问题 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码,程序每次运行结果和单线程运行的结果是一样的,而且程序中的变量值和和预期的一样,那么线程就是安全的,如果...
  • Java学习笔记-等待唤醒机制

    千次阅读 2019-07-09 10:21:41
    等待唤醒机制 线程间通信 概念:多个线程再处理同一个资源,但是处理的动作(线程的任务)却不相同。 比如:线程A用来生成包子的,线程B是用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是...
  • 案例:有家餐馆的取餐口只能放10份快餐,厨师做完快餐放在取餐口,服务员从这个取餐口取出快餐。现在有1个厨师和1个服务员。且服务员不是等到10份餐放满才取菜,厨师不是等到10份菜都取完才做菜。...
  • 线程等待唤醒机制介绍线程间通信方式 1、全局变量(基于内存共享) 2、Message消息机制 备注:基于内存共享比较容易实现 如果多线程只是处理完全相同的任务时,那么事情就简单了,似乎也不需要线程之间相互协同。如果...
  • 我们都知道,Android的Handler机制,会在线程中开启消息循环,不断的从消息队列中取出消息,这个机制保证了主线程能够及时的接收和处理消息。 通常在消息队列中(MessageQueue)中没有消息的时候,会调用MessageQueue...
  • 什么是等待/通知机制 假设有两个线程,一个生产数据,一个消费数据。如何保证生产线程生产出数据后通知给消费线程,消费线程在没有数据可被消费情况下等待有数据呢? 一般的做法是采用轮询方法,一直 while 循环...
  • 三个方法 wait() notify() notifyAll() 三个方法都使用在同步中,因为要对持有锁(又叫监控)的线程操作。...只有同一个锁上的被等待线程,可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒
  • 等待唤醒机制概述

    2019-10-07 11:36:10
    什么是等待唤醒机制: 这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是 故事的全部,线程间也会有协作机制。就好比在公司里你和你的同事们,你们可能存在在...
  • 等待唤醒机制3 .生产者与消费者问题 1.线程间的通信 概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。 比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与...
  • 不用等待唤醒机制实现的生产者与消费者代码package com.hust.juc;/* * 生产者和消费者案例 */ public class TestProductorAndConsumer { public static void main(String[] args) { Clerk clerk = new Clerk(); ...
  • 线程的知识点讲解,,,线程安全,线程同步,,等待唤醒机制,单例设计模式,,,,,,,,,让你更好理解多线程
  • java线程等待唤醒,以及线程同步。  在Object.java中,定义了wait(), notify()和notifyAll()等接口。wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。而notify()和...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 67,332
精华内容 26,932
关键字:

java线程等待唤醒机制

java 订阅