Java教程

阿里、腾讯大厂面试必问之并发编程(二):线程之间的共享和协作

本文主要是介绍阿里、腾讯大厂面试必问之并发编程(二):线程之间的共享和协作,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

上一篇我们介绍了线程的基础,相信大家都有大概的了解了。这一篇则是线程的共享和协作,一起来看看吧!

一、线程间的共享

Synchronized

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

   1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象; 
   2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象; 
   3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象; 
   4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

volatile

volatile可以说是java虚拟机提供的最轻量级的同步机制。但它同时不容易被正确理解,也至于在并发编程中很多程序员遇到线程安全的问题就会使用synchronized。Java内存模式告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。线程在工作内存进行操作后何时会写到主内存中?这个时机对普通变量是没有规定的,而针对volatile修饰的变量给java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。

现在我们有了一个大概的印象就是:被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。

ThreadLocal

threadlocal使用方法很简单

static final ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
sThreadLocal.set()
sThreadLocal.get()

threadlocal而是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据,官方解释如下。

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 */

大致意思就是ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。

做个不恰当的比喻,从表面上看ThreadLocal相当于维护了一个map,key就是当前的线程,value就是需要存储的对象。

这里的这个比喻是不恰当的,实际上是ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。

作为一个存储数据的类,关键点就在get和set方法。

二、线程间的协作

1. 等待和通知机制的实现

wait() 方法

wait() 是 Object 类的方法,它的作用是使当前执行wait方法的线程进行等待,该方法将当前线程置入“预执行队列”中,并在 wait() 所在的代码行处停止执行,直到接到通知或者被中断才能继续执行。线程必须获得该对象的对象锁,即只能在同步方法或者同步方法块中调用 wait() 方法,在执行 wait() 方法后,当前线程释放所拥有的对象锁,如果 wait() 没有持有对象锁就执行,会抛出 IllegalMonitorStateException 异常

notify() 方法

notify() 是 Object 类的方法,作用是使停止的线程继续运行,也要在同步方法或者同步块中调用。该方法用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选处一个呈 wait 状态的线程,对其发出通知 notify,但是被通知的线程不会马上执行 wait 后面的代码,因为使用 notify 的线程不会马上释放锁,所以被通知的线程也不会马上得到锁。如果调用 notify 时没有持有对象锁,就会抛出 IllegalMonitorStateException 异常

class MyThread1 extends Thread {
​
    private Object lock;
​
    public MyThread1(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName()
                    + " 开始 wait time = " + System.currentTimeMillis());
            try {
                //wait 使线程 thread1 停止运行
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + " 结束 wait time = " + System.currentTimeMillis());
        }
    }
}
​
class MyThread2 extends Thread {
​
    private Object lock;
​
    public MyThread2(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName()
                    + " 开始 notify time = " + System.currentTimeMillis());
            //线程 thread2 使停止的 thread1 继续运行
            lock.notify();
            System.out.println(Thread.currentThread().getName()
                    + " 结束 notify time = " + System.currentTimeMillis());
        }
    }
    
}
​
public class Test extends Thread {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        MyThread1 thread1 = new MyThread1(lock);
        thread1.start();
        Thread.sleep(2000);
        MyThread2 thread2 = new MyThread2(lock);
        thread2.start();
    }
    
}
​

结果是:

AAA 开始 wait time = 1540528981658
BBB 开始 notify time = 1540528983660
BBB 结束 notify time = 1540528983661
AAA 结束 wait time = 1540528983661

简单分析一下整个过程,线程 AAA 先执行 run() 方法,获得了 lock 对象锁,输出一行之后执行 lock.wait() 方法,表示线程 AAA 释放对象 lock,然后持有该对象锁的线程 AAA 进入等待状态,但是线程 AAA 依然在 synchronized 同步块中; 由于线程 AAA 停止了,此时线程 BBB 开始执行 run() 方法,获取 lock 对象锁,输出一行之后,调用 lock.notify() 唤醒正在等待对象锁 lock 的线程 AAA,使其进入就绪状态,但线程 BBB 并不马上释放对象锁 lock,而是继续执行自己同步方法中的剩余方法。只有当线程 BBB 执行完 syncrhonized 同步块之后,才释放对象锁,此时被唤醒的线程 AAA 才可以重新获得该对象锁,然后执行 wait() 方法之后的代码

img

整个过程如图所示,简单来说,就是线程 AAA 被 wait,然后线程 BBB 使用 notify 唤醒线程 AAA,但是不立即释放锁,直到线程 BBB 执行完同步块之后,才释放锁,此时线程 AAA 得到锁,开始执行 wait 后的方法

