高并发及线程安全

一.高并发及线程安全

1.高并发及线程安全的概念

1.高并发:在某个时间点上,有多个线程同时访问某一个资源。例如:双十一,12306 , 秒杀

2.线程安全性问题:当多个线程无序的访问同一个资源(例如:同一个变量、同一数据库、同一个文件……),而且访问同一资源的代码不具有“原子性”,这时对这一资源的方法就会产生安全性问题——导致此资源最终的结果是错误。

3.高并发所产生的安全性问题主要表现:

1).可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

2).有序性:即程序执行的顺序按照代码的先后顺序执行。

3).原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

2.高并发问题一_不可见性

产生的原因:定义一个变量。一个线程修改变量的值,另一个线程由于访问频率太快,导致一直使用本线程区内的变量副本,而没有实时的到主内存中获取变量的新值。

点击查看代码
/*
    演示多线程安全问题的_不可见性
 */
public class MyThread extends Thread{
    //定义一个供多个线程共享使用的静态变量
    public static int a = 0;

    @Override
    public void run() {
        System.out.println("Thread-0线程开始执行线程任务,睡眠2秒钟,等待main线程先执行!");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread-0过了2秒钟,睡醒了,修改变量a的值为1");
        a=1;
        System.out.println("Thread-0执行线程任务结束了!"+a);
    }
}
点击查看代码
public class Demo01Visible {
    public static void main(String[] args) {
        //创建MyThread对象
        MyThread mt = new MyThread();
        //调用start方法,开启一个新的线程,执行run方法
        mt.start();

        //main线程在开启新的线程之后,会继续执行main方法中的代码
        System.out.println("main线程执行一个死循环");
        while (true){
            //判断变量a的值是否为1
            if(MyThread.a==1){
                System.out.println("main线程判断变量a的值为1,结束死循环!");
                break;
            }
        }
    }
}
程序的执行结果:**死循环并没有结束
点击查看代码
main线程执行一个死循环
Thread-0线程开始执行线程任务,睡眠2秒钟,等待main线程先执行!
Thread-0过了2秒钟,睡醒了,修改变量a的值为1
Thread-0执行线程任务结束了!1

3.Java内存模型JMM

概述:JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。

Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

所有的共享变量都存储于主内存。这里所说的变量指的是实例变量(成员变量)和类变量(静态成员变量)。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。

4.高并发问题二_无序性

1).有序性:多行代码的编写(.java)顺序和编译(.class)顺序。
有些时候,编译器在编译代码时,为了提高效率,会对代码“重排”:

点击查看代码
.java文件
int a = 10;		//第一行
int b = 20;		//第二行
int c = a * b;	//第三行

在执行第三行之前,由于第一行和第二行的先后顺序无所谓,所以编译器可能会对“第一行”和“第二行”进行代码重排:

点击查看代码
.class
int b = 20;
int a = 10;
int c = a * b;
2).但在多线程环境下,这种重排可能是我们不希望发生的,因为:重排,可能会影响另一个线程的结果,所以我们不需要代码进行重排

5.高并发问题三_非原子性

原子:不可分割 100行代码是一个原子,线程执行100行代码不可以分开执行,要么都执行,要都不执行

需求:
1.定义多线程共享的静态变量money
2.Thread-0线程把money的值增加1000
3.main线程把money的值增加1000
4.查看money的最终结果

点击查看代码
/*
    高并发问题三_非原子性
 */
public class MyThread extends Thread{
    //1.定义多线程共享的静态变量money
    public static int money = 0;

    @Override
    public void run() {
        //2.Thread-0线程把money的值增加1000
        System.out.println("Thread-0线程开始执行线程任务,把money的值增加1000");
        for (int i = 0; i < 1000; i++) {
            money++;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread-0线程增加money的值完毕,结束线程任务!");
    }
}
点击查看代码
public class Demo01Atomic {
    public static void main(String[] args) throws InterruptedException {
        //创建MyThread对象
        MyThread mt = new MyThread();
        //调用start方法,开启一个新的线程,执行run方法
        mt.start();

        //main线程在开启新的线程之后,继续执行main方法中的代码
        //3.main线程把money的值增加1000
        System.out.println("main线程开启把money的值增加1000");
        for (int i = 0; i < 1000; i++) {
            MyThread.money++;
            //增加睡醒1毫秒,为了提高线程安全问题出现的几率(让两个线程抢夺cpu的执行权次数变多)
            Thread.sleep(1);
        }
        System.out.println("main先增加moeny的值完毕,睡眠2秒钟,等待Thread-0线程增加结束");
        Thread.sleep(3000);

        //4.查看money的最终结果
        System.out.println("变量moeny最终的值为:"+MyThread.money);
    }
}
点击查看代码
main线程开启把money的值增加1000
main先增加moeny的值完毕,睡眠2秒钟,等待Thread-0线程增加结束
Thread-0线程开始执行线程任务,把money的值增加1000
Thread-0线程增加money的值完毕,结束线程任务!
变量moeny最终的值为:2000
    
main线程开启把money的值增加1000
Thread-0线程开始执行线程任务,把money的值增加1000
main先增加moeny的值完毕,睡眠2秒钟,等待Thread-0线程增加结束
Thread-0线程增加money的值完毕,结束线程任务!
变量moeny最终的值为:1997
    
main线程开启把money的值增加1000
Thread-0线程开始执行线程任务,把money的值增加1000
main先增加moeny的值完毕,睡眠2秒钟,等待Thread-0线程增加结束
Thread-0线程增加money的值完毕,结束线程任务!
变量moeny最终的值为:1995 

原子性原理


每个线程访问money变量,都需要三步:
1).取money的值;
2).将money++
3).将money写回
这三步就不具有“原子性”——执行某一步时,很可能会被暂停(失去了cpu的执行权),执行另外一个线程,就会导致变量的最终结果错误!!!!

二.volatile关键字

1.volatile解决可见性

点击查看代码
/*
    使用volatile关键字解决不可见性
 */
public class MyThread extends Thread{
    //定义一个供多个线程共享使用的静态变量
    /*
        变量被volatile关键字修饰,当变量的值被改变,volatile会让变量所有的变量副本都失效
        线程想要再次使用变量,必须重新获取
     */
    public static volatile int a = 0;

    @Override
    public void run() {
        System.out.println("Thread-0线程开始执行线程任务,睡眠2秒钟,等待main线程先执行!");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread-0过了2秒钟,睡醒了,修改变量a的值为1");
        a=1;
        System.out.println("Thread-0执行线程任务结束了!"+a);
    }
}
点击查看代码
public class Demo01Visible {
    public static void main(String[] args) throws InterruptedException {
        //创建MyThread对象
        MyThread mt = new MyThread();
        //调用start方法,开启一个新的线程,执行run方法
        mt.start();

        //main线程在开启新的线程之后,会继续执行main方法中的代码
        System.out.println("main线程执行一个死循环");
        while (true){
            //判断变量a的值是否为1
            if(MyThread.a==1){
                System.out.println("main线程判断变量a的值为1,结束死循环!");
                break;
            }
        }
    }
}
点击查看代码
main线程执行一个死循环
Thread-0线程开始执行线程任务,睡眠2秒钟,等待main线程先执行!
Thread-0过了2秒钟,睡醒了,修改变量a的值为1
main线程判断变量a的值为1,结束死循环!
Thread-0执行线程任务结束了!1

2.volatile解决有序性

注意:变量添加了volatile关键字,就不会在进行重排了

3.volatile不能解决原子性

点击查看代码
/*
    高并发问题三_非原子性(重点)
 */
public class MyThread extends Thread{
    //1.定义多线程共享的静态变量money
    public static volatile int money = 0;

    @Override
    public void run() {
        //2.Thread-0线程把money的值增加1000
        System.out.println("Thread-0线程开始执行线程任务,把money的值增加1000");
        for (int i = 0; i < 1000; i++) {
            money++;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread-0线程增加money的值完毕,结束线程任务!");
    }
}
点击查看代码
public class Demo01Atomic {
    public static void main(String[] args) throws InterruptedException {
        //创建MyThread对象
        MyThread mt = new MyThread();
        //调用start方法,开启一个新的线程,执行run方法
        mt.start();

        //main线程在开启新的线程之后,继续执行main方法中的代码
        //3.main线程把money的值增加1000
        System.out.println("main线程开启把money的值增加1000");
        for (int i = 0; i < 1000; i++) {
            MyThread.money++;
            //增加睡醒1毫秒,为了提高线程安全问题出现的几率(让两个线程抢夺cpu的执行权次数变多)
            Thread.sleep(1);
        }
        System.out.println("main先增加moeny的值完毕,睡眠2秒钟,等待Thread-0线程增加结束");
        Thread.sleep(3000);

        //4.查看money的最终结果
        System.out.println("变量moeny最终的值为:"+ MyThread.money);
    }
}
程序执行的结果: volatile不能把原子性的三步变成一个原子执行

​ 1).取money的值;
​ 2).将money++
​ 3).将money写回

