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

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

1、成员变量访问

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

public class AccessVar01 { public static void main(String[] args) { Var01Test var01Test = new Var01Test() ; VarThread01A varThread01A = new VarThread01A(var01Test) ; varThread01A.start(); VarThread01B varThread01B = new VarThread01B(var01Test) ; varThread01B.start(); } } class VarThread01A extends Thread { Var01Test var01Test = new Var01Test() ; public VarThread01A (Var01Test var01Test){ this.var01Test = var01Test ; } @Override public void run() { var01Test.addNum(50); } } class VarThread01B extends Thread { Var01Test var01Test = new Var01Test() ; public VarThread01B (Var01Test var01Test){ this.var01Test = var01Test ; } @Override public void run() { var01Test.addNum(10); } } class Var01Test { private Integer num = 0 ; public void addNum (Integer var){ try { if (var == 50){ num = num + 50 ; Thread.sleep(3000); } else { num = num + var ; } System.out.println("var="+var+";num="+num); } catch (Exception e){ e.printStackTrace(); } } }

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

var=10;num=60 var=50;num=60

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

2、方法私有变量

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

class Var01Test { // private Integer num = 0 ; public void addNum (Integer var){ Integer num = 0 ; try { if (var == 50){ num = num + 50 ; Thread.sleep(3000); } else { num = num + var ; } System.out.println("var="+var+";num="+num); } catch (Exception e){ e.printStackTrace(); } } }

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

二、同步控制 1、Synchronized关键字

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

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

2、修饰方法

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

public class AccessVar02 { public static void main(String[] args) { Var02Test var02Test = new Var02Test (); VarThread02A varThread02A = new VarThread02A(var02Test) ; varThread02A.start(); VarThread02B varThread02B = new VarThread02B(var02Test) ; varThread02B.start(); var02Test.readValue(); } } class VarThread02A extends Thread { Var02Test var02Test = new Var02Test (); public VarThread02A (Var02Test var02Test){ this.var02Test = var02Test ; } @Override public void run() { var02Test.change("my","name"); } } class VarThread02B extends Thread { Var02Test var02Test = new Var02Test (); public VarThread02B (Var02Test var02Test){ this.var02Test = var02Test ; } @Override public void run() { var02Test.change("you","age"); } } class Var02Test { public String key = "cicada" ; public String value = "smile" ; public synchronized void change (String key,String value){ try { this.key = key ; Thread.sleep(2000); this.value = value ; System.out.println("key="+key+";value="+value); } catch (InterruptedException e) { e.printStackTrace(); } } public void readValue (){ System.out.println("读取:key="+key+";value="+value); } }

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

3、同步控制逻辑

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

线程对Object的访问,首先要先获得Object的监视器 ;

如果获取成功,则会独占该对象 ;

其他线程会掉进同步队列,线程状态变为阻塞 ;

等Object的持有线程释放锁,会唤醒队列中等待的线程,尝试重启获取对象监视器;

4、修饰代码块

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

public class AccessVar03 { public static void main(String[] args) { Var03Test var03Test1 = new Var03Test() ; Thread thread1 = new Thread(var03Test1) ; thread1.start(); Thread thread2 = new Thread(var03Test1) ; thread2.start(); Thread thread3 = new Thread(var03Test1) ; thread3.start(); } } class Var03Test implements Runnable { private Integer count = 0 ; public void countAdd() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized(this) { count++ ; System.out.println("count="+count); } } @Override public void run() { countAdd() ; } }

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

5、修饰静态方法

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgzszz.html