线程间通信

109 阅读5分钟

一、stop和interrupt

stop方法会立即打断线程,此方法已经被废弃,因为立即打断线程无法判断程序执行的位置,无法做一下补救操作。

推荐使用interrupt,见如下代码:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private Thread mThread;
    private TextView mTv;
    private int value;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTv = findViewById(R.id.tv);
        run();
    }

    public void run(){
        mThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10_0000; i++) {
                    //收到通知需要打断线程,则打断线程,Thread.interrupt()不会打断线程,只是起通知作用
                    //Thread.interrupted()也是判断线程是否被打断,但它与isInterrupted()的区别是它会重置线程打断的标记位为false
                    //建议放在耗时操作之前
                    if(mThread.isInterrupted()){
                      //做一些线程被打断的补救操作
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                mTv.setText("线程被打断"+(value++));
                            }
                        });
                        //需要加return,因为这部分会多次被回调
                        return;
                    }else{
                        System.out.println("线程操作:当前的值:"+i);
                    }
                }
            }
        });
        //开启线程
        mThread.start();
        //等待300毫秒后结束线程
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //结束线程
        //mThread.stop();
        //通知打断线程,只是起通知的作用
        //打断线程的操作是在主动获取线程状态是否有被打断的时候,如果为true就打断线程,让程序员有机会补救
        mThread.interrupt();
    }

}

主逻辑见代码注释。

InterruptedException这个异常就是线程被通知Interrupted了而抛出来的,因为线程被Interrupted,某些操作执行不下去了,比如sleep则抛出了这个异常。


二、wait、join、yield、notify和notifyAll

看下面一个死锁的例子:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private TextView mTv;
    private String mName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTv = findViewById(R.id.tv);
        run();
    }

    public void run(){
        Thread readThread = new Thread(() -> {
            //模拟后台线程耗时操作
            SystemClock.sleep(1000);
            getValueNotNull();
        });
        readThread.start();

        Thread writeThread = new Thread(() -> {
            //模拟后台线程耗时操作
            SystemClock.sleep(2000);
            setValue();
        });
        writeThread.start();
    }

    //获取值
    public synchronized void getValueNotNull(){
        while(mName==null){
            Log.d(TAG,"mName为null,会一直循环,走不到下面,除非mName不为null");
        }
        //不为null,主线程更新UI
        runOnUiThread(() -> mTv.setText(mName));
    }

    //设置值
    public synchronized void setValue(){
        mName = "ZhangSan";
    }

}

run方法中有个读的线程和写的线程(赋值),其中读的线程延时1秒,写的线程延时2秒。getValueNotNull方法和setValue方法都是synchronized的,它们使用同一把锁。

程序的运行情况是先调用getValueNotNull方法,进入循环,这时候锁一直被getValueNotNull方法(循环)占用,等2秒后因为锁被占用,无法调用setValue方法,所以mName无法被赋值永远为null,getValueNotNull方法的循环因为mName永远为null也就永远不能被break,永远无法释放锁。

解决办法就是使用waitnotify,见如下改进代码:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private TextView mTv;
    private String mName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTv = findViewById(R.id.tv);
        run();
    }

    public void run(){
        Thread readThread = new Thread(() -> {
            //模拟后台线程耗时操作
            SystemClock.sleep(1000);
            getValueNotNull();
        });
        readThread.start();

        Thread writeThread = new Thread(() -> {
            //模拟后台线程耗时操作
            SystemClock.sleep(2000);
            setValue();
        });
        writeThread.start();
    }

    //获取值
    public synchronized void getValueNotNull(){
        while(mName==null){
            Log.d(TAG,"mName为null,会一直循环,走不到下面....");
            try {
                //线程等待
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //不为null,主线程更新UI
        runOnUiThread(() -> mTv.setText(mName));
    }

    //设置值
    public synchronized void setValue(){
        mName = "ZhangSan";
        //被赋值后通知线程
        notifyAll();
    }

}

进入getValueNotNull()方法,当检测到mName==null时,调用wait方法使线程释放锁等待,2秒后setValue()方法拿到锁赋值成功调用notifyAll方法唤醒等待的线程使程序继续往下执行。

为什么不使用notify而使用notifyAll,因为notify只会随机唤醒一个线程,可能唤醒的不是你期待唤醒的线程,所以使用notifyAll唤醒所有的线程更保险。

如果明确的知道哪个线程先执行完,哪个线程后执行完,则使用join可以优化上面的代码:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private TextView mTv;
    private String mName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTv = findViewById(R.id.tv);
        run();
    }

    public void run() {
        Thread writeThread = new Thread(() -> {
            //模拟后台线程耗时操作
            SystemClock.sleep(2000);
            setValue();
        });
        writeThread.start();

        Thread readThread = new Thread(() -> {
            //模拟后台线程耗时操作
            SystemClock.sleep(1000);
            //已知writeThread后执行完,readThread先执行完
            //让readThread阻塞,直到writeThread执行完
            try {
                writeThread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            getValueNotNull();
        });
        readThread.start();
    }

    //获取值
    public  void getValueNotNull() {
        //不为null,主线程更新UI
        runOnUiThread(() -> mTv.setText(mName));
    }

    //设置值
    public  void setValue() {
        mName = "ZhangSan";
    }
}

