Java并发编程(03):多线程并发访问,同步控制

本文源码:GitHub·点这里 || GitEE·点这里

一、并发问题

多线程学习的时候,要面对的第一个复杂问题就是,并发模式下变量的访问,如果不理清楚内在流程和原因,经常会出现这样一个问题:线程处理后的变量值不是自己想要的,可能还会一脸懵的说:这不合逻辑吧?

1、成员变量访问

多个线程访问类的成员变量,可能会带来各种问题。

1public class AccessVar01 { 2 public static void main(String[] args) { 3 Var01Test var01Test = new Var01Test() ; 4 VarThread01A varThread01A = new VarThread01A(var01Test) ; 5 varThread01A.start(); 6 VarThread01B varThread01B = new VarThread01B(var01Test) ; 7 varThread01B.start(); 8 } 9} 10class VarThread01A extends Thread { 11 Var01Test var01Test = new Var01Test() ; 12 public VarThread01A (Var01Test var01Test){ 13 this.var01Test = var01Test ; 14 } 15 @Override 16 public void run() { 17 var01Test.addNum(50); 18 } 19} 20class VarThread01B extends Thread { 21 Var01Test var01Test = new Var01Test() ; 22 public VarThread01B (Var01Test var01Test){ 23 this.var01Test = var01Test ; 24 } 25 @Override 26 public void run() { 27 var01Test.addNum(10); 28 } 29} 30class Var01Test { 31 private Integer num = 0 ; 32 public void addNum (Integer var){ 33 try { 34 if (var == 50){ 35 num = num + 50 ; 36 Thread.sleep(3000); 37 } else { 38 num = num + var ; 39 } 40 System.out.println("var="+var+";num="+num); 41 } catch (Exception e){ 42 e.printStackTrace(); 43 } 44 } 45}

这里案例的流程就是并发下运算一个成员变量,程序的本意是:var=50,得到num=50,可输出的实际结果是:

1var=10;num=60 2var=50;num=60

VarThread01A线程处理中进入休眠,休眠时num已经被线程VarThread01B进行一次加10的运算,这就是多线程并发访问导致的结果。

2、方法私有变量

修改上述的代码逻辑,把num变量置于方法内,作为私有的方法变量。

1class Var01Test { 2 // private Integer num = 0 ; 3 public void addNum (Integer var){ 4 Integer num = 0 ; 5 try { 6 if (var == 50){ 7 num = num + 50 ; 8 Thread.sleep(3000); 9 } else { 10 num = num + var ; 11 } 12 System.out.println("var="+var+";num="+num); 13 } catch (Exception e){ 14 e.printStackTrace(); 15 } 16 } 17}

方法内部的变量是私有的,且和当前执行方法的线程绑定,不会存在线程间干扰问题。

二、同步控制

1、Synchronized关键字

使用方式:修饰方法,或者以控制同步块的形式,保证多个线程并发下,同一时刻只有一个线程进入方法中,或者同步代码块中,从而使线程安全的访问和处理变量。如果修饰的是静态方法,作用的是这个类的所有对象。

独占锁属于悲观锁一类,synchronized就是一种独占锁,假设处于最坏的情况,只有一个线程执行,阻塞其他线程,如果并发高,处理耗时长,会导致多个线程挂起,等待持有锁的线程释放锁。

2、修饰方法

这个案例和第一个案例原理上是一样的,不过这里虽然在修改值的地方加入的同步控制,但是又挖了一个坑,在读取的时候没有限制,这个现象俗称脏读。

1public class AccessVar02 { 2 public static void main(String[] args) { 3 Var02Test var02Test = new Var02Test (); 4 VarThread02A varThread02A = new VarThread02A(var02Test) ; 5 varThread02A.start(); 6 VarThread02B varThread02B = new VarThread02B(var02Test) ; 7 varThread02B.start(); 8 var02Test.readValue(); 9 } 10} 11class VarThread02A extends Thread { 12 Var02Test var02Test = new Var02Test (); 13 public VarThread02A (Var02Test var02Test){ 14 this.var02Test = var02Test ; 15 } 16 @Override 17 public void run() { 18 var02Test.change("my","name"); 19 } 20} 21class VarThread02B extends Thread { 22 Var02Test var02Test = new Var02Test (); 23 public VarThread02B (Var02Test var02Test){ 24 this.var02Test = var02Test ; 25 } 26 @Override 27 public void run() { 28 var02Test.change("you","age"); 29 } 30} 31class Var02Test { 32 public String key = "cicada" ; 33 public String value = "smile" ; 34 public synchronized void change (String key,String value){ 35 try { 36 this.key = key ; 37 Thread.sleep(2000); 38 this.value = value ; 39 System.out.println("key="+key+";value="+value); 40 } catch (InterruptedException e) { 41 e.printStackTrace(); 42 } 43 } 44 public void readValue (){ 45 System.out.println("读取:key="+key+";value="+value); 46 } 47}

在线程中,逻辑上已经修改了,只是没执行到,但是在main线程中读取的value毫无意义,需要在读取方法上也加入同步的线程控制。

3、同步控制逻辑

同步控制实现是基于Object的监视器。

  • 线程对Object的访问,首先要先获得Object的监视器 ;
  • 如果获取成功,则会独占该对象 ;
  • 其他线程会掉进同步队列,线程状态变为阻塞 ;
  • 等Object的持有线程释放锁,会唤醒队列中等待的线程,尝试重启获取对象监视器;

4、修饰代码块

说明一点,代码块包含方法中的全部逻辑,锁定的粒度和修饰方法是一样的,就写在方法上吧。同步代码块一个很核心的目的,减小锁定资源的粒度,就如同表锁和行级锁。

1public class AccessVar03 { 2 public static void main(String[] args) { 3 Var03Test var03Test1 = new Var03Test() ; 4 Thread thread1 = new Thread(var03Test1) ; 5 thread1.start(); 6 Thread thread2 = new Thread(var03Test1) ; 7 thread2.start(); 8 Thread thread3 = new Thread(var03Test1) ; 9 thread3.start(); 10 } 11} 12class Var03Test implements Runnable { 13 private Integer count = 0 ; 14 public void countAdd() { 15 try { 16 Thread.sleep(1000); 17 } catch (InterruptedException e) { 18 e.printStackTrace(); 19 } 20 synchronized(this) { 21 count++ ; 22 System.out.println("count="+count); 23 } 24 } 25 @Override 26 public void run() { 27 countAdd() ; 28 } 29}

这里就是锁定count处理这个动作的核心代码逻辑,不允许并发处理。

5、修饰静态方法

静态方法属于类层级的方法,对象是不可以直接调用的。但是synchronized修饰的静态方法锁定的是这个类的所有对象。

1public class AccessVar04 { 2 public static void main(String[] args) { 3 Var04Test var04Test1 = new Var04Test() ; 4 Thread thread1 = new Thread(var04Test1) ; 5 thread1.start(); 6 Var04Test var04Test2 = new Var04Test() ; 7 Thread thread2 = new Thread(var04Test2) ; 8 thread2.start(); 9 } 10} 11class Var04Test implements Runnable { 12 private static Integer count ; 13 public Var04Test (){ 14 count = 0 ; 15 } 16 public synchronized static void countAdd() { 17 System.out.println(Thread.currentThread().getName()+";count="+(count++)); 18 } 19 @Override 20 public void run() { 21 countAdd() ; 22 } 23}

如果不是使用同步控制,从逻辑和感觉上,输出的结果应该如下:

1Thread-0;count=0 2Thread-1;count=0

加入同步控制之后,实际测试输出结果:

1Thread-0;count=0 2Thread-1;count=1

6、注意事项

  • 继承中子类覆盖父类方法,synchronized关键字特性不能继承传递,必须显式声明;
  • 构造方法上不能使用synchronized关键字,构造方法中支持同步代码块;
  • 接口中方法,抽象方法也不支持synchronized关键字 ;

三、Volatile关键字

1、基本描述

Java内存模型中,为了提升性能,线程会在自己的工作内存中拷贝要访问的变量的副本。这样就会出现同一个变量在某个时刻,在一个线程的环境中的值可能与另外一个线程环境中的值,出现不一致的情况。

使用volatile修饰成员变量,不能修饰方法,即标识该线程在访问这个变量时需要从共享内存中获取,对该变量的修改,也需要同步刷新到共享内存中,保证了变量对所有线程的可见性。

2、使用案例

1class Var05Test { 2 private volatile boolean myFlag = true ; 3 public void setFlag (boolean myFlag){ 4 this.myFlag = myFlag ; 5 } 6 public void method() { 7 while (myFlag){ 8 try { 9 System.out.println(Thread.currentThread().getName()+myFlag); 10 Thread.sleep(1000); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 } 15 } 16}

3、注意事项

  • 可见性只能确保每次读取的是最新的值,但不支持变量操作的原子性;
  • volatile并不会阻塞线程方法,但是同步控制会阻塞;
  • Java同步控制的根本:保证并发下资源的原子性和可见性;

四、源代码地址

1GitHub·地址 2https://github.com/cicadasmile/java-base-parent 3GitEE·地址 4https://gitee.com/cicadasmile/java-base-parent

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )