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

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

一、并发问题

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

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

原文地址:https://blog.51cto.com/14439672/2482929

时间: 2024-08-04 11:58:55

Java并发编程(03):多线程并发访问,同步控制的相关文章

Java并发编程之多线程同步

线程安全就是防止某个对象或者值在多个线程中被修改而导致的数据不一致问题,因此我们就需要通过同步机制保证在同一时刻只有一个线程能够访问到该对象或数据,修改数据完毕之后,再将最新数据同步到主存中,使得其他线程都能够得到这个最新数据.下面我们就来了解Java一些基本的同步机制. Java提供了一种稍弱的同步机制即volatile变量,用来确保将变量的更新操作通知到其他线程.当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的.然而,在访问volatile变量时不会执行加锁操作

day10-python并发编程之多线程协程及MySQL

第1章 python并发编程之多线程 1.1 死锁现象与递归锁 1.1.1 死锁概念 进程也有死锁与递归锁,在进程那里忘记说了,放到这里一切说了额 所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去.此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁 1.1.2 博客实例 from threading import Thread,Lock import time mutexA=L

长文慎入-探索Java并发编程与高并发解决方案

所有示例代码,请见/下载于https://github.com/Wasabi1234/concurrency #1 基本概念##1.1 并发同时拥有两个或者多个线程,如果程序在单核处理器上运行多个线程将交替地换入或者换出内存,这些线程是同时"存在"的,每个线程都处于执行过程中的某个状态,如果运行在多核处理器上,此时,程序中的每个线程都将分配到一个处理器核上,因此可以同时运行.##1.2 高并发( High Concurrency) 互联网分布式系统架构设计中必须考虑的因素之一,通常是指

并发编程专题(一)-并发与多线程

1.并发 1.1 并发与并行 首先介绍一下并发与并行,两者虽然只有一字之差,但实际上却有着本质的区别,其概念如下: 并行性(parallel):指在同一时刻,有多条指令在多个处理器上同时执行: 并发性(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果. 在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不

并发编程之多线程

一.并发编程之多线程 1.线程简单介绍 进程是资源单位,把所有资源集中到一起,而线程是执行单位,真正执行的是线程 每个进程都有一个地址空间,而且默认就有一个控制线程 多线程:在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间.进程之间是竞争关系,线程之间是协作关系 线程的创建开销比进程小很多,运行较快 主线程从执行层面上代表了其所在进程的执行过程 2.线程开启方式 方式一:使用替换threading模块提供的Thread from threading import Thread  d

python 学习_第四模块 并发编程(多线程)

python 学习_第四模块 并发编程(多线程) 1  开启线程方式 from threading import Thread import time def say(name): time.sleep(2) print("%s hello"%name) if __name__ =="__main__": t = Thread(target=say,args=("alex",)) t.start() print("主线程")

Python并发编程03/僵尸孤儿进程,互斥锁,进程之间的通信

目录 Python并发编程03/僵尸孤儿进程,互斥锁,进程之间的通信 1.昨日回顾 2.僵尸进程和孤儿进程 2.1僵尸进程 2.2孤儿进程 2.3僵尸进程如何解决? 3.互斥锁,锁 3.1互斥锁的应用 3.2Lock与join的区别 4.进程之间的通信 进程在内存级别是隔离的 4.1基于文件通信 (抢票系统) 4.2基于队列通信 Python并发编程03/僵尸孤儿进程,互斥锁,进程之间的通信 1.昨日回顾 1.创建进程的两种方式: 函数, 类. 2.pid: os.getpid() os.get

Python并发编程04/多线程

目录 Python并发编程04/多线程 1.生产消费者模型 2.线程的理论知识 2.1什么是线程 2.2线程vs进程 2.3线程的应用 3.开启进程的两种方式 3.1第一种方式 3.2第一种方式 4.线程vs进程的代码对比 4.1开启速度对比 4.2对比pid 4.3同一个进程内线程共享内部数据 5.线程的其他方法 6.join与守护线程 6.1join 6.2守护线程 7.互斥锁 Python并发编程04/多线程 1.生产消费者模型 #编程思想,模型,设计模式,理论等等,都是交给你一种编程的方

Java并发编程、多线程、线程池…

Java多线程干货系列(1):Java多线程基础http://www.importnew.com/21136.html#comment-651146 40个Java多线程问题总结http://www.importnew.com/18459.html#comment-651217 Java线程面试题 Top 50http://www.importnew.com/12773.html Java并发编程:Thread类的使用http://www.cnblogs.com/dolphin0520/p/39