本文源码: GitHub. 点这里 https://github.com/cicadasmile/java-base-parent || GitEE. 点这里 https://gitee.com/cicadasmile/java-base-parent
一, 并发问题
多线程学习的时候, 要面对的第一个复杂问题就是, 并发模式下变量的访问, 如果不理清楚内在流程和原因, 经常会出现这样一个问题: 线程处理后的变量值不是自己想要的, 可能还会一脸懵的说: 这不合逻辑吧?
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://www.cnblogs.com/cicada-smile/p/12594505.html