有一点需要注意,wait() 方法是使 拥有对象锁的那个线程暂时等待,而与谁是那个对象锁没有关系,就像这个例子中,线程 AAA 拥有 Object 对象的对象锁 lock,同时在 run() 方法中用对象锁 lock 调用 wait() 方法,然后对象锁 lock 被释放,注意,是线程 AAA 持有的对象锁 lock被释放,因为线程 AAA 在同步块中,随即造成的是,线程 AAA 进入等待状态。这一点很重要!!!

同时,在同步代码块中,必须调用获取的锁对象的 wait、nofity 或者 notifyAll 方法,在等待线程中,调用 wait() 方法的那个对象一定是 synchronized() 括号里的那个对象,而在通知线程中,调用 notify() 方法的那个对象也一定是 synchronized 括号里的那个对象,如果不是的话,会抛出 IllegalMonitorStateException 异常

notify()只能唤醒一个线程

如果有多个线程处于等待状态,那么 notify() 方法只能随机唤醒一个线程,其他没有没唤醒的线程依旧处于等待状态。但是可以多次调用 notity() 方法来随机唤醒多个线程

//Service2 方法
class Service2 {
​
    public void testMethod(Object lock) {
​
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName()
                    + " beg1in wait " + System.currentTimeMillis());
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + " end wait " + System.currentTimeMillis());
        }
​
    }
​
}

创建三个线程 ThreadA6、ThreadB6 和 ThreadC6

class ThreadA6 extends Thread {
​
    private Object lock;
​
    public ThreadA6(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        Service2 service2 = new Service2();
        service2.testMethod(lock);
    }
​
}
​
class ThreadB6 extends Thread {
​
    private Object lock;
​
    public ThreadB6(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        Service2 service2 = new Service2();
        service2.testMethod(lock);
    }
​
}
​
class ThreadC6 extends Thread {
​
    private Object lock;
​
    public ThreadC6(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        Service2 service2 = new Service2();
        service2.testMethod(lock);
    }
}

随机唤醒一个正在等待的线程的方法

class NotifyOne extends Thread {
​
    private Object lock;
​
    public NotifyOne(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("NotifyOne");
            lock.notify();
        }
    }
}

多次调用可以随机唤醒多个正在等待的线程的方法

class NotifyMulti extends Thread {
​
    private Object lock;
​
    public NotifyMulti(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("NotifyMulti");
            lock.notify();
            lock.notify();
            lock.notify();
            lock.notify();
            lock.notify();
        }
    }
}

测试方法

public class Test2 {
​
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        ThreadA6 threadA6 = new ThreadA6(lock);
        threadA6.start();
        ThreadB6 threadB6 = new ThreadB6(lock);
        threadB6.start();
        ThreadC6 threadC6 = new ThreadC6(lock);
        threadC6.start();
​
        Thread.sleep(2000);
​
        NotifyOne notifyOne = new NotifyOne(lock);
        notifyOne.start();
        /*NotifyMulti notifyMulti = new NotifyMulti(lock);
        notifyMulti.start();*/
    }
​
}

结果是:

Thread-0 beg1in wait 1540536524678
Thread-1 beg1in wait 1540536524679
Thread-2 beg1in wait 1540536524679
notifyOne 唤醒了一个线程 1540536526679
Thread-0 end wait 1540536526679

由于只能唤醒一个线程,另外两个还处于等待状态的线程因为没有被唤醒,就处于永远等待的状态了,如果调用后面被注释的语句,那么就能唤醒多个线程了

Thread-0 beg1in wait 1540536666626
Thread-2 beg1in wait 1540536666626
Thread-1 beg1in wait 1540536666626
NotifyMulti
Thread-0 end wait 1540536668627
Thread-1 end wait 1540536668627
Thread-2 end wait 1540536668627

notifyAll()可以唤醒多个线程

由于不知道有多少个线程处于等待的状态,我们也不可能一直不停调用 notify() 方法,这样会很麻烦,因此可以使用 notifyAll() 方法唤醒全部正在等待的方法

当interrupt方法遇到wait方法

当线程呈 wait() 状态时,调用线程对象的 interrupt() 方法会出现 InterruptedException 异常

class Service5 {
​
    public void testMethod(Object lock) {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " be1gin wait "
                + System.currentTimeMillis());
            try {
                lock.wait();
                System.out.println(Thread.currentThread().getName() + " end wait "
                        + System.currentTimeMillis());
            } catch (InterruptedException e) {
                System.out.println("发生异常...");
                e.printStackTrace();
            }
        }
    }
​
    public void testMethod2(Object lock) {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " begin 2");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " end 2");
        }
    }
