• Java学习笔记——并发编程(一)


    写在前面
          建议学习Java并发编程前,先学习一下jvm,这样可以理解的更清楚些。

    一、共享带来的问题

    1.上下文切换——分析

    Java体现
          两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0么?

    static int counter = 0;
    public static void main(String[] args) throws InterruptedException{
    	Thread t1 = new Thread(()->{
    		for(int i=0;i<5000;i++){
    			counter++;
    		}
    	},"t1");
    	
    	Thread t2 = new Thread(()->{
    		for(int i=0;i<5000;i++){
    			counter--;
    		}
    	},"t2");
    	
    	t1.start();
    	t2.start();
    	t1.join();
    	t2.join();
    	log.debug("{}",counter);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    问题分析
          例如对于i++而言(i为静态变量),实际会产生如下的JVM字节码指令:

    getstatic   i  //获取静态变量i的值
    iconst_1       //准备常量1
    iadd           //自增
    putstatic   i  //将修改后的值存入静态变量i
    
    • 1
    • 2
    • 3
    • 4

          而对于i–也是类似:

    getstatic   i  //获取静态变量i的值
    iconst_1       //准备常量1
    isub           //自减
    putstatic   i  //将修改后的值存入静态变量i
    
    • 1
    • 2
    • 3
    • 4
    • 1为-1的情况
      在这里插入图片描述
    • 为1的情况
      在这里插入图片描述

    2.临界区与竞态条件

    临界区Critical Section

    • 一个程序运行多个线程本身是没有问题的
    • 问题出在多个线程访问共享资源
      • 多个线程读共享资源其实也没有问题
      • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
    • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

    例如,下面代码中的临界区

    static int counter = 0;
    
    static void increment()
    //临界区
    {
    	counter++;
    }
    static void decrement()
    //临界区
    {
    	counter--;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    竞态条件Race Condition
          多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。

    二、synchronized解决方案

    为了避免临界区的竞态条件发生,有多种手段可以达到目的。

    • 阻塞式的解决方案:synchronized,Lock
    • 非阻塞式的解决方案:原子变量

          本节先讲解使用synchronized来解决,其余方法将在后续讲到。加了synchronized的Java代码如下:

    static int counter = 0;
    static Object lock = new Object();
    public static void main(String[] args) throws InterruptedException{
    	Thread t1 = new Thread(()->{
    		for(int i=0;i<5000;i++){
    			synchronized(lock){
    				counter++;
    			}
    		}
    	},"t1");
    	
    	Thread t2 = new Thread(()->{
    		for(int i=0;i<5000;i++){
    			synchronized(lock){
    				counter--;
    			}
    		}
    	},"t2");
    	
    	t1.start();
    	t2.start();
    	t1.join();
    	t2.join();
    	log.debug("{}",counter);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

          从下图中可以看出,当线程2完成isub减法后,虽然当前时间片到了,线程2暂时离开,但是锁还在,所以在线程1尝试获取锁,获取不到时,线程1会被阻塞(后面会介绍自旋的概念,线程并不是一开始就会被阻塞);当再次到线程2的时间片时,线程2执行putstatic操作,写入-1,然后把当前锁释放,并唤醒阻塞的线程。
    在这里插入图片描述
          在讲解了synchronized是如何解决问题的,下面将讲解synchronized的语法。
          synchronized可以修饰代码块、方法(静态方法、普通方法)。

    • 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
    • 修饰静态方法:作用于当前类对象,进入同步代码前要获得当前类对象的锁
    • 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁
    class Test{
    	public synchronized static void test(){
    		
    	}
    }
    //等价于
    class Test{
    	public static void test(){
    		synchronized(Test.class){
    		
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    class Test{
    	public synchronized void test(){
    		
    	}
    }
    //等价于
    class Test{
    	public void test(){
    		synchronized(this){
    		
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

          下面将举例来具体理解synchronized到底锁的是什么!

    情况1

    class Number{
    	public synchronized void a(){  //锁的是  对象实例
    		lod.debug("1");
    	}
    	public synchronized void b(){  //锁的是  对象实例
    		lod.debug("2");
    	}
    }
    
    public static void main(String[] args){
    	Number n1 = new Number();//同一个对象实例,有互斥
    	new Thread(()->{n1.a();}).start(); 
    	new Thread(()->{n1.b();}).start();
    }
    //输出分析
    //12 或 21
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    情况2

    class Number{
    	public synchronized void a(){  //锁的是  对象实例
    		sleep(1); //注意sleep不会释放锁
    		lod.debug("1");
    	}
    	public synchronized void b(){  //锁的是  对象实例
    		lod.debug("2");
    	}
    }
    
    public static void main(String[] args){
    	Number n1 = new Number();//不是同一个对象实例,不存在互斥
    	Number n2 = new Number();
    	new Thread(()->{n1.a();}).start(); 
    	new Thread(()->{n2.b();}).start();
    }
    //输出分析
    //2 1s后1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    情况3

    class Number{
    	public static synchronized void a(){  //锁的是  类对象
    		sleep(1); //注意sleep不会释放锁
    		lod.debug("1");
    	}
    	public synchronized void b(){  //锁的是  对象实例
    		lod.debug("2");
    	}
    }
    
    public static void main(String[] args){
    	Number n1 = new Number();//不存在互斥
    	new Thread(()->{n1.a();}).start(); 
    	new Thread(()->{n1.b();}).start();
    }
    //输出分析
    //2 1s后1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    情况4

    class Number{
    	public static synchronized void a(){  //锁的是  类对象
    		sleep(1); //注意sleep不会释放锁
    		lod.debug("1");
    	}
    	public static synchronized void b(){  //锁的是  类对象
    		lod.debug("2");
    	}
    }
    
    public static void main(String[] args){
    	Number n1 = new Number();//有互斥
    	new Thread(()->{n1.a();}).start(); 
    	new Thread(()->{n1.b();}).start();
    }
    //输出分析
    //2 1s后1 或 1s后12
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    情况5

    class Number{
    	public static synchronized void a(){  //锁的是  类对象
    		sleep(1); //注意sleep不会释放锁
    		lod.debug("1");
    	}
    	public synchronized void b(){  //锁的是  对象实例
    		lod.debug("2");
    	}
    }
    
    public static void main(String[] args){
    	Number n1 = new Number();//不存在互斥
    	Number n2 = new Number();
    	new Thread(()->{n1.a();}).start(); 
    	new Thread(()->{n2.b();}).start();
    }
    //输出分析
    //2 1s后1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    情况6

    class Number{
    	public static synchronized void a(){  //锁的是  类对象
    		sleep(1); //注意sleep不会释放锁
    		lod.debug("1");
    	}
    	public static synchronized void b(){  //锁的是  类对象
    		lod.debug("2");
    	}
    }
    
    public static void main(String[] args){
    	Number n1 = new Number();//有互斥
    	Number n2 = new Number();
    	new Thread(()->{n1.a();}).start(); 
    	new Thread(()->{n2.b();}).start();
    }
    //输出分析
    //2 1s后1 或 1s后12
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    三、线程安全分析

    成员变量和静态变量是否线程安全?

    • 如果它们没有共享,则线程安全
    • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
      • 如果只有读操作,则线程安全
      • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

    局部变量是否线程安全?

    • 局部变量是线程安全的
    • 但局部变量引用的对象则未必
      • 如果该对象没有逃离方法的作用范围,它是线程安全的
      • 如果该对象逃离方法的作用范围,需要考虑线程安全

          下面对文字信息进行举例讲解。
    局部变量线程安全分析

    局部变量为基本数据类型

    public static void test1(){
    	int i = 10;
    	i++;
    }
    
    • 1
    • 2
    • 3
    • 4

          每个线程调用test1()方法时局部变量i,会在每个线程的栈帧内存中被创建多份,因此不存在共享。

    public static void test1();
    	descriptor:()V
    	flags: ACC_PULIC,ACC_STATIC
    	Code:
    		stack=1,locals=1,args_size=0
    			0:bipush     10
    			2:istore_0
    			3:iinc       0,1
    			6:return
    		LineNumberTable
    			line 10:0
    			line 11:3
    			line 12:6
    		LocalVariableTable:
    			Start  Length  Slot  Name  Signature
    				3		4	0		i		I
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    如图
    在这里插入图片描述
    局部变量为引用类型
          先看一个成员变量的例子。

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    
    public static void main(String[] args){
    	ThreadUnsafe test = new ThreadUnsafe();
    	for(int i=0;i<THREAD_NUMBER;i++){
    		new Thread(()->{
    			test.method1(LOOP_NUMBER);
    		},"Thread"+(i+1)).start();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    class ThreadUnsafe{
    	ArrayList<String> list = new ArrayList<>();
    	public void method1(int loopNumber){
    		for(int i=0;i<loopNumber;i++){
    			method2();
    			method3();
    		}
    	}
    	private void method2(){list.add("1")};
    	
    	private void method3(){list.remove(0)};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

          因为add不保证原子性,线程1在添加的时候有可能线程2也在添加,然后就只添加了一个元素,但是接下来却碰到了两个移除0位置的操作,就报错了:
    在这里插入图片描述
          无论哪个线程中的method2引用的都是同一个对象中的list成员变量。method3和method2分析相同。
    在这里插入图片描述
          将list修改为局部变量

    static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    
    public static void main(String[] args){
    	ThreadSafe test = new ThreadSafe();
    	for(int i=0;i<THREAD_NUMBER;i++){
    		new Thread(()->{
    			test.method1(LOOP_NUMBER);
    		},"Thread"+(i+1)).start();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    class ThreadSafe{
    	public final void method1(int loopNumber){
    		ArrayList<String> list = new ArrayList<>();
    		for(int i=0;i<loopNumber;i++){
    			method2(list);
    			method3(list);
    		}
    	}
    	private void method2(ArrayList<String> list){list.add("1")};
    	
    	private void method3(ArrayList<String> list){list.remove(0)};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

          为什么改成局部变量就没有问题了呢?
    分析

    • list是局部变量,每隔线程调用时会创建其不同实例,没有共享
    • method2的参数是从method1中传递过来的,与method1中引用同一个对象
    • method3的参数分析与method2相同
      在这里插入图片描述
      常见类——组合调用

    常见线程安全类

    • String
    • Integer
    • StringBuffer
    • Random
    • Vector
    • Hashtable
    • java.util.concurrent包下的类

    这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为

    Hashtable table = new Hashtable();
    
    new Thread(()->{
    	table.put("key","value1");
    }).start();
    
    new Thread(()->{
    	table.put("key","value2");
    }).start();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 它们的每个方法是原子的
    • 注意它们多个方法的组合不是原子的,见后面分析

    ** 线程安全类方法的组合**
    分析下面代码是否线程安全?

    Hashtable table = new Hashtable();
    //线程1,线程2
    if(table.get("key") == null){
    	table.put("key",value);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    学习视频
    黑马程序员全面深入学习Java并发编程,JUC并发编程全套教程

  • 相关阅读:
    Redisson资料
    一个配线光交的网络覆盖能力有多大?
    Python tkinter-- 第16章 菜单(Menu)方法
    (C++)把字符串转换成整数
    如何将heic转换成jpg呢?
    【2023集创赛】加速科技杯三等奖作品:私密性高精度刷手身份认证系统
    Atcoder abc131
    Docker-数据卷-DockerFile
    Python异步编程之web框架 异步vs同步 文件IO任务压测对比
    外贸是什么意思?和跨境电商的区别是什么?
  • 原文地址:https://blog.csdn.net/l903445981/article/details/126018141