【多线程】线程同步

技术博客 (413) 2023-09-16 16:12:01

一,线程同步机制简介

线程同步机制是一套用于协调线程之间数据访问的机制,该机制可以保障线程安全。
Java平台提供的线程同步机制包括:锁,volatile关键字,final关键字,static关键字,以及相关的API,如Object.wait()/Object.notify()等。

二,锁概述

线程安全问题的产生前提是多个线程并发访问共享数据。
将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问。所就是复用这种思路来保障线程安全的。
锁(Lock)可以理解为对共享数据进行保护的一个许可证,对于同一个许可证保护的共享数据来说,任何线程想要访问这些共享数据必须先持有该许可证。一个线程只有在持有许可证的情况下才能对这些共享数据进行访问并且一个许可证一次只能被一个线程持有;许可证线程在结束对共享数据的访问后必须释放其持有的许可证。
一个线程在访问共享数据前必须先获得锁;获得锁的线程称为锁的持有线程;一个锁一次只能被一个线程持有。锁的持有线程在获得锁之后和释放锁之前这段时间所执行的代码被称为临界区(Critical Section)。
锁具有排他性(Exclusive),即一个锁一次只能被一个线程持有。这种锁称为排他锁或互斥锁(Mutex)。
【多线程】线程同步 (https://mushiming.com/) 技术博客 第1张
JVM把锁分为内部锁和显示锁两种。内部锁通过synchronized关键字实现;显示锁通过java.concurrent.locks.Lock接口的实现类实现的。

1,锁的作用

锁可以实现对共享数据的安全访问。保障线程的原子性,可见性与有序性。
锁是通过互斥保障原子性。一个锁只能被一个线程持有,这就保证临街区的代码一次只能被一个线程执行。使得临界区代码所执行的操作自然而然的具有不可分割的特性,即具备了原子性。
可见性的保障是通过写线程冲刷处理器的缓存和读线程刷新处理器缓存这两个动作实现的。在Java平台中,锁的获得隐含着刷新处理器缓存的动作,锁的释放隐含着冲刷处理器缓存的动作。
锁能够保障有序性。写线程在临界区所执行的在读线程所执行的临界区看来像是完全按照源码顺序执行的。
注意!在使用锁保障线程安全性,必须满足以下条件:

  • 这些线程在访问共享数据时必须使用统一个锁;
  • 即使是读取共享数据的线程也需要使用同步锁。

2,锁的相关概念

1)可重入性
可重入性描述这样一个问题:一个线程持有该锁的时候能否再次(多次)申请该锁

void methodA() { 
   
    申请a锁
    methodB();
    释放a锁
}

void methodB() { 
   
	申请a锁
	......
	释放a锁

如果一个线程持有一个锁的时候还能够继续成功申请该锁,称该锁是可重入的,否则就称该锁为不可重入的。
2)锁的争用与调度
Java平台中内部锁属于非公平锁,显示Lock锁既支持公平锁又支持非公平锁。
3)锁的粒度
一个锁可以保护的共享数据的数量大小称为锁的粒度。
锁保护共享数据量大,称该锁的粒度粗,否则就称该锁的粒度细。
锁的粒度过粗会导致线程在申请锁时会进行不必要的等待。锁的粒度过细会增加锁调度的开销

三,内部锁:synchronized关键字

Java中的每个对象都有一个与之关联的内部锁(Intrinsic lock),这种锁也称为监视器(Monitor),这种内部锁是一种排他锁,可以保障原子性,可见性与有序性。
内部锁是通过synchronized关键字实现的。synchronized关键字修饰代码块,修饰该方法。
修饰代码块的语法:

synchronized(对象锁) { 
   
	同步代码块,可以在同步代码块中访问共享数据
}

修饰实例方法就称为同步实例方法。
修饰静态方法就称为同步静态方法。

1,同步实例方法

没加锁

public class TestSynchronized01 { 
   
    public static void main(String[] args) { 
   
        //创建两个线程,分别调用setNumber()方法
        //先创建Test01对象,通过对象名调用setNumber()方法
        TestSynchronized01 t = new TestSynchronized01();
        new Thread(t::setNumber).start();  

        new Thread(t::setNumber).start();

    }

    //定义方法,打印100行字符窜
    public void setNumber() { 
   
        for (int i = 1; i <= 100; i++) { 
   
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }

}

没加锁时,结果如下,两个线程存在交互的现象。
【多线程】线程同步 (https://mushiming.com/) 技术博客 第2张

加锁

/** * synchronized同步实例方法 * 把整个方法体作为同步代码块 * 默认的锁对象是this对象 */
public class TestSynchronized01 { 
   
    public static void main(String[] args) { 
   
        //创建两个线程,分别调用setNumber()方法
        //先创建Test01对象,通过对象名调用setNumber()方法
        TestSynchronized01 t = new TestSynchronized01();
        new Thread(t::setNumber).start();  //使用的锁对象this就是t

        new Thread(t::setNumber).start();  //使用的锁对象this就是t

    }

    //定义方法,打印100行字符窜
    public void setNumber() { 
   
        synchronized (this) { 
    //经常使用this当前对象作为锁对象
            for (int i = 1; i <= 100; i++) { 
   
                System.out.println(Thread.currentThread().getName() + "-->" + i);
            }
        }
    }

}

加锁后,则是先执行完一个线程后,在执行另一个线程
【多线程】线程同步 (https://mushiming.com/) 技术博客 第3张

第一个线程为Thread-0,第二个线程为Thread-1
1)假设Thread-0线程获取CPU执行权
调用t对象的setNumber()方法,
执行方法体,先获得this对象的object的锁
执行for循环;
2)假设Thread-0在执行for循环期间,Thread-1线程获得了CPU执行权
调用t对象的setNumber()方法,
执行方法体,先获得this对象t的锁;

现在Thread-0线程持有this对象t的锁,synchronized内部锁是排他锁,只能被一个线程持有,Thread-1进入等待区等待this对象t的锁
3)当Thread-0线程重新获得CPU执行权,把for循环执行完,即执行完同步代码块,Thread-0线程会释放this对象t的锁;
4)等待区的Thread-1线程获得this对象t的锁。

使用常量作为锁对象

public class TestSynchronized02 { 
   

    public static void main(String[] args) { 
   
        TestSynchronized02 t = new TestSynchronized02();
        TestSynchronized02 t2 = new TestSynchronized02();
        new Thread(t::setNumber).start();  //使用的锁对象this就是t

        new Thread(t2::setNumber).start();  //使用的锁对象this就是t2
    }

    public static final Object OBJ = new Object();    //定义一个常量

    //定义方法,打印100行字符窜
    public void setNumber() { 
   
        synchronized (OBJ) { 
    //经常使用this当前对象作为锁对象
            for (int i = 1; i <= 100; i++) { 
   
                System.out.println(Thread.currentThread().getName() + "-->" + i);
            }
        }
    }

}

即使是创建两个对象,也会实现同步锁的功能。
【多线程】线程同步 (https://mushiming.com/) 技术博客 第4张
修饰方法

public class TestSynchronized03 { 
   
    public static void main(String[] args) { 
   
        TestSynchronized03 t = new TestSynchronized03();
        new Thread(t::setNumber).start();
        new Thread(t::setNumber2).start();
    }

    public void setNumber() { 
   
        synchronized (this) { 
    //经常使用this当前对象作为锁对象
            for (int i = 1; i <= 100; i++) { 
   
                System.out.println(Thread.currentThread().getName() + "-->" + i);
            }
        }
    }

    //使用synchronized修饰实例方法,同步实例方法,默认this作为锁对象
    public synchronized void setNumber2() { 
   
        for (int i = 1; i <= 200; i++) { 
   
            System.out.println(Thread.currentThread().getName() + " --> " + i);
        }
    }

}

修饰方法效果一样
【多线程】线程同步 (https://mushiming.com/) 技术博客 第5张

2,同步静态方法

/** * synchronized同步静态方法 * 把整个方法作为同步代码块 * 默认的锁对象是当前类的运行时类对象 */
public class TestSynchronized04 { 
   
    public static void main(String[] args) { 
   
        TestSynchronized04 t = new TestSynchronized04();
        new Thread(t::setNumber).start();
        new Thread(TestSynchronized04::setNumber2).start();
    }

    public void setNumber() { 
   
        //使用当前类的运行时类对象作为锁对象,可以简单的理解为把当前类的字节码文件作为锁对象
        synchronized (TestSynchronized04.class) { 
   
            for (int i = 1; i <= 100; i++) { 
   
                System.out.println(Thread.currentThread().getName() + " -- > " + i);
            }
        }
    }

    //使用synchronized修饰静态方法,同步静态方法,默认运行时类作为对象
    public synchronized static void setNumber2() { 
   
        for (int i = 1; i <= 100; i++) { 
   
            System.out.println(Thread.currentThread().getName() + " -- > " + i);
        }
    }

}

synchronized修饰静态方法,默认运行时类作为对象,所以同样是同步的
【多线程】线程同步 (https://mushiming.com/) 技术博客 第6张

3,同步方法与同步代码

同步代码代码块

/** * 同步方法与同步代码块如何抉择 */
public class TestSynchronized05 { 
   
    public static void main(String[] args) { 
   
        TestSynchronized05 t = new TestSynchronized05();
        new Thread(t::doLongTimeTask).start();
        new Thread(t::doLongTimeTask).start();
    }
    //同步代码块,锁的粒度细,并发效率高
    public void doLongTimeTask() { 
   
        try { 
   
            System.out.println("Task Begin");
            Thread.sleep(3000);         //模拟任务需要准备3秒钟
            synchronized (this) { 
   
                System.out.println("开始同步");
                for (int i = 0; i <= 100; i++) { 
   
                    System.out.println(Thread.currentThread().getName() + " --> " + i);
                }
            }
            System.out.println("Task End");
        } catch (Exception e) { 
   
            e.printStackTrace();
        }
    }

}

【多线程】线程同步 (https://mushiming.com/) 技术博客 第7张
【多线程】线程同步 (https://mushiming.com/) 技术博客 第8张
可见,第一个线程执行后在睡眠的同时第二个线程开始执行了,但是是在第一个线程的同步代码块执行完成后,第二个线程才开始执行同步代码块的内容。

同步方法

/** * 同步方法与同步代码块如何抉择 */
public class TestSynchronized05 { 
   
    public static void main(String[] args) { 
   
        TestSynchronized05 t = new TestSynchronized05();
        new Thread(t::doLongTimeTask2).start();
        new Thread(t::doLongTimeTask2).start();
    }

    //同步方法,锁的粒度粗,并发效率低
    public synchronized void doLongTimeTask2() { 
   
        try { 
   
            System.out.println("Task Begin");
            Thread.sleep(3000);         //模拟任务需要准备3秒钟
            System.out.println("开始同步");
            for (int i = 0; i <= 100; i++) { 
   
                System.out.println(Thread.currentThread().getName() + " --> " + i);
            }
            System.out.println("Task End");
        } catch (Exception e) { 
   
            e.printStackTrace();
        }
    }

}

【多线程】线程同步 (https://mushiming.com/) 技术博客 第9张
【多线程】线程同步 (https://mushiming.com/) 技术博客 第10张
可见同步方法,是在第一个线程完全执行完后,才开始执行第二个线程,那么第一个线程睡眠的3s就白白浪费了

由此可以得出结论:同步代码块,锁的粒度细,并发效率高;同步方法,锁的粒度粗,并发效率低。

4,脏读

/** * 脏读 * 出现读取属性值出现了一些意外,读取的是中间值,而不是修改之后的值 * 出现脏读的原因是对共享数据的修改与对共享数据的读取不同步 * 解决方法: * 不仅对修改数据的代码块进行同步,还要对读取数据的代码进行同步 */
public class TestDirtyRead01 { 
   
    public static void main(String[] args) throws InterruptedException { 
   
        //开启子线程设置用户名和密码
        PublicValue publicValue = new PublicValue();
        SubThread t1 = new SubThread(publicValue);
        t1.start();
        //为了确定设置成功
        Thread.sleep(100);
        //在main线程中读取用户名,密码
        publicValue.getValue();
    }

    static class SubThread extends Thread { 
   
        private PublicValue publicValue;
        public SubThread(PublicValue publicValue) { 
   
            this.publicValue = publicValue;
        }

        @Override
        public void run() { 
   
            publicValue.setValue("dh", "123");
        }

    }

    static class PublicValue { 
   
        private String name = "drhj";
        private String pwd = "520";

        public void getValue() { 
   
            System.out.println(Thread.currentThread().getName() + ", getter -- name: " + name + ", -- pwd: " + pwd);
        }

        public void setValue(String name, String pwd) { 
   
            this.name = name;
            try { 
   
                Thread.sleep(1000);         //模拟操作name属性需要一定时间
            } catch (Exception e) { 
   
                e.printStackTrace();
            }
            this.pwd = pwd;
            System.out.println(Thread.currentThread().getName() + ", setter -- name: " + name + ", -- pwd: " + pwd);
        }

    }

}

【多线程】线程同步 (https://mushiming.com/) 技术博客 第11张
出现了脏读的情况
出现读取属性值出现了一些意外,读取的是中间值,而不是修改之后的值,出现脏读的原因是对共享数据的修改与对共享数据的读取不同步。
解决方法:不仅对修改数据的代码块进行同步,还要对读取数据的代码进行同步

/** * 脏读 * 出现读取属性值出现了一些意外,读取的是中间值,而不是修改之后的值 * 出现脏读的原因是对共享数据的修改与对共享数据的读取不同步 * 解决方法: * 不仅对修改数据的代码块进行同步,还要对读取数据的代码进行同步 */
public class TestDirtyRead01 { 
   
    public static void main(String[] args) throws InterruptedException { 
   
        //开启子线程设置用户名和密码
        PublicValue publicValue = new PublicValue();
        SubThread t1 = new SubThread(publicValue);
        t1.start();
        //为了确定设置成功
        Thread.sleep(100);
        //在main线程中读取用户名,密码
        publicValue.getValue();
    }

    static class SubThread extends Thread { 
   
        private PublicValue publicValue;
        public SubThread(PublicValue publicValue) { 
   
            this.publicValue = publicValue;
        }

        @Override
        public void run() { 
   
            publicValue.setValue("dh", "123");
        }

    }

    static class PublicValue { 
   
        private String name = "drhj";
        private String pwd = "520";

        public synchronized void getValue() { 
   
            System.out.println(Thread.currentThread().getName() + ", getter -- name: " + name + ", -- pwd: " + pwd);
        }

        public synchronized void setValue(String name, String pwd) { 
   
            this.name = name;
            try { 
   
                Thread.sleep(1000);         //模拟操作name属性需要一定时间
            } catch (Exception e) { 
   
                e.printStackTrace();
            }
            this.pwd = pwd;
            System.out.println(Thread.currentThread().getName() + ", setter -- name: " + name + ", -- pwd: " + pwd);
        }

    }

}

【多线程】线程同步 (https://mushiming.com/) 技术博客 第12张

5,异常

当一个线程在执行时,发生了异常会怎么样呢?其他线程一直等着?死锁?

public class TestSynchronized06 { 
   
    public static void main(String[] args) { 
   
        TestSynchronized06 t = new TestSynchronized06();
        new Thread(t::setNumber).start();
        new Thread(t::setNumber2).start();
    }

    public void setNumber() { 
   
        synchronized (this) { 
   
            for (int i = 1; i <= 100; i++) { 
   
                System.out.println(Thread.currentThread().getName() + " --> " + i);
                if (i == 50) { 
   
                    Integer.parseInt("abc");
                }
            }
        }
    }

    public synchronized void setNumber2() { 
   
        for (int i = 1; i <= 100; i++) { 
   
            System.out.println(Thread.currentThread().getName() + " --> " + i);
        }
    }

}

【多线程】线程同步 (https://mushiming.com/) 技术博客 第13张
【多线程】线程同步 (https://mushiming.com/) 技术博客 第14张
可见,在遇到异常时,直接释放锁,下一个线程继续执行。

6,死锁

在多线程程序中,同步时可能需要使用多个锁,如果获得锁的顺序不一致,可能会导致死锁

/** * 死锁 * 在多线程程序中,同步时可能需要使用多个锁,如果获得锁的顺序不一致,可能会导致死锁 */
public class TestDeadLock { 
   

    public static void main(String[] args) { 
   
        SubThread t1 = new SubThread();
        SubThread t2 = new SubThread();
        t1.setName("a");
        t2.setName("b");
        t1.start();
        t2.start();
    }

    static class SubThread extends Thread { 
   
        private static final Object lock1 = new Object();
        private static final Object lock2 = new Object();

        @Override
        public void run() { 
   
            if ("a".equals(Thread.currentThread().getName())) { 
   
                synchronized (lock1) { 
   
                    System.out.println("a线程获得了lock1锁,还需要获得lock2锁");
                    synchronized (lock2) { 
   
                        System.out.println("a线程获得lock1锁后又获得了lock2,可以想干任何想干的事");
                    }
                }
            }
            if ("b".equals(Thread.currentThread().getName())) { 
   
                synchronized (lock2) { 
   
                    System.out.println("b线程获得了lock2锁,还需要获得lock1锁");
                    synchronized (lock1) { 
   
                        System.out.println("b线程获得lock2后又获得了lock1,可以想干任何想干的事");
                    }
                }
            }
        }
    }

}

结果如下:
【多线程】线程同步 (https://mushiming.com/) 技术博客 第15张
那如何避免死锁呢?
当需要获得多个锁时,所有线程获得锁的顺序保持一致即可。

四,轻量级同步机制:volatile关键字

1,volatile的作用

volatile的作用是使变量在多个线程之间可见。

public class TestVolatile01 { 
   
    public static void main(String[] args) { 
   
        PrintString ps = new PrintString();
        new Thread(()-> ps.printStringMethod()).start();
        //main线程睡眠1000ms
        try { 
   
            Thread.sleep(1000);
        } catch (Exception e) { 
   
            e.printStackTrace();
        }
        System.out.println("在main线程中修改打印标志");
        ps.setContinuePrint(false);
        //程序运行,查看在main线程中修改了打印标志之后,子线程打印是否可以结束
        //程序运行后,可能会出现死循环情况
        //分析原因:main线程修改了ps对象的打印标志后,子线程读不到
        //解决办法:使用volatile关键字修饰
        //volatile可以强制线程从公共内存中读取变量的值,而不是从工作内存中读取
    }

    //定义一个人打印字符窜
    static class PrintString { 
   
        private boolean continuePrint = true;
        public PrintString setContinuePrint(boolean continuePrint) { 
   
            this.continuePrint = continuePrint;
            return this;
        }
        public void printStringMethod() { 
   
            System.out.println(Thread.currentThread().getName() + "开始....");
            while (continuePrint) { 
   
            }
            System.out.println(Thread.currentThread().getName() + "打印结束");
        }
    }

}

运行发现,可能会存在main线程修改了ps对象的打印标志后,子线程读不到的情况,即无法运行结束
【多线程】线程同步 (https://mushiming.com/) 技术博客 第16张
【多线程】线程同步 (https://mushiming.com/) 技术博客 第17张
当加了volatile后,运行结束了
【多线程】线程同步 (https://mushiming.com/) 技术博客 第18张
volatile可以强制线程从公共内存中读取变量的值,而不是从工作内存中读取
volatile与synchronized比较:

  1. volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好;
  2. volatile只能修饰变量,而synchronized可以修饰方法,代码块;
  3. 随着JDK新版本的发布,synchronized的执行效率也有较大的提升,在开发中使用synchronied的比率还是很大的;
  4. 多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞;
  5. volatile能保证数据的可见性,但是不能保证原子性;而synchronized可以保证原子性,也可以保证可见性;
  6. volatile解决的是变量在多个线程之间的可见性;synchronized解决对个线程之间访问共享资源的同步性。

2,volatile非原子特性

/** * volatile不具备原子性 */
public class TestVolatile02 { 
   
    public static void main(String[] args) { 
   
        //在main线程当中创建10个子线程
        for (int i = 0; i < 100; i++) { 
   
            new MyThread().start();
        }
    }

    static class MyThread extends Thread { 
   
        //volatile关键仅仅是表示所有线程从内存读取count变量的值
        volatile public static int count;
        public static void addCount() { 
   
            for (int i = 0; i < 1000; i++) { 
   
                count++;
            }
            System.out.println(Thread.currentThread().getName() + " count=" + count);
        }

        @Override
        public void run() { 
   
            addCount();
        }
    }

}

当我们执行这段程序时,如果保证线程的原子性,那么打印出来的一定都是1000的倍数,但结果不是,说明volatile不能保证线程的原子性。
【多线程】线程同步 (https://mushiming.com/) 技术博客 第19张
解决方法是给addCount方法添加synchronized同步锁,那么也就没有必要给count添加volatile关键字了。

3,常用的原子类进行自增和自减操作

我们知道i++操作不是原子操作,除了使用synchronized进行同步外,也可以使用AtomicInteger/AtomicLong原子类进行实现。
AtomicInteger

public class TestAtomic01 { 
   
    public static void main(String[] args) throws InterruptedException { 
   
        //在main线程当中创建10个子线程
        for (int i = 0; i < 100; i++) { 
   
            new MyThread().start();
        }
        Thread.sleep(1000);
        System.out.println(MyThread.count.get());
    }

    static class MyThread extends Thread { 
   
        //使用AtomicInteger对象
        private static AtomicInteger count = new AtomicInteger();

        public static void addCount() { 
   
            for (int i = 0; i < 1000; i++) { 
   
                count.getAndIncrement();
            }
            System.out.println(Thread.currentThread().getName() + " count=" + count.get());
        }

        @Override
        public void run() { 
   
            addCount();
        }
    }
}

保证了原子性,其结果如下
【多线程】线程同步 (https://mushiming.com/) 技术博客 第20张
仔细看,会出现不是1000整数的情况,原子性?
此次存在有不是1000的整数倍是因为method这个方法不是同步的。如当thread-0刚执行完循环的时候是1000,但是其他线程仍然在自增,打印结果就不会是1000,但是原子类对象的自增是同步的,所以不会出现脏读现象,即最终结果始终正确。

五,CAS

1,概述

CAS(Compare And Swap)是由硬件实现的。
CAS可以将read-modify-write这类的操作转换为原子操作。
i++自增操作包括三个子操作:
从内存读取i变量值
对i的值加1
再把加1之后的值保存到主内存
CAS原理:在把数据更新到主内存时,再次读取主内存变量的值,如果在变量的值与期望的值(操作起始时读取的值)一样的值就更新
【多线程】线程同步 (https://mushiming.com/) 技术博客 第21张
在第七步撤销后可能重做,或者不做。

2,使用CAS实现线程的计数器

/** * 使用CAS实现一个线程安全的计数器 */
public class TestCAS { 
   
    public static void main(String[] args) { 
   
        CASCounter cc = new CASCounter();
        for (int i = 0; i < 1000; i++) { 
   
            new Thread(()-> System.out.println(cc.incrementAndGet())).start();
        }
    }
}

class CASCounter { 
   
    //使用volatile修饰value值,使线程可见
    volatile private long value;
    public long getValue() { 
   
        return value;
    }

    //定义compare and swap方法
    private boolean compareAndSwap(long expectedValue, long newValue) { 
   
        //如果当前value的值与期望的expectedValue值一样,就把当前的Value字段替换为newValue值
        synchronized (this) { 
   
            if (value == expectedValue) { 
   
                value = newValue;
                return true;
            } else { 
   
                return false;
            }
        }
    }

    //定义自增的方法
    public long incrementAndGet() { 
   
        long oldValue;
        long newValue;
        do { 
   
            oldValue = value; //一直去读最新的value值,知道对为止
            newValue = oldValue + 1;
        } while (!compareAndSwap(oldValue, newValue));
        return newValue;
    }

}

【多线程】线程同步 (https://mushiming.com/) 技术博客 第22张
CAS实现原子操作背后有一个假设:共享变量的当前值与当前线程提供的期望值相同,就认为这个变量没有被其他线程修改过。
实际上这种假设不一定总是成立。如果共享变量count = 0
A 线程对count值修改为10
B 线程对count值修改为20
C 线程对count值修改为0
当前线程看到count变量的值现在是0,现在是否认为count变量的值没有被其他线程更新呢?这种结果是否能够接受?
这就是CAS中的ABA问题,即共享变量经历了A->B-A的更新。
是否能够接收ABA问题跟实现的算法有关,如果想要规避ABA问题,可以为共享变量引入一个修订号(时间戳),每次修改共享变量时,相应的修订号就会增加1.ABA变量更新过程变量:[A,0] -> [B,1] -> [A,2],每次对共享变量的修改都会导致修订号的增加,通过修订号依然可以准确判断变量是否被其他线程修改过。AtomicStampedReference类就是基于这种思想产生的。

六,原子变量类

原子变量类基于CAS实现的,当对共享变量进行read-modify-write更新操作时,通过原子变量类可以保障操作的原子性与可见性。对变量的read-modify-write更新操作是指当前操作不是一个简单的赋值,而是变量的新值依赖变量的旧值,如自增操作i++。由于volatile只能保证可见性,无法保障原子性,原子变量类内部就是借助一个volatile变量,并且保障了该变量的read-modify-write操作的原子性,有时把原子变量类看作增强的volatile变量。原子变量类有12个,如:

分组 原子变量类
基础数据型 AtomicInteger, AtomicLong, AtomicBoolean
数组型 AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray
字段更新器 AtomicFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater
引用型 AtomicReference, AtomicStampedReference, AtomicMarkableReference

1,AtomicLong

首先写个共用的计数类

/** * 使用原子变量类定义一个计数器 * 该计数器,在整个程序中都能使用,并且所有的地方都使用这一个计数器,这个计数器可以设计为单例 * Author: DRHJ * Date: 2022/7/5 21:21 */
public class Indicator { 
   
    //构造方法私有化
    private Indicator() { 
   }
    //定义一个私有的本类静态的对象
    private static final Indicator INSTANCE = new Indicator();
    //提供一个公共静态方法返回该类唯一实例
    public static Indicator getInstance() { 
   
        return INSTANCE;
    }

    //使用原子变量类保存请求总数,成功数,失败数
    private final AtomicLong requestCount = new AtomicLong(0);//记录请求总数
    private final AtomicLong successCount = new AtomicLong(0);//处理成功总数
    private final AtomicLong failCount = new AtomicLong(0);   //处理失败总数

    //有新的请求
    public void requestProcessReceive() { 
   
        requestCount.incrementAndGet();
    }

    //处理成功
    public void requestProcessSuccess() { 
   
        successCount.incrementAndGet();
    }

    //处理失败
    public void requestCountFailure() { 
   
        failCount.incrementAndGet();
    }

    //查看总数,成功数,失败数
    public long getRequestCount() { 
   
        return requestCount.get();
    }

    public long getSuccessCount() { 
   
        return successCount.get();
    }

    public long getFailCount() { 
   
        return failCount.get();
    }

}
/** * 模拟服务器请求综述,处理成功数,处理失败数 */
public class TestAtomicLong01 { 
   
    public static void main(String[] args) { 
   
        //通过线程模拟请求,在实际应用中可以在ServletFilter中调用Indicator计数器的相关方法
        for (int i = 0; i < 10000; i++) { 
   
            new Thread(()-> { 
   
                //每个线程就是一个请求,请求总数要加1
                Indicator.getInstance().requestProcessReceive();
                int num = new Random().nextInt();
                if (num % 2 == 0) { 
    //偶数模拟成功
                    Indicator.getInstance().requestProcessSuccess();
                } else { 
   
                    Indicator.getInstance().requestCountFailure();
                }
            }).start();
        }

        try { 
   
            Thread.sleep(1000);
        } catch (Exception e) { 
   
            e.printStackTrace();;
        }

        //打印结果
        System.out.println(Indicator.getInstance().getRequestCount()); //总的请求数
        System.out.println(Indicator.getInstance().getSuccessCount()); //成功数
        System.out.println(Indicator.getInstance().getFailCount());    //失败数

    }
}

结果如下:
【多线程】线程同步 (https://mushiming.com/) 技术博客 第23张
失败数 + 成功数 = 请求数 因此可见,保证了原子性。

2,AtomicIntegerArray

原子数组更新

/** * 原子更新数组 * Author: DRHJ * Date: 2022/7/5 21:59 */
public class TestAtomicIntegerArray { 
   
    public static void main(String[] args) { 
   
        //创建一个指定长度的原子数组
        AtomicIntegerArray arr = new AtomicIntegerArray(10);
        System.out.println(arr);
        //返回指定位置的元素
        System.out.println(arr.get(0));
        System.out.println(arr.get(1));
        //设置指定位置的元素
        arr.set(0, 10);
        //在设置数组元素的新值时,同时返回数组元素的旧值
        System.out.println(arr.getAndSet(1, 11));
        System.out.println(arr);
        //修改数组元素的值,把数组元素加上某个值
        System.out.println(arr.addAndGet(0, 22));
        //先返回再修改
        System.out.println(arr.getAndAdd(1, 33));
        System.out.println(arr);
        //CAS操作
        //如果数组中索引值为0的元素的值是32,就修改为222
        System.out.println(arr.compareAndSet(0, 32, 222));
        System.out.println(arr);
        System.out.println(arr.compareAndSet(1, 11, 333));
        System.out.println(arr);
        //自增/自减
        System.out.println(arr.incrementAndGet(0));
        System.out.println(arr.getAndIncrement(1));
        System.out.println(arr);
        System.out.println(arr.decrementAndGet(2));
        System.out.println(arr);
        System.out.println(arr.getAndDecrement(3));
        System.out.println(arr);
    }
}

【多线程】线程同步 (https://mushiming.com/) 技术博客 第24张
多线程调用原子数组

/** * 在多线程中使用原子数组 * Author: DRHJ * Date: 2022/7/5 22:13 */
public class TestAtomicIntegerArray02 { 
   
    //定义原子数组
    static AtomicIntegerArray arr = new AtomicIntegerArray(10);

    public static void main(String[] args) { 
   
        //定义线程数组
        Thread[] threads = new Thread[10];
        //给线程数组元素赋值
        for (int i = 0; i < threads.length; i++) { 
   
            threads[i] = new AddThread();
        }
        //开启子线程
        for (Thread thread : threads) { 
   
            thread.start();
        }
        //在主线程中查看自增完以后原子数组中的各个元素的值,在主线程中需要在所有子线程中都执行完后再查看
        //把所有的子线程合并到当前主线程中
        for (Thread thread : threads) { 
   
            try { 
   
                thread.join();
            } catch (Exception e) { 
   
                e.printStackTrace();
            }
        }
        System.out.println(arr);
    }

    //定义一个线程类,在线程类中修改原子数组
    static class AddThread extends Thread { 
   
        @Override
        public void run() { 
   
            //把原子数组的每个元素自增1000次
            for (int i = 0; i < 10000; i++) { 
   
                arr.getAndIncrement(i % arr.length());
            }
        }
    }
}

【多线程】线程同步 (https://mushiming.com/) 技术博客 第25张

3,AtomicIntegerFieldUpdater

AtomicIntegerFieldUpdater可以对原子整数字段进行更新,要求:
1)字符必须使用volatile修饰,使线程之间可见;
2)只能是实例变量,不能是静态变量,也不能使用final修饰。

/** * Author: DRHJ * Date: 2022/7/5 23:22 */
public class TestFieldUpdater { 
   
    public static void main(String[] args) { 
   
        User user = new User(1, 22);
        //开启10个进程
        for (int i = 0; i < 10; i++) { 
   
            new SubThread(user).start();
        }

        try { 
   
            Thread.sleep(1000);
        } catch (Exception e) { 
   
            e.printStackTrace();
        }

        System.out.println(user);

    }
}

class User { 
   
    private int id;
    volatile int age;
    public User(int id, int age) { 
   
        this.id = id;
        this.age = age;
    }

    @Override
    public String toString() { 
   
        return "User{" +
                "id=" + id +
                ", age=" + age +
                '}';
    }
}

class SubThread extends Thread { 
   
    private User user;         //更新的User对象
    //创建AtomicIntegerFieldUpdater更新器
    private AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

    public SubThread(User user) { 
   
        this.user = user;
    }

    @Override
    public void run() { 
   
        //在子线程中对user对象的age字段自增10次
        for (int i = 0; i < 10; i++) { 
   
            System.out.println(updater.getAndIncrement(user));
        }
    }
}

【多线程】线程同步 (https://mushiming.com/) 技术博客 第26张

4,AtomicReference

4.1,演示

可以原子读写一个对象

import java.util.concurrent.atomic.AtomicReference;

/** * 使用AtomicReference原子读写一个对象 * Author: DRHJ * Date: 2022/7/5 23:39 */
public class TestReference { 
   
    //创建一个AtomicReference对象
    static AtomicReference<String> atomicReference = new AtomicReference<>("abc");

    public static void main(String[] args) { 
   
        //创建100个线程修改字符窜
        for (int i = 0; i < 100; i++) { 
   
            new Thread(()-> { 
   
                if (atomicReference.compareAndSet("abc", "def")) { 
   
                    System.out.println(Thread.currentThread().getName() + "把字符窜abc更改为def");
                }
            }).start();
        }
        //再创建100个线程
        for (int i = 0; i < 100; i++) { 
   
            new Thread(()-> { 
   
                if (atomicReference.compareAndSet("def", "abc")) { 
   
                    System.out.println(Thread.currentThread().getName() + "把字符窜def更改为abc");
                }
            }).start();
        }
        System.out.println(atomicReference);
    }

}

【多线程】线程同步 (https://mushiming.com/) 技术博客 第27张

4.2,ABA问题

/** * 演示AtomicReference可能会出现CAS的ABA问题 * Author: DRHJ * Date: 2022/7/5 23:47 */
public class TestReference02 { 
   
    private static AtomicReference<String> a = new AtomicReference<>("abc");

    public static void main(String[] args) { 
   
        //创建第一个线程,先把abc字符窜改为def,再把字符窜还原为abc
        Thread t1 = new Thread(()-> { 
   
            a.compareAndSet("abc", "def");
            System.out.println(Thread.currentThread().getName() + "--" + a.get());
            a.compareAndSet("def", "abc");
        });

        Thread t2 = new Thread(()-> { 
   
            try { 
   
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) { 
   
                e.printStackTrace();
            }
            System.out.println(a.compareAndSet("abc", "ghg"));
        });
        t1.start();
        t2.start();
        try { 
   
            t1.join();
            t2.join();
        } catch (InterruptedException e) { 
   
            e.printStackTrace();
        }
    }
}

【多线程】线程同步 (https://mushiming.com/) 技术博客 第28张

4.3,ABA问题解决

/** * AtomicStampedReference原子类可以解决CAS中ABA问题 * 在AtomicStampedReference原子类中有一个整数标记值stamp,每次执行CAS操作时,需要对比他的版本,即比较stamp的值 * Author: DRHJ * Date: 2022/7/6 23:43 */
public class TestAtomicStampedReference01 { 
   
    //private static AtomicReference<String> a = new AtomicReference<>();
    //定义AtomicStampedReference引用操作"abc"字符窜,指定初始化版本号为0
    private static AtomicStampedReference<String> as = new AtomicStampedReference<>("abc", 0);

    public static void main(String[] args) throws InterruptedException { 
   
        Thread t1 = new Thread(()->{ 
   
            as.compareAndSet("abc", "def", as.getStamp(), as.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "--" + as.getReference());
            as.compareAndSet("def", "abc", as.getStamp(), as.getStamp() + 1);
        });
        Thread t2 = new Thread(()-> { 
   
            int stamp = as.getStamp(); //获得版本号
            try { 
   
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) { 
   
                e.printStackTrace();
            }
            System.out.println(as.compareAndSet("abc", "ggg", stamp, stamp + 1));
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(as.getReference());
    }
}

【多线程】线程同步 (https://mushiming.com/) 技术博客 第29张
因为是在睡眠1s前执行,所以线程2获得的版本号可能是将abc变成def时的版本号,所以更新失败。

/** * AtomicStampedReference原子类可以解决CAS中ABA问题 * 在AtomicStampedReference原子类中有一个整数标记值stamp,每次执行CAS操作时,需要对比他的版本,即比较stamp的值 * Author: DRHJ * Date: 2022/7/6 23:43 */
public class TestAtomicStampedReference01 { 
   
    //private static AtomicReference<String> a = new AtomicReference<>();
    //定义AtomicStampedReference引用操作"abc"字符窜,指定初始化版本号为0
    private static AtomicStampedReference<String> as = new AtomicStampedReference<>("abc", 0);

    public static void main(String[] args) throws InterruptedException { 
   
        Thread t1 = new Thread(()->{ 
   
            as.compareAndSet("abc", "def", as.getStamp(), as.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "--" + as.getReference());
            as.compareAndSet("def", "abc", as.getStamp(), as.getStamp() + 1);
        });
        Thread t2 = new Thread(()-> { 
   
            try { 
   
                TimeUnit.SECONDS.sleep(1);
            } catch (Exception e) { 
   
                e.printStackTrace();
            }
            int stamp = as.getStamp(); //获得版本号
            System.out.println(as.compareAndSet("abc", "ggg", stamp, stamp + 1));
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(as.getReference());
    }
}

【多线程】线程同步 (https://mushiming.com/) 技术博客 第30张
这里获得的是可能是def又变成abc时的版本号,所以执行成功了。

THE END

发表回复