​
}
​
class ThreadB5 extends Thread {
​
    private Object lock;
​
    public ThreadB5(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        Service5 service5 = new Service5();
        service5.testMethod2(lock);
    }
}
​
public class ThreadA5 extends Thread {
​
    private Object lock;
​
    public ThreadA5(Object lock) {
        this.lock = lock;
    }
​
    @Override
    public void run() {
        Service5 service5 = new Service5();
        service5.testMethod(lock);
    }
​
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        ThreadA5 threadA5 = new ThreadA5(lock);
        threadA5.start();
        threadA5.interrupt();
        Thread.sleep(2000);
​
        ThreadB5 threadB5 = new ThreadB5(lock);
        threadB5.start();
    }
}

结果是:

Thread-0 be1gin wait 1540534325308
发生异常...
java.lang.InterruptedException
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:502)
    at edu.just.Service5.testMethod(ThreadA5.java:10)
    at edu.just.ThreadA5.run(ThreadA5.java:60)
Thread-1 begin 2
Thread-1 end 2
​

可以看到,报错了,同时还执行了 wait() 方法之后的代码,这是因为:在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放,此时如果还有线程持有对象锁,那么 wait 后面的代码将不会执行,而是直接报错

通知时间

wait(long)方法

如果 wait() 方法里面带有参数,表示在一段时间内,如果没有其他线程对等待的线程进行唤醒,那么等待的线程在超过这个时间之后会自动唤醒

  1. 如果在规定时间之内就被唤醒,那么会先执行其他线程的代码,然后在执行 wait 之后的代码

  2. 如果在规定直接之外被唤醒,那么就会先执行 wait 之后代码,在执行其他线程的代码

public class MyRunnable {
​
    private static Object lock = new Object();
​
    private static Runnable runnable = new Runnable() {
        @Override
        public void run() {
            synchronized (lock) {
​
                System.out.println(Thread.currentThread().getName()
                        + " wait begin time " + System.currentTimeMillis());
                try {
                    //规定 1s 之后线程自动被唤醒
                    lock.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + " wait end time " + System.currentTimeMillis());
            }
        }
    };
​
    private static Runnable runnable1 = new Runnable() {
        @Override
        public void run() {
            synchronized (lock) {
​
                System.out.println(Thread.currentThread().getName()
                        + " wait begin time2 " + System.currentTimeMillis());
                try {
                    lock.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + " wait end time " + System.currentTimeMillis());
            }
        }
    };
​
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(runnable);
        thread.setName("AAA");
        thread.start();
        //在规定时间之内线程 AAA 被唤醒     语句1
        Thread.sleep(1100);
        //在规定时间之外线程 AAA 被唤醒     语句2
//        Thread.sleep(900);
        Thread thread2 = new Thread(runnable1);
        thread2.setName("BBB");
        thread2.start();
    }
​
}

先把语句2注释掉,结果是:

AAA wait begin time 1540538435802
AAA wait end time 1540538436802
BBB wait begin time2 1540538436902
BBB wait end time 1540538436903

看到此时线程 AAA 在被线程 BBB 手动唤醒之前就自动唤醒,所以直接执行了 wait 后面的方法

在执行语句2,把语句1注释,结果是:

AAA wait begin time 1540538528885
BBB wait begin time2 1540538529784
BBB wait end time 1540538529784
AAA wait end time 1540538529885

此时线程 BBB 在线程 AAA 被自动唤醒前就将线程 AAA 唤醒了,此时先执行完线程 BBB 的代码,在执行线程 AAA wait() 方法后面的代码

wait()和sleep()方法的区别

wait() 方法和 sleep() 方法很类似,下面可以做个对比:

  1. wait 是 Object 类的成员变量,而 sleep 是 Thread 类的静态方法

  2. 调用 wait 方法前需要先获取对象锁,而调用 sleep 方法不需要先获取对象锁

  3. 调用 wait 方法的线程需要用 notify 来唤醒,而 sleep 方法必须设置超时值

  4. 线程调用 wait 方法后会先释放锁,而 sleep 方法不会释放锁

2. join方法

下面是join的原理图

img

功能演示

public class JoinDemo implements Runnable{
    public void run() {
        System.err.println("join thread demo ");
    }
​
    public static void main(String[] args) throws Exception {
        System.err.println("main thread start... ");
        Runnable r = new JoinDemo();
        Thread t = new Thread(r);
        t.setName("ibli joinTest ...");
        t.start();
//        t.join();
        System.err.println("main thread end... ");
    }
}

以上将t.join();注释掉,执行的一种可能结果如下:

main thread start... 
main thread end... 
join thread demo
​
还有可能是这种结果:
main thread start... 
join thread demo
main thread end... 
​

但是把注释去掉,结果如下:

main thread start... 
join thread demo 
main thread end... 
​

这是一个非常简单的demo,效果是显而易见的。当main线程去调用t.join()是,会将自己当前线程阻塞,等到t线程执行完成到达完结状态,main线程才可以继续执行。

我们看一下join()设置超时时间的方法:

public class JoinDemo implements Runnable{
    public void run() {
        System.err.println("join thread demo ");
        try {
            // 线程睡眠4s
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        List<String> strings = null;
        System.err.println(strings.get(0));
    }
​
    public static void main(String[] args) throws Exception {
        System.err.println("main thread start... ");
        Runnable r = new JoinDemo();
        Thread t = new Thread(r);
        t.setName("ibli joinTest ...");
        t.start();
        // 但是主线程join的超时时间是1s
        t.join(1000);
        System.err.println("main thread end... ");
    }
}

执行效果:

main thread start... 
join thread demo 
main thread end... 
Exception in thread "ibli joinTest ..." java.lang.NullPointerException
    at com.ibli.threadTest.api.JoinDemo.run(JoinDemo.java:14)
    at java.lang.Thread.run(Thread.java:748)
​

join()源码

首先会调用join(0)方法,其实是join的重载方法;

public final void join() throws InterruptedException {
        join(0);
}

下面是join的核心实现:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
        
        // 首先校验参数是否合法
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
​
        // 如果join方法没有参数,则相当于直接调用wait方法
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

下面是isAlive方法的源码

public final native boolean isAlive();

这是一个本地方法,作用是判断当前的线程是否处于活动状态。什么是活动状态呢?活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的。

  • 这里有一个点要注意,join为什么阻塞的是主线程,而不是子线程呢?

  • 不理解的原因是阻塞主线程的方法是放在previousThread这个实例作用,让大家误以为应该阻塞previousThread线程。实际上主线程会持有previousThread这个对象的锁,然后调用wait方法去阻塞,而这个方法的调用者是在主线程中的。所以造成主线程阻塞。

  • 其实join()方法的核心在于wait(),在主线程中调用t.join()相当于在main方法中添加 new JoinDemo().wait();是一样的效果;在这里只不过是wait方法写在了子线程的方法中。

  • 再次重申一遍,join方法的作用是在主线程阻塞,等在子线程执行完之后,由子线程唤醒主线程,再继续执行主线程调用t.join()方法之后的逻辑。

其实大家可以去翻看JVM的源码实现,Thread.cpp文件中,有一段代码:

void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
  // Notify waiters on thread object. This has to be done after exit() is called
  // on the thread (if the thread is the last thread in a daemon ThreadGroup the
  // group should have the destroyed bit set before waiters are notified).
  ensure_join(this);
}

其中调用ensure_join方法

static void ensure_join(JavaThread* thread) {
  // We do not need to grap the Threads_lock, since we are operating on ourself.
  Handle threadObj(thread, thread->threadObj());
  assert(threadObj.not_null(), "java thread object must exist");
  ObjectLocker lock(threadObj, thread);
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
  // Thread is exiting. So set thread_status field in  java.lang.Thread class to TERMINATED.
  java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
  // Clear the native thread instance - this makes isAlive return false and allows the join()
  // to complete once we've done the notify_all below
  //这里是清除native线程,这个操作会导致isAlive()方法返回false
  java_lang_Thread::set_thread(threadObj(), NULL);
  // 在这里唤醒等待的线程
  lock.notify_all(thread);
  // Ignore pending exception (ThreadDeath), since we are exiting anyway
  thread->clear_pending_exception();
}

在JVM的代码中,线程执行结束的最终调用了lock.notify_all(thread)方法来唤醒所有处于等到的线程

使用场景

  • 比如我们使用Callable执行异步任务,需要在主线程处理任务的返回值时,可以调用join方法;

  • 还有一些场景希望线程之间顺序执行的;

join()方法与sleep()的比较

我们先说一下sleep方法:

  • 让当前线程休眠指定时间。

  • 休眠时间的准确性依赖于系统时钟和CPU调度机制。

  • 不释放已获取的锁资源,如果sleep方法在同步上下文中调用,那么其他线程是无法进- 入到当前同步块或者同步方法中的。

  • 可通过调用interrupt()方法来唤醒休眠线程。

  • sleep是静态方法,可以在任何地方调用

相比与sleep方法 sleep是静态方法,而且sleep的线程不是放锁资源,而join方法是对象方法,并且在等待的过程中会释放掉对象锁;

以上就是这些,如果想要了解更多可以面试类敬请关注哦!

这篇关于阿里、腾讯大厂面试必问之并发编程(二):线程之间的共享和协作的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!