day16【原子类和synchronized关键字】

今日内容

  • 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.方法

1int get() 获取当前值。 
2int getAndIncrement() 以原子方式将当前值加 1

1.2 AtomicInteger类示例

  • 我们可以通过AtomicInteger类来解决之前发生的原子性问题。

    1. 线程类:
    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(同步锁){
     需要同步操作的代码
}

同步锁:

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

  1. 锁对象 可以是任意类型。
  2. 多个线程对象要使用同一把锁才能起到同步作用。
  3. 操作共享数据的代码需要加同步

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

娃娃 哈哈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值