这三步在执行的过程中,还是有可能把其他线程打断的

点击查看代码
main线程开启把money的值增加1000
Thread-0线程开始执行线程任务,把money的值增加1000
main先增加moeny的值完毕,睡眠2秒钟,等待Thread-0线程增加结束
Thread-0线程增加money的值完毕,结束线程任务!
变量moeny最终的值为:1984

三.原子类

概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,

多个操作是一个不可以分割的整体(原子)。

比如:从张三的账户给李四的账户转1000元,这个动作将包含两个基本的操作:从张三的账户扣除1000元,给李四的账户增加1000元。这两个操作必须符合原子性的要求,

要么都成功要么都失败。

1.原子类概述

概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。

1). java.util.concurrent.atomic.AtomicInteger:对int变量进行原子操作的类。

2). java.util.concurrent.atomic.AtomicLong:对long变量进行原子操作的类。

3). java.util.concurrent.atomic.AtomicBoolean:对boolean变量进行原子操作的类。

这些类可以保证对“某种类型的变量”原子操作,多线程、高并发的环境下,就可以保证对变量访问的有序性,从而保证最终的结果是正确的。

AtomicInteger原子型Integer,可以实现原子更新操作

点击查看代码
构造方法:
public AtomicInteger():	   				初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue): 初始化一个指定值的原子型Integer
成员方法:
int get():   			 				 获取AtomicInteger对象中存储的int值
int getAndIncrement(): i++     			 以原子方式将当前值加1,注意,这里返回的是自增前的值。
int incrementAndGet(): ++i    		     以原子方式将当前值加1,注意,这里返回的是自增后的值。
####2.AtomicInteger类的基本使用 AtomicInteger也是一个原子的包装类:里边包含了一个int类型的整数值

点击查看代码
import java.util.concurrent.atomic.AtomicInteger;

/*
    AtomicInteger类的基本使用
 */
public class Demo01AtomicInteger {
    public static void main(String[] args) {
        //使用空参数构造方法,创建AtomicInteger对象
        AtomicInteger ai1 = new AtomicInteger();
        System.out.println(ai1.get());//0

        //使用带参数构造方法,创建AtomicInteger对象
        AtomicInteger ai2 = new AtomicInteger(10);
        System.out.println(ai2.get());//10

        //int getAndIncrement(): i++ 以原子方式将当前值加1,注意,这里返回的是自增前的值。
        int a = ai2.getAndIncrement();
        System.out.println("a:"+a);//a:10
        System.out.println(ai2.get());//11

        //int incrementAndGet(): ++i  以原子方式将当前值加1,注意,这里返回的是自增后的值。
        int b = ai2.incrementAndGet();
        System.out.println("b:"+b);//b:12
        System.out.println(ai2.get());//12
    }
}

3.AtomicInteger解决变量的原子性(可见性、有序性)

点击查看代码
import java.util.concurrent.atomic.AtomicInteger;

/*
    AtomicInteger解决变量的原子性
 */
public class MyThread extends Thread{
    //1.定义多线程共享的静态变量money,使用AtomicInteger类型
    public static AtomicInteger money = new AtomicInteger(0);

    @Override
    public void run() {
        //2.Thread-0线程把money的值增加1000
        System.out.println("Thread-0线程开始执行线程任务,把money的值增加1000");
        for (int i = 0; i < 1000; i++) {
            money.getAndIncrement();//money++;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread-0线程增加money的值完毕,结束线程任务!");
    }
}
点击查看代码
public class Demo01Atomic {
    public static void main(String[] args) throws InterruptedException {
        //创建MyThread对象
        MyThread mt = new MyThread();
        //调用start方法,开启一个新的线程,执行run方法
        mt.start();

        //main线程在开启新的线程之后,继续执行main方法中的代码
        //3.main线程把money的值增加1000
        System.out.println("main线程开启把money的值增加1000");
        for (int i = 0; i < 1000; i++) {
            MyThread.money.getAndIncrement();//MyThread.money++;
            //增加睡醒1毫秒,为了提高线程安全问题出现的几率(让两个线程抢夺cpu的执行权次数变多)
            Thread.sleep(1);
        }
        System.out.println("main先增加moeny的值完毕,睡眠2秒钟,等待Thread-0线程增加结束");
        Thread.sleep(3000);

        //4.查看money的最终结果
        System.out.println("变量moeny最终的值为:"+ MyThread.money);
    }
}
执行结果:
点击查看代码
main线程开启把money的值增加1000
Thread-0线程开始执行线程任务,把money的值增加1000
main先增加moeny的值完毕,睡眠2秒钟,等待Thread-0线程增加结束
Thread-0线程增加money的值完毕,结束线程任务!
变量moeny最终的值为:2000
####3.AtomicInteger的原理_CAS机制(乐观锁) **CAS:compareAndSwap 比较并交换**

四.synchronized关键字

1.售票案例引发的安全性问题

2.售票案例的代码实现

点击查看代码
/*
    卖票案例
 */
public class RunnableImpl implements Runnable{
    //定义一个供多个线程访问的票源
    private static int ticket = 100;

    //线程任务:卖票
    @Override
    public void run() {
        //增加一个死循环,让卖票操作重复执行
        while (true){
            //对ticket进行判断
            if(ticket>0){
                //每卖一张票需要10毫秒
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
                ticket--;
            }else{
                break;
            }
        }

    }
}
点击查看代码
/*
    创建3个线程,卖同100张票
 */
public class Demo01PayTicket {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        //创建3个线程
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();
    }
}
程序执行的结果:
点击查看代码
...
Thread-2线程正在卖第15张票!
Thread-1线程正在卖第13张票!
Thread-2线程正在卖第12张票!
Thread-0线程正在卖第11张票!
Thread-1线程正在卖第10张票!
Thread-0线程正在卖第9张票!
Thread-2线程正在卖第8张票!
Thread-1线程正在卖第7张票!
Thread-2线程正在卖第6张票!
Thread-0线程正在卖第6张票!
Thread-1线程正在卖第4张票!
Thread-2线程正在卖第3张票!
Thread-0线程正在卖第3张票!
Thread-1线程正在卖第1张票!
Thread-2线程正在卖第0张票!
Thread-0线程正在卖第-1张票! 


继承方式实现卖票案例

点击查看代码
public class MyThread extends Thread {
    //定义一个供多个线程访问的票源
    private static int ticket = 100;

    //线程任务:卖票
    @Override
    public void run() {
        //增加一个死循环,让卖票操作重复执行
        while (true){
            //对ticket进行判断
            if(ticket>0){
                //每卖一张票需要10毫秒
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
                ticket--;
            }else{
                break;
            }
        }

    }
}
点击查看代码
public class MyThread extends Thread {
    //定义一个供多个线程访问的票源
    private static int ticket = 100;

    //线程任务:卖票
    @Override
    public void run() {
        //增加一个死循环,让卖票操作重复执行
        while (true){
            //对ticket进行判断
            if(ticket>0){
                //每卖一张票需要10毫秒
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
                ticket--;
            }else{
                break;
            }
        }

    }
}
####3.线程安全问题的产生原理

4.解决线程安全问题的第一种方式使用同步代码块

点击查看代码
/*
    卖票案例出现了线程安全问题:卖出了重复的票,和不存在的票
        100 100 100   0,-1
    解决线程安全问题的第一种方案:使用同步代码块
        格式:
            synchronized(锁对象){
                可能产生线程安全问题的代码(访问了共享数据的代码)
            }
        原理:
            使用同步代码块把一段代码锁住,只让一个线程进入到代码块中执行卖票的代码
        注意:
            1.同步代码块使用的锁对象可以是任意的对象
                   Student s = new Student();
                   Object obj = new Object();
                   String s = "abc"; //底层是一个字符数组
            2.必须保证所有的线程使用的是同一个锁对象
 */
public class RunnableImpl implements Runnable{
    //定义一个供多个线程访问的票源
    private int ticket = 100;
    //定义一个同步代码块使用的锁对象
    //Person p = new Person();
    //Object obj = new Object();
    String str = "aaa";

    //线程任务:卖票
    @Override
    public void run() {
        //增加一个死循环,让卖票操作重复执行
        while (true){
            //同步代码块
            synchronized (str){
                //对ticket进行判断
                if(ticket>0){
                    //每卖一张票需要10毫秒
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
                    ticket--;
                }else{
                    break;
                }
            }
        }

    }
}
点击查看代码
/*
    创建3个线程,卖同100张票
 */
public class Demo01PayTicket {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        //创建3个线程
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();


    }
}
####5.同步的原理

6.解决线程安全问题的第二种方式:使用同步方法

点击查看代码
/*
    卖票案例出现了线程安全问题:卖出了重复的票,和不存在的票
        100 100 100   0,-1
    解决线程安全问题的第二种方案:使用同步方法
        步骤:
            1.把访问了共享数据的代码,抽取出来,放到一个方法中
            2.给方法添加一个synchronized关键字,这个方法就是一个同步方法
        格式:
            修饰符 synchronized 返回值类型 方法名(参数列表){
                可能产生线程安全问题的代码(访问了共享数据的代码)
            }
        原理:
            使用一个同步方法把代码锁住,只让一个线程进入到方法执行卖票的代码
 */
public class RunnableImpl implements Runnable{
    //定义一个供多个线程访问的票源
    private static int ticket = 100;

    //线程任务:卖票
    @Override
    public void run() {
        System.out.println("this:"+this);
        //增加一个死循环,让卖票操作重复执行
        while (true){
            //payTicket();
            payTicketStatic();
            if(ticket<=0){
                break;
            }
        }
    }

    /*
        定义一个静态的同步方法(了解)
        使用一个锁对象,把方法锁住,只让一个线程获取锁对象进入到方法执行
        静态方法的锁对象是谁?
            还是this吗? 不是this是创建对象之后才出现的
        是本类的class文件对象(反射): RunnableImpl.class
     */
    public static /*synchronized*/ void payTicketStatic(){
        synchronized (RunnableImpl.class){
            //对ticket进行判断
            if(ticket>0){
                //每卖一张票需要10毫秒
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
                ticket--;
            }
        }
    }

    /*
        定义一个同步方法
        使用一个锁对象,把方法锁住,只让一个线程获取锁对象进入到方法执行
        锁对象是谁? 是this==>本类对象==>  RunnableImpl run = new RunnableImpl();
            run:com.itheima.demo10synchronized.RunnableImpl@4554617c
            this:com.itheima.demo10synchronized.RunnableImpl@4554617c
     */
    public synchronized void payTicket(){
        //对ticket进行判断
        if(ticket>0){
            //每卖一张票需要10毫秒
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
            ticket--;
        }
    }

    /*public void payTicket(){
        synchronized (this){
            //对ticket进行判断
            if(ticket>0){
                //每卖一张票需要10毫秒
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
                ticket--;
            }
        }
    }*/
}
点击查看代码

/*
    创建3个线程,卖同100张票
 */
public class Demo01PayTicket {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        System.out.println("run:"+run);
        //创建3个线程
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();

    }
}
####7.解决线程安全问题的第三方式:使用Lock锁
点击查看代码
import java.util.concurrent.locks.ReentrantLock;

/*
    卖票案例出现了线程安全问题:卖出了重复的票,和不存在的票
        100 100 100   0,-1
    解决线程安全问题的第三种方案:使用Lock锁
    java.util.concurrent.locks.Lock接口
        Lock锁比同步代码块和同步方法使用起来更广泛。
    Lock接口中的成员方法:
        void lock()获取锁。
        void unlock() 释放锁。
    java.util.concurrent.locks.ReentrantLock类 implements Lock接口
    Lock锁的使用步骤:
        1.在成员位置创建ReentrantLock对象
        2.在访问共享数据的代码前调用lock方法,获取锁对象
        3.在访问共享数据的代码后调用unlock方法,释放锁对象
    原理:
        使用lock方法和unlock方法把一段代码锁住,只让一个线程进入执行
 */
public class RunnableImpl implements Runnable{
    //定义一个供多个线程访问的票源
    private int ticket = 100;
    //1.在成员位置创建ReentrantLock对象
    private ReentrantLock l = new ReentrantLock();

    //线程任务:卖票
    @Override
    public void run() {
        //增加一个死循环,让卖票操作重复执行
        while (true){
            //2.在访问共享数据的代码前调用lock方法,获取锁对象
            l.lock();
                //对ticket进行判断
                if(ticket>0){
                    //每卖一张票需要10毫秒
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"线程正在卖第"+ticket+"张票!");
                    ticket--;
                }/*else{
                    System.exit(0);//终止JVM
                }*/
            //3.在访问共享数据的代码后调用unlock方法,释放锁对象
            l.unlock();
            if(ticket<=0){
                break;
            }
        }

    }
}
点击查看代码
/*
    创建3个线程,卖同100张票
 */
public class Demo01PayTicket {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        //创建3个线程
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        t0.start();
        t1.start();
        t2.start();
    }
}
####8.CAS与Synchronized **AtomicInteger:只能解决一个变量的原子性** **synchronized:可以解决一段代码的原子性** CAS和Synchronized都可以保证多线程环境下共享数据的安全性。那么他们两者有什么区别?

Synchronized是从悲观的角度出发:

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁
共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。因此Synchronized我们也将其称之为悲观锁。jdk中的ReentrantLock也是一种悲观锁。

CAS是从乐观的角度出发:

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。

CAS这种机制我们也可以将其称之为乐观锁。

五.并发包

在JDK的并发包java.util.concurrent里提供了几个非常有用的并发容器和并发工具类。供我们在多线程开发中进行使用。这些集合和工具类都可以保证高并发的线程安全问题.

1.并发List集合_CopyOnWriteArrayLis

List集合的特点:

​ 1.是一个有序的集合

​ 2.允许存储重复的元素

​ 3.包含一些带索引的方法

1).java.util.concurrent.CopyOnWriteArrayList(类):它是一个“线程安全”的ArrayList,我们之前学习的java.utils.ArrayList不是线程安全的。
2).如果是多个线程,并发访问同一个ArrayList,我们要使用:CopyOnWriteArrayList

需求:

1.创建一个被多个线程共享使用静态的ArrayList集合对象

2.使用Thread-0线程往集合中添加1000个元素

3.使用main线程往集合中添加1000个元素

4.统计集合的长度

点击查看代码
import java.util.ArrayList;
import java.util.Vector;
import java.util.concurrent.CopyOnWriteArrayList;

public class MyThread extends Thread{
    //1.创建一个被多个线程共享使用静态的ArrayList集合对象
    /*
        ArrayList集合是JDK1.2之后出现的,是一个多线程不安全的集合
            1.存储的元素少了
            2.索引越界异常
     */
    //public static ArrayList<Integer> list = new ArrayList<>();

    /*
        java.util.Vector<E>集合,是JDK1.0时期,最早期的单列集合
            底层也是数组结构
            与新 collection 实现不同,Vector 是同步的。
            Vector集合使用synchronized同步技术,保证多线程安全
            synchronized同步技术是悲观锁,效率低
     */
    //public static Vector<Integer> list = new Vector<>();

    /*
        java.util.concurrent.CopyOnWriteArrayList<E>集合,是JDK1.5之后出现的
            CopyOnWriteArrayList集合底层采用的是CAS机制,保证多线程安全
            CAS机制是乐观锁,效率高
     */
    public static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();

