并发编程知识详解(二)

        在关于并发编程相关的知识点,本人打算分三篇文章上传到,下面还是照旧惯例,列出相关知识点。


并发编程相关知识点汇总:

一、为什么出现多线程并发问题

1.1、Java内存模型介绍

1.2、java对象的组成(对象头,实例数据,填充区域)


二、线程安全常见的关键字使用详解

2.1、synchronized

2.2、lock

2.3、synchronized与lock的区别

2.4、Atomic

2.5、volatile

2.6、Threadlocal详解


三、Java并发编程中常用的类和集合

四、线程间的协作(wait/notify/sleep/yield/join)

五、线程安全的级别。

六、并发编程常见问题汇总

七:扩展阅读


三、Java并发编程中常用的类和集合

1、AtomicInteger

2、ArrayBlockingQueue

3、LinkedBlockingQueue

4、ConcurrentLinkedQueue

5、ConcurrentHashMap

6、CopyOnWriteArrayList

6、Collections类 中有多个静态方法:

public static Collection synchronizedCollention(Collection c)

public static List synchronizedList(list l)

public static Map synchronizedMap(Map m)

public static Set synchronizedSet(Set s)


四、线程间的协作(wait/notify/sleep/yield/join)

线程的状态图:(6种状态及切换)

并发编程知识详解(二)_第1张图片

1.初始状态

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

2.1.就绪状态

就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。

调用线程的start()方法,此线程进入就绪状态。

当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。

当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。

锁池里的线程拿到对象锁后,进入就绪状态。

2.2.运行中状态

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

3.阻塞状态

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

4.等待

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

5.超时等待

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

6.终止状态

当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。

在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。


二、wait/notify/notifyAll方法的使用

JDK中一共提供了这三个版本的方法,

  (1)wait()方法的作用是将当前运行的线程挂起(即让其进入阻塞状态),直到notify或notifyAll方法来唤醒线程.

  (2)wait(long timeout),该方法与wait()方法类似,唯一的区别就是在指定时间内,如果没有notify或notifAll方法的唤醒,也会自动唤醒。

  (3)至于wait(longtimeout,longnanos),本意在于更精确的控制调度时间,不过从目前版本来看,该方法貌似没有完整的实现该功能。

wait()方法的使用:

public classWaitTest{

   public voidtestWait(){

       System.out.println("Start-----");

       try {

           wait(1000);

       } catch (InterruptedExceptione) {

           e.printStackTrace();

       }

       System.out.println("End-------");

   }

   public static void main(String[]args) {

       finalWaitTesttest = newWaitTest();

       new Thread(new Runnable() {

           @Override

           public void run() {

               test.testWait();

           }

       }).start();

   }

}

解析:

Exception in thread "Thread-0"java.lang.IllegalMonitorStateException

       atjava.lang.Object.wait(Native Method)

       atcom.paddx.test.concurrent.WaitTest.testWait(WaitTest.java:8)

       at com.paddx.test.concurrent.WaitTest$1.run(WaitTest.java:20)

       atjava.lang.Thread.run(Thread.java:745)

  这段程序并没有按我们的预期输出相应结果,而是抛出了一个异常。大家可能会觉得奇怪为什么会抛出异常?而抛出的IllegalMonitorStateException异常又是什么?我们可以看一下JDK中对IllegalMonitorStateException的描述:

   Thrown to indicate that a thread has attempted to wait on an object's monitor or to notify other threads waiting on an object's monitor without owning the specified monitor.

  这句话的意思大概就是:线程试图等待对象的监视器或者试图通知其他正在等待对象监视器的线程,但本身没有对应的监视器的所有权。

  wait方法是一个本地方法,其底层是通过一个叫做监视器锁的对象来完成的。所以上面之所以会抛出异常,是因为在调用wait方式时没有获取到monitor对象的所有权,那如何获取monitor对象所有权?Java中只能通过Synchronized关键字来完成,修改上述代码,增加Synchronized关键字。

wait()方法的使用:(添加synchronized关键字)

