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

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

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

    等待唤醒机制示例

    下面代码是一个简单的线程唤醒机制示例,主要就是在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问题,改用传统的互斥同步可能会比原子类更高效。
     

    展开全文
  •  线程等待一定的时间,如果超时或有信号触发,线程唤醒。  通过上表,可以看出pthread_cond_timedwait函数是最为灵活,使用也最为广泛。sleep的缺陷是当有紧急事件到达时,线程无法及时唤醒。pthread_cond_...

    pthread_cond_timedwait()函数和pthread_cond_wait()函数,其实2个函数都差不多,我主要是要用pthread_cond_timedwait()函数。

    pthread_cond_timedwait()函数有三个入口参数:

    (1)pthread_cond_t __cond:条件变量(触发条件)

    (2)pthread_mutex_t __mutex: 互斥锁

    (3)struct timespec __abstime: 等待时间(其值为系统时间 + 等待时间)

    当在指定时间内有信号传过来时,pthread_cond_timedwait()返回0,否则返回一个非0数(我没有找到返回值的定义);

    在使用pthread_cond_timedwait()函数时,必须有三步:

    1:加互斥锁:pthread_mutex_lock(&__mutex)

    2:等待:pthread_cond_timedwait(&__cond, &__mutex, &__abstime)   //解锁->等待->加锁

    3:解互斥锁:pthread_mutex_unlock(&__mutex)

    发送信号量时,也要有三步:

    1:加互斥锁:pthread_mutex_lock(&__mutex)

    2:发送:pthread_cond_signal(&__cond)

    3:解互斥锁:pthread_mutex_unlock(&__mutex)

    那么,这里就有一个问题,等待的时候已经加上锁了,那么我发送的时候怎么才能运行到发送函数呢?其实这是因为在pthread_cond_timedwait()函数中已经对互斥锁进行解锁操作了,所以这个时候发送信号量是不会阻塞的。其实仔细想想,这样不是才能保证同步吗?(写完代码后考虑一下)

    #include <stdio.h>
    #include <pthread.h>
    #include <string.h>
    #include <unistd.h>
    #include <semaphore.h>
    #include <sys/time.h>
     
    #define SENDSIGTIME 10
     
    pthread_cond_t g_cond;
    pthread_mutex_t g_mutex;
     
    void thread1(void *arg)
    {
        int inArg = (int)arg;
        int ret = 0;
        struct timeval now;
        struct timespec outtime;
     
        pthread_mutex_lock(&g_mutex);
     
        gettimeofday(&now, NULL);
        outtime.tv_sec = now.tv_sec + 5;
        outtime.tv_nsec = now.tv_usec * 1000;
     
        ret = pthread_cond_timedwait(&g_cond, &g_mutex, &outtime);
        //ret = pthread_cond_wait(&g_cond, &g_mutex);
        pthread_mutex_unlock(&g_mutex);
     
        printf("thread 1 ret: %d\n", ret);
     
    }
     
    int main(void)
    {
        pthread_t id1;
        int ret;
     
        pthread_cond_init(&g_cond, NULL);
        pthread_mutex_init(&g_mutex, NULL);
     
        ret = pthread_create(&id1, NULL, (void *)thread1, (void *)1);
        if (0 != ret)
        {
    	printf("thread 1 create failed!\n");
    	return 1;
        }
     
        printf("等待%ds发送信号!\n", SENDSIGTIME);
        sleep(SENDSIGTIME);
        printf("正在发送信号....\n");
        pthread_mutex_lock(&g_mutex);
        pthread_cond_signal(&g_cond);
        pthread_mutex_unlock(&g_mutex);
     
     
        pthread_join(id1, NULL);
     
        pthread_cond_destroy(&g_cond);
        pthread_mutex_destroy(&g_mutex);
     
        return 0;
    }
    

    线程等待和唤醒函数比较

     平台提供了线程等待相关函数,这些函数之间用法也有些差异:

    sleep线程等待,等待期间线程无法唤醒。
    pthread_cond_wait 线程等待信号触发,如果没有信号触发,无限期等待下去。
    pthread_cond_timedwait 线程等待一定的时间,如果超时或有信号触发,线程唤醒。

      通过上表,可以看出pthread_cond_timedwait函数是最为灵活,使用也最为广泛。sleep的缺陷是当有紧急事件到达时,线程无法及时唤醒。pthread_cond_wait缺陷是:必须借助别的线程触发信号,否则线程自身无法唤醒,如果使用函数,线程无法处理定时任务。

      一般情况下,线程要做的工作可能有:定期处理某个事物;无事可做时,线程挂起;有事可做时,立即唤醒工作。要完成上面所述的功能,必须用pthread_cond_timedwait函数,本文介绍的就是对该函数封装。

      线程唤醒操作还涉及互斥量pthread_mutex_t,感觉与我们理解的等待和唤醒操作无关;此函数的引入,增加了理解难度。

    函数定义和接口封装如下

    //函数涉及的变量
    typedef struct ThreadSignal_T
    {
        BOOL  relativeTimespan; //是否采用相对时间
        pthread_cond_t cond;
        pthread_mutex_t mutex;
        pthread_condattr_t cattr;
    } ThreadSignal;
    
    //初始化
    void ThreadSignal_Init(ThreadSignal *signal,BOOL relativeTimespan);
    //关闭
    void ThreadSignal_Close(ThreadSignal *signal);
    //等待n毫秒
    void ThreadSignal_Wait(ThreadSignal *signal, int ms);
    //唤醒线程
    void ThreadSignal_Signal(ThreadSignal *signal);
    1)ThreadSignal_Init
    void ThreadSignal_Init(ThreadSignal *signal, BOOL relativeTimespan)
    {
        signal->relativeTimespan = relativeTimespan;
    
        pthread_mutex_init(&signal->mutex, NULL);
    
        if (relativeTimespan)
        {
            int ret = pthread_condattr_init(&signal->cattr);
            ret = pthread_condattr_setclock(&signal->cattr, CLOCK_MONOTONIC);
            ret = pthread_cond_init(&signal->cond, &signal->cattr);
        }
        else
        {
            pthread_cond_init(&signal->cond, NULL);
        }
    }

    2) ThreadSignal_Close

    void ThreadSignal_Close(ThreadSignal *signal)
    {
        if (signal->relativeTimespan)
        {
            pthread_condattr_destroy(&(signal->cattr));
        }
    
        pthread_mutex_destroy(&signal->mutex);
        pthread_cond_destroy(&signal->cond);
    }

    3) ThreadSignal_Wait

    void ThreadSignal_Wait(ThreadSignal *signal, int ms)
    {
        pthread_mutex_lock(&signal->mutex);
    
        if (signal->relativeTimespan)
        {
            //获取时间
            struct timespec outtime;
            clock_gettime(CLOCK_MONOTONIC, &outtime);
            //ms为毫秒,换算成秒
            outtime.tv_sec += ms/1000;
          
            //在outtime的基础上,增加ms毫秒
            //outtime.tv_nsec为纳秒,1微秒=1000纳秒
            //tv_nsec此值再加上剩余的毫秒数 ms%1000,有可能超过1秒。需要特殊处理
            uint64_t  us = outtime.tv_nsec/1000 + 1000 * (ms % 1000); //微秒
            //us的值有可能超过1秒,
            outtime.tv_sec += us / 1000000; 
    
            us = us % 1000000;
            outtime.tv_nsec = us * 1000;//换算成纳秒
    
            int ret = pthread_cond_timedwait(&signal->cond, &signal->mutex, &outtime);
        }
        else
        {
            struct timeval now;
            gettimeofday(&now, NULL);
    
            //在now基础上,增加ms毫秒
            struct timespec outtime;
            outtime.tv_sec = now.tv_sec + ms / 1000;
    
            //us的值有可能超过1秒
            uint64_t  us = now.tv_usec + 1000 * (ms % 1000); 
            outtime.tv_sec += us / 1000000; 
    
            us = us % 1000000;
            outtime.tv_nsec = us * 1000;
    
            int ret = pthread_cond_timedwait(&signal->cond, &signal->mutex, &outtime);
        }
        pthread_mutex_unlock(&signal->mutex);
    }

    struct timespec outtime;结构中有两个值:tv_sec ,tv_usec 。分别是秒和纳秒。等待一段时间就是:在这两个值上增加一定的数值。tv_usec 此值有范围限制的,就是不能超过1秒暨1000000000纳秒。如果超出1秒,就要在tv_sec 此值增加一秒;tv_usec 减去一秒。笔者是在实践中发现此问题的,不是无中生有。如果tv_usec 此值溢出,调用pthread_cond_timedwait函数,会立马返回。

    4)ThreadSignal_Signal

    void ThreadSignal_Signal(ThreadSignal *signal)
    {
        pthread_mutex_lock(&signal->mutex);
        pthread_cond_signal(&signal->cond);
        pthread_mutex_unlock(&signal->mutex);
    }

     

    展开全文
  • Linux C 线程等待

    万次阅读 2013-10-08 19:47:13
      ...linux 下面的sleep,usleep,nanosleep 和select比较 sleep 时间单位是秒 usleep的时间单位是微秒 select的精度是微妙,精确 struct timeval delay; delay.tv_



    http://blog.csdn.net/wind19/article/details/7541811

     

    linux 下面的sleep,usleep,nanosleep 和select比较


    sleep 时间单位是秒

    usleep的时间单位是微秒

    select的精度是微妙,精确

    struct timeval delay;
    delay.tv_sec 
    = 0;
    delay.tv_usec 
    = 20 * 1000// 20 ms
    select(0, NULL, NULL, NULL, &delay);

    usleep()有有很大的问题

    1. 在一些平台下不是线程安全,如HP-UX以及Linux
    2. usleep()会影响信号
    3. 在很多平台,如HP-UX以及某些Linux下,当参数的值必须小于1 * 1000 * 1000也就是1秒,否则该函数会报错,并且立即返回。
    4. 大部分平台的帮助文档已经明确说了,该函数是已经被舍弃的函数。

    还好,POSIX规范中有一个很好用的函数,nanosleep(),该函数没有usleep()的这些缺点,它的精度是纳秒级。在Solaris的多线程环境下编译器会自动把usleep()连接成nanosleep()

    Linux下短延时推荐使用select函数,因为准确.



    ******************************************************************************************




    http://blog.csdn.net/horstlinux/article/details/7911457

    linux多线程编程,替代sleep的几种方式

    我只想要进程的某个线程休眠一段时间的,可是用sleep()是将整个进程都休眠的,这个可能就达不到,我们想要的效果了。 目前我知道有三种方式:

    1 usleep

       这个是轻量级的, 听说能可一实现线程休眠, 我个人并不喜欢这种方式,所以我没有验证它的可行信(个人不推荐)。

    2 select

       这个可以,我也用过这种方式, 它是在轮询。

    3  pthread_cond_timedwait

            采用pthread_cond_timedwait(pthread_cond_t* cond, pthread_mutex_t *mutex, const struct timespec *abstime)可以优雅的解决该问题,设置等待条件变量cond,如果超时,则返回;如果等待到条件变量cond,也返回。本文暂不将内部机理,仅演示一个demo。

           首先,看这段代码,thr_fn为一个线程函数:

    [cpp]  view plain copy
    1. #include <stdio.h>  
    2. #include <stdlib.h>  
    3.   
    4. int flag = 1;  
    5. void * thr_fn(void * arg) {  
    6.   while (flag){  
    7.     printf("******\n");  
    8.     sleep(10);  
    9.   }  
    10.   printf("sleep test thread exit\n");  
    11. }  
    12.    
    13. int main() {  
    14.   pthread_t thread;  
    15.   if (0 != pthread_create(&thread, NULL, thr_fn, NULL)) {  
    16.     printf("error when create pthread,%d\n", errno);  
    17.     return 1;  
    18.   }  
    19.    
    20.   char c ;  
    21.   while ((c = getchar()) != 'q');  
    22.    
    23.   printf("Now terminate the thread!\n");  
    24.   flag = 0;  
    25.   printf("Wait for thread to exit\n");  
    26.   pthread_join(thread, NULL);  
    27.   printf("Bye\n");  
    28.   return 0;  
    29. }  
       输入q后,需要等线程从sleep中醒来(由挂起状态变为运行状态),即最坏情况要等10s,线程才会被join。采用sleep的缺点:不能及时唤醒线程。
    采用pthread_cond_timedwait函数实现的如下:

    [cpp]  view plain copy
    1. #include <stdio.h>  
    2. #include <sys/time.h>  
    3. #include <unistd.h>  
    4. #include <pthread.h>  
    5. #include <errno.h>  
    6.    
    7. static pthread_t thread;  
    8. static pthread_cond_t cond;  
    9. static pthread_mutex_t mutex;  
    10. static int flag = 1;  
    11.    
    12. void * thr_fn(void * arg)   
    13. {  
    14.   struct timeval now;  
    15.   struct timespec outtime;  
    16.   pthread_mutex_lock(&mutex);  
    17.   while (flag) {  
    18.     printf("*****\n");  
    19.     gettimeofday(&now, NULL);  
    20.     outtime.tv_sec = now.tv_sec + 5;  
    21.     outtime.tv_nsec = now.tv_usec * 1000;  
    22.     pthread_cond_timedwait(&cond, &mutex, &outtime);  
    23.   }  
    24.   pthread_mutex_unlock(&mutex);  
    25.   printf("cond thread exit\n");  
    26. }  
    27.    
    28. int main(void)   
    29. {  
    30.   pthread_mutex_init(&mutex, NULL);  
    31.   pthread_cond_init(&cond, NULL);  
    32.   if (0 != pthread_create(&thread, NULL, thr_fn, NULL)) {  
    33.     printf("error when create pthread,%d\n", errno);  
    34.     return 1;  
    35.   }  
    36.   char c ;  
    37.   while ((c = getchar()) != 'q');  
    38.   printf("Now terminate the thread!\n");  
    39.   
    40.   pthread_mutex_lock(&mutex);  
    41.   flag = 0;  
    42.   pthread_cond_signal(&cond);  
    43.   pthread_mutex_unlock(&mutex);  
    44.   printf("Wait for thread to exit\n");  
    45.   pthread_join(thread, NULL);  
    46.   printf("Bye\n");  
    47.   return 0;  
    48. }  

            pthread_cond_timedwait()函数阻塞住调用该函数的线程,等待由cond指定的条件被触发(pthread_cond_broadcast() or pthread_cond_signal())。

            当pthread_cond_timedwait()被调用时,调用线程必须已经锁住了mutex。函数pthread_cond_timedwait()会对mutex进行【解锁和执行对条件的等待】(原子操作)。这里的原子意味着:解锁和执行条件的等待是原则的,一体的。(In this case, atomically means with respect to the mutex andthe condition variable and other access by threads to those objectsthrough the pthread condition variable interfaces.)

           如果等待条件满足或超时,或线程被取消,调用线程需要在线程继续执行前先自动锁住mutex,如果没有锁住mutex,产生EPERM错误。即,该函数返回时,mutex已经被调用线程锁住。

           等待的时间通过abstime参数(绝对系统时间,过了该时刻就超时)指定,超时则返回ETIMEDOUT错误码。开始等待后,等待时间不受系统时钟改变的影响。

           尽管时间通过秒和纳秒指定,系统时间是毫秒粒度的。需要根据调度和优先级原因,设置的时间长度应该比预想的时间要多或者少点。可以通过使用系统时钟接口gettimeofday()获得timeval结构体。





    展开全文
  • Java线程阻塞唤醒

    2018-05-07 00:35:38
    Thread.suspend和Thread.resume因为容易导致死锁,很早以前就被标记为@deprecated,不建议使用了。我们把线程使用互斥锁访问的共享资源叫做...如果使用resume方法唤醒线程前需要获取监视器,那么死锁就会发生。当然...

        Thread.suspend和Thread.resume因为容易导致死锁,很早以前就被标记为@deprecated,不建议使用了。我们把线程使用互斥锁访问的共享资源叫做临界资源,把加锁和解锁之间的代码叫做临界区。如果当前线程获取了监视器并进入了临界区,这时当前线程被调用suspend方法,当前线程不会释放持有的监视器。如果使用resume方法唤醒线程前需要获取监视器,那么死锁就会发生。当然如果合理Thread.suspend和Thread.resume,可以避免死锁。不过,对于线程阻塞和唤醒,我们有更好的选择。

        下面的例子中,blinker线程负责控制控件闪烁,通过鼠标点击可以改变blinker线程状态。我们通过一个变量来标志线程状态。我们对mousePressed的方法调用没有加锁,因此可能出现并发问题。如果线程A和B同时进入方法体,线程A已经到了阶段3,线程B刚到阶段1,blinker.resume或blinker.suspend会被执行两次。

        private volatile boolean threadSuspended;
    
        public void mousePressed(MouseEvent e) {
            // 阶段1:刚进入方法体
            e.consume();
            // 阶段2:判断threadSuspended之前
            if (threadSuspended)
                blinker.resume(); // resume
            else
                blinker.suspend();  // suspend之后会继续占用对象监视器
            // 阶段3:修改threadSuspended之前
            threadSuspended = !threadSuspended; 
        }

        为了解决并发问题,假设方法mousePressed被synchronized修饰,blinker.resume挂起时,会继续占用对象监视器,之后调用mousePressed方法的线程都会阻塞,出现死锁。如果对mousePressed方法加锁,由于Thread.suspend调用之后会继续占用对象的监视器,导致死锁。如下所示:

        private volatile boolean threadSuspended;
    
        public synchronized void mousePressed(MouseEvent e) {
            e.consume();
            if (threadSuspended)
                blinker.resume(); // resume
            else
                blinker.suspend();  // suspend之后会继续占用对象的监视器
    
            threadSuspended = !threadSuspended; 
        }

     

         使用Object.wait和Object.notify可以实现线程的阻塞与唤醒,同时避免出现死锁的情况。因为Object.wait在阻塞线程的时候,会释放线程占用的监视器。调用对象的wait或者notify方法之前,都需要获得这个对象的监视器。获得对象监视器的方法有:1. 执行对象的synchronized方法;2. 用当前对象同步的同步块 synchronized(object) {};3. 对于类,通过执行类的静态方法获取。

      使用wait/notify来实现线程的阻塞与唤醒,如下所示:

        public synchronized void mousePressed(MouseEvent e) { // blinker线程内部,使用blinker线程对象同步
            e.consume();
    
            threadSuspended = !threadSuspended;
    
            if (!threadSuspended)
                notify();
        }

        然后在线程的run方法中,循环检查状态。

        // 用volatile修饰threadSuspended变量,保证多个线程间的内存可见性。
        private volatile boolean threadSuspended;
    
        public void run() {
            while (true) {
                try {
                    if (threadSuspended) { 
                        // 只在threadSuspended为true时请求加锁,减少加锁
                        synchronized(this) {
                            while (threadSuspended)
                            // 线程可能被假唤醒,所以用while而不是if
                                wait();
                        }
                    }
                } catch (InterruptedException e){
                }
                repaint();
            }
        }

        在run方法中读threadSuspended变量和在mousePressed方法中修改它,都会先获得同一个监视器。

        wait方法放在while循环中,是因为线程因为"no reason"可能被唤醒,这是官方文档的提法,有一种可能的原因是,notify被调用后,等待队列头的线程被唤醒,此时又有新的线程通过非公平的策略(自旋)争夺到监视器。对于threadSuspended变量的修改实现了同步,避免了并发问题。由于Thread.wait会释放占有的锁,因此不会出现死锁。

        上面的例子,只会有一个线程(blinker)会wait,而会有多个线程调用notify。wait/notify可以允许多个线程等待同一个对象的监视器。

    ------------------------------分割线----------------------------

    虚假唤醒:

    虚假唤醒描述了某些多线程API(如POSIX线程和Windows API)提供的条件变量使用的复杂性。即使条件变量似乎已经从等待线程的角度收到信号,等待的条件仍然可能是错误的。 造成这种情况的原因之一是虚假的唤醒; 也就是说,即使没有线程通知条件变量,线程也可能被从其等待状态唤醒。 为了正确,有必要在线程完成等待之后验证条件是否确实为真。 因为虚假唤醒可以重复发生,所以通过在条件为真时终止循环来实现。

    Linux中的pthread_cond_wait()函数是使用futex系统调用实现的。 当进程收到信号时,Linux上的每个阻塞系统调用都会使用EINTR突然返回。 因为它可能会在futex系统调用之外的一小段时间内错过真正的唤醒,因此pthread_cond_wait()实现的时候不能用while。 只有调用者在返回之后再次检查条件才能避免这种竞争条件。 因此POSIX信号将产生虚假唤醒。

    展开全文
  • 文章目录编程环境:条件变量是锁????吗?条件变量的两个动作?使用条件变量流程:生产者-消费者模型:理论模型:代码实现:代码分析:运行结果:...pthread_cond_t g_cond() //条件变量–阻塞线程等待条件满足*...
  • LinuxLinux线程技术

    万次阅读 2018-09-05 15:57:23
    Linux线程概念 线程的概念 线程是计算机科学中的一个术语,是指运行中的程序的调度单位。一个线程指的是进程中一个单一顺序的控制流,也称为轻量进程。它是系统独立调度和分配的基本单位。同一进程中的多个线程...
  • 一、怎样唤醒一个阻塞线程? 如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程是遇到了IO阻塞,无能为力,因为IO是操作系统...
  • 最近在做一个项目,需要实现两个线程之间的...一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条...
  • poll是不能正常工作的,比如当前poll正在阻塞,即使新增加的fd有事件发生,仍然阻塞;如果当前解除阻塞,新增的事件会被监测到。 epoll是可以的。 关于动态监测,就是当前监测5个fd, 要减少一个是否可以? ...
  • 今天在编程时,在一个线程中使用 pthread_cond_signal试图唤醒
  • t 甚或几 p 的数据的数据库系统,到手机上的一个有良好用户响应能力的 app,为了充分利用每个 CPU 内核,都会想到是否可以使用多线程技术。这里所说的“充分利用”包含了两个层面的意思,一
  • Linux - 线程通信

    千次阅读 2015-05-16 09:28:23
    线程互斥机制Mutex变量就像一把“锁”,是线程同步和保护共享数据的主要方式 ...其他线程需要等待,直到获取Mutex的线程放弃Mutex 线程必须轮流访问需要保护的数据 线程经常利用mutex来加锁需要更新的全局变量,
  • Linux线程编程(不限Linux

    千次阅读 2011-11-11 16:57:18
    ——本文一个例子展开,介绍Linux下面线程的操作、多线程的同步和互斥。 前言 线程?为什么有了进程还需要线程呢,他们有什么区别?使用线程有什么优势呢?还有多线程编程的一些细节问题,如线程之间怎样同步、...
  • linux线程的创建

    2015-09-12 10:11:43
    linux线程的创建
  • Linux线程编程

    千次阅读 2011-08-26 10:24:55
    线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期, solaris是这方面的佼佼者。传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就...
  • linux线程

    2011-11-11 09:09:01
    pthread/Linux线程编程   本文出自:http://www.china-pub.com 作者: 姚继锋 (2001-08-11 09:05:00)    1 引言  线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代...
  • Linux内核可以看作一个服务进程(管理软硬件资源,响应...内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。 这与用户线程是不一样的。因为内核线程只运行在内核态
  • 关于linux下信号量和条件变量的使用,在很多地方都可以找到相关文章,信号量、条件变量、互斥锁都是线程同步原语,在平时多线程编程中只要知道一两种就可以轻松搞定,我也是这么认为的,但是今天发现,有时还是有...
  • Linux内核线程

    千次阅读 2013-08-14 15:15:56
    内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,与系统中其他进程“并行”执行(实际上,也并行于内核自身的执行),内核线程经常被称为内核“守护... 线程启动后一直等待,直至内
  • Linux线程

    千次阅读 2009-01-08 16:14:00
    一:Linux线程编程: 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一...
  • Linux线程使用详解

    万次阅读 2015-12-30 21:14:58
    Linux线程和进程的区别:http://blog.csdn.net/qq_21792169/article/details/50437304 线程退出的条件:下面任意一个都可以。 1.调用pthread_exit函数退出。 2.其他线程调用pthread_cancel取消该...
  • Linux驱动阻塞与非阻塞IO之等待队列

    千次阅读 2012-03-22 20:42:17
    上次我和大家一起探讨了Linux驱动中的竞态问题,本环节为们来探讨一下Linux 驱动编写中的阻塞与非阻塞I/O 阻塞与非阻塞I/O简介 阻塞操作:是指在执行设备操作时,若不能获得资源,则挂起进程...
  • Linux C线程总结(2)

    2017-08-05 10:42:12
    Linux C线程总结(2)
  • Linux——线程深度剖析(二)互斥同步临界资源临界区线程安全如何保证线程安全0.互斥锁1.互斥锁的初始化2.加锁3.解锁4.互斥锁的销毁实例的运用:黄牛抢票 互斥 是指散布在不同进程之间的若干程序片断,当某个进程...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 34,292
精华内容 13,716
关键字:

linux阻塞线程等待唤醒

linux 订阅