    @Override
    public void run() {
        //2.使用Thread-0线程往集合中添加1000个元素
        System.out.println("Thread-0线程开始执行线程任务了,往集合中添加1000个元素!");
        for (int i = 0; i < 1000; i++) {
            list.add(i);//[0,1,2,...999]
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread-0线程,添加完毕,执行完线程任务了!");
    }
}
点击查看代码
public class Demo01List {
    public static void main(String[] args) throws InterruptedException {
        //创建MyThread对象,调用start方法,开启一个新的线程,执行run方法
        MyThread mt = new MyThread();
        mt.start();

        //main线程在开启新的线程之后,会继续执行main方法中的代码
        //3.使用main线程往集合中添加1000个元素
        System.out.println("main线程往集合中添加1000个元素!");
        for (int i = 0; i < 1000; i++) {
            MyThread.list.add(i);//[0,1,2,...999]
            Thread.sleep(1);
        }
        System.out.println("main线程,添加1000个元素完毕,睡眠3秒钟,等待Thread-0线程添加完毕!");
        Thread.sleep(3000);

        //4.统计集合的长度
        System.out.println("两个线程都执行完毕,集合的长度为:"+MyThread.list.size());
    }
}
ArrayList集合并发的问题:

1.存储的元素个数不对

2.会引发索引越界异常

点击查看代码
main线程往集合中添加1000个元素!
Thread-0线程开始执行线程任务了,往集合中添加1000个元素!
main线程,添加1000个元素完毕,睡眠3秒钟,等待Thread-0线程添加完毕!
Thread-0线程,添加完毕,执行完线程任务了!
两个线程都执行完毕,集合的长度为:1983

main线程往集合中添加1000个元素!
Thread-0线程开始执行线程任务了,往集合中添加1000个元素!
main线程,添加1000个元素完毕,睡眠3秒钟,等待Thread-0线程添加完毕!
Thread-0线程,添加完毕,执行完线程任务了!
两个线程都执行完毕,集合的长度为:1978 
####2.并发Set集合_CopyOnWriteArraySe Set集合的特点:

​ 1.不允许存储重复元素

​ 2.不包含索引的方法

需求:

1.创建一个被多个线程共享使用静态的HashSet集合对象

2.使用Thread-0线程往集合中添加1000个元素

3.使用main线程往集合中添加1000个元素

4.统计集合的长度

点击查看代码
import java.util.HashSet;
import java.util.concurrent.CopyOnWriteArraySet;


public class MyThread extends Thread{
    //1.创建一个被多个线程共享使用静态的HashSet集合对象
    /*
        HashSet集合是JDK1.2之后出现的,是一个多线程不安全的集合
            1.存储的元素少了
     */
    //public static HashSet<Integer> set = new HashSet<>();

    /*
        java.util.concurrent.CopyOnWriteArraySet<E>集合,是JDK1.5之后出现的
            CopyOnWriteArraySet集合底层采用的是CAS机制,保证多线程安全
            CAS机制是乐观锁,效率高
     */
    public static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();

    @Override
    public void run() {
        //2.使用Thread-0线程往集合中添加1000个元素
        System.out.println("Thread-0线程开始执行线程任务了,往集合中添加1000个元素!");
        for (int i = 0; i < 1000; i++) {
            set.add(i);//[0,1,2,...999]
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread-0线程,添加完毕,执行完线程任务了!");
    }
}
点击查看代码
public class Demo01Set {
    public static void main(String[] args) throws InterruptedException {
        //创建MyThread对象,调用start方法,开启一个新的线程,执行run方法
        MyThread mt = new MyThread();
        mt.start();

        //main线程在开启新的线程之后,会继续执行main方法中的代码
        //3.使用main线程往集合中添加1000个元素
        System.out.println("main线程往集合中添加1000个元素!");
        for (int i = 1000; i < 2000; i++) {
            MyThread.set.add(i);//[1000,1001,1002,...1999]
            Thread.sleep(1);
        }
        System.out.println("main线程,添加1000个元素完毕,睡眠3秒钟,等待Thread-0线程添加完毕!");
        Thread.sleep(3000);

        //4.统计集合的长度
        System.out.println("两个线程都执行完毕,集合的长度为:"+ MyThread.set.size());
    }
}
HashSet集合存在并发问题:
点击查看代码
main线程往集合中添加1000个元素!
Thread-0线程开始执行线程任务了,往集合中添加1000个元素!
Thread-0线程,添加完毕,执行完线程任务了!
main线程,添加1000个元素完毕,睡眠3秒钟,等待Thread-0线程添加完毕!
两个线程都执行完毕,集合的长度为:1975
####3.并发Map集合_ConcurrentHashMap 需求:

1.创建一个被多个线程共享使用静态的HashMap集合对象

2.使用Thread-0线程往集合中添加1000个元素

3.使用main线程往集合中添加1000个元素

4.统计集合的长度

点击查看代码
import java.util.HashMap;
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;

public class MyThread extends Thread{
    //1.创建一个被多个线程共享使用静态的HashMap集合对象
    /*
        HashMap集合是JDK1.2之后出现的,是一个多线程不安全的集合
            1.存储的元素少了
     */
    //public static HashMap<Integer,Integer> map = new HashMap<>();