public classWaitTest{

   public synchronized voidtestWait(){//增加Synchronized关键字

       System.out.println("Start-----");

       try {

           wait(1000);

       } catch (InterruptedExceptione) {

           e.printStackTrace();

       }

       System.out.println("End-------");

   }

   public static void main(String[]args) {

       finalWaitTesttest = newWaitTest();

       new Thread(new Runnable() {

           @Override

           public void run() {

               test.testWait();

           }

       }).start();

   }

}

解析:现在再运行上述代码,就能看到预期的效果了:

       Start-----

       End-------

   所以,通过这个例子,大家应该很清楚,wait方法的使用必须在同步的范围内,否则就会抛出IllegalMonitorStateException异常,wait方法的作用就是阻塞当前线程等待notify/notifyAll方法的唤醒,或等待超时后自动唤醒。

notify/notifyAll方法:

   有了对wait方法原理的理解,notify方法和notifyAll方法就很容易理解了。既然wait方式是通过对象的monitor对象来实现的,所以只要在同一对象上去调用notify/notifyAll方法,就可以唤醒对应对象monitor上等待的线程了。notify和notifyAll的区别在于前者只能唤醒monitor上的一个线程,对其他线程没有影响,而notifyAll则唤醒所有的线程。

看下面的例子很容易理解这两者的差别:

public classNotifyTest{

   public synchronized voidtestWait(){

       System.out.println(Thread.currentThread().getName() +" Start-----");

       try {

           wait(0);

       } catch (InterruptedExceptione) {

           e.printStackTrace();

       }

       System.out.println(Thread.currentThread().getName() +" End-------");

   }

public static void main(String[]args) throwsInterruptedException{

       finalNotifyTesttest = newNotifyTest();

       for(inti=0;i<5;i++) {

           new Thread(new Runnable() {

               @Override

               public void run() {

                   test.testWait();

               }

           }).start();

       }

       synchronized (test) {

           test.notify();

       }

       Thread.sleep(3000);

       System.out.println("-----------分割线-------------");


       synchronized (test) {

           test.notifyAll();

       }

   }

}

解析:

输出结果如下:

Thread-0 Start-----

Thread-1 Start-----

Thread-2 Start-----

Thread-3 Start-----

Thread-4 Start-----

Thread-0 End-------

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

Thread-4 End-------

Thread-3 End-------

Thread-2 End-------

Thread-1 End-------

   从结果可以看出:调用notify方法时只有线程Thread-0被唤醒,但是调用notifyAll时,所有的线程都被唤醒了。


两点需要注意:

1、调用wait方法后,线程是会释放对monitor对象的所有权的。

2、一个通过wait方法阻塞的线程,必须同时满足以下两个条件才能被真正执行:

2.1、线程需要被唤醒(超时唤醒或调用notify/notifyll)。

2.2、线程唤醒后需要竞争到锁(monitor)。

三、sleep/yield/join方法解析

解析:这组方法跟上面方法的最明显区别是:这几个方法都位于Thread类中,而上面三个方法都位于Object类中。至于为什么,大家可以先思考一下。

Sleep方法:

作用:让当前线程暂停指定的时间(毫秒),sleep方法是最简单的方法,在上述的例子中也用到过,比较容易理解。唯一需要注意的是其与wait方法的区别。最简单的区别是,wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。

代码实例:

public classSleepTest{

   public synchronized voidsleepMethod(){

       System.out.println("Sleep start-----");

       try {

           Thread.sleep(1000);

       } catch (InterruptedExceptione) {

           e.printStackTrace();

       }

       System.out.println("Sleep end-----");

   }

public synchronized voidwaitMethod(){

       System.out.println("Wait start-----");

       synchronized (this){

           try {

               wait(1000);

           } catch (InterruptedExceptione) {

               e.printStackTrace();

           }

       }

       System.out.println("Wait end-----");

   }

   public static void main(String[]args) {

       finalSleepTesttest1 = newSleepTest();

       for(inti= 0;i<3;i++){

           new Thread(new Runnable() {

               @Override

               public void run() {

                   test1.sleepMethod();

               }

           }).start();

       }

try {

           Thread.sleep(10000);//暂停十秒,等上面程序执行完成

       } catch (InterruptedExceptione) {

           e.printStackTrace();

       }

       System.out.println("-----分割线-----");

       finalSleepTesttest2 = newSleepTest();

       for(inti= 0;i<3;i++){

           new Thread(new Runnable() {

               @Override

               public void run() {

                   test2.waitMethod();

               }

           }).start();

       }

   }

}

