概述

  • 使用锁是为了解决安全问题
  • 使用并发主要是让线程既安全,又效率
  • 安全问题主要是由于内存的是否被安全使用了
  • 效率问题主要考虑的是线程是否被cpu合理调用了
  • 经测试,当并发执行累加操作不超过百万次时,并行速度会比串行化执行累加操作要慢。主要原因是因为并行会导致更多的线程的上下文切换 ```java public class TestThread { static final long count =1000010000; public static void main(String[] args) throws InterruptedException {

    1. concurrency(); //多线程执行
    2. serial();//单线程执行

    }

    static void concurrency() throws InterruptedException {

    1. long start = System.currentTimeMillis();
    2. Thread thread = new Thread(()->{
    3. int a =0;
    4. for(long i=0;i<count;i++){
    5. a+=5;
    6. }
    7. });
    8. thread.start();
    9. int b =0;
    10. for(long i=0;i<count;i++){
    11. b-=5;
    12. }
    13. long end =System.currentTimeMillis();
    14. thread.join();//确保thread线程执行完毕
    15. System.out.println("多线程使用时间:"+(end-start));

    }

    //单线程执行 static void serial(){

    1. long start = System.currentTimeMillis();
    2. int a =0;
    3. for(long i=0;i<count;i++){
    4. a+=5;
    5. }
    6. int b =0;
    7. for(long i=0;i<count;i++){
    8. b-=5;
    9. }
    10. long end =System.currentTimeMillis();
    11. System.out.println("单线程使用时间:"+(end-start));

    }

}

  1. - 减少上下文切换的方式
  2. - 无锁并发编程
  3. - CAS算法
  4. - 使用最少线程,通过使用线程池控制线程的数量
  5. - 协程:比线程还要小调用cpu的单位
  6. <a name="L1O3r"></a>
  7. ## Volatile
  8. - volatilejdk提供一个关键字,可作为轻量级的锁,不会引起上下文件线程的切换
  9. - volatile的作用
  10. - 保证线程的可见性
  11. - 防止指令重排
  12. - volatile可以一定程度解决并发问题,但是遇到原子性导致的并发就无法解决
  13. - 想要保证线程安全需要解决以下三个问题
  14. - 原子性
  15. - 可见性
  16. - 有序性
  17. - volatile只能修饰成员变量,不能修饰局部变量
  18. <a name="YRhuV"></a>
  19. ## 线程可见性问题
  20. - 可见性代码问题
  21. ```java
  22. public static class RecoderExample {
  23. volatile int a=0;
  24. //实现了获取a的值的方法
  25. public void get(){
  26. int local_value = a;
  27. while (local_value < 15) {
  28. if (local_value != a) {
  29. System.out.println("获取a的值 : " + a);
  30. local_value = a;
  31. }
  32. }
  33. }
  34. //修改a的值
  35. public void change(){
  36. while (a < 15) {
  37. System.out.println("新增之后的a的值 " + (a + 1));
  38. a++;
  39. try {
  40. Thread.sleep(100);
  41. } catch (InterruptedException e) {
  42. e.printStackTrace();
  43. }
  44. }
  45. }
  46. }
  47. public static void main(String[] args) {
  48. RecoderExample recoderExample = new RecoderExample();
  49. //创建一个获取值的线程
  50. Thread t1 = new Thread(()->{
  51. recoderExample.get();
  52. });
  53. //创建一个线程修改值
  54. Thread t2 = new Thread(()->{
  55. recoderExample.change();
  56. });
  57. t1.start();
  58. t2.start();
  59. }
  • 为什么会有可见性的问题?
    • Java共享内存模型JMM的机制
      • java中所有的变量是存储在主内存中的
      • 每个线程要去修改主内存中的变量的时候,是将主内存中的变量复制一份到线程的本地内存中进行修改
      • 然后线程修改变量成功之后,将线程的本地的内存的变量再刷新到主内存中
    • 在多线程编程的时候,可能有几个线程同时将一个变量值复制到本地内存中然后进行修改,这样就导致了并发问题
    • 所以线程中的变量默认情况下是不可见的
  • 在使用了volatile修饰变量之后,当一个线程的本地变量被修改了,其他的变量都是可以见的

    线程操作的原子性

  • 所谓的原子性是不可分割的操作

  • volatile不可以保证操作的原子性,synchronized可以保证
  • volatile用于信号灯的操作,即一个线程改,多个线程查询等操作
  • 可以使用JUC中提供的原子类解决原子性的问题

    1. public class TestVolatile002 {
    2. int count =0;
    3. AtomicInteger atomicInteger = new AtomicInteger(0);
    4. void m() {
    5. for (int i = 0; i < 10000; i++) {
    6. count++;
    7. atomicInteger.incrementAndGet();
    8. }
    9. }
    10. public static void main(String[] args) {
    11. TestVolatile002 t = new TestVolatile002();
    12. List<Thread> threads = new ArrayList<>();
    13. //创建10个线程
    14. for (int i = 0; i < 10; i++) {
    15. threads.add( new Thread(t::m,"thread"+i));
    16. }
    17. //启动10个线程
    18. threads.forEach((o)->o.start());
    19. //当前主线程等待所有的线程完成
    20. threads.forEach((o)->{
    21. try {
    22. o.join();
    23. } catch (InterruptedException e) {
    24. // TODO Auto-generated catch block
    25. e.printStackTrace();
    26. }
    27. });
    28. System.out.println(t.count);
    29. System.out.println("原子操作的值:"+t.atomicInteger.get());
    30. }
    31. }

    线程操作的顺序性

  • java的编译器为了提高程序运行的效率,在编译时候会将源码的顺序进行重排,以便提高程序运行的效率

    1. int x = 10; int x = 10;
    2. int y = 9; x = x+10
    3. x = x+10; int y=9;
  • 在单线程的情况下问题不大,但是在多线程的情况有可能导致并发的问题

    1. public class TestVolatile003 {
    2. public static int a = 0,b=0;
    3. public static int i = 0,j=0;
    4. public static void main(String[] args) throws InterruptedException {
    5. int cnt=0;
    6. while(true){
    7. a = 0;
    8. b=0;
    9. i=0;
    10. j=0;
    11. Thread t1 = new Thread(()->{
    12. a=1;
    13. i=b;
    14. });
    15. Thread t2 = new Thread(()->{
    16. b=1;
    17. j=a;
    18. });
    19. t1.start();
    20. t2.start();
    21. t1.join();
    22. t2.join();
    23. cnt++;
    24. System.out.println("第"+cnt+"次执行,i="+i+",j="+j);
    25. if(i==0&& j==0){
    26. break;
    27. }
    28. }
    29. }
    30. }
  • 在单例模式(Double Check Lock)解决指令重排的问题

    1. public class MySingleton {
    2. static volatile MySingleton instance;
    3. List list;
    4. private MySingleton(){
    5. list = new ArrayList();//这个代码可能会在赋值之后执行
    6. }
    7. public int getNum(){
    8. return list.size();
    9. }
    10. public static MySingleton getInstance(){
    11. if(instance == null){//由于指令重排instance已经不为空,但是构造方法没有被调用
    12. synchronized(MySingleton.class){
    13. if(instance==null){
    14. instance = new MySingleton();
    15. }
    16. }
    17. }
    18. return instance;
    19. }
    20. }
  • new的正常顺序

    • 1内存为对象分配空间
    • 2调用构造方法
    • 3将结果返回的引用
  • 由于指令重排之后以上步骤变成一下情况

    • 1内存为对象分配空间
    • 3将结果返回的引用
    • 2调用构造方法

      volatile 和 synchronized

      1、volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
      2、volatile保证数据的可见性,但是不保证原子性; 而synchronized是一种排他(互斥)的机制,既保证可见性,又保证原子性。
      3、volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
      4、volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。

      CAS算法

  • 传统的锁会在加锁,或是释放的时候导致过多的上下文切换

  • CAS算法:compareAndSwap

    1. public final int incrementAndGet() {
    2. for (;;) {
    3. int current = get();//获取当前的变量的值
    4. int next = current + 1; //预判操作
    5. if (compareAndSet(current, next))//判断预操作是否成功
    6. return next; //成功则返回
    7. //不成功则继续
    8. }
    9. }
  • 通过以上的算法,线程就不会轻易的释放cpu资源,从而减少了上下文的切换

  • CAS算法主要是通过Unsafe类实现
  • 在CAS的基础上出现快速解决并发问题的各种工具包

    • JUC(原子包),提供各种类型原子操作类

      1. AtomicBoolean:原子更新布尔类型。
      2. AtomicInteger:原子更新整型。
      3. AtomicLong:原子更新长整型。
      4. AtomicInteger
    • QAS(各种支持并发的集合)

      • ArrayList并不是线程安全的,Vector是线程安全的,但是Vector使用synchronized
        • CopyOnWriteArrayList是线程安全的,且效率比Vector高
      • Hashmap并不是线程安全的,
        • ConcurrentHashMap是线程安全
        • 主要使用了分段锁进行实现
      • ConcurrentLinkedQueue:线程安全的队列
      • 阻塞队列的使用

        1. public class TestBlockQueue {
        2. static AtomicInteger cnt = new AtomicInteger(0);
        3. public static void main(String[] args) {
        4. ArrayBlockingQueue<String> queue = new ArrayBlockingQueue(5);
        5. //多个消费则
        6. List<Thread> products = new ArrayList<>();
        7. for(int i=0;i<3;i++){
        8. products.add(new Thread(()->{
        9. String name = Thread.currentThread().getName();
        10. for(int j=0;j<5;j++){
        11. try {
        12. Thread.sleep(100);
        13. } catch (InterruptedException e) {
        14. e.printStackTrace();
        15. }
        16. try {
        17. // System.out.println(name+"做的食品"+(cnt.get()+1));
        18. queue.put(name+"做的食品"+cnt.incrementAndGet()); //put是阻塞的方法
        19. } catch (InterruptedException e) {
        20. e.printStackTrace();
        21. }
        22. }
        23. }));
        24. }
        25. //启动线程
        26. products.forEach(thread -> {
        27. thread.start();
        28. });
        29. //创建消费者
        30. new Thread(()->{
        31. for(int i=0;i<5;i++){
        32. String food= null;//出队列
        33. try {
        34. food = queue.take();//获取队列中元素
        35. } catch (InterruptedException e) {
        36. e.printStackTrace();
        37. }
        38. try {
        39. Thread.sleep(100);
        40. } catch (InterruptedException e) {
        41. e.printStackTrace();
        42. }
        43. System.out.println("消费1食品:"+food);
        44. }
        45. }).start();
        46. new Thread(()->{
        47. for(int i=0;i<5;i++){
        48. String food= null;//出队列
        49. try {
        50. food = queue.take();//获取队列中元素
        51. } catch (InterruptedException e) {
        52. e.printStackTrace();
        53. }
        54. try {
        55. Thread.sleep(100);
        56. } catch (InterruptedException e) {
        57. e.printStackTrace();
        58. }
        59. System.out.println("消费2食品:"+food);
        60. }
        61. }).start();
        62. new Thread(()->{
        63. for(int i=0;i<5;i++){
        64. String food= null;//出队列
        65. try {
        66. food = queue.take();//获取队列中元素
        67. } catch (InterruptedException e) {
        68. e.printStackTrace();
        69. }
        70. try {
        71. Thread.sleep(100);
        72. } catch (InterruptedException e) {
        73. e.printStackTrace();
        74. }
        75. System.out.println("消费3食品:"+food);
        76. }
        77. }).start();
        78. }
        79. }