马士兵老师高并发编程基础入门

锁是指谁?


Object o = new Object();

Synchronized(o);

我们一般认为Synchronized锁定的是这段代码块但事实上,Synchronized锁定的是锁这个对象。不仅如此Synchronized锁定的是heap内存中的这个对象而不是这个引用。

一个例子


  1. /**
  2. * 锁定某对象o,如果o的属性发生改变,不影响锁的使用
  3. * 但是如果o变成另外一个对象,则锁定的对象发生改变
  4. * 应该避免将锁定对象的引用变成另外的对象
  5. * @author mashibing
  6. */
  7. package yxxy.c_017;
  8. import java.util.concurrent.TimeUnit;
  9. public class T {
  10. Object o = new Object();
  11. void m() {
  12. synchronized(o) {
  13. while(true) {
  14. try {
  15. TimeUnit.SECONDS.sleep(1);
  16. } catch (InterruptedException e) {
  17. e.printStackTrace();
  18. }
  19. System.out.println(Thread.currentThread().getName());
  20. }
  21. }
  22. }
  23. public static void main(String[] args) {
  24. T t = new T();
  25. //启动第一个线程
  26. new Thread(t::m, "t1").start();
  27. try {
  28. TimeUnit.SECONDS.sleep(3);
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. //创建第二个线程
  33. Thread t2 = new Thread(t::m, "t2");
  34. t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
  35. t2.start();
  36. }

Java的锁的本质就是内存对象上的一段信息,刚开始t2线程是抢不到那一把锁的因为被t1所占。但是后来o指向了另一个全新的对象这个在堆内存中的对象还没有被当做锁使用所以t2就拿它来当做自己的锁。见下图:

拓展:

由于锁的特性所以一般我们不要使用字符串常量来作为锁对象,这样会使得线程莫名的阻塞。看起来是两个字符串的引用但是他们指向的是同一段的内存。

重入锁

在同一个线程中同步代码块可以多次获得同一把锁。这种情况叫做可重入锁。当然还有一些引用计数的规则等等。这里重点强调的是调用,一旦是调用那么也就说明他们两个是在同一个方法中的。


Synchronized(m){

Synchronized(m){

}

}

ReentrantLock的特性

·这是一种重入锁的实现。它有一个很大的特点就是必须的手动开启锁和释放锁。尤其是这个释放锁,不能忘记否则程序则会一直阻塞。与Synchronized不同的是Synchronized在遇到异常的时候就会释放锁但是ReentrantLock在异常下是不会释放锁的,因此经常在finally中进行锁的释放。

·locked = lock.tryLock(5, TimeUnit.SECONDS);尝试去获得锁,如果5秒还是没有获得到那么就会向下是执行。

·可以被打断的锁,如果一段代码被lock.lockInterruptibly(); 这个锁锁住,那么他能够被

t2.interrupt();这样的语句去手动打断。

·公平锁与不公平锁,通常来说Synchronized的锁是一种不公平的锁,而ReentrantLock可以实现公平锁。那什么是不公平呢?就是由于随机化的原因有的线程会由于运气不好久久得不到执行。公平就是使用了一种时间上的调度算法来使得每个线程的都能够得到公平的执行。

线程通信的底层

线程通信通常有两种,一种是读取共享的一段内存,还有一种就是线程之间互相通信。Java的线程通讯采用的是读取共享的一段内存。

Volatile关键字

这是一个案例,在上述代码不开volatile的时候new出来的那个线程把running从主内存中读取出来读到自己的缓冲区中并且以此为标签来执行while()中的代码块。而且一直执行下去在繁忙的情况下不会去读取主内存中的值,即使main线程对这个值做了修改。所以我们会看到while()中的代码一直被执行。

当volatile开启的时候,这两个线程之间也就变成了可见的了。具体原因就是在主内存的running的值被修改之后这时有一个线程会通知new出来的线程但缓冲区说你的running的值已经过期了,所以缓冲区的running的值会变成false随之while的执行结束。

值得一提的是在线程空闲的时候有可能会去主内存中读取值。

拓展:Volatile与Synchronized的联系与区别

Volatile只保证可见性,就是线程之间的变量是互相可见的,但是不能够保证原子性。

Synchronized,同时保证了原子性和可见性,因为Synchronized会将程序串行执行,当上一个程序执行完毕之后会将值写入到主内存中下一个线程来到时肯定会读取到这段内容。

这种感觉类似于数据库中的事务隔离级别。

AtomicXXX类型

因为++操作(当然也包含其他的系列操作)不是一个原子操作,所以在需要保证原子的++时可以通过上锁来解决问题,也可以通过以下的案例来解决:


  1. /**
  2. * 解决同样的问题的更高效的方法,使用AtomXXX类
  3. * AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的
  4. * @author mashibing
  5. */
  6. package yxxy.c_015;
  7. import java.util.ArrayList;
  8. import java.util.List;
  9. import java.util.concurrent.atomic.AtomicInteger;
  10. public class T {
  11. /*volatile*/ //int count = 0;
  12. AtomicInteger count = new AtomicInteger(0);
  13. /*synchronized*/ void m() {
  14. for (int i = 0; i < 10000; i++)
  15. //if count.get() < 1000
  16. count.incrementAndGet(); //count++
  17. }
  18. public static void main(String[] args) {
  19. T t = new T();
  20. List<Thread> threads = new ArrayList<Thread>();
  21. for (int i = 0; i < 10; i++) {
  22. threads.add(new Thread(t::m, "thread-" + i));
  23. }
  24. threads.forEach((o) -> o.start());
  25. threads.forEach((o) -> {
  26. try {
  27. o.join();
  28. } catch (InterruptedException e) {
  29. e.printStackTrace();
  30. }
  31. });
  32. System.out.println(t.count);
  33. }
  34. }

一道淘宝面试题的演化(Volatile与门闩机制)

实现一个容器,提供两个方法,add,size。写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束。

方案一:

正常的思维,单纯的判断容器的大小是无效的,因为线程之间是不可见的


  1. public class MyContainer1 {
  2. List lists = new ArrayList();
  3. public void add(Object o) {
  4. lists.add(o);
  5. }
  6. public int size() {
  7. return lists.size();
  8. }
  9. public static void main(String[] args) {
  10. MyContainer1 c = new MyContainer1();
  11. new Thread(() -> {
  12. for(int i=0; i<10; i++) {
  13. c.add(new Object());
  14. System.out.println("add " + i);
  15. try {
  16. TimeUnit.SECONDS.sleep(1);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. }
  21. }, "t1").start();
  22. new Thread(() -> {
  23. while(true) {
  24. if(c.size() == 5) {
  25. break;
  26. }
  27. }
  28. System.out.println("t2 结束");
  29. }, "t2").start();
  30. }
  31. }

方案二:

在方案一的基础上加上volatile关键字,成功运行。因为两个线程之间是彼此可见的。


volatile List lists = new ArrayList();

方案三:

由于方案二的while的循环一直在监视所以十分的浪费CPU。因此我们将机制改为了wait()与notify()。也就是当t1检测到到达size到达5的时候叫醒正在沉睡的t2。但是t2、t1使用的是通一把锁而notify不会释放锁(当然它与notify起作用的前提是获得锁)。所以我们的最终方案是当t1叫醒t2的同时自己要wait()这样才保证了t2能够拿到锁然后t2执行完之后也要唤醒t1(由于这时候t2已经执行完毕了所以锁自然释放)。代码如下:


  1. public class MyContainer4 {
  2. //添加volatile,使t2能够得到通知
  3. volatile List lists = new ArrayList();
  4. public void add(Object o) {
  5. lists.add(o);
  6. }
  7. public int size() {
  8. return lists.size();
  9. }
  10. public static void main(String[] args) {
  11. MyContainer4 c = new MyContainer4();
  12. final Object lock = new Object();
  13. new Thread(() -> {
  14. synchronized(lock) {
  15. System.out.println("t2启动");
  16. if(c.size() != 5) {
  17. try {
  18. lock.wait();
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. System.out.println("t2 结束");
  24. //通知t1继续执行
  25. lock.notify();
  26. }
  27. }, "t2").start();
  28. try {
  29. TimeUnit.SECONDS.sleep(1);
  30. } catch (InterruptedException e1) {
  31. e1.printStackTrace();
  32. }
  33. new Thread(() -> {
  34. System.out.println("t1启动");
  35. synchronized(lock) {
  36. for(int i=0; i<10; i++) {
  37. c.add(new Object());
  38. System.out.println("add " + i);
  39. if(c.size() == 5) {
  40. lock.notify();
  41. //释放锁,让t2得以执行
  42. try {
  43. lock.wait();
  44. } catch (InterruptedException e) {
  45. e.printStackTrace();
  46. }
  47. }
  48. try {
  49. TimeUnit.SECONDS.sleep(1);
  50. } catch (InterruptedException e) {
  51. e.printStackTrace();
  52. }
  53. }
  54. }
  55. }, "t1").start();
  56. }
  57. }

终极方案:

使用门闩机制

使用Latch(门闩)替代wait notify来进行通知

好处是通信方式简单,同时也可以指定等待时间

使用await和countdown方法替代wait和notify

CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行

当不涉及同步,只是涉及线程通信的时候,用synchronized + wait/notify就显得太重了

这时应该考虑countdownlatch/cyclicbarrier/semaphore

@author mashibing

首先new一个门闩CountDownLatch latch = new CountDownLatch(1);里面有一个参数,同时他有一个方法就是latch.await();,它会插在代码之间阻拦代码的执行。同时latch.countDown();方法会减少门闩的数量当门闩的数量减少为0的时候这时门会自动打开这时候latch.await();会向下执行。整个过程不涉及锁的机制,高效得实现了线程之间的通信。


  1. public class MyContainer5 {
  2. // 添加volatile,使t2能够得到通知
  3. volatile List lists = new ArrayList();
  4. public void add(Object o) {
  5. lists.add(o);
  6. }
  7. public int size() {
  8. return lists.size();
  9. }
  10. public static void main(String[] args) {
  11. MyContainer5 c = new MyContainer5();
  12. CountDownLatch latch = new CountDownLatch(1);
  13. new Thread(() -> {
  14. System.out.println("t2启动");
  15. if (c.size() != 5) {
  16. try {
  17. latch.await();
  18. //也可以指定等待时间
  19. //latch.await(5000, TimeUnit.MILLISECONDS);
  20. } catch (InterruptedException e) {
  21. e.printStackTrace();
  22. }
  23. }
  24. System.out.println("t2 结束");
  25. }, "t2").start();
  26. try {
  27. TimeUnit.SECONDS.sleep(1);
  28. } catch (InterruptedException e1) {
  29. e1.printStackTrace();
  30. }
  31. new Thread(() -> {
  32. System.out.println("t1启动");
  33. for (int i = 0; i < 10; i++) {
  34. c.add(new Object());
  35. System.out.println("add " + i);
  36. if (c.size() == 5) {
  37. // 打开门闩,让t2得以执行
  38. latch.countDown();
  39. }
  40. try {
  41. TimeUnit.SECONDS.sleep(1);
  42. } catch (InterruptedException e) {
  43. e.printStackTrace();
  44. }
  45. }
  46. }, "t1").start();
  47. }
  48. }

最后感谢马老师,一个专心做教育的老师。

原文地址:https://blog.csdn.net/qq_34993631/article/details/82425052

原文地址:https://www.cnblogs.com/jpfss/p/9913335.html

时间: 2024-10-11 22:18:37

马士兵老师高并发编程基础入门的相关文章

马士兵老师高并发编程同步容器

手写固定同步容器 写一个固定容量同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用. 使用wait与notify 思路:使用一个集合来当做生产或者消费的中转站,然后每当生产或者消费的时刻都判断集合的容量,如果不满足条件那么就对这种操作进行阻塞也就是wait同时notify其它的所有线程.当其它线程启动之后也会遇到"不合格的线程"这时候也会阻塞,直到合格的线程进行执行. 核心代码: public class MyContain

马士兵java高并发编程三

1.使用静态内部类实现线程安全的单例模式 package com.weiyuan.test; /** * 采用内部类的形式实现单例模式 * 是同步安全的,并且实现了懒加载 * */ public class Sigleton { private Sigleton(){ } private static class Inner{ private static Sigleton s = new Sigleton(); } public static Sigleton getInstance(){ r

Java 面试知识点解析(二)——高并发编程篇

前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大部分内容参照自这一篇文章,有一些自己补充的,也算是重新学习一下 Java 吧. 前序文章链接: Java 面试知识点解析(一)--基础知识篇 (一)高并发编程基础知识 这里涉及到一些基础的概念,我重新捧起了一下<实战 Java 高并发程序设计>这一本书,感觉到心潮澎湃,这或许就是笔者叙述功底扎实的

高并发编程必备基础 -- 转载自 并发编程网

文章转载自 并发编程网  本文链接地址:高并发编程必备基础 一. 前言 借用Java并发编程实践中的话"编写正确的程序并不容易,而编写正常的并发程序就更难了",相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的,本文算是对多线程情况下同步策略的一个简单介绍. 二. 什么是线程安全问题 线程安全问题是指当多个线程同时读写一个状态变量,并且没有任何同步措施时候,导致脏数据或者其他不可预见的结果的问题.Java中首

高并发编程专题说明

大家好,并发编程是一个提升程序员level的关键专题,本专题会从理论结合实践逐步深入,尽量用通俗的语言和跑的通的程序来给大家讲解,重点每个地方都会形成一个闭环,让大家真正掌握高并发编程的核心要点,让我们一起来学习,感受技术的乐趣. 对该专题感兴趣的,欢迎点赞!!! 最后给大家一个经验之谈,要提升自己的技术逼格,一句话,"不断走出自己的舒适区" 技术说明:本专题会以java作为编程语言,要求大家具备javase的基础知识,如果不具备就需要先掌握这部分知识再来看这个专题了. 原文地址:ht

高并发编程之线程安全与内存模型

微信公众号:Java修炼指南关注可与各位开发者共同探讨学习经验,以及进阶经验.如果有什么问题或建议,请在公众号留言.博客:https://home.cnblogs.com/u/wuyx/ 前几期简单介绍了一些线程方面的基础知识,以及一些线程的一些基础用法(想看往期文章的小伙伴可以直接拉到文章最下方飞速前往).本文通过java内存模型来介绍线程之间不可见的原因. 本期精彩原子性有序性指令重排序可见性Happen-Before规则 原子性 原子性对于我们开发者来说应该算是比较熟悉的了,通俗点说就是执

Java并发编程-基础概念全解

1.基础 1.1.什么是进程和线程 进程和线程都是操作系统所运行的程序运行的基本单元.进程可以说是是线程的集合. 进程:从系统资源讲,进程都有自己独立的地址空间,一个进程的崩溃不会影响另一个进程的执行. 线程:进程中的一个执行路径,一个进程中可以同时有多个线程在执行,当其中一个线程对公共资源做了修改,其他线程是可以看到的. 1.2.什么是并行和并发 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时. 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正

高并发编程之无锁

前几期简单介绍了一些线程方面的基础知识,以及一些线程的一些基础用法以及通过jvm内存模型的方式去介绍了一些并发中常见的问题(想看往期文章的小伙伴可以直接拉到文章最下方飞速前往).本文重点介绍一个概念“无锁” 本期精彩什么是无锁无锁类的原理AtomicIntegerUnsafeAtomicReferenceAtomicStampedReference 什么是无锁 在高并发编程中最重要的就是获取临界区资源,保证其中操作的原子性.一般来说使用synchronized关键字进行加锁,但是这种操作方式其实

Java高并发编程(一)

1.原子量级操作(读.++操作.写分为最小的操作量单位,在多线程中进行原子量级编程保证程序可见性(有序性人为规定)) 由于某些问题在多线程条件下:产生了竞争的问题,(例如:在多线程中一个简单的计数器增加)如果在程序中不采用同步的机制,那么在程序的运行结果中,多个线程在访问此资源时候,产生Racing.解决这个问题,采用某种方式阻止其他线程在该线程使用该变量的时候使用该变量 采用原子级操作:1.采用加锁的机制(最好的操作)2.Java.concurrent.atomic包包含一些原子量操作:Ato