    /*
        java.util.Hashtable<K,V>集合,是JDK1.0时期,最早期的双列集合
            底层也是哈希表结构,和HashMap一样
            不像新的 collection 实现,Hashtable 是同步的
            Hashtable集合使用synchronized同步技术,保证多线程安全
            synchronized同步技术是悲观锁,效率低
     */
    //public static Hashtable<Integer,Integer> map = new Hashtable<>();

    /*
        java.util.concurrent.ConcurrentHashMap<K,V>集合,是JDK1.5之后出现的
            ConcurrentHashMap集合底层采用的是CAS机制,保证多线程安全
            CAS机制是乐观锁,效率高
     */
    public static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();

    @Override
    public void run() {
        //2.使用Thread-0线程往集合中添加1000个元素
        System.out.println("Thread-0线程开始执行线程任务了,往集合中添加1000个元素!");
        for (int i = 0; i < 1000; i++) {
            map.put(i,i);//[0-1,1-1,2-2,...999-999]
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Thread-0线程,添加完毕,执行完线程任务了!");
    }
}
点击查看代码
public class Demo01Map {
    public static void main(String[] args) throws InterruptedException {
        //创建MyThread对象,调用start方法,开启一个新的线程,执行run方法
        MyThread mt = new MyThread();
        mt.start();

        //main线程在开启新的线程之后,会继续执行main方法中的代码
        //3.使用main线程往集合中添加1000个元素
        System.out.println("main线程往集合中添加1000个元素!");
        for (int i = 1000; i < 2000; i++) {
            MyThread.map.put(i,i);//[1000-1000,1001-1001,1002-1002,...1999-1999]
            Thread.sleep(1);
        }
        System.out.println("main线程,添加1000个元素完毕,睡眠3秒钟,等待Thread-0线程添加完毕!");
        Thread.sleep(3000);

        //4.统计集合的长度
        System.out.println("两个线程都执行完毕,集合的长度为:"+ MyThread.map.size());
    }
}
hashMap集合存在并发问题:
点击查看代码
main线程往集合中添加1000个元素!
Thread-0线程开始执行线程任务了,往集合中添加1000个元素!
Thread-0线程,添加完毕,执行完线程任务了!
main线程,添加1000个元素完毕,睡眠3秒钟,等待Thread-0线程添加完毕!
两个线程都执行完毕,集合的长度为:1980
####4.比较ConcurrentHashMap和Hashtable的效率 Java类库中,从1.0版本也提供一个线程安全的Map:Hashtable Hashtable和ConcurrentHashMap有什么区别: Hashtable采用的synchronized——悲观锁,效率更低。 ConcurrentHashMap:采用的CAS 机制——乐观锁,效率更高。

需求:

1.创建一个被多个线程共享使用静态的Hashtable集合(ConcurrentHashMap集合)对象

2.开启1000个线程,每个线程往集合中存储100000个数据

点击查看代码
import java.util.Hashtable;
import java.util.concurrent.ConcurrentHashMap;

/*
    Java类库中,从1.0版本也提供一个线程安全的Map:Hashtable
    Hashtable和ConcurrentHashMap有什么区别:
    Hashtable采用的synchronized——悲观锁,效率更低。
    ConcurrentHashMap:采用的CAS 机制——乐观锁,效率更高。
 */
public class MyThread extends Thread{
    //1.创建一个被多个线程共享使用静态的Hashtable集合(ConcurrentHashMap集合)对象
    //public static Hashtable<Integer,Integer> map = new Hashtable<>();
    public static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();

    @Override
    public void run() {
        long s = System.currentTimeMillis();
        //每个线程往集合中存储100000个数据
        for (int i = 0; i < 100000; i++) {
            map.put(i,i);
        }
        long e = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName()+"线程往集合中添加10万个元素,耗时:"+(e-s)+"毫秒");
    }
}
点击查看代码
public class Demo01Map {
    public static void main(String[] args) {
        //开启1000个线程
        for (int i = 0; i < 1000; i++) {
            new MyThread().start();
        }
    }
}
Hashtable效率低下原因: public synchronized V put(K key, V value) public synchronized V get(Object key) Hashtable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下Hashtable的效率非常低下。因为当一个线程访问Hashtable的同步方法,其他线程也访问Hashtable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。
posted @ 2021-10-13 22:27  一路走走逛逛  阅读(318)  评论(0)    收藏  举报