精华内容
下载资源
问答
  • 高并发和线程安全

    2020-06-21 19:15:18
    高并发线程安全的介绍 高并发: 是指在某个时间点上,有大量的用户(线程)同时访问同一资源 线程安全:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现...

    高并发及线程安全的介绍

    高并发: 是指在某个时间点上,有大量的用户(线程)同时访问同一资源
    线程安全:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现"数据污染"的问题

    多线程的运行机制【内存方面]

    在这里插入图片描述

    多线程安全性问题-可见性

    多个线程拥有独立的栈,但是堆是共享的
    我们一般称为独立的栈是线程的工作内存,共享的堆一般称为主内存
    结论: 成员变量和共享变量保存主内存的,局部变量保存到工作内存中的
    在这里插入图片描述

    多线程的安全性问题-有序性

    在这里插入图片描述

    多线程的安全性问题-原子性

    在这里插入图片描述

    展开全文
  • 高并发和线程安全 1.高并发和线程安全 高并发:就是在一段时间内有大量的线程要执行. 双11,春运12306,大学选选修课 ​线程安全:在高并发的情况下,多个线程之间有互相影响的效果。 2.多线程内存运行机制 当一个...

    一.高并发和线程安全

    1.高并发和线程安全

    高并发:就是在一段时间内有大量的线程要执行. 双11,春运12306,大学选选修课

    ​线程安全:在高并发的情况下,多个线程之间有互相影响的效果。

    2.多线程内存运行机制

    当一个线程启动后,JVM会为其分配一个独立的"线程栈区",这个线程会在这个独立的栈区中运行。

    • 看一下简单的线程的代码:

    1.一个线程类:

    public class MyThread extends Thread {
     @Override 
     public void run() { 
     	for (int i = 0; i < 100; i++) { 
    	 	System.out.println("i = " + i);
      		} 
    	 } 
     }
    
    1. 测试类:
    public class Demo {
    	 public static void main(String[] args) { 
    		 	//1.创建两个线程对象
    		 	 MyThread t1 = new MyThread(); 
    			 MyThread t2 = new MyThread(); 
    			 
    			 //2.启动两个线程 
    			 t1.start(); 
    			 t2.start(); 
    		} 
     }
    
    • 启动后,内存的运行机制
      在这里插入图片描述

    3.安全性问题-可见性

    • 介绍
      多个线程在执行的过程中,一个线程可能看不到另一个线程对变量的改变,这个叫线程的可见性问题.
    • 代码演示
    public class AAA extends Thread {
        //定义变量
        static int num = 0;
        @Override
        public void run() {
            System.out.println("AAA线程开始执行了");
    
            //睡眠2秒钟
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            num = 10;
            System.out.println("AAA执行结束了");
        }
    }
    
    public class BBB extends Thread{
        @Override
        public void run() {
            //循环
            while(true){
                //判断
                if(AAA.num == 10){
                    System.out.println("BBB线程知道数字是10了");
                    break;
                }
            }
        }
    }
    
    public class Test01 {
        public static void main(String[] args) {
            AAA a = new AAA();
            a.start();
    
            BBB b = new BBB();
            b.start();
        }
    }
    

    图解:
    在这里插入图片描述

    4.安全性问题-有序性

    • 介绍

      • 在程序的编译期间,程序可能会把没有上下逻辑关系的代码上下打乱顺序这个叫代码重排,一个线程代码的重排可能会对别的线程造成影响。

      • 这是一个小概率事件所以无法通过代码演示。

    • 图解
      在这里插入图片描述

    5.安全性问题-原子性

    • 介绍

      ​ 一个不可分割的语句,在多线程的情况下被分割成了多个步骤,导致线程之间互相有影响。

    • 代码演示

    public class CCC extends Thread {
        static int num = 0;
    
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                num++;
            }
        }
    }
    
    public class Test02 {
        public static void main(String[] args) throws InterruptedException {
            //开启线程
            CCC c1 = new CCC();
            c1.start();
            CCC c2 = new CCC();
            c2.start();
    
            //让主线程睡眠,为了让循环先执行
            Thread.sleep(2000);
    
            //打印num
            System.out.println(CCC.num);
    
        }
    }
    
    
    • 图解
      在这里插入图片描述

    二.volatile关键字

    ​ 可以解决可见性有序性问题。用关键字修饰变量即可。

    ​ 关键字可以让线程每次都获取最新值,并且不允许代码重排。

    //定义变量
    static volatile int num = 0;

    三.原子类

    1.AtomicInteger演示

    ​ 原子类可以解决可见性 有序性原子性

    创建线程:

    import java.util.concurrent.atomic.AtomicInteger;
    public class CCC extends Thread {
        //static int num = 0;
        static AtomicInteger num = new AtomicInteger(0);
    
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                //num++;
                num.getAndIncrement();
            }
        }
    }
    

    测试类:

    public class Test02 {
        public static void main(String[] args) throws InterruptedException {
            //开启线程
            CCC c1 = new CCC();
            c1.start();
    
            CCC c2 = new CCC();
            c2.start();
    
            //让主线程睡眠,为了让循环先执行
            Thread.sleep(2000);
            //打印num
            System.out.println(CCC.num);
    
        }
    }
    

    2.AtomicInteger工作机制-CAS机制

    在这里插入图片描述

    3.AtomicIntegerArray类示例[了解]

    数组存储的元素在多线程的情况下也会有线程安全问题。

    线程类:

    import java.util.concurrent.atomic.AtomicIntegerArray;
    public class DDD extends Thread {
        //static int[] arr = new int[1000];
    
        //原子类数组代替普通数字,小括号里面写的也是数组长度
        static AtomicIntegerArray arr = new AtomicIntegerArray(1000);
    
        @Override
        public void run() {
            //1000循环
            //0 0 0 0 0 0 0 0 0 0 0 0 0
            //1 1 1 1 1 1 1 1 1 1 1 1 1
    
            //1000 1000 1000 1000 1000 1000
            for (int i = 0; i < 1000; i++) {
                //arr[i]++;
                //给i索引的元素加一
                arr.addAndGet(i,1);
            }
        }
    }
    
    

    测试类

    import java.util.Arrays;
    public class Test03 {
        public static void main(String[] args) throws InterruptedException {
    
            //开启了100个线程
            for (int i = 0; i < 1000; i++) {
                DDD d = new DDD();
                d.start();
            }
    
            //睡眠2秒钟
            Thread.sleep(2000);
    
            //打印数组
            //System.out.println(Arrays.toString(DDD.arr));
            System.out.println(DDD.arr);
        }
    }
    

    四.多行代码的线程安全问题【重点】

    1.执行顺序的问题

    • 火车卖票问题

    线程类:

    public class AAA implements Runnable {
    
        //定义火车站一共有100张票
        int ticket = 100;
        @Override
        public void run() {
            //循环
            while(true){
                //判断有没有票
                if(ticket <= 0){
                    break;
                }
    
                //如果有票就卖票
                System.out.println(Thread.currentThread().getName() +  "卖出了" + ticket + "号票");
                //给票减一
                ticket--;
            }
        }
    }
    

    测试类:

    public class Test01 {
        public static void main(String[] args) {
            AAA a = new AAA();
            //开启线程
            Thread t1 = new Thread(a);
            t1.setName("窗口一");
            t1.start();
    
            //开启线程
            Thread t2 = new Thread(a);
            t2.setName("窗口二");
            t2.start();
    
    
        }
    }
    
    • 图解
      在这里插入图片描述

    2.synchronized关键字

    表示同步,同步的意思是一个线程在执行的时候,别的线程只能等待。一个线程执行结束之后别的线程才能执行。

    3.同步代码块

    • 格式

      synchronized(锁对象){
          同步代码
      }
      
    • 同步锁

      • 锁对象可以是任意类型的对象
      • 多个线程如果要同步必须使用同一个对象作为锁
    • 代码演示

    线程实现类:

    public class AAA implements Runnable {
    
        //定义火车站一共有100张票
        int ticket = 100;
    
        @Override
        public void run() {
            //循环
            while(true){
                //同步代码块
                synchronized ("abc") {
                    //判断有没有票
                    if (ticket <= 0) {
                        break;
                    }
    
                    //先获取当前线程对象,再获取线程名字
                    String name = Thread.currentThread().getName();
                    //如果有票就卖票
                    System.out.println(name + "卖出了" + ticket + "号票");
    
                    //给票减一
                    ticket--;
                }
            }
        }
    }
    

    测试类:

    public class Test01 {
        public static void main(String[] args) {
            AAA a = new AAA();
            //开启线程
            Thread t1 = new Thread(a);
            t1.setName("窗口一");
            t1.start();
    
            //开启线程
            Thread t2 = new Thread(a);
            t2.setName("窗口二");
            t2.start();
        }
    }
    

    4.同步方法

    • 格式

      public synchronized void method(){
          同步代码
      }
      
    • 同步方法的锁对象

      • 同步方法里面也是有锁对象的,但是锁对象不需要我们指定,同步方法的锁对象是固定的。
      • 非静态同步方法:this (代表当前类的对象)
      • 静态同步方法: 类的字节码对象(每一个类只有一个.class对象,所以这个对象一定是唯一的)
    • 代码演示

    线程实现类:

    public class AAA implements Runnable {
    
        //定义火车站一共有100张票
        int ticket = 100;
    
        @Override
        public void run() {
            //循环
            while(true){
                //判断有没有票
                if(ticket <= 0){
                    break;
                }
                //调用方法
                method();
            }
        }
        //定义同步方法
        public  synchronized void method(){
            //再次判断
            if(ticket > 0) {
                //先获取当前线程对象,再获取线程名字
                String name = Thread.currentThread().getName();
                //如果有票就卖票
                System.out.println(name + "卖出了" + ticket + "号票");
    
                //给票减一
                ticket--;
            }
        } 
    }
    
    

    测试类:

    public class Test01 {
        public static void main(String[] args) {
    
            AAA a = new AAA();
            //开启线程
            Thread t1 = new Thread(a);
            t1.setName("窗口一");
            t1.start();
    
            //开启线程
            Thread t2 = new Thread(a);
            t2.setName("窗口二");
            t2.start();
    
        }
    }
    

    5.Lock锁

    ​ Lock锁的方式更符合面向对象的调用方式。更符合程序员的写代码习惯。

    ​ Lock是一个接口,有一个子类ReentrantLock

    • 两个方法
     public void lock()   :加同步锁 
     public void unlock() :释放同步锁
    
    • 代码演示

    线程实现类:

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    public class AAA implements Runnable {
        //定义火车站一共有100张票
        int ticket = 100;
        //创建锁对象
        Lock lock = new ReentrantLock();
        
        public void run() {
            //循环
            while(true){
                //加锁
                lock.lock();
                //判断有没有票
                if(ticket <= 0){
                    lock.unlock();
                    break;
                }
                //先获取当前线程对象,再获取线程名字
                String name = Thread.currentThread().getName();
                //如果有票就卖票
                System.out.println(name +  "卖出了" + ticket + "号票");
                //给票减一
                ticket--;
                //解锁
                lock.unlock();
            }
        }
    }
    

    测试类:

    public class Test01 {
        public static void main(String[] args) {
    
            AAA a = new AAA();
            //开启线程
            Thread t1 = new Thread(a);
            t1.setName("窗口一");
            t1.start();
    
            //开启线程
            Thread t2 = new Thread(a);
            t2.setName("窗口二");
            t2.start();
    
        }
    }
    

    五.并发包

    之前学习的集合类型都是会有线程安全问题的,如果遇到了多线程情况的,需要用并发包解决问题。

    1.CopyOnWriteArrayList[了解]

    • ArrayList和CopyOnWriteArrayList效果演示

    线程实现类:

    import java.util.ArrayList;
    import java.util.concurrent.CopyOnWriteArrayList;
    
    public class AAA extends Thread {
    
        //定义集合
        //static ArrayList<Integer> list = new ArrayList<>();
        //使用并发包集合
        static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
    
        @Override
        public void run() {
            //给集合添加10000个元素
            for (int i = 0; i < 10000; i++) {
                list.add(i);
            }
        }
    }
    
    

    测试类:

    public class Test01 {
        public static void main(String[] args) throws InterruptedException {
    
            //开启线程
            new AAA().start();
            new AAA().start();
    
            //让主线程睡眠让循环先执行
            Thread.sleep(2000);
    
            //打印集合长度
            System.out.println(AAA.list.size());
    
            //出现问题一:可能长度小于20000
            //出现问题二:可能出现索引越界异常
    
        }
    }
    

    2.CopyOnWriteArraySet[了解]

    • HashSet和CopyOnWriteArraySet效果演示

    线程实现类:

    import java.util.HashSet;
    import java.util.concurrent.CopyOnWriteArraySet;
    public class BBB extends Thread {
        //定义集合
        //static HashSet<Integer> set = new HashSet<>();
        //定义并发包集合
        static CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>();
        @Override
        public void run() {
            //给集合添加10000个元素
            for (int i = 0; i < 10000; i++) {
                set.add(i);
            }
        }
    }
    

    测试类:

    public class Test02 {
        public static void main(String[] args) throws InterruptedException {
            //开启线程
            new BBB().start();
            new BBB().start();
    
            //让主线程睡眠让循环先执行
            Thread.sleep(2000);
            //打印集合长度
            System.out.println(BBB.set.size());
    
            //出现问题:集合的长度大于10000
        }
    }
    

    3.ConcurrentHashMap

    1. HashMap和Hashtable和ConcurrentHashMap效果演示

    线程实现类:

    import java.util.HashMap;
    import java.util.Hashtable;
    import java.util.concurrent.ConcurrentHashMap;
    
    public class CCC extends Thread {
    
        //创建集合
        //static HashMap<Integer,Integer> map = new HashMap<>();
    
        //使用Hashtable集合
        //static Hashtable<Integer,Integer> map = new Hashtable<>();
    
        //使用ConcurrentHashMap集合
        static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();
    
        @Override
        public void run() {
            //给集合添加10000个元素
            for (int i = 0; i < 10000; i++) {
                map.put(i,i);
            }
        }
    }
    

    测试类:

    public class Test03 {
        public static void main(String[] args) throws InterruptedException {
            //开启线程
            new CCC().start();
            new CCC().start();
    
            //让主线程睡眠
            Thread.sleep(2000);
    
            //打印集合长度
            System.out.println(CCC.map.size());
    
            //出现问题:集合的长度大于10000
        }
    }
    

    2.Hashtable和ConcurrentHashMap的速度区别【重点】

    • Hashtable执行速度慢
    • ConcurrentHashMap执行速度快

    线程实现类:

    import java.util.HashMap;
    import java.util.Hashtable;
    import java.util.concurrent.ConcurrentHashMap;
    public class CCC extends Thread {
        //创建集合
        //static HashMap<Integer,Integer> map = new HashMap<>();
        //使用Hashtable集合
    //    static Hashtable<Integer,Integer> map = new Hashtable<>();
        //使用ConcurrentHashMap集合
        static ConcurrentHashMap<Integer,Integer> map = new ConcurrentHashMap<>();
    
        @Override
        public void run() {
            //获取系统当前时间
            long time1 = System.currentTimeMillis();
    
            //给集合添加10000个元素
            for (int i = 0; i < 10000; i++) {
                map.put(i,i);
            }
            //获取系统当前时间
            long time2 = System.currentTimeMillis();
    
            System.out.println( (time2-time1)  + "毫秒");
        }
    }
    

    测试类:

    public class Test04 {
        public static void main(String[] args) {
            //创建1000个线程
            for (int i = 0; i < 1000; i++) {
                new CCC().start();
            }
        }
    }
    

    3.速度区别的原因

    • Hashtable方法都是同步的,一个线程在执行的时候,别的线程只能等待。
    public synchronized V put(K key, V value) {
    
    • ConcurrentHashMap用到了CAS机制部分同步代码块.

    4.悲观锁和乐观锁

    • CAS机制称为乐观锁,执行效率
    • 同步机制称为悲观锁,执行效率
    展开全文
  • 一,多线程 并行与并发 并行: 两个事件,在同一个时刻,都在发生 并发: 两个事件,在同一个时间段内,都在发生(交替执行) 进程与线程 进程: 正在内存中运行的程序,我们称为进程 线程: 进程中完成某个小功能的模块...

    一,多线程

    并行与并发

    并行: 两个事件,在同一个时刻,都在发生
    并发: 两个事件,在同一个时间段内,都在发生(交替执行)

    进程与线程

    进程: 正在内存中运行的程序,我们称为进程
    线程: 进程中完成某个小功能的模块(进程中用执行某个功能的执行单元)

    线程是属于某个进程的
          每个进程都有独立的内存空间(独立的栈独立的堆等),并且至少有一个线程
          每个线程都会跟进程申请一块独立栈,共享进程的堆 

    线程调用是指CPU在不同的进程不同的线程之间进行快速切换
        
    线程调度的分类:
        分时调度: 每个线程平均拥有CPU的执行权
        抢占式调用: 每个线程随机分配CPU的执行权(具体的分配多少和线程优先级有关)
        我们Java程序(Java进程)中所有线程采用抢占式调度 

    线程的状态

    Thread对象共有6种状态:NEW(新建),RUNNABLE(运行),BLOCKED(阻塞),WAITING(等待),TIMED_WAITING(有时间的等待),TERMINATED(终止);状态转换如下:

     

    1.Thread类

    a.Thread类是什么?
        Thread是Java定义好的,代表线程的类,只要创建该类的一个对象,其实就是创建了一个线程
       
    b.Thread类的构造方法
        public Thread(); // 无参构造,线程会有默认的名字,Thread-0,Thread-1等...
    	public Thread(String name); //带有线程名字的构造
    
    	public Thread(Runnable r);//带有线程任务的构造
    	public Thread(Runnable r,String name); //即带有线程名字,又带有线程任务的构造
        
    c.Thread类的成员方法
        public String getName(); //获取线程的名字
    	public void setName(String name);//修改线程的名字
    
    	public void run();//代表线程要执行的任务,任务有关的代码需要写在次方法中
    	public void start();//线程只创建并不会执行,必须调用start开启后才会执行任务
    
    	public static void sleep(long millis); //让当前线程"休眠/暂停"多少毫秒
    			这里的当前线程只指 Thread.sleep(1000)这句代码写哪个线程中,哪个线程就是当前线程
    	public static Thread currentThread();//获取当前线程对象
    			这里的当前线程是指 Thread.currentThread() 这句代码写哪个线程中,哪个线程就是当前线程
    
    
    线程执行有优先级,优先级越高先执行机会越大(并不是一定先执行!!)。优先级用int的priority参数表示。
    线程优先级最高为10,最低为1。默认为5 

    2.创建新的线程的方式-继承

    a.描述:
    	将类声明为 Thread 的子类。该子类应重写 Thread 类的 run 方法。接下来可以分配并启动该子类的实例
    b.分析创建的步骤:
    	i.创建子类 继承 Thread
        ii.子类中重写run方法(在run中编写线程要执行的任务代码)
        iii.创建子类对象(实际上就是创建一个线程对象)
        iv. 调用线程对象的start方法(启动该线程)    
    c.案例:
    	//i.创建子类 继承 Thread
        public class MyThread extends Thread {
            //ii.子类中重写run方法(在run中编写线程要执行的任务代码)
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    System.out.println("子线程..."+i);
                }
            }
        }
    	public class ThreadDemo02 {
        public static void main(String[] args) {
            // iii.创建子类对象(实际上就是创建一个线程对象)
            MyThread mt = new MyThread();
            //iv. 调用线程对象的start方法(启动该线程)
            mt.start();
            //主线程 不会等待子线程任务结束
            for (int i = 0; i < 50; i++) {
                System.out.println("主线程..."+i);
            }
        }
    }
    
    注意:
    	a.我们可以给线程起名字,也可以使用默认的名字
        b.我们获取线程的名字时:
    			建议使用通用方式: Thread.currentThread().getName();
    			如果是子线程内部也可以直接调用getName()获取子线程的名字

    创建新的线程方式二_实现方式

    a.描述
        声明实现 Runnable 接口的类。该类然后实现 run 方法。
        然后可以分配该类的实例,在创建 Thread 时作为一个参数来传递	并启动.
    b.分析步骤:
    	i.创建实现类 实现 Runnable接口(实际上接口中一个任务方法,run方法)
        ii.实现类重写run方法(run中编写具体的任务代码)
        iii.创建实现类对象(该实现类对象并不是线程对象,我们称为任务对象) 
        iv. 创建Thread对象,同时传入实现类对象
            public Thread(Runnable r);//带有线程任务的构造
    	v. 启动该线程(调用线程对象的start方法)
    c.代码实现
        //i.创建实现类 实现 Runnable接口(实际上接口中一个任务方法,run方法)
        public class MyRunnable implements Runnable {
            //ii.实现类重写run方法(run中编写具体的任务代码)
            @Override
            public void run() {
                //run中写任务代码
                for (int i = 0; i < 50; i++) {
                    System.out.println("子线程..."+i);
                }
            }
        }
        
    	public class TestThread {
            public static void main(String[] args) {
                //iii.创建实现类对象(该实现类对象并不是线程对象,我们称为任务对象)
                MyRunnable mr = new MyRunnable();
                //iv. 创建Thread对象,同时传入实现类对象
                Thread tt = new Thread(mr);
                //v. 启动该线程(调用线程对象的start方法)
                tt.start();
    
                //主线程不会等待子线程执行完毕
                for (int i = 0; i < 50; i++) {
                    System.out.println("主线程..."+i);
                }
            }
        }

    两种方式的优劣比较

    两种创建线程的方式,实现方式比较好
        a.实现方式比较好,因为实现方式线程和任务是分开,是由程序员自己组合
        b.实现方式避免了Java单继承不足
        c.实现方式线程和任务是解耦的,继承方式线程和任务是耦合的
        d.对于线程池来说,我们需要的是Runnable的实现类,而不需要Thread的子类
    综上所述: 在开发中我们建议使用实现方式(并不是说继承方式不对)   

    匿名内部类简化创建线程方式

    匿名内部类作用:
    	可以快速创建一个类的子类对象或者一个接口的实现类对象
    public class TestDemo {
        public static void main(String[] args) {
            //1.继承方式创建线程
            new Thread(){
                @Override
                public void run() {
                    for (int i = 0; i < 50; i++) {
                        System.out.println(Thread.currentThread().getName()+"..."+i);
                    }
                }
            }.start();
            //2.实现方式创建线程
            new Thread(new Runnable(){
                @Override
                public void run() {
                    for (int i = 0; i < 50; i++) {
                        System.out.println(Thread.currentThread().getName()+"..."+i);
                    }
                }
            }).start();
            //主线程不会等待子线程任务结束
            for (int i = 0; i < 50; i++) {
                System.out.println(Thread.currentThread().getName()+"..."+i);
            }
        }
    }        

     

    二。高并发,和线程安全

    高并发及线程安全的介绍

    什么是高并发: 是指在某个时间点上,有大量的用户(线程)同时访问同一资源
    线程安全: 是指在某个时间点上,发生高并后,访问的数据出现"不合符实际的数据",称为线程安全有问题 

    多线程的运行机制

    public class MyThread extends Thread { 
        @Override 
        public void run() { 
            for (int i = 0; i < 100; i++) { 
                System.out.println("i = " + i); 
            } 
        } 
    }
    
    public class Demo { 
        public static void main(String[] args) { 
            //1.创建两个线程对象 
            MyThread t1 = new MyThread(); 
            MyThread t2 = new MyThread(); 
            //2.启动两个线程 
            t1.start(); 
            t2.start(); 
    	} 
    }

    多线程的安全性问题--可见性

    什么有可见性:
    	当一个共性变量,被多个线程使用时,其中某个线程对共性变量进行了修改,对于其他线程来说并不是立刻可见的
            其他线程获取的值还是以前的副本(旧的值)
    案例:
    	public class MyThread extends Thread {
            //无论创建多个MyThread对象,他们共性一个静态变量a
            public static int a = 0;
            @Override
            public void run() {
                System.out.println("线程启动,休息2秒...");
                try { Thread.sleep(1000 * 2); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println("将a的值改为1");
                a = 1;
                System.out.println("线程结束...");
            }
        }
    
    	public class TestSafaDemo01 {
            public static void main(String[] args) {
                //1.启动线程
                MyThread t = new MyThread();
                t.start();
                //2.主线程继续
                while (true) {
                    if (MyThread.a == 1) {
                        System.out.println("主线程读到了a = 1");
                    }
                }
            }
        }

    多线程的安全性问题-有序性

    什么是有序性:
        在不影响代码的结果的程度上对代码进行"重排"
        如果在多线程的情况下,"重排"可能对一样的代码,执行后得出不一样的结果
        我们要保证在多线程的情况下,不对代码进行"重排",保证代码是有序(不要使用重排!!) 

    多线程的安全性问题-原子性

    什么原子性:
        线程对一个共性变量,进行++时,这个++分成两步操作,先取出值加1 然后给共性变量赋值
        如果取出值加1后,还没有来得及赋值,被其他线程抢走CPU,此时我们称为++操作不具有原子性 

    三。volatile关键字

    volatile是一个关键字,用来修饰成员变量(静态变量),被他修饰的变量,具有可见性和有序性

    volatile解决可见性

    public class MyThread extends Thread {
        //无论创建多个MyThread对象,他们共性一个静态变量a
        public volatile static int a = 0;
        @Override
        public void run() {
            System.out.println("线程启动...");
            try { Thread.sleep(1000 * 2); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("将a的值改为1");
            a = 1;
            System.out.println("线程结束...");
        }
    }
    
    public class TestSafaDemo01 {
        public static void main(String[] args) {
            //1.启动线程
            MyThread t = new MyThread();
            t.start();
            //2.主线程继续
            while (true) {
                if (MyThread.a == 1) {
                    System.out.println("主线程读到了a = 1");
                }
            }
        }
    }

    volatile解决有序性

    volatile不能解决原子性

    volatile不能解决原子性问题
    public class MyThread extends Thread {
        public volatile static int a = 0;
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                a++;
            }
            System.out.println("修改完毕!");
        }
    }
    
    public class TestSafeDemo {
        public static void main(String[] args) throws InterruptedException {
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
    
            t1.start(); //线程1 对a加了10000次
            t2.start(); // 线程2 对a加了 10000次
            Thread.sleep(1000);
            System.out.println("获取a最终值:" + MyThread.a);
            //总是不准确的。原因:两个线程访问a 的步骤不具有:原子性
        }
    }

    volatile的作用

    a.解决变量的可见性,一旦变量发生改变,所有使用到该变量的线程都会取到最新值
    b.解决变量的有序性,一旦变量加上volatile,那么编译器不会该变量的代码进行重排
    c.无法解决变量操作过程中原子性,对变量的操作还是有可能被其他线程打断  

    四。原子类

    a.什么是原子类?
        是对普通类型(比如:int,Integer,double,Double)的原子类封装,使其的操作成员原子操作
    b.原子类的作用?
        对原子类的增加或者减少操作,保证是原子性,保证中间不会被其他线程"打断"
    c.原子类有哪些?
        比如:
            AtomicInteger是对int变量进行操作的原子类
            AtomicLong是对long变量进行操作的原子类
            AtomicBoolean对boolean变量操作的“原子类”;
    注意: 原子类,既可以解决原子性,也可以解决有序性和可见性

    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变量操作的原子类”;

    AtomicInteger类

    a.AtomicInteger是什么?
        是对int类型变量进行操作的原子类
    b.AtomicInteger的构造方法
        public AtomicInteger(int num);
    c.AtomicInteger的成员方法
        public int getAndIncrement();//就相当于 变量++  
    	public int incrementAndGet();//就相当于 ++变量 
        
    d.使用AtomicInteger改写案例    
        public class MyThread extends Thread {
            public static AtomicInteger a = new AtomicInteger(0);
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    a.getAndIncrement();//相当于 a++  先获取在自增1
                }
                System.out.println("修改完毕!");
            }
        }
    
    	public class TestSafeDemo {
            public static void main(String[] args) throws InterruptedException {
                MyThread t1 = new MyThread();
                MyThread t2 = new MyThread();
    
                t1.start(); //线程1 对a加了10000次
                t2.start(); // 线程2 对a加了 10000次
                Thread.sleep(1000);
                System.out.println("获取a最终值:" + MyThread.a);
                //总是不准确的。原因:两个线程访问a 的步骤不具有:原子性
            }
        }

    Unsafe类中,调用了一个:compareAndSwapInt()方法,此方法的几个参数:
    var1:传入的AtomicInteger对象
    var2AtommicInteger内部变量的偏移地址
    var5:之前取出的AtomicInteger中的值;
    var5 + var4:预期结果
    此方法使用了一种"比较并交换(Compare And Swap)"的机制,它会用var1var2先获取内存中
    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使用var1var2AtomicInteger中获取的值为:1,而传入的var50,比较失败,返回
    false,继续循环。
    线程A执行:var5 = this.getIntVolatile(var1,var2);获取的结果为:1
    线程A执行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
    此时线程A使用var1var2AtomicInteger中获取的值为:1,而传入的var51,比较成功,将其修改
    var5 + var4,也就是2,将AtomicInteger中的值改为2,结束。
    CAS机制也被称为:乐观锁。因为大部分比较的结果为true,就直接修改了。只有少部分多线程并发的情况会
    导致CAS失败,而再次循环。

    AtomicIntegerArray类

    常用的数组操作的原子类: 1).java.util.concurrent.atomic.AtomicIntegetArray:int数组操作的原子类。
    2).java.util.concurrent.atomic.AtomicLongArray:对long数组操作的原子类。
    3).java.utio.concurrent.atomic.AtomicReferenceArray:对引用类型数组操作的原子类。

    非原子类数组在多线程并发时会有问题

    public class MyThread extends Thread {
        public static int[] intArray = new int[1000];//不直接使用数组
    
        @Override
        public void run() {
            for (int i = 0; i < intArray.length; i++) {
                intArray[i]++;
            }
        }
    }
    
    public class TestDemo01 {
        public static void main(String[] args) throws InterruptedException {
            //创建1000个线程,每个线程为数组的每个元素+1
            for (int i = 0; i < 1000; i++) {
                new MyThread().start();
            }
    
            Thread.sleep(1000 * 5);//让所有线程执行完毕
            System.out.println("主线程休息5秒醒来");
            for (int i = 0; i < MyThread.intArray.length; i++) {
                System.out.println(MyThread.intArray[i]);
            }
        }
    }
    打印结果:
    	1000,1000,1000,999,1000,1000,1000,1000,....
    有个别元素是小于1000的,因为int[]是非原子类数组,不能保存原子性!!!

    使用原子类数组,保证原子性,解决问题

    public class MyThread extends Thread {
        public static int[] intArray = new int[1000];//不直接使用数组
        public static AtomicIntegerArray arr = new AtomicIntegerArray(intArray);
        @Override
        public void run() {
    //        for (int i = 0; i < intArray.length; i++) {
    //            intArray[i]++;
    //        }
            for (int i = 0; i < arr.length(); i++) {
                arr.addAndGet(i, 1);//将i位置上的元素 + 1,相当于 ++数组[i]
            }
    
        }
    }
    
    public class TestDemo01 {
        public static void main(String[] args) throws InterruptedException {
            //创建1000个线程,每个线程为数组的每个元素+1
            for (int i = 0; i < 1000; i++) {
                new MyThread().start();
            }
    
            Thread.sleep(1000 * 5);//让所有线程执行完毕
            System.out.println("主线程休息5秒醒来");
    //        for (int i = 0; i < MyThread.intArray.length; i++) {
    //            System.out.println(MyThread.intArray[i]);
    //        }
            for (int i = 0; i < MyThread.arr.length(); i++) {
                System.out.println(MyThread.arr.get(i));
            }
        }
    }

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    展开全文
  • 所以在使用多线程并发地访问这些容器时可能出现线程安全问题。因此要求开发人员在任何用到这些的地方需要做同步处理。如此导致使用时极为不便。对此,java中提供了一些相应的同步容器供使用。 2、常见的同步容器...

    一、同步容器

    1、同步容器出现原因:

         因为ArrayList  HashSet  HashMap 这几个容器都是线程不安全的,但是使用频率又最为频繁。所以在使用多线程并发地访问这些容器时可能出现线程安全问题。因此要求开发人员在任何用到这些的地方需要做同步处理。如此导致使用时极为不便。对此,java中提供了一些相应的同步容器供使用。

    2、常见的同步容器举例:

    》ArrayList——》Vector  、Stack

    》HashMap——》HashTable(Key  value 不能为null)

    》Collections.synchronizedXXX(List、Set、Map)

    3、代码演示:

    》Vector:

    package com.mmall.practice.example.syncContainer;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Vector;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @Slf4j
    public class VectorExample1 {
    
        private static Vector<Integer> list = new Vector<>();
        //请求总数
        private static int threadNum = 200;
        //同时并发执行的线程数
        private static int clientNum = 5000;
    
        public static void main(String[] args) throws Exception {
            ExecutorService exec = Executors.newCachedThreadPool();
            final Semaphore semp = new Semaphore(threadNum);
            for (int index = 0; index < clientNum; index++) {
                final int threadNum = index;
                exec.execute(() -> {
                    try {
                        semp.acquire();
                        func(threadNum);
                        semp.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                });
            }
            exec.shutdown();
            log.info("size:{}", list.size());
        }
        public static void func(int threadNum) {
            list.add(threadNum);
        }
    }
    

     

    运行上方代码可以确认,可以上方当前是符合条件的。

    但Vecor这个同步容器在某些情况下仍然是线程不安全的。

    package com.mmall.practice.example.syncContainer;
    
    import java.util.Vector;
    
    public class VectorExample2 {
    
        private static Vector<Integer> vector = new Vector<>();
    
        public static void main(String[] args) throws InterruptedException {
    
            while (true) {
    
                for (int i = 0; i < 10; i++) {
                    vector.add(i);
                }
    
                Thread thread1 = new Thread() {
                    public void run() {
                        for (int i = 0; i < vector.size(); i++) {
                            vector.remove(i);
                        }
                    }
                };
                Thread thread2 = new Thread() {
                    public void run() {
                        for (int i = 0; i < vector.size(); i++) {
                            vector.get(i);
                        }
                    }
                };
    
                thread1.start();
                thread2.start();
            }
        }
    }
    

    运行如上代码,可以发现,运行抛出异常。证实该程序是线程不安全的,vector在某些情况下线程不安全。

    》Hashtable  代替hashMap ,

    package com.mmall.practice.example.syncContainer;
    
    import com.google.common.collect.Maps;
    import com.mmall.practice.annoations.NotThreadSafe;
    import com.mmall.practice.annoations.ThreadSafe;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Hashtable;
    import java.util.Map;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @ThreadSafe
    @Slf4j
    public class HashTableExample1 {
    
        private static Map<Integer, Integer> map = new Hashtable<>();
    
        //请求总数
        private static int threadNum = 200;
        //同时并发执行的线程数
        private static int clientNum = 5000;
    
        public static void main(String[] args) throws Exception {
            ExecutorService exec = Executors.newCachedThreadPool();
            final Semaphore semp = new Semaphore(threadNum);
            for (int index = 0; index < clientNum; index++) {
                final int threadNum = index;
                exec.execute(() -> {
                    try {
                        semp.acquire();
                        func(threadNum);
                        semp.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                });
            }
            exec.shutdown();
            log.info("size:{}", map.size());
        }
    
        public static void func(int threadNum) {
            map.put(threadNum, threadNum);
        }
    }
    

     上述代码,预期结果为5000,最后运行下如上代码,你可以发现,结果是5000,即证实hashtable是线程安全的容器。

    》Collections 类中的工厂方法创建的线程安全对象,

    》》Collections.synchronizedXXX  方法作用于List时:

    package com.mmall.practice.example.syncContainer;
    
    import com.google.common.collect.Lists;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Collections;
    import java.util.List;
    import java.util.Vector;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @Slf4j
    public class CollectionsExample1 {
    
        private static List<Integer> list = Collections.synchronizedList(Lists.newArrayList());
    
        //请求总数
        private static int threadNum = 200;
        //同时并发执行的线程数
        private static int clientNum = 5000;
    
        public static void main(String[] args) throws Exception {
            ExecutorService exec = Executors.newCachedThreadPool();
            final Semaphore semp = new Semaphore(threadNum);
            for (int index = 0; index < clientNum; index++) {
                final int threadNum = index;
                exec.execute(() -> {
                    try {
                        semp.acquire();
                        func(threadNum);
                        semp.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                });
            }
            exec.shutdown();
            log.info("size:{}", list.size());
        }
    
        public static void func(int threadNum) {
            list.add(threadNum);
        }
    }
    

    运行结果如下:

    》》Collections.synchronizedXXX  方法作用于Sett时:

    package com.mmall.practice.example.syncContainer;
    
    import com.google.common.collect.Sets;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Collections;
    import java.util.Set;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @Slf4j
    public class CollectionsExample2 {
    
        private static Set<Integer> set = Collections.synchronizedSet(Sets.newHashSet());
    
        //请求总数
        private static int threadNum = 200;
        //同时并发执行的线程数
        private static int clientNum = 5000;
    
        public static void main(String[] args) throws Exception {
            ExecutorService exec = Executors.newCachedThreadPool();
            final Semaphore semp = new Semaphore(threadNum);
            for (int index = 0; index < clientNum; index++) {
                final int threadNum = index;
                exec.execute(() -> {
                    try {
                        semp.acquire();
                        func(threadNum);
                        semp.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                });
            }
            exec.shutdown();
            log.info("size:{}", set.size());
        }
    
        public static void func(int threadNum) {
            set.add(threadNum);
        }
    }
    

    运行结果如下:

    》》Collections.synchronizedXXX  方法作用于Mapt时:

    package com.mmall.practice.example.syncContainer;
    
    import com.mmall.practice.annoations.ThreadSafe;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Collections;
    import java.util.Hashtable;
    import java.util.Map;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @ThreadSafe
    @Slf4j
    public class CollectionsExample3 {
    
        private static Map<Integer, Integer> map = Collections.synchronizedMap(new Hashtable<>());
    
        //请求总数
        private static int threadNum = 200;
        //同时并发执行的线程数
        private static int clientNum = 5000;
    
        public static void main(String[] args) throws Exception {
            ExecutorService exec = Executors.newCachedThreadPool();
            final Semaphore semp = new Semaphore(threadNum);
            for (int index = 0; index < clientNum; index++) {
                final int threadNum = index;
                exec.execute(() -> {
                    try {
                        semp.acquire();
                        func(threadNum);
                        semp.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                });
            }
            exec.shutdown();
            log.info("size:{}", map.size());
        }
    
        public static void func(int threadNum) {
            map.put(threadNum, threadNum);
        }
    }
    

    运行结果如下:

    总结:经过如上多次测试发现,Collections.synchronizedXXX方法可以创建出线程安全的容器对象。

    扩展:

    集合在使用时容易出现的问题代码示例:集合中删除数据时,会出现如下结果(因此删除数据时,要选择如下success 执行结果的方案哦!)

    package com.mmall.practice.example.syncContainer;
    
    import java.util.Iterator;
    import java.util.Vector;
    
    public class CollectionRemoveExample {
    
        // java.util.ConcurrentModificationException  结果报异常
        private static void test1(Vector<Integer> v1) { // foreach
            for(Integer i : v1) {
                if (i.equals(3)) {
                    v1.remove(i);
                }
            }
        }
    
        // java.util.ConcurrentModificationException  结果报异常
        private static void test2(Vector<Integer> v1) { // iterator
            Iterator<Integer> iterator = v1.iterator();
            while (iterator.hasNext()) {
                Integer i = iterator.next();
                if (i.equals(3)) {
                    v1.remove(i);
                }
            }
        }
    
        // success   结果ok
        private static void test3(Vector<Integer> v1) { // for
            for (int i = 0; i < v1.size(); i++) {
                if (v1.get(i).equals(3)) {
                    v1.remove(i);
                }
            }
        }   
    
        public static void main(String[] args) {
    
            Vector<Integer> vector = new Vector<>();
            vector.add(1);
            vector.add(2);
            vector.add(3);
            vector.add(4);
            vector.add(5);
            test3(vector);
        }
    }
    

    如上,注意,在遍历 集合时删除元素容易出问题,在for中是ok的,但在iterator或者vector中是有问题的,所以注意方法的选取。

    二、线程安全之并发容器 J.U.C (java.util.concurrent):

    1、此处针对之前讲述的线程不安全类与写法中提到的问题提出解决方案,在此讲述下java提供的线程安全的同步容器。

    》ArrayList ——》CopyOnWriteArrayList

     相比ArrayList,后者是线程安全的,通过名称,可以简单表现其实现原理。字面意思为:写操作时,复制。当有新元素拷贝到copyOnWriteList中时,它先从原有的数组中拷贝一份出来,在新的数组上进行写操作。写完后,再将原来的数组引用指向新的数组。CopyOnWriteArrayList的整个add 操作都是在锁保护下进行,目的是为了防止在多线程下操作时复制出多个对象,将数据搞混乱。

        但CopyOnWriteArrayList 类也存在缺点,即因为要复制,所以会消耗内存。如果元素比较多,容易引发young GC 或者for GC问题。具体可查看java的垃圾回收机制相关资料。其次,该方案无法实现实时读写的需求。比如set完数据之后,读取时可能读取到旧的数据。虽然最终结果肯定是一致的,但是无法满足即时性需求。

        综上,CopyOnWriteArrayList 更适合读多写少的操作。

        它的设计思想:读写分离、最终一致性、使用时另外开辟空间,防止错误,

       最终需要记住,copyOnWriteArrayList的读操作都是原子性操作不需要加锁,但是写操作都是在锁保护下进行,防止多线程并发出现线程安全问题。

    代码示例:

    package com.mmall.practice.example.concurrent;
    
    import com.mmall.practice.annoations.ThreadSafe;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.List;
    import java.util.concurrent.CopyOnWriteArrayList;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @Slf4j
    @ThreadSafe
    public class CopyOnWriteArrayListExample {
    
        // 请求总数
        public static int clientTotal = 5000;
    
        // 同时并发执行的线程数
        public static int threadTotal = 200;
    
        private static List<Integer> list = new CopyOnWriteArrayList<>();
    
        public static void main(String[] args) throws Exception {
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal; i++) {
                final int count = i;
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        update(count);
                        semaphore.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            log.info("size:{}", list.size());
        }
    
        private static void update(int i) {
            list.add(i);
        }
    }
    

    运行如上代码,结果没有问题。

    》HashSet 、TreeSet ——》copyOnWriteArraySet 、ConcurrentSkipListSet

    copyOnWriteArraySet   线程安全的,底层用了CopyOnWriteArrayList,它通常也适用于大小较小(防止元素多造成yang GC问题)、只读操作大于可变的操作。适用迭代器遍历时,速度很快,不会发生线程安全问题。

    ConcurrentSkipListSet   它是jdk6 添加,支持自然排序、并且在构造器中支持自定义比较器。底层基于map集合,在多线程操作下,执行remove  add  contains 方法都是线程安全的。但是对于批量操作,addAll  removeAll  contrainsAll 无法保证原子性,故无法保证线程安全。所以在多线程中,对于批量操作的方法,要加锁同步保证线程安全。

        注意,这两个类都是无法使用null元素的。因为null无法与其他元素区分开。

    代码示例:

    package com.mmall.practice.example.concurrent;
    
    import com.mmall.practice.annoations.ThreadSafe;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Set;
    import java.util.concurrent.CopyOnWriteArraySet;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @Slf4j
    @ThreadSafe
    public class CopyOnWriteArraySetExample {
    
        // 请求总数
        public static int clientTotal = 5000;
    
        // 同时并发执行的线程数
        public static int threadTotal = 200;
    
        private static Set<Integer> set = new CopyOnWriteArraySet<>();
    
        public static void main(String[] args) throws Exception {
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal; i++) {
                final int count = i;
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        update(count);
                        semaphore.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            log.info("size:{}", set.size());
        }
    
        private static void update(int i) {
            set.add(i);
        }
    }
    
    package com.mmall.practice.example.concurrent;
    
    import com.mmall.practice.annoations.ThreadSafe;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Set;
    import java.util.concurrent.ConcurrentSkipListSet;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    @Slf4j
    @ThreadSafe
    public class ConcurrentSkipListSetExample {
    
        // 请求总数
        public static int clientTotal = 5000;
    
        // 同时并发执行的线程数
        public static int threadTotal = 200;
    
        private static Set<Integer> set = new ConcurrentSkipListSet<>();
    
        public static void main(String[] args) throws Exception {
            ExecutorService executorService = Executors.newCachedThreadPool();
            final Semaphore semaphore = new Semaphore(threadTotal);
            final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
            for (int i = 0; i < clientTotal; i++) {
                final int count = i;
                executorService.execute(() -> {
                    try {
                        semaphore.acquire();
                        update(count);
                        semaphore.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                    countDownLatch.countDown();
                });
            }
            countDownLatch.await();
            executorService.shutdown();
            log.info("size:{}", set.size());
        }
    
        private static void update(int i) {
            set.add(i);
        }
    }
    

    运行如上两个方法,结果都是5000,和预期结果相符,证实为线程安全的。

    再次注意:相应的批量操作是不安全的。

    》HashMap 、TreeMap ——》ConcurrentHashMap  、ConcurrentSkipListMap

      ConcurrentHashMap :hashmap线程安全的版本,不允许空值。除了少数的删除操作,该类在只读操作大于写操作时,具有很高的并发性,因为该类为读取操作做了很多优化处理。高并发场景下表现优良

    ConcurrentSkipListMap :treemap的线程安全版本,底层适用了SkipList 这种跳表的结构实现。

     ConcurrentHashMap和ConcurrentSkipListMap相比,在数据量大时,前者的查询速度是后者的4倍(4个线程1.6万数据时),但是后者的某些特点或优势是前者无法实现的,比如:

       1)ConcurrentSkipListMap中的key是有序的,而ConcurrentHashMap无法实现;

       2)ConcurrentSkipListMap支持更高的并发,存取时间几乎和线程数无关。

       3)在数量级越大时,ConcurrentSkipListMap的优势越明显。在非多线程的情况下,尽量使用TreeMap。对于并发性较低的情况下,使用Collections中的类也是很好的选择。对于高并发的程序,还是使用ConcurrentSkipListMap,需要对key排序时,也用这个类。

    代码示例:

    package com.mmall.practice.example.concurrent;
    
    import com.mmall.practice.annoations.ThreadSafe;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    /**
     * ConcurrentHashMap 不允许空值
     * 在实际的应用中,散列表一般的应用场景是:除了少数插入操作和删除操作外,绝大多数都是读取操作,而且读操作在大多数时候都是成功的。
     * 正是基于这个前提,ConcurrentHashMap 针对读操作做了大量的优化。通过 HashEntry 对象的不变性和用 volatile 型变量协调线程间的内存可见性,
     * 使得 大多数时候,读操作不需要加锁就可以正确获得值。这个特性使得 ConcurrentHashMap 的并发性能在分离锁的基础上又有了近一步的提高。
     * ConcurrentHashMap 是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。
     * 相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。
     * 在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。
     * 同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。
     * 这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。
     * <p>
     * ConcurrentHashMap 的高并发性主要来自于三个方面:
     * <p>
     * 用分离锁实现多个线程间的更深层次的共享访问。
     * 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
     * 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。
     * 使用分离锁,减小了请求 同一个锁的频率。
     * <p>
     * 通过 HashEntery 对象的不变性及对同一个 Volatile 变量的读 / 写来协调内存可见性,使得 读操作大多数时候不需要加锁就能成功获取到需要的值。
     * 由于散列映射表在实际应用中大多数操作都是成功的 读操作,所以 2 和 3 既可以减少请求同一个锁的频率,也可以有效减少持有锁的时间。
     * 通过减小请求同一个锁的频率和尽量减少持有锁的时间 ,使得 ConcurrentHashMap 的并发性相对于 HashTable 和用同步包装器包装的 HashMap有了质的提高。
     */
    @ThreadSafe
    @Slf4j
    public class ConcurrentExample1 {
    
        private static Map<Integer, Integer> map = new ConcurrentHashMap<>();
    
        private static int threadNum = 200;
        private static int clientNum = 5000;
    
        public static void main(String[] args) throws Exception {
            ExecutorService exec = Executors.newCachedThreadPool();
            final Semaphore semp = new Semaphore(threadNum);
            for (int index = 0; index < clientNum; index++) {
                final int threadNum = index;
                exec.execute(() -> {
                    try {
                        semp.acquire();
                        func(threadNum);
                        semp.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                });
            }
            exec.shutdown();
            log.info("size:{}", map.size());
        }
    
        public static void func(int threadNum) {
            map.put(threadNum, threadNum);
        }
    }
    
    package com.mmall.practice.example.concurrent;
    
    import com.mmall.practice.annoations.ThreadSafe;
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Map;
    import java.util.concurrent.ConcurrentSkipListMap;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Semaphore;
    
    /**
     * ConcurrentSkipListMap提供了一种线程安全的并发访问的排序映射表
     * 内部是SkipList(跳表)结构实现,在理论上能够在O(log(n))时间内完成查找、插入、删除操作
     * ConcurrentHashMap是HashMap的线程安全版本,ConcurrentSkipListMap是TreeMap的线程安全版本
     *
     * concurrentHashMap与ConcurrentSkipListMap性能测试
     * 在4线程1.6万数据的条件下,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右
     *
     * 但ConcurrentSkipListMap有几个ConcurrentHashMap 不能比拟的优点:
     * 1、ConcurrentSkipListMap 的key是有序的。
     * 2、ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap 的存取时间是log(N),和线程数几乎无关。
     *      也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势。
     *
     *  在非多线程的情况下,应当尽量使用TreeMap。
     *  此外对于并发性相对较低的并行程序可以使用Collections.synchronizedSortedMap将TreeMap进行包装,也可以提供较好的效率。
     *  对于高并发程序,应当使用ConcurrentSkipListMap,能够提供更高的并发度
     *  所以在多线程程序中,如果需要对Map的键值进行排序时,请尽量使用ConcurrentSkipListMap,可能得到更好的并发度
     */
    @ThreadSafe
    @Slf4j
    public class ConcurrentExample2 {
    
        private static Map<Integer, Integer> map = new ConcurrentSkipListMap<>();
    
        private static int threadNum = 200;
        private static int clientNum = 5000;
    
        public static void main(String[] args) throws Exception {
            ExecutorService exec = Executors.newCachedThreadPool();
            final Semaphore semp = new Semaphore(threadNum);
            for (int index = 0; index < clientNum; index++) {
                final int threadNum = index;
                exec.execute(() -> {
                    try {
                        semp.acquire();
                        func(threadNum);
                        semp.release();
                    } catch (Exception e) {
                        log.error("exception", e);
                    }
                });
            }
            exec.shutdown();
            log.info("size:{}", map.size());
        }
    
        public static void func(int threadNum) {
            map.put(threadNum, threadNum);
        }
    }
    

    如上展示了这两个类的使用方法,当然,运行结果都是线程安全的。

    三、安全共享对象策略总结:

     1、线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改;

     2、共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它;

     3、线程安全对象:一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共的接口随意访问它。

    4、被守护对象:被守护对象只能通过获取特定的锁来访问。

    如上通过不可变对象、线程封闭、同步容器、并发容器总结而来。

    展开全文

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,439
精华内容 975
关键字:

高并发和线程安全