执行结果:

       Sleep start-----

       Sleep end-----

       Sleep start-----

       Sleep end-----

       Sleep start-----

       Sleep end-----

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

       Wait start-----

       Wait start-----

       Wait start-----

       Wait end-----

       Wait end-----

       Wait end-----

总结:这个结果的区别很明显,通过sleep方法实现的暂停,程序是顺序进入同步块的,只有当上一个线程执行完成的时候,下一个线程才能进入同步方法,sleep暂停期间一直持有monitor对象锁,其他线程是不能进入的。而wait方法则不同,当调用wait方法后,当前线程会释放持有的monitor对象锁,因此,其他线程还可以进入到同步方法,线程被唤醒后,需要竞争锁,获取到锁之后再继续执行。

yield方法:yield方法的作用是暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。yield方法只是将Running状态转变为Runnable状态。

代码实例:

public classYieldTestimplements Runnable {

   @Override

   public void run() {

       try {

           Thread.sleep(100);

       } catch (InterruptedExceptione) {

           e.printStackTrace();

       }

       for(inti=0;i<5;i++){

           System.out.println(Thread.currentThread().getName() + ": " +i);

           Thread.yield();

       }

   }

   public static void main(String[]args) {

       YieldTestrunn= newYieldTest();

       Thread t1 = new Thread(runn,"FirstThread");

       Thread t2 = new Thread(runn,"SecondThread");

       t1.start();

       t2.start();

   }

}

运行结果如下:

       FirstThread: 0

       SecondThread: 0

       FirstThread: 1

       SecondThread: 1

       FirstThread: 2

       SecondThread: 2

       FirstThread: 3

       SecondThread: 3

       FirstThread: 4

       SecondThread: 4

  这个例子就是通过yield方法来实现两个线程的交替执行。不过请注意:这种交替并不一定能得到保证。


源码中对它的使用的解释:

1、调度器可能会忽略该方法。

2、使用的时候要仔细分析和测试,确保能达到预期的效果。

3、很少有场景要用到该方法,主要使用的地方是调试和测试。

join方法:join方法的作用是父线程等待子线程执行完成后再执行,换句话说就是将异步执行的线程合并为同步的线程。JDK中提供三个版本的join方法,其实现与wait方法类似,join()方法实际上执行的join(0),而join(longmillis,intnanos)也与wait(longmillis,intnanos)的实现方式一致,暂时对纳秒的支持也是不完整的。

join(longmillis)方法的实现,可以看出join方法就是通过wait方法来将线程的阻塞,如果join的线程还在执行,则将当前线程阻塞起来,直到join的线程执行完成,当前线程才能执行。不过有一点需要注意,这里的join只调用了wait方法,却没有对应的notify方法,原因是Thread的start方法中做了相应的处理,所以当join的线程执行完成以后,会自动唤醒主线程继续往下执行。

不使用join方法:

public classJoinTestimplements Runnable{

   @Override

   public void run() {

       try {

           System.out.println(Thread.currentThread().getName() + " start-----");

           Thread.sleep(1000);

           System.out.println(Thread.currentThread().getName() + " end------");

       } catch (InterruptedExceptione) {

           e.printStackTrace();

       }

   }

   public static void main(String[]args) {

       for (inti=0;i<5;i++) {

           Thread test = new Thread(newJoinTest());

           test.start();

       }

       System.out.println("Finished~~~");

   }

}

执行结果如下:

       Thread-0 start-----

       Thread-1 start-----

       Thread-2 start-----

       Thread-3 start-----

       Finished~~~

       Thread-4 start-----

       Thread-2 end------

       Thread-4 end------

       Thread-1 end------

       Thread-0 end------

       Thread-3 end------

使用join方法:

public classJoinTestimplements Runnable{

   @Override

   public void run() {

       try {

           System.out.println(Thread.currentThread().getName() + " start-----");

           Thread.sleep(1000);

           System.out.println(Thread.currentThread().getName() + " end------");

       } catch (InterruptedExceptione) {

           e.printStackTrace();

       }

   }

   public static void main(String[]args) {

       for (inti=0;i<5;i++) {

           Thread test = new Thread(newJoinTest());

           test.start();

           try {

               test.join(); //调用join方法

           } catch (InterruptedExceptione) {

               e.printStackTrace();

           }

       }

       System.out.println("Finished~~~");

   }

}

执行结果如下:

       Thread-0 start-----

       Thread-0 end------

       Thread-1 start-----

       Thread-1 end------

       Thread-2 start-----

       Thread-2 end------

       Thread-3 start-----

       Thread-3 end------

       Thread-4 start-----

       Thread-4 end------

       Finished~~~

总结:对比两段代码的执行结果很容易发现,在没有使用join方法之间,线程是并发执行的,而使用join方法后,所有线程是顺序执行的。

等待队列:

调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj)代码段内。


等待队列相关的步骤和流程图:

并发编程知识详解(二)_第2张图片

解析:

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线程进入同步队列,去争夺对象锁。

同步队列是在同步的环境下才有的概念,一个对象对应一个同步队列。

方法的比较与总结:

1、Thread.sleep(longmillis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。

2、Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。

3、t.join()/t.join(longmillis),当前线程里调用其它线程t的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程进入就绪状态。

4、obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。

5、obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。


五、线程安全的级别

1、不可变

不变的对象绝对是线程安全的,不需要线程同步,如String、Long、BigInteger。

2、无条件的线程安全

对象自身做了足够的内部同步,也不需要外部同步,如Random、ConcurrentHashMap、Concurrent集合、atomic。

3、有条件的线程安全

对象的部分方法可以无条件安全使用,但是有些方法需要外部同步,需要Collections.synchronized;有条件线程安全的最常见的例子是遍历由Hashtable或者Vector或者返回的迭代器。

4、非线程安全(线程兼容)

对象本身不提供线程安全机制,但是通过外部同步,可以在并发环境使用, 如ArrayList,HashMap。

5、线程对立

即使外部进行了同步调用,也不能保证线程安全,这种情况非常少,例如System.setOut()、System.runFinalizersOnExit()。


七:扩展阅读

1、https://www.xuebuyuan.com/3253276.html(synchronized的4种用法)

2、一文带你彻底搞懂ThreadLocal(微信公众号)

3、https://blog.csdn.net/javazejian/article/details/72772461(全面理解Java内存模型(JMM)及volatile关键字)

4、https://juejin.im/post/5d2c97bff265da1bc552954b(图解Java线程安全)

5、https://www.cnblogs.com/paddix/p/5367116.html(Java并发编程:Synchronized及其实现原理,系列)

6、https://www.jianshu.com/p/cfac5c131a9b(面试字节跳动Android研发岗,已拿到offer,这些知识点该放出来了)

7、http://www.infoq.com/cn/articles/java-se-16-synchronized(聊聊并发(二)——Java SE1.6中的Synchronized)

8、https://blog.csdn.net/niuwei22007/article/details/51433669(synchronized的JVM底层实现(很详细 很底层))

9、https://blog.csdn.net/qq_22771739/article/details/82529874(Java线程的6种状态及切换(透彻讲解))

10、https://blog.csdn.net/lkforce/article/details/81128115(Java的对象头和对象组成详解)

11、Synchronized和Lock的区别和使用场景(微信公众号)

12、一文带你理解Java中Lock的实现原理(微信公众号)

13、https://www.cnblogs.com/0616--ataozhijia/p/6869657.html(AtomicInteger的用法)

14、面试必问的volatile,你了解多少 (微信公众号)

15、Java中Volatile关键字详解(微信公众号)

16、https://segmentfault.com/a/1190000017766364(Java中15种锁的介绍:公平锁,可重入锁,独享锁,互斥锁,乐观锁,分段锁,自旋锁等等)

17、ThreadLocal到底是什么?它解决了什么问题(微信公众号)

18、Java并发集合的实现原理(微信公众号)

19、https://blog.csdn.net/jackyrongvip/article/details/89472397(笔记:Collections的synchronized XXX方法)

20、https://blog.csdn.net/qq_34039315/article/details/78549311(Java并发编程75道面试题及答案——稳了)

你可能感兴趣的:(并发编程知识详解(二))