总结:

sleep:

1、使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会执行。但是时间到了之后线程会进入就绪队列,重新去竞争cpu资源。

2.sleep()会释放cpu资源,但是不会释放同步锁(类锁和对象锁)。

yield:

使当前正在执行的线程向另一个线程交出运行权。注意这是一个静态方法。
该方法与sleep类似,只是不能由用户指定暂停多长时间,并且yield方法只能让同优先级的线程有执行的机会。 1、yield执行后线程直接进入就绪状态。 2、yield会释放cpu资源,但是不会释放同步锁(类锁和对象锁)。

join:

执行后线程进入阻塞状态,例如在线程B中调用线程A的join,那线程B会进入到阻塞队列,直到join结束或中断线程B才开始进入阻塞队列。 可以实现一个线程的顺序执行。

wait、notify和notifyAll:

1、wait方法用于协调多个线程对共享数据的存取,所以必须在Synchronized语句块内使用
2、wait方法使当前线程暂停执行并释放会cpu资源,以及同步锁(类锁和对象锁)
3、调用wait后必须调用notifynotifyAll后线程才会从等待池进入到锁池,当我们的线程竞争得到同步锁后就会重新进入绪状态等待cpu资源分配。

当调用notify方法后,将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有锁标志等待池中线程能够获取锁标志;如果锁标志等待池中没有线程,则notify不起作用。

notifyAll则从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中。

三、Condition

Condition 是 JDK 1.5 中提供的用来替代 wait 和 notify 的线程通讯方法,替代的原因是:
1、使用 notify 在极端环境下会造成线程“假死”;
2、Condition 性能更高。

使用Condition改写上面wait的代码:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private TextView mTv;
    private String mName;
    private Condition mCondition;
    private ReentrantLock mReentrantLock;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTv = findViewById(R.id.tv);
        initThread();
        run();
    }

    private void initThread() {
        mReentrantLock = new ReentrantLock();
        mCondition = mReentrantLock.newCondition();
    }

    public void run() {
        Thread writeThread = new Thread(() -> {
            //模拟后台线程耗时操作
            SystemClock.sleep(2000);
            setValue();
        });
        writeThread.start();

        Thread readThread = new Thread(() -> {
            //模拟后台线程耗时操作
            SystemClock.sleep(1000);
            getValueNotNull();
        });
        readThread.start();
    }

    //获取值
    public void getValueNotNull() {
        //加锁
        mReentrantLock.lock();
        try {
            if (mName == null) {
                //为null让线程等待
                try {
                    mCondition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //主线程更新UI
            runOnUiThread(() -> mTv.setText(mName));
        } finally {
            //解锁
            mReentrantLock.unlock();
        }
    }

    //设置值
    public void setValue() {
        //加锁
        mReentrantLock.lock();
        try {
            mName = "ZhangSan";
            //唤醒等待的线程
            mCondition.signalAll();
        } finally {
            //解锁
            mReentrantLock.unlock();
        }
    }

}

Condition需要使用 Lock 锁来创建,Condition 提供了 3 个重要的方法:

  • await:对应 wait 方法;
  • signal:对应 notify 方法;
  • signalAllnotifyAll 方法。

Condition 的使用和 wait/notify 类似,也是先获得锁然后在锁中进行等待和唤醒操作。


本篇内容参考了以下文章,表示感谢:

并发编程——线程中sleep(),yield(),join(),wait(),notify(),notifyAll()区别

求求你,别再用wait和notify了!


--个人学习笔记