39

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

 4 years ago
source link: https://mp.weixin.qq.com/s/xWhd3vgYPNp-Mm2lJpmFmA
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

一、并发问题

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

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、修饰静态方法

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

public class AccessVar04 {
public static void main(String[] args) {
Var04Test var04Test1 = new Var04Test() ;
Thread thread1 = new Thread(var04Test1) ;
thread1.start();
Var04Test var04Test2 = new Var04Test() ;
Thread thread2 = new Thread(var04Test2) ;
thread2.start();
}
}
class Var04Test implements Runnable {
private static Integer count ;
public Var04Test (){
count = 0 ;
}
public synchronized static void countAdd() {
System.out.println(Thread.currentThread().getName()+";count="+(count++));
}
@Override
public void run() {
countAdd() ;
}
}

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

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

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

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

6、注意事项

  • 继承中子类覆盖父类方法,synchronized关键字特性不能继承传递,必须显式声明;

  • 构造方法上不能使用synchronized关键字,构造方法中支持同步代码块;

  • 接口中方法,抽象方法也不支持synchronized关键字 ;

三、Volatile关键字

1、基本描述

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

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

2、使用案例

class Var05Test {
private volatile boolean myFlag = true ;
public void setFlag (boolean myFlag){
this.myFlag = myFlag ;
}
public void method() {
while (myFlag){
try {
System.out.println(Thread.currentThread().getName()+myFlag);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

3、注意事项

  • 可见性只能确保每次读取的是最新的值,但不支持变量操作的原子性;

  • volatile并不会阻塞线程方法,但是同步控制会阻塞;

  • Java同步控制的根本:保证并发下资源的原子性和可见性;

四、源代码地址

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

MzYveqr.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK