今日内容
- volatile关键字
- 原子性
- synchronized关键字
教学目标
- 能够说出volatile关键字的作用
- 能够掌握原子类AtomicInteger的使用
- 能够理解原子类的工作机制
- 能够使用同步代码块解决线程安全问题
第一章 原子类
1.1 原子类概述
1.在java.util.concurrent.atomic包下定义了一些对“变量”操作的“原子类”:
1).java.util.concurrent.atomic.AtomicInteger:对int变量操作的“原子类”;
2).java.util.concurrent.atomic.AtomicLong:对long变量操作的“原子类”;
3).java.util.concurrent.atomic.AtomicBoolean:对boolean变量操作的“原子类”;
它们可以保证对“变量”操作的:原子性、有序性、可见性。
2.构造方法
AtomicInteger(int initialValue) 创建具有给定初始值的新 AtomicInteger
3.方法
1)int get() 获取当前值。
2)int getAndIncrement() 以原子方式将当前值加 1。
1.2 AtomicInteger类示例
-
我们可以通过AtomicInteger类来解决之前发生的原子性问题。
- 线程类:
import java.util.concurrent.atomic.AtomicInteger; public class MyRun implements Runnable{ //static int a = 0;//不直接使用基本类型变量,直接使用原子类 static AtomicInteger a = new AtomicInteger(0); //重写run方法 @Override public void run() { for (int i = 0; i < 10000; i++) { //a++; //先获取,再自增1:a++ a.getAndIncrement();//也是给当前a变量加1,代替++操作 } } }
//1. 测试类:
public class Test01 {
public static void main(String[] args) throws InterruptedException {
//创建子类对象
MyRun mr = new MyRun();
//创建线程对象
Thread t = new Thread(mr);
//开启线程
t.start();
//再开启线程
Thread t2 = new Thread(mr);
t2.start();
//为了让循环先执行结束再打印 我们在这里睡两秒钟
Thread.sleep(2000);
//get()是获取变量a的值
System.out.println(MyRun.a.get());
}
}
执行效果:20000
我们能看到,无论程序运行多少次,其结果总是正确的!
1.3 AtomicInteger类的工作原理-CAS机制
使用原子性问题讲解CAS机制,如下图所示:
说明:
1.在AtomicInteger类的getAndIncrement()加1的方法中底层调用一个compareAndSwapInt()方法,此方法使用了一种"比较并交换(Compare And Swap)"的机制,简称CAS机制。
2.大概的思想就是上图中表达的思想,在往主内存中放数值的时候先用获取到的值(例如0)和目前的值(静态区a的值0)进行比较判断:
如果相等:就将a修改后的值1放到静态区中;
如果不相等:就继续循环,重新获取静态区中的值1,再进行比较并交换,直至成功交换为止。
3.通过查看源码发现底层使用的while循环控制重新从主内存中获取数据,compareAndSwapInt()方法是"线程安全"的。CAS机制也被称为:乐观锁。因为大部分比较的结果为true,就直接修改了。只有少部分多线程并发的情况会导致CAS失败,而再次循环
【下面是源码扩展,有兴趣的同学可以学习下,其实原理思想和我们上面分析是一样的】
- 先来看一下调用过程:
-
在Unsafe类中,调用了一个:compareAndSwapInt()方法,此方法的几个参数:
- var1:传入的AtomicInteger对象
- var2:AtommicInteger内部变量的偏移地址
- var5:之前取出的AtomicInteger中的值;
- var5 + var4:预期结果
此方法使用了一种"比较并交换(Compare And Swap)"的机制,它会用var1和var2先获取内存中AtomicInteger中的值,然后和传入的,之前获取的值var5做一下比较,也就是比较当前内存的值和预期的值是否一致,如果一致就修改为var5 + var4,否则就继续循环,再次获取AtomicInteger中的值,再进行比较并交换,直至成功交换为止。
-
compareAndSwapInt()方法是"线程安全"的。
-
我们假设两个线程交替运行的情况,看看它是怎样工作的:
-
初始AtomicInteger的值为0
-
线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0
-
线程A被暂停
-
线程B执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:0
-
线程B执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
-
线程B成功将AtomicInteger中的值改为1
-
线程A恢复运行,执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
此时线程A使用var1和var2从AtomicInteger中获取的值为:1,而传入的var5为0,比较失败,返回false,继续循环。
-
线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:1
-
线程A执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
此时线程A使用var1和var2从AtomicInteger中获取的值为:1,而传入的var5为1,比较成功,将其修改为var5 + var4,也就是2,将AtomicInteger中的值改为2,结束。
-
-
CAS机制也被称为:乐观锁。因为大部分比较的结果为true,就直接修改了。只有少部分多线程并发的情况会导致CAS失败,而再次循环。
1.4 AtomicIntegerArray类示例
-
使用普通数组可能出现原子性问题
-
需求:定义长度是1000的数组,给数组中的每一个元素+1。
package com.itheima.sh.demo_12; public class MyRun implements Runnable { //定义数组 static int[] arr = new int[1000]; @Override public void run() { //给数组中的每一个元素+1 for (int i = 0; i < arr.length; i++) { arr[i]++; } } } package com.itheima.sh.demo_12; public class Test01 { public static void main(String[] args) throws InterruptedException { //创建子类对象 MyRun mr = new MyRun(); //开启1000个线程 for (int i = 0; i < 1000; i++) { Thread t = new Thread(mr); t.start(); } //开启一个线程 /*Thread t = new Thread(mr); t.start();*/ //让这里睡2秒钟,等循环结束再打印 Thread.sleep(2000); //打印数组看看结果 for (int i = 0; i < MyRun.arr.length; i++) { System.out.print(MyRun.arr[i] +" "); } } } 本来是加了1000次,但是出来的结果有很多999. 数据出现了错误
-
使用原子类解决
1).java.util.concurrent.atomic.AtomicIntegerArray:对int数组操作的 原子类。
构造方法: AtomicIntegerArray(int length) 创建给定长度的新 AtomicIntegerArray。 方法: 1.int addAndGet(int i, int delta) 以原子方式将给定值与索引 i 的元素相加。 参数:i - 数组索引 delta - 要加上的值 2.int length() 返回该数组的长度。 3.int get(int i) 获取位置 i 的当前值。
2).java.util.concurrent.atomic.AtomicLongArray:对long数组操作的原子类。 3).java.util.concurrent.atomic.AtomicReferenceArray:对引用类型数组操作的原子类
public class MyRun implements Runnable {
//定义数组
//static int[] arr = new int[1000];
//定义原子类数组
static AtomicIntegerArray arr = new AtomicIntegerArray(1000);
@Override
public void run() {
//给数组中的每一个元素+1
for (int i = 0; i < arr.length(); i++) {
//arr[i]++;
arr.addAndGet(i,1);//给i索引的元素加1
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
//创建子类对象
MyRun mr = new MyRun();
//开启1000个线程
for (int i = 0; i < 1000; i++) {
Thread t = new Thread(mr);
t.start();
}
//让这里睡2秒钟,等循环结束再打印
Thread.sleep(2000);
//打印数组看看结果
for (int i = 0; i < MyRun.arr.length(); i++) {
//获取i索引的元素
System.out.print(MyRun.arr.get(i) +" ");
}
}
}
出来的都是1000, 全都是正确的结果。没有了原子性的问题。
第二章 synchronized关键字
2.1 多行代码的原子性问题
-
之前的AtomicInteger类只能保证"变量"的原子性操作,而对多行代码进行"原子性"操作,使用AtomicInteger类就不能达到效果了。
-
我们通过一个案例,演示线程的安全问题:
分析:
最近万达影城上映:《葫芦娃大战奥特曼》 , 我们现在模拟一下电影院卖票:
我就卖一个放映厅中100张票。
我们有多个售票窗口,同时对外出售这100张票。
我们可以用线程来模拟售票的窗口。每个窗口可以认为是一个线程。窗口售票的过程,就可以认为是线程的任务
步骤:
1)定义一个测试类SellTicektDemo ,并定义一个main函数;
2)定义一个线程类SellTicketTask 来实现Runnable接口;
3)在SellTicketTask 任务类中定义一个变量tickets来存储100张票;
4)在run函数中使用循环模拟一直卖票,使用判断结构根据变量票数tickets是否大于0来确定是否还有余票;
5)如果有余票,使用打印语句来模拟卖票,然后票数量变量tickets-1;
6)在main函数中创建任务类对象stt,同时并创建四个线程类对象来模拟四个窗口,最后启动线程;
/*
* 售票,多个售票窗口卖100张票
*/
//定义线程任务类
class SellTicketTask implements Runnable
{
//定义100张票
private int tickets=100;
//实现run函数
public void run()
{
//使用循环模拟一直卖票
while(true)
{
//判断是否还有余票
if(tickets>0)
{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//有余票 使用打印语句来模拟卖票
System.out.println(Thread.currentThread().getName()+"出票:"+tickets);
//票数量-1
tickets--;
}
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
//创建线程任务类对象
SellTicketTask stt = new SellTicketTask();
//创建线程对象,四个线程模拟四个窗口
Thread t1 = new Thread(stt,"窗口1");
Thread t2 = new Thread(stt,"窗口2");
Thread t3 = new Thread(stt,"窗口3");
Thread t4 = new Thread(stt,"窗口4");
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
2.2 多线程安全问题分析
通过上述代码,我们发现输出的结果有如下问题:
上述代码出现的问题:重复票、跳票等问题。
出现上述问题的图解如下图所示:
原因分析:
A:多线程程序,如果是单线程就不会出现上述卖票的错误信息;
B:多个线程操作共享资源,如果多线程情况下,每个线程操作自己的也不会出现上述问题;
C:操作资源的代码有多行,如果代码是一行或者很少的情况下,那么一行代码很快执行完毕,也不会出现上述情况;
D:CPU的随机切换。本质原因是CPU在处理多个线程的时候,在操作共享数据的多条代码之间进行切换导致的;
2.3 多线程安全问题解决
解决方案:
A:无法改变,就是多线程程序。
B:无法改变,多个线程就是要操作同一资源。
C:无法改变,因为就是有多行代码
D:CPU的运行我们无法解决。针对CPU的切换,由操作系统去控制,而我们人为是无法干预。因此这个问题解决不了。
要解决安全问题:
可以人为的控制CPU在执行某个线程操作共享数据的时候,不让其他线程进入到操作共享数据的代码中去,这样就可以保证安全。
上述的这个解决方案:称为线程的同步。使用 synchronized关键字。
synchronized关键字概述
-
synchronized关键字:表示“同步”的。它可以对“多行代码”进行“同步”——将多行代码当成是一个完整的整体,一个线程如果进入到这个代码块中,会全部执行完毕,执行结束后,其它线程才会执行。这样可以保证这多行的代码作为完整的整体,被一个线程完整的执行完毕。
-
synchronized被称为“重量级的锁”方式,也是“悲观锁”——效率比较低。
-
synchronized有几种使用方式:
a).同步代码块b).同步方法【常用】
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。
要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与跳票问题,Java中提供了同步机制(synchronized)来解决。
根据案例简述:
窗口1线程进入操作的时候,窗口2、窗口3和窗口4线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3和窗口4才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
同步代码块
- 同步代码块:
synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
- 锁对象 可以是任意类型。
- 多个线程对象要使用同一把锁才能起到同步作用。
- 操作共享数据的代码需要加同步
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED锁阻塞)。
使用同步代码块解决代码:
上述代码为了避免多线程的安全问题,我们需要把上述卖票的代码加上同步代码块,这样就可以解决多线程的安全问题。
/*
* 售票,多个售票窗口卖100张票
*/
//定义线程任务类
class SellTicketTask implements Runnable
{
//定义100张票
private int tickets=100;
//定义一个对象充当同步代码块上的锁
private Object obj = new Object();
//实现run函数
public void run()
{
//使用循环模拟一直卖票
while(true)//t1 t2
{
//为了解决多线程的安全问题,给操作的共享资源代码加同步
synchronized(obj)//t1 进来 关上门 上锁,此时t2进不来
{
//判断是否还有余票
if(tickets>0)
{
//休眠1毫秒,模拟延迟
try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
//有余票 使用打印语句来模拟卖票
System.out.println(Thread.currentThread().getName()+"出票:"+tickets);
//票数量-1
tickets--;
}
}
//t1出来,释放锁,打开门 其他线程可以进入
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
//创建线程任务类对象
SellTicketTask stt = new SellTicketTask();
//创建线程对象,四个线程模拟四个窗口
Thread t1 = new Thread(stt,"窗口1");
Thread t2 = new Thread(stt,"窗口2");
Thread t3 = new Thread(stt,"窗口3");
Thread t4 = new Thread(stt,"窗口4");
//启动线程
t1.start();
t2.start();
t3.start();
t4.start();
}
}
乐观锁和悲观锁
说明:
1.
原子类使用的乐观锁,使用多线程操作的是同一个变量
同步属于悲观锁,使用多线程操作一段代码
乐观锁:线程A在操作变量的时候,允许线程B操作,只是会判断,如果有问题,就放弃本次操作。判断如 果没有问题,则会正常执行本次操作
悲观锁:当线程A正在操作的时候,不允许B线程执行,要等A出来之后B才有可能进去。
3.相对来说悲观锁效率更低,乐观锁效率高。
同步方法
1)演示同步方法
代码和上述代码几乎差不多,只是将同步代码块处调用方法method,然后将同步代码块变为同步方法。
如下所示:
如上图所示,如果一个方法进来后,直接就是同步,也就是说,这个方法的所有代码都需要被同步。此时我们可以考虑把同步直接加到方法上:
以上被synchronized关键字修饰的方法称为同步方法。
注意:
1.非静态同步方法的锁是this;
2.如果一个方法内部,所有代码都需要被同步,那么就用同步方法;
静态同步方法
1)演示
既然有非静态同步方法,那么肯定也会有静态同步方法。
将上述非静态同步方法改为静态同步方法,代码如下所示:
问题:非静态同步方法有隐式变量this作为锁,那么静态方法中没有this,那么静态同步方法中的锁又是什么呢?
静态同步方法的锁是:当前类的字节码文件对象(Class对象)。
其实可以这么理解:什么是字节码文件对象呢?其实就是class文件,类名.class。比如这里,就是 SellTicketTask.class
获取Class对象的方式:类名.class; (每个类只有一个字节码对象)
总结:
同步代码块:锁是任意对象,但是必须唯一;
非静态同步方法:锁是this;
静态同步方法:锁是当前类的字节码文件对象;类名.class