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、修饰静态方法
静态方法属于类层级的方法,对象是不可以直接调用的。但是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

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
自研大数据分析技术,「安尔法」为采矿业打造智能运维系统
云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 随着物联网、大数据技术的成熟,越来越多的传统制造业开始智能化改造,包括电力、钢铁、化工等领域。这些领域的生产受设备影响大,对于设备的智能运维往往能带来较大幅度的效率提升。36氪近期接触到一家工业智能服务商「安尔法」,面向矿业领域提供设备数字化运维解决方案。 「安尔法」成立于2017年,利用工业互联网和信息化技术,为矿业客户提供设备预测性维护系统以及大数据智能监控平台,从而降低工厂运维成本、提高生产效率。公司客户以选煤厂、选矿厂为主。 矿业领域的大型设备占比高,生产环节互联,单台设备的故障停机就可能造成百万元级别的损失。公司研发负责人吴德勇表示,安尔法的智能运维解决方案通过采集设备数据、实时监控设备运行状况,能协助避免故障停机带来的经济损失,并将生产效率提升10%。 据了解,「安尔法」设备预测性维护的技术核心体现在大数据分析上。传感器采集设备数据之后,由后台的数据平台基于自研算法统一分析处理,并通过机器学习判断设备故障率。由于设备在不同的使用周期故障率不同,安尔法的数据处理算法能根据时间参...
-
下一篇
通晓多种编程语言的程序员,真香?
云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 你是否遇到过自称"多语种程序员"(polyglot programmer)的人?他们能够掌握多种编程语言。 在程序员的世界里,很多编程高手会被冠以各类称呼,有些称呼听起来很“狗血”,比如: “Code Ninja”(编程小忍者) “Rock Star developer”(摇滚巨星开发者) “Power Programmer”(编程高手) “Open Source Pundits”(开源专家) “Multiprocessing Moguls”(多处理大亨) “Server Monks”(服务器大师)等等…… 相比之下,多语种程序员这个称呼还挺正常的。 Merriam-Webster 将“多语种”定义为编程语言或术语的一种混合或融合。 https://www.merriam-webster.com/dictionary/polyglot 一般认为多语种编程这一术语是 Neal Ford 在 2006 年的一篇博客文章中创造出来的。Dean Wampler 在 2010 年的演讲中进一步补充...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Dcoker安装(在线仓库),最新的服务器搭配容器使用
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS7,8上快速安装Gitea,搭建Git服务器