精华内容
下载资源
问答
  • 线程出现问题的根本原因是因为线程上下文切换,导致线程里的指令没有执行完就切换执行其它线程了,下面举一个例子 public class Test { static int count = 0; public static void main(String[] args) throws ...

    承接上文:

    在这里插入图片描述

    一、 线程安全问题 (重点)

    Java3y : 多线程基础

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

    1、 线程出现问题的根本原因分析

    • 线程出现问题的根本原因是因为线程上下文切换,导致线程里的指令没有执行完就切换执行其它线程了,下面举一个例子
    public class Test {
    	static int count = 0;
    	public static void main(String[] args) throws InterruptedException {
    	    Thread t1 = new Thread(()->{
    	        for (int i = 1; i < 5000; i++){
    	            count++;
    	        }
    	    });
    	    Thread t2 =new Thread(()->{
    	        for (int i = 1; i < 5000; i++){
    	            count--;
    	        }
    	    });
    	    t1.start();
    	    t2.start();
    	    t1.join(); // 主线程等待t1线程执行完
    	    t2.join(); // 主线程等待t2线程执行完
    	    
    	    // main线程只有等待t1, t2线程都执行完之后, 才能打印count, 否则main线程不会等待t1,t2
    	    // 直接就打印count的值为0
    	    log.debug("count的值是{}",count);
    	}
    }
    
    // 打印: 并不是我们期望的0值, 为什么呢? 看下文分析
    09:42:42.921 guizy.ThreadLocalDemo [main] - count的值是511 
    

    我将从字节码的层面进行分析:

    • 因为在Java中对静态变量自增/自减 并不是原子操作

    1583568350082

    1583568587168

    getstatic i // 获取静态变量i的值
    iconst_1 // 准备常量1
    iadd // 自增
    putstatic i // 将修改后的值存入静态变量i
        
    getstatic i // 获取静态变量i的值
    iconst_1 // 准备常量1
    isub // 自减
    putstatic i // 将修改后的值存入静态变量i
    
    • 可以看到count++count-- 操作实际都是需要这个4个指令完成的,那么这里问题就来了!Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

    1583569253392

    如果代码是正常按顺序运行的,那么count的值不会计算错

    1583569326977

    • 出现负数的情况:一个线程没有完成一次完整的自增/自减(多个指令) 的操作, 就被别的线程进行操作, 此时就会出现线程安全问题

      下图解释:

      • 首先线程2静态变量中读取到值0, 准备常数1, 完成isub减法,变-1操作, 正常还剩下一个putstatic i写入-1的过程; 最后的指令没有执行, 就被线程1抢去了cpu的执行权;
      • 此时线程1进行操作, 读取静态变量0, 准备常数1, iadd加法, i=1, 此时将putstatic i写入 1; 当线程2重新获取到cpu的执行权时, 它通过自身的程序计数器知道自己该执行putstatic 写入-1了; 此时它就直接将结果写为-1

    1583569380639

    出现正数的情况:同上类似; 主要就是因为线程的++/--操作不是一个原子操作, 在执行4条指令期间被其他线程抢夺cpu

    1583569416016


    2、 问题的进一步描述

    临界区

    • 一个程序运行多线程本身是没有问题的
    • 问题出现在多个线程共享资源(临界资源)的时候
      • 多个线程同时对共享资源进行读操作本身也没有问题 - 对读操作没问题
      • 问题出现在对对共享资源同时进行读写操作时就有问题了 - 同时读写操作有问题
    • 先定义一个叫做临界区的概念:一段代码内如果存在对共享资源的多线程读写操作,那么称这段代码为临界区; 共享资源也成为临界资源
    static int counter = 0;
    static void increment() 
    // 临界区 
    {   
       counter++; 
    }
    
    static void decrement() 
    // 临界区 
    { 
       counter--; 
    }
    

    竞态条件

    • 多个线程在临界区执行,那么由于代码指令的执行不确定而导致的结果问题,称为竞态条件

    3、 synchronized 解决方案

    为了避免临界区中的竞态条件发生,由多种手段可以达到

    • 阻塞式解决方案: synchronized , Lock (ReentrantLock)
    • 非阻塞式解决方案: 原子变量 (CAS)

    现在讨论使用synchronized来进行解决,即俗称的对象锁,它采用互斥的方式同一时刻至多只有一个线程持有对象锁,其他线程如果想获取这个锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

    注意: 虽然Java 中互斥同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

    • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区的代码
    • 同步是由于线程执行的先后,顺序不同但是需要一个线程等待其它线程运行到某个点

    3.1、synchronized语法

    synchronized(对象) { // 线程1获得锁, 那么线程2的状态是(blocked)
     	临界区
    }
    
    • 上面的实例程序使用synchronized后如下,计算出的结果是正确!
    static int counter = 0;
    static final Object room = new Object();
    public static void main(String[] args) throws InterruptedException {
         Thread t1 = new Thread(() -> {
             for (int i = 0; i < 5000; i++) {
             	 // 对临界资源(共享资源的操作) 进行 加锁
                 synchronized (room) {
                 counter++;
            	}
     		}
     	}, "t1");
         Thread t2 = new Thread(() -> {
             for (int i = 0; i < 5000; i++) {
                 synchronized (room) {
                 counter--;
             }
         }
         }, "t2");
         t1.start();
         t2.start();
         t1.join();
         t2.join();
         log.debug("{}",counter);
    }
    
    09:56:24.210 guizy.ThreadLocalDemo [main] - count的值是0
    

    3.2、synchronized原理

    • synchronized实际上利用对象锁保证了临界区代码的原子性,临界区内的代码在外界看来是不可分割的,不会被线程切换所打断

    • 小故事
      在这里插入图片描述

    在这里插入图片描述

    1583571633729
    思考:

    • 如果把synchronized(obj)放在for循环的外面, 如何理解?
      • for循环也是一个原子操作, 表现出原子性
    • 如果t1 synchronized(obj1) 而 t2 synchronized(obj2)会怎么运行?
      • 因为t1, t2拿到不是同一把对象锁, 所以他们仍然会发现安全问题 – 必须要是同一把对象锁
    • 如果t1 synchronized(obj) 而 t2 没有加会怎么样 ?
      • 因为t2没有加锁, 所以t2, 不需要获取t1的锁, 直接就可以执行下面的代码, 仍然会出现安全问题

    小总结:

    • 当多个线程对临界资源进行写操作的时候, 此时会造成线程安全问题, 如果使用synchronized关键字, 对象锁一定要是多个线程共有的, 才能避免竞态条件的发生。

    3.3、synchronized 加在方法上

    • 加在实例方法上, 锁对象就是对象实例
    public class Demo {
    	//在方法上加上synchronized关键字
    	public synchronized void test() {
    	
    	}
    	//等价于
    	public void test() {
    		synchronized(this) {
    		
    		}
    	}
    }
    
    • 加在静态方法上, 锁对象就是当前类的Class实例
    public class Demo {
    	//在静态方法上加上synchronized关键字
    	public synchronized static void test() {
    	
    	}
    	//等价于
    	public void test() {
    		synchronized(Demo.class) {
    		
    		}
    	}
    }
    
    面向对象的改进
    class Room {
        int value = 0;
    
        public void increment() {
            synchronized (this) {
                value++;
            }
        }
    
        public void decrement() {
            synchronized (this) {
                value--;
            }
        }
    
        public int get() {
            synchronized (this) {
                return value;
            }
        }
    }
    
    @Slf4j
    public class Test1 {
        public static void main(String[] args) throws InterruptedException {
            Room room = new Room();
            Thread t1 = new Thread(() -> {
                for (int j = 0; j < 5000; j++) {
                    room.increment()
                }
            }, "t1");
    
            Thread t2 = new Thread(() -> {
                for (int j = 0; j < 5000; j++) {
                    room.decrement();
                }
            }, "t2");
    
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            log.debug("count: {}", room.get());
        }
    }
    

    二、线程八锁案例分析

    • 其实就是考察synchronized 锁住的是哪个对象, 如果锁住的是同一对象, 就不会出现线程安全问题
    1、锁住同一个对象都是this(e1对象),结果为:1,2或者2,1
    /**
     * Description: 不会出现安全问题, 打印结果顺序为: 1/2 或 2/1
     *
     * @author guizy
     * @date 2020/12/19 11:24
     */
    @Slf4j(topic = "guizy.EightLockTest")
    public class EightLockTest {
        // 锁对象就是this, 也就是e1
        public synchronized void a() {
            log.debug("1");
        }
    //    public void a () {
    //        synchronized (this) {
    //            log.debug("1");
    //        }
    //    }
    
        // 锁对象也是this, e1
        public synchronized void b() {
            log.debug("2");
        }
    
        public static void main(String[] args) {
            EightLockTest e1 = new EightLockTest();
            new Thread(() -> e1.a()).start();
            new Thread(() -> e1.b()).start();
        }
    }
    
    2、锁住同一个对象都是this(e1对象),结果为:1s后1,2 || 2,1s后1
    /**
     * Description: 不会出现安全问题, 打印结果顺序为: 1s后1,2 || 2,1s后1
     *
     * @author guizy
     * @date 2020/12/19 11:24
     */
    @Slf4j(topic = "guizy.EightLockTest")
    public class EightLockTest {
        // 锁对象就是this, 也就是e1
        public synchronized void a(){
            Thread.sleep(1000);
            log.debug("1");
        }
    
        // 锁对象也是this, e1
        public synchronized void b() {
            log.debug("2");
        }
    
        public static void main(String[] args) {
            EightLockTest e1 = new EightLockTest();
            new Thread(() -> e1.a()).start();
            new Thread(() -> e1.b()).start();
        }
    }
    
    3、a,b锁住同一个对象都是this(e1对象),c没有上锁。结果为:3,1s后1,2 || 2,3,1s后1 || 3,2,1s后1
    /**
     * Description: 会出现安全问题, 因为前两个线程, 执行run方法时, 都对相同的对象加锁;
     *              而第三个线程,调用的方法c, 并没有加锁, 所以它可以同前两个线程并行执行;
     *  打印结果顺序为: 分析: 因为线程3和线程1,2肯定是并行执行的, 所以有以下情况
     *               3,1s后1,2 || 2,3,1s后1 || 3,2,1s后1
     *               至于 1,3,2的情况是不会发生的, 可以先调用到1,但需要sleep一秒.3肯定先执行了
     *
     * @author guizy
     * @date 2020/12/19 11:24
     */
    @Slf4j(topic = "guizy.EightLockTest")
    public class EightLockTest {
        // 锁对象就是this, 也就是e1
        public synchronized void a() throws InterruptedException {
            Thread.sleep(1000);
            log.debug("1");
        }
    
        // 锁对象也是this, e1
        public synchronized void b() {
            log.debug("2");
        }
    
        public void c() {
            log.debug("3");
        }
    
        public static void main(String[] args) {
            EightLockTest e1 = new EightLockTest();
            new Thread(() -> {
                try {
                    e1.a();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
            new Thread(() -> e1.b()).start();
            new Thread(() -> e1.c()).start();
        }
    }
    
    4、a锁住对象this(n1对象),b锁住对象this(n2对象),不互斥。结果为:2,1s后1
    /**
     * Description: 会出现安全问题, 线程1的锁对象为e1, 线程2的锁对象为e2. 所以他们会同一时刻执行1,2
     *
     * @author guizy
     * @date 2020/12/19 11:24
     */
    @Slf4j(topic = "guizy.EightLockTest")
    public class EightLockTest {
        // 锁对象是e1
        public synchronized void a() {
        	Thread.sleep(1000);
            log.debug("1");
        }
    
        // 锁对象是e2
        public synchronized void b() {
            log.debug("2");
        }
    
        public static void main(String[] args) {
            EightLockTest e1 = new EightLockTest();
            EightLockTest e2 = new EightLockTest();
            new Thread(() -> e1.a()).start();
            new Thread(() -> e2.b()).start();
        }
    }
    
    5、a锁住的是EightLockTest.class对象, b锁住的是this(e1),不会互斥; 结果: 2,1s后1
    /**
     * Description: 会发生安全问题, 因为a锁住的是EightLockTest.class对象, b锁住的是this(e1),不会互斥
     *              结果: 2,1s后1
     *
     * @author guizy
     * @date 2020/12/19 11:24
     */
    @Slf4j(topic = "guizy.EightLockTest")
    public class EightLockTest {
        // 锁对象是EightLockTest.class类对象
        public static synchronized void a() {
            Thread.sleep(1000);
            log.debug("1");
        }
    
        // 锁对象是e2
        public synchronized void b() {
            log.debug("2");
        }
    
        public static void main(String[] args) {
            EightLockTest e1 = new EightLockTest();
            new Thread(() -> e1.a()).start();
            new Thread(() -> e1.b()).start();
        }
    }
    
    6、a,b锁住的是EightLockTest.class对象, 会发生互斥; 结果为:2,1s后1 || 1s后1,2
    /**
     * Description: 不会发生安全问题, 因为a,b锁住的是EightLockTest.class对象, 会发生互斥
     *              结果: 2,1s后1 || 1s后1,2
     *
     * @author guizy
     * @date 2020/12/19 11:24
     */
    @Slf4j(topic = "guizy.EightLockTest")
    public class EightLockTest {
        // 锁对象是EightLockTest.class类对象
        public static synchronized void a() {
            Thread.sleep(1000);
            log.debug("1");
        }
    
        // 锁对象是EightLockTest.class类对象
        public static synchronized void b() {
            log.debug("2");
        }
    
        public static void main(String[] args) {
            EightLockTest e1 = new EightLockTest();
            new Thread(() -> e1.a()).start();
            new Thread(() -> e1.b()).start();
        }
    }
    
    7、a锁住的是EightLockTest.class对象, b锁住的是this(e1),不会互斥; 结果: 2,1s后1
    /**
     * Description: 会发生安全问题, 因为a锁住的是EightLockTest.class对象, b锁住的是this(e1),不会互斥
     *              结果: 2,1s后1
     *
     * @author guizy
     * @date 2020/12/19 11:24
     */
    @Slf4j(topic = "guizy.EightLockTest")
    public class EightLockTest {
        // 锁对象是EightLockTest.class类对象
        public static synchronized void a() {
            Thread.sleep(1000);
            log.debug("1");
        }
    
        // 锁对象是this,e2对象
        public synchronized void b() {
            log.debug("2");
        }
    
        public static void main(String[] args) {
            EightLockTest e1 = new EightLockTest();
            EightLockTest e2 = new EightLockTest();
            new Thread(() -> e1.a()).start();
            new Thread(() -> e2.b()).start();
        }
    }
    
    8、a,b锁住的是EightLockTest.class对象, 会发生互斥; 结果为:2,1s后1 || 1s后1,2
    /**
     * Description: 不会发生安全问题, 因为a,b锁住的是EightLockTest.class对象, 会发生互斥
     *              结果: 2,1s后1 || 1s后1,2
     *
     * @author guizy
     * @date 2020/12/19 11:24
     */
    @Slf4j(topic = "guizy.EightLockTest")
    public class EightLockTest {
        // 锁对象是EightLockTest.class类对象
        public static synchronized void a() {
            Thread.sleep(1000);
            log.debug("1");
        }
    
        // 锁对象是EightLockTest.class类对象
        public static synchronized void b() {
            log.debug("2");
        }
    
        public static void main(String[] args) {
            EightLockTest e1 = new EightLockTest();
            EightLockTest e2 = new EightLockTest();
            new Thread(() -> e1.a()).start();
            new Thread(() -> e2.b()).start();
        }
    }
    

    三、 变量的线程安全分析

    1、 成员变量和静态变量的线程安全分析 (重要)

    • 如果变量没有在线程间共享,那么变量是安全的
    • 如果变量在线程间共享
      • 如果只有读操作,则线程安全
      • 如果有读写操作,则这段代码是临界区需要考虑线程安全

    2、 局部变量线程安全分析 (重要)

    • 局部变量【局部变量被初始化为基本数据类型】是安全的
    • 但局部变量引用的对象则未必 (要看该对象是否被共享且被执行了读写操作)
      • 如果该对象没有逃离方法的作用范围,它是线程安全的
      • 如果该对象逃离方法的作用范围,需要考虑线程安全

    3、线程安全的情况 (重要)

    • 局部变量表是存在于栈帧中, 而虚拟机栈中又包括很多栈帧, 虚拟机栈是线程私有的;
    • 局部变量【局部变量被初始化为基本数据类型】是安全的,示例如下
    public static void test1() {
         int i = 10;
         i++;
    }
    
    • 每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
    public static void test1();
     descriptor: ()V
     flags: ACC_PUBLIC, ACC_STATIC
     Code:
    	 stack=1, locals=1, args_size=0
    	 0: bipush 10
    	 2: istore_0
    	 3: iinc 0, 1
    	 6: return
    	 LineNumberTable:
    	 line 10: 0
    	 line 11: 3
    	 line 12: 6
    	 LocalVariableTable:
    	 Start Length Slot Name Signature
    	 3 4 0 i I
    

    在这里插入图片描述

    4、线程不安全的情况

    • 如果局部变量引用的对象逃离方法的范围,那么要考虑线程安全问题的,代码示例如下
    • 循环创建了100个线程, 在线程体里面都调用了method1方法, 在method1方法中又循环调用了100次method2,method3方法。方法2,3都使用到了成员变量arrayList, 此时的问题就是: 1个线程它会循环调用100次方法2和3, 一共有100个线程, 此时100个线程操作的共享资源就是arrayList成员变量 , 而且还进行了读写操作. 必然会造成线程不安全的问题
    public class Test15 {
        public static void main(String[] args) {
            UnsafeTest unsafeTest = new UnsafeTest();
            for (int i =0;i<100;i++){
                new Thread(()->{
                    unsafeTest.method1();
                },"线程"+i).start();
            }
        }
    }
    class UnsafeTest{
        ArrayList<String> arrayList = new ArrayList<>();
        public void method1(){
            for (int i = 0; i < 100; i++) {
                method2();
                method3();
            }
        }
        private void method2() {
            arrayList.add("1");
        }
        private void method3() {
            arrayList.remove(0);
        }
    }
    
    Exception in thread "线程1" Exception in thread "线程2" java.lang.ArrayIndexOutOfBoundsException: -1
    
    4.1、不安全原因分析
    • 无论哪个线程中的 method2 和 method3 引用的都是同一个对象中的 list 成员变量
    • 一个 ArrayList ,在添加一个元素的时候,它可能会有两步来完成:
      • 第一步: 在 arrayList[size]的位置存放此元素
      • 第二步: size++
    • 单线程运行的情况下,如果 size = 0,添加一个元素后,此元素在位置 0,而且 size=1;(没问题)
    • 多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 进行上下文切换 (线程A还没来得及size++)线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍等于0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 size 的值
    • 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 size 却等于 2。这就是“线程不安全”了。

    1583589268096

    1583587571334

    4.2、解决方法

    • 可以将list修改成局部变量,局部变量存放在栈帧中, 栈帧又存放在虚拟机栈中, 虚拟机栈是作为线程私有的;
    • 因为method1方法, 将arrayList传给method2,method3方法, 此时他们三个方法共享这同一个arrayList, 此时不会被其他线程访问到, 所以不会出现线程安全问题, 因为这三个方法使用的同一个线程
    • 在外部, 创建了100个线程, 每个线程都会调用method1方法, 然后都会再从新创建一个新的arrayList对象, 这个新对象再传递给method2,method3方法.
    class UnsafeTest {
        public void method1() {
            ArrayList<String> arrayList = new ArrayList<>();
            for (int i = 0; i < 100; i++) {
                method2(arrayList);
                method3(arrayList);
            }
        }
    
        private void method2(List<String> arrayList) {
            arrayList.add("1");
        }
    
        private void method3(List<String> arrayList) {
            arrayList.remove(0);
        }
    }
    

    在这里插入图片描述

    4.3、思考 privatefinal的重要性 (重要)

    提高线程的安全性

    • 方法访问修饰符带来的思考: 如果把method2和method3 的方法修改为public 会不会导致线程安全问题; 分情况:
    • 情况1:有其它线程调用 method2 和 method3
      • 只修改为public修饰,此时不会出现线程安全的问题, 即使线程2调用method2/3方法, 给2/3方法传过来的list对象也是线程2调用method1方法时,传递给method2/3的list对象, 不可能是线程1调用method1方法传的对象。 具体原因看上面: 4.2解决方法
    • 情况2:在情况1 的基础上,为ThreadSafe 类添加子类,子类覆盖method2 或 method3方法,即如下所示: 从这个例子可以看出 privatefinal 提供【安全】的意义所在,请体会开闭原则中的【闭】
      • 如果改为public, 此时子类可以重写父类的方法, 在子类中开线程来操作list对象, 此时就会出现线程安全问题: 子类和父类共享了list对象
      • 如果改为private, 子类就不能重写父类的私有方法, 也就不会出现线程安全问题; 所以所private修饰符是可以避免线程安全问题.
      • 所以如果不想子类, 重写父类的方法的时候, 我们可以将父类中的方法设置为private, final修饰的方法, 此时子类就无法影响父类中的方法了!
    class ThreadSafe {
        public final void method1(int loopNumber) {
            ArrayList<String> list = new ArrayList<>();
            for (int i = 0; i < loopNumber; i++) {
                method2(list);
                method3(list);
            }
        }
        private void method2(ArrayList<String> list) {
            list.add("1");
        }
        public void method3(ArrayList<String> list) {
            list.remove(0);
        }
    }
    class ThreadSafeSubClass extends ThreadSafe{
        @Override
        public void method3(ArrayList<String> list) {
            new Thread(() -> {
                list.remove(0);
            }).start();
        }
    }
    

    4.4、 常见线程安全类

    • String
    • Integer
    • StringBuffer
    • Random
    • Vector
    • Hashtable
    • java.util.concurrent 包下的类 JUC

    重点:

    • 这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的 , 也可以理解为 它们的每个方法是原子的
    • 它们的每个方法是原子的(方法都被加上了synchronized)
    • 但注意它们多个方法的组合不是原子的,所以可能会出现线程安全问题
    Hashtable table = new Hashtable();
    
    new Thread(()->{
    	// put方法增加了synchronized
     	table.put("key", "value1");
    }).start();
    
    new Thread(()->{
     	table.put("key", "value2");
    }).start();
    

    线程安全类方法的组合

    • 但注意它们多个方法的组合不是原子的,见下面分析
      • 这里只能是get方法内部是线程安全的, put方法内部是线程安全的. 组合起来使用还是会受到上下文切换的影响
    Hashtable table = new Hashtable();
    // 线程1,线程2
    if( table.get("key") == null) {
     table.put("key", value);
    }
    

    1583590979975

    不可变类的线程安全

    • StringInteger类都是不可变的类,因为其类内部状态是不可改变的,因此它们的方法都是线程安全的, 都被final修饰, 不能被继承.
    • 肯定有些人他们知道Stringreplacesubstring 等方法【可以】改变值啊其实调用这些方法返回的已经是一个新创建的对象了! (在字符串常量池中当修改了String的值,它不会再原有的基础上修改, 而是会重新开辟一个空间来存储)

    4.5、 示例分析-是否线程安全

    示例一
    • Servlet运行在Tomcat环境下并只有一个实例,因此会被Tomcat的多个线程共享使用,因此存在成员变量的共享问题。
    public class MyServlet extends HttpServlet {
    	 // 是否安全?  否:HashMap不是线程安全的,HashTable是
    	 Map<String,Object> map = new HashMap<>();
    	 // 是否安全?  是:String 为不可变类,线程安全
    	 String S1 = "...";
    	 // 是否安全? 是
    	 final String S2 = "...";
    	 // 是否安全? 否:不是常见的线程安全类
    	 Date D1 = new Date();
    	 // 是否安全?  否:引用值D2不可变,但是日期里面的其它属性比如年月日可变。与字符串的最大区别是Date里面的属性可变。
    	 final Date D2 = new Date();
     
    	 public void doGet(HttpServletRequest request,HttpServletResponse response) {
    	  // 使用上述变量
    	 }
    }
    
    示例二
    • 分析线程是否安全,先对类的成员变量,类变量,局部变量进行考虑,如果变量会在各个线程之间共享,那么就得考虑线程安全问题了,如果变量A引用的是线程安全类的实例,并且只调用该线程安全类的一个方法,那么该变量A是线程安全的的。下面对实例一进行分析:此类不是线程安全的。MyAspect切面类只有一个实例,成员变量start 会被多个线程同时进行读写操作
    • Spring中的Bean都是单例的, 除非使用@Scope修改为多例。
    @Aspect
    @Component 
    public class MyAspect {
            // 是否安全?不安全, 因为MyAspect是单例的
            private long start = 0L;
    
            @Before("execution(* *(..))")
            public void before() {
                start = System.nanoTime();
            }
    
            @After("execution(* *(..))")
            public void after() {
                long end = System.nanoTime();
                System.out.println("cost time:" + (end-start));
            }
        }
    
    示例三
    • 此例是典型的三层模型调用,MyServlet UserServiceImpl UserDaoImpl类都只有一个实例,UserDaoImpl类中没有成员变量,update方法里的变量引用的对象不是线程共享的,所以是线程安全的;UserServiceImpl类中只有一个线程安全的UserDaoImpl类的实例,那么UserServiceImpl类也是线程安全的,同理 MyServlet也是线程安全的
    • Servlet调用Service, Service调用Dao这三个方法使用的是同一个线程
    public class MyServlet extends HttpServlet {
    	 // 是否安全    是:UserService不可变,虽然有一个成员变量,
    	 			// 但是是私有的, 没有地方修改它
    	 private UserService userService = new UserServiceImpl();
    	 
    	 public void doGet(HttpServletRequest request, HttpServletResponse response) {
    	 	userService.update(...);
    	 }
    }
    
    public class UserServiceImpl implements UserService {
    	 // 是否安全     是:Dao不可变, 其没有成员变量
    	 private UserDao userDao = new UserDaoImpl();
    	 
    	 public void update() {
    	 	userDao.update();
    	 }
    }
    
    public class UserDaoImpl implements UserDao { 
    	 // 是否安全   是:没有成员变量,无法修改其状态和属性
    	 public void update() {
    	 	String sql = "update user set password = ? where username = ?";
    	 	// 是否安全   是:不同线程创建的conn各不相同,都在各自的栈内存中
    	 	try (Connection conn = DriverManager.getConnection("","","")){
    	 	// ...
    	 	} catch (Exception e) {
    	 	// ...
    	 	}
    	 }
    }
    
    示例四
    • 跟示例二大体相似,UserDaoImpl类中有成员变量,那么多个线程可以对成员变量conn 同时进行操作,故是不安全的
    public class MyServlet extends HttpServlet {
        // 是否安全
        private UserService userService = new UserServiceImpl();
    
        public void doGet(HttpServletRequest request, HttpServletResponse response) {
            userService.update(...);
        }
    }
    
    public class UserServiceImpl implements UserService {
        // 是否安全
        private UserDao userDao = new UserDaoImpl();
        public void update() {
           userDao.update();
        }
    }
    
    public class UserDaoImpl implements UserDao {
        // 是否安全: 不安全; 当多个线程,共享conn, 一个线程拿到conn,刚创建一个连接赋值给conn, 此时另一个线程进来了, 直接将conn.close
        //另一个线程恢复了, 拿到conn干事情, 此时conn都被关闭了, 出现了问题
        private Connection conn = null;
        public void update() throws SQLException {
            String sql = "update user set password = ? where username = ?";
            conn = DriverManager.getConnection("","","");
            // ...
            conn.close();
        }
    }
    
    示例五
    • 跟示例三大体相似,UserServiceImpl类的update方法中UserDao是作为局部变量存在的,所以每个线程访问的时候都会新建有一个UserDao对象,新建的对象是线程独有的,所以是线程安全的
    public class MyServlet extends HttpServlet {
        // 是否安全
        private UserService userService = new UserServiceImpl();
        public void doGet(HttpServletRequest request, HttpServletResponse response) {
            userService.update(...);
        }
    }
    public class UserServiceImpl implements UserService {
        public void update() {
            UserDao userDao = new UserDaoImpl();
            userDao.update();
        }
    }
    public class UserDaoImpl implements UserDao {
        // 是否安全
        private Connection = null;
        public void update() throws SQLException {
            String sql = "update user set password = ? where username = ?";
            conn = DriverManager.getConnection("","","");
            // ...
            conn.close();
        }
    }
    
    示例六
    • 私有变量sdf被暴露出去了, 发生了逃逸
    public abstract class Test {
        public void bar() {
            // 是否安全
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            foo(sdf);
        }
        public abstract foo(SimpleDateFormat sdf);
        public static void main(String[] args) {
            new Test().bar();
        }
    }
    
    • 其中foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法因为foo方法可以被重写,导致线程不安全。 在String类中就考虑到了这一点,String类是final的,子类不能重写它的方法。
    public void foo(SimpleDateFormat sdf) {
        String dateStr = "1999-10-11 00:00:00";
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                try {
                    sdf.parse(dateStr);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
    

    4.6 习题分析

    • 卖票练习
      测试下面代码是否存在线程安全问题,并尝试改正
    package cn.itcast.n4.exercise;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;
    import java.util.Vector;
    
    @Slf4j(topic = "c.ExerciseSell")
    public class ExerciseSell {
        public static void main(String[] args) throws InterruptedException {
            // 模拟多人买票
            TicketWindow window = new TicketWindow(1000);
    
            // 所有线程的集合(由于threadList在主线程中,不被共享,因此使用ArrayList不会出现线程安全问题)
            List<Thread> threadList = new ArrayList<>();
            // 卖出的票数统计(Vector为线程安全类)
            List<Integer> amountList = new Vector<>();
            for (int i = 0; i < 2000; i++) {
                Thread thread = new Thread(() -> {
                    // 买票
                    int amount = window.sell(random(5));
                    // 统计买票数
                    amountList.add(amount);
                });
                threadList.add(thread);
                thread.start();
            }
    
            for (Thread thread : threadList) {
                thread.join();
            }
    
            // 统计卖出的票数和剩余票数
            log.debug("余票:{}",window.getCount());
            log.debug("卖出的票数:{}", amountList.stream().mapToInt(i -> i).sum());
        }
    
        // Random 为线程安全
        static Random random = new Random();
    
        // 随机 1~5
        public static int random(int amount) {
            return random.nextInt(amount) + 1;
        }
    }
    
    // 售票窗口
    class TicketWindow {
    	// 票总数
        private int count;
    
        public TicketWindow(int count) {
            this.count = count;
        }
    
        // 获取余票数量
        public int getCount() {
            return count;
        }
    
        // 售票
        public synchronized int sell(int amount) {
            if (this.count >= amount) {
                this.count -= amount;
                return amount;
            } else {
                return 0;
            }
        }
    }
    
    • 转账练习
      测试下面代码是否存在线程安全问题,并尝试改正
    package cn.itcast.n4.exercise;
    
    import lombok.extern.slf4j.Slf4j;
    
    import java.util.Random;
    
    @Slf4j(topic = "c.ExerciseTransfer")
    public class ExerciseTransfer {
        public static void main(String[] args) throws InterruptedException {
            Account a = new Account(1000);
            Account b = new Account(1000);
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    a.transfer(b, randomAmount());
                }
            }, "t1");
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    b.transfer(a, randomAmount());
                }
            }, "t2");
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            // 查看转账2000次后的总金额
            log.debug("total:{}", (a.getMoney() + b.getMoney()));
        }
    
        // Random 为线程安全
        static Random random = new Random();
    
        // 随机 1~100
        public static int randomAmount() {
            return random.nextInt(100) + 1;
        }
    }
    
    // 账户
    class Account {
        private int money;
    
        public Account(int money) {
            this.money = money;
        }
    
        public int getMoney() {
            return money;
        }
    
        public void setMoney(int money) {
            this.money = money;
        }
    
        // 转账
        public void transfer(Account target, int amount) {
            synchronized(Account.class) {   //锁住Account类,因为涉及到A.money和B.money。
                if (this.money >= amount) {
                    this.setMoney(this.getMoney() - amount);
                    target.setMoney(target.getMoney() + amount);
                }
            }
        }
    }
    
    // 没问题, 最终的结果仍然是 2000元
    
    展开全文
  • 同步锁-线程安全问题解决方案

    万次阅读 多人点赞 2021-03-21 15:12:05
    我们如何判断程序有没有可能出现线程安全问题,主要有以下三个条件: 在多线程程序中 + 有共享数据 + 多条语句操作共享数据 多线程的场景和共享数据的条件是改变不了的(就像4个窗口一起卖100张票,这个是业务) 所以思路...

    1 同步锁

    1.1 前言

    经过前面多线程编程的学习,我们遇到了线程安全的相关问题,比如多线程售票情景下的超卖/重卖现象.
    上节笔记点这里-进程与线程笔记

    我们如何判断程序有没有可能出现线程安全问题,主要有以下三个条件:

    在多线程程序中 + 有共享数据 + 多条语句操作共享数据

    多线程的场景和共享数据的条件是改变不了的(就像4个窗口一起卖100张票,这个是业务)
    所以思路可以从第3点"多条语句操作共享数据"入手,既然是在这多条语句操作数据过程中出现了问题
    那我们可以把有可能出现问题的代码都包裹起来,一次只让一个线程来执行

    1.2 同步与异步

    那怎么"把有可能出现问题的代码都包裹起来"呢?我们可以使用synchronized关键字来实现同步效果
    也就是说,当多个对象操作共享数据时,可以使用同步锁解决线程安全问题,被锁住的代码就是同步的

    接下来介绍下同步与异步的概念:
    同步:体现了排队的效果,同一时刻只能有一个线程独占资源,其他没有权利的线程排队。
    坏处就是效率会降低,不过保证了安全。
    异步:体现了多线程抢占资源的效果,线程间互相不等待,互相抢占资源。
    坏处就是有安全隐患,效率要高一些。

    1.3 synchronized同步关键字

    1.3.1 写法

    synchronized (锁对象){
    需要同步的代码(也就是可能出现问题的操作共享数据的多条语句);
    }

    1.3.2 前提

    同步效果的使用有两个前提:

    • 前提1:同步需要两个或者两个以上的线程(单线程无需考虑多线程安全问题)
    • 前提2:多个线程间必须使用同一个锁(我上锁后其他人也能看到这个锁,不然我的锁锁不住其他人,就没有了上锁的效果)

    1.3.3 特点

    1. synchronized同步关键字可以用来修饰代码块,称为同步代码块,使用的锁对象类型任意,但注意:必须唯一!
    2. synchronized同步关键字可以用来修饰方法,称为同步方法
    3. 同步的缺点是会降低程序的执行效率,但我们为了保证线程的安全,有些性能是必须要牺牲的
    4. 但是为了性能,加锁的范围需要控制好,比如我们不需要给整个商场加锁,试衣间加锁就可以了

    为什么同步代码块的锁对象可以是任意的同一个对象,但是同步方法使用的是this呢?
    因为同步代码块可以保证同一个时刻只有一个线程进入
    但同步方法不可以保证同一时刻只能有一个线程调用,所以使用本类代指对象this来确保同步

    同步与异步

    1.4.1练习-改造售票案例

    创建包: cn.tedu.tickets
    创建类:TestRunnableV2.java

    package cn.tedu.tickets;
    
    /*本类用于改造多线程售票案例,解决数据安全问题*/
    public class TestRunnableV2 {
        public static void main(String[] args) {
            //5.创建目标业务类对象
            TicketR2 target = new TicketR2();
            //6.创建线程对象
            Thread t1 = new Thread(target);
            Thread t2 = new Thread(target);
            Thread t3 = new Thread(target);
            Thread t4 = new Thread(target);
            //7.以多线程的方式运行
            t1.start();
            t2.start();
            t3.start();
            t4.start();
        }
    }
    
    /*1.多线程中出现数据安全问题的原因:多线程程序+共享数据+多条语句操作共享数据*/
    /*2.同步锁:相当于给容易出现问题的代码加了一把锁,包裹了所有可能会出现数据安全问题的代码
     * 加锁之后,就有了同步(排队)的效果,但是加锁的话,需要考虑:
     * 锁的范围:不能太大,太大,干啥都得排队,也不能太小,太小,锁不住,还是会有安全隐患*/
    //1.创建自定义多线程类
    class TicketR2 implements Runnable {
        //3.定义成员变量,保存票数
        int tickets = 100;
        //创建锁对象
        Object o = new Object();
    
        //2.实现接口中未实现的方法,run()中放着的是我们的业务
        @Override
        public void run() {
            //4.通过循环结构完成业务
            while (true) {
                /*3.同步代码块:synchronized(锁对象){会出现安全隐患的所有代码}
                 * 同步代码块在同一时刻,同一资源只会被一个线程独享*/
                /*这种写法不对,相当于每个线程进来的时候都会new一个锁对象,线程间使用的并不是同一把锁*/
                //synchronized (new Object()){
                //修改同步代码块的锁对象为成员变量o,因为锁对象必须唯一
                synchronized (o) {//同步代码块解决的是重卖的问题
                    //如果票数>0就卖票
                    if (tickets > 0) {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //4.1打印当前正在售票的线程名以及票数-1
                        System.out.println(Thread.currentThread().getName() + "=" + tickets--);
                    }
                    //4.2退出死循环--没票的时候就结束
                    if (tickets <= 0) break;
                }
            }
        }
    }
    

    1.4.2 练习-改造售票案例

    创建包: cn.tedu.tickets
    创建类:TestThreadV2.java

    package cn.tedu.tickets;
    
    /*本类用于改造多线程售票案例,解决数据安全问题*/
    public class TestThreadV2 {
        public static void main(String[] args) {
            //5.创建多个线程对象并以多线程的方式运行
            TickectT2 t1 = new TickectT2();
            TickectT2 t2 = new TickectT2();
            TickectT2 t3 = new TickectT2();
            TickectT2 t4 = new TickectT2();
            t1.start();
            t2.start();
            t3.start();
            t4.start();
        }
    }
    
    //1.自定义多线程类
    class TickectT2 extends Thread {
        //3.新增成员变量用来保存票数
        static int tickets = 100;
        //static Object o = new Object();
    
        //2.添加重写的run()来完成业务
        @Override
        public void run() {
            //3.创建循环结构用来卖票
            while (true) {
                //Ctrl+Alt+L调整代码缩进
                //7.添加同步代码块,解决数据安全问题
                //synchronized (new Object()) {
                /*static的Object的对象o这种写法也可以*/
                //synchronized (o) {
                /*我们每通过class关键字创建一个类,就会在工作空间中生成一个唯一对应的类名.class字节码文件
                * 这个类名.class对应的对象我们称之为这个类的字节码对象
                * 字节码对象极其重要,是反射技术的基石,字节码对象中包含了当前类所有的关键信息
                * 所以,用这样一个唯一且明确的对象作为同步代码块的锁对象,再合适不过了*/
                synchronized (TickectT2.class) {/*比较标准的写法*/
                    if(tickets > 0){
                        //6.添加线程休眠,暴露问题
                        try {
                            Thread.sleep(10);//让线程休眠,增加线程状态切换的频率
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //4.1打印当前正在售票的线程名与票数-1
                        System.out.println(getName() + "=" + tickets--);
                    }
                    //4.2给程序设置一个出口,没有票的时候就停止卖票
                    if (tickets <= 0) break;
                }
            }
        }
    }
    

    注意:如果是继承的方式的话,锁对象最好用"类名.class",否则创建自定义线程类多个对象时,无法保证锁的唯一

    1.5 之前遇到过的同步例子

    StringBuffer JDK1.0
    加了synchronized ,性能相对较低(要排队,同步),安全性高
    StringBuilder JDK1.5
    去掉了synchronized,性能更高(不排队,异步),存在安全隐患
    其他同步异步的例子

    快速查找某个类的快捷键:Ctrl+Shift+T

    2 线程创建的其他方式

    2.1 ExecutorService/Executors

    ExecutorService:用来存储线程的池子,把新建线程/启动线程/关闭线程的任务都交给池来管理

    • execute(Runnable任务对象) 把任务丢到线程池

    Executors 辅助创建线程池的工具类

    • newFixedThreadPool(int nThreads) 最多n个线程的线程池
    • newCachedThreadPool() 足够多的线程,使任务不必等待
    • newSingleThreadExecutor() 只有一个线程的线程池

    2.2 练习:线程的其他创建方式

    创建包: cn.tedu.tickets
    创建类: TestThreadPool.java

    package cn.tedu.tickets;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /*本类用于测试线程池*/
    public class TestThreadPool {
        public static void main(String[] args) {
            //5.创建接口实现类TicketR3类的对象作为目标业务对象
            TicketR3 target = new TicketR3();
            /*Executors是用来辅助创建线程池的工具类对象
            * 常用方法是newFixedThreadPool(int)这个方法可以创建指定数目的线程池对象
            * 创建出来的线程池对象是ExecutorService:用来存储线程的池子,负责:新建/启动/关闭线程*/
            //6.使用Executors工具创建一个最多有5个线程的线程池对象ExecutorService池对象
            ExecutorService pool = Executors.newFixedThreadPool(5);
            for (int i = 0; i < 5; i++) {
                /*execute()让线程池中的线程来执行业务,每次调用都会将一个线程加入到就绪队列*/
                pool.execute(target);/*本方法的参数就是你要执行的业务,也就是目标业务类对象*/
            }
        }
    }
    //同步锁问题解决方案笔记:1.4.1从26行复制到58行,TicketR2改成TicketR3
    //1.创建自定义多线程类
    class TicketR3 implements Runnable {
        //3.定义成员变量,保存票数
        int tickets = 100;
        //创建锁对象
        Object o = new Object();
    
        //2.实现接口中未实现的方法,run()中放着的是我们的业务
        @Override
        public void run() {
            //4.通过循环结构完成业务
            while (true) {
                /*3.同步代码块:synchronized(锁对象){会出现安全隐患的所有代码}
                 * 同步代码块在同一时刻,同一资源只会被一个线程独享*/
                /*这种写法不对,相当于每个线程进来的时候都会new一个锁对象,线程间使用的并不是同一把锁*/
                //synchronized (new Object()){
                //修改同步代码块的锁对象为成员变量o,因为锁对象必须唯一
                synchronized (o) {//同步代码块解决的是重卖的问题
                    //如果票数>0就卖票
                    if (tickets > 0) {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //4.1打印当前正在售票的线程名以及票数-1
                        System.out.println(Thread.currentThread().getName() + "=" + tickets--);
                    }
                    //4.2退出死循环--没票的时候就结束
                    if (tickets <= 0) break;
                }
            }
        }
    }
    

    3 拓展:线程锁

    3.1 悲观锁和乐观锁

    悲观锁:像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态.
    悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。

    乐观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态.
    乐观锁认为竞争不总是会发生,因此它不需要持有锁,将”比较-替换”这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

    3.2 两种常见的锁

    synchronized 互斥锁(悲观锁,有罪假设)

    采用synchronized修饰符实现的同步机制叫做互斥锁机制,它所获得的锁叫做互斥锁。
    每个对象都有一个monitor(锁标记),当线程拥有这个锁标记时才能访问这个资源,没有锁标记便进入锁池。任何一个对象系统都会为其创建一个互斥锁,这个锁是为了分配给线程的,防止打断原子操作。每个对象的锁只能分配给一个线程,因此叫做互斥锁。

    ReentrantLock 排他锁(悲观锁,有罪假设)

    ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,实际上独占锁是一种相对比较保守的锁策略,在这种情况下任何“读/读”、“读/写”、“写/写”操作都不能同时发生,这在一定程度上降低了吞吐量。然而读操作之间不存在数据竞争问题,如果”读/读”操作能够以共享锁的方式进行,那会进一步提升性能。

    ReentrantReadWriteLock 读写锁(乐观锁,无罪假设)

    因此引入了ReentrantReadWriteLock,顾名思义,ReentrantReadWriteLock是Reentrant(可重入)Read(读)Write(写)Lock(锁),我们下面称它为读写锁。
    读写锁内部又分为读锁和写锁,读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。
    读锁和写锁分离从而提升程序性能,读写锁主要应用于读多写少的场景。

    3.3 尝试用读写锁改造售票案例

    package cn.tedu.thread;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    /**
     * 本类用于改造售票案例,使用可重入读写锁
     * ReentrantReadWriteLock
     * */
    public class TestSaleTicketsV3 {
    	public static void main(String[] args) {
    		SaleTicketsV3 target = new SaleTicketsV3();
    		Thread t1 = new Thread(target);
    		Thread t2 = new Thread(target);
    		Thread t3 = new Thread(target);
    		Thread t4 = new Thread(target);
    		t1.start();
    		t2.start();
    		t3.start();
    		t4.start();
    	}
    }
    class SaleTicketsV3 implements Runnable{
    	static int tickets = 100;
    	//1.定义可重入读写锁对象,静态保证全局唯一
    	static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
    	@Override
    	public void run() {
    		while(true) {
    			//2.在操作共享资源前上锁
    			lock.writeLock().lock();
    			try {
    				if(tickets > 0) {
    					try {
    						Thread.sleep(10);
    					} catch (InterruptedException e) {
    						e.printStackTrace();
    					}
    					System.out.println(Thread.currentThread().getName() + "=" + tickets--);
    				}
    				if(tickets <= 0) break;
    			} catch (Exception e) {
    				e.printStackTrace();
    			}finally {
    				//3.finally{}中释放锁,注意一定要手动释放,防止死锁,否则就独占报错了
    				lock.writeLock().unlock();
    			}
    		}
    	}
    } 
    

    3.4 两种方式的区别

    需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁会自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!
    与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)从理论上讲,与互斥锁定相比,使用读-写锁允许的并发性增强将带来更大的性能提高。

    恭喜你,线程与线程锁的学习可以暂时告一段落啦,接着我们可以继续学习别的内容

    下一节 设计模式 点这里

    展开全文
  • java多线程编程中,存在很多线程安全问题,至于什么是线程安全呢,给出一个通俗易懂的概念还是蛮难的,如同《java并发编程实践》中所说:写道给线程安全下定义比较困难。存在很多种定义,如:“一个类在可以被多个...

    java多线程编程中,存在很多线程安全问题,至于什么是线程安全呢,给出一个通俗易懂的概念还是蛮难的,如同《java并发编程实践》中所说:

    写道

    给线程安全下定义比较困难。存在很多种定义,如:“一个类在可以被多个线程安全调用时就是线程安全的”。

    此处不赘述了,首先给出静态变量、实例变量、局部变量在多线程环境下的线程安全问题结论,然后用示例验证,请大家擦亮眼睛,有错必究,否则误人子弟!

    静态变量:线程非安全。

    静态变量即类变量,位于方法区,为所有对象共享,共享一份内存,一旦静态变量被修改,其他对象均对修改可见,故线程非安全。

    实例变量:单例模式(只有一个对象实例存在)线程非安全,非单例线程安全。

    实例变量为对象实例私有,在虚拟机的堆中分配,若在系统中只存在一个此对象的实例,在多线程环境下,“犹如”静态变量那样,被某个线程修改后,其他线程对修改均可见,故线程非安全;如果每个线程执行都是在不同的对象中,那对象与对象之间的实例变量的修改将互不影响,故线程安全。

    局部变量:线程安全。

    每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。

    静态变量线程安全问题模拟:

    ----------------------------------------------------------------------------------

    Java代码  a75233fd81882e44118bc178a9736643.png

    /**

    * 线程安全问题模拟执行

    *  ------------------------------

    *       线程1      |    线程2

    *  ------------------------------

    *   static_i = 4;  | 等待

    *   static_i = 10; | 等待

    *    等待          | static_i = 4;

    *   static_i * 2;  | 等待

    *  -----------------------------

    * */

    public class Test implements Runnable

    {

    private static int static_i;//静态变量

    public void run()

    {

    static_i = 4;

    System.out.println("[" + Thread.currentThread().getName()

    + "]获取static_i 的值:" + static_i);

    static_i = 10;

    System.out.println("[" + Thread.currentThread().getName()

    + "]获取static_i*3的值:" + static_i * 2);

    }

    public static void main(String[] args)

    {

    Test t = new Test();

    //启动尽量多的线程才能很容易的模拟问题

    for (int i = 0; i 

    {

    //t可以换成new Test(),保证每个线程都在不同的对象中执行,结果一样

    new Thread(t, "线程" + i).start();

    }

    }

    }

    根据代码注释中模拟的情况,当线程1执行了static_i = 4;  static_i = 10; 后,线程2获得执行权,static_i = 4; 然后当线程1获得执行权执行static_i * 2;  必然输出结果4*2=8,按照这个模拟,我们可能会在控制台看到输出为8的结果。

    写道

    [线程27]获取static_i 的值:4  [线程22]获取static_i*2的值:20  [线程28]获取static_i 的值:4  [线程23]获取static_i*2的值:8  [线程29]获取static_i 的值:4  [线程30]获取static_i 的值:4  [线程31]获取static_i 的值:4  [线程24]获取static_i*2的值:20

    看红色标注的部分,确实出现了我们的预想,同样也证明了我们的结论。

    实例变量线程安全问题模拟:

    ----------------------------------------------------------------------------------

    Java代码  a75233fd81882e44118bc178a9736643.png

    public class Test implements Runnable

    {

    private int instance_i;//实例变量

    public void run()

    {

    instance_i = 4;

    System.out.println("[" + Thread.currentThread().getName()

    + "]获取instance_i 的值:" + instance_i);

    instance_i = 10;

    System.out.println("[" + Thread.currentThread().getName()

    + "]获取instance_i*3的值:" + instance_i * 2);

    }

    public static void main(String[] args)

    {

    Test t = new Test();

    //启动尽量多的线程才能很容易的模拟问题

    for (int i = 0; i 

    {

    //每个线程对在对象t中运行,模拟单例情况

    new Thread(t, "线程" + i).start();

    }

    }

    }

    按照本文开头的分析,犹如静态变量那样,每个线程都在修改同一个对象的实例变量,肯定会出现线程安全问题。

    写道

    [线程66]获取instance_i 的值:10  [线程33]获取instance_i*2的值:20  [线程67]获取instance_i 的值:4  [线程34]获取instance_i*2的值:8  [线程35]获取instance_i*2的值:20  [线程68]获取instance_i 的值:4

    看红色字体,可知单例情况下,实例变量线程非安全。

    将new Thread(t, "线程" + i).start();改成new Thread(new Test(), "线程" + i).start();模拟非单例情况,会发现不存在线程安全问题。

    局部变量线程安全问题模拟:

    ----------------------------------------------------------------------------------

    Java代码  a75233fd81882e44118bc178a9736643.png

    public class Test implements Runnable

    {

    public void run()

    {

    int local_i = 4;

    System.out.println("[" + Thread.currentThread().getName()

    + "]获取local_i 的值:" + local_i);

    local_i = 10;

    System.out.println("[" + Thread.currentThread().getName()

    + "]获取local_i*2的值:" + local_i * 2);

    }

    public static void main(String[] args)

    {

    Test t = new Test();

    //启动尽量多的线程才能很容易的模拟问题

    for (int i = 0; i 

    {

    //每个线程对在对象t中运行,模拟单例情况

    new Thread(t, "线程" + i).start();

    }

    }

    }

    控制台没有出现异常数据。

    ---------------------------------------------------------------

    以上只是通过简单的实例来展示静态变量、实例变量、局部变量等的线程安全问题,

    并未进行底层的分析,下一篇将对线程问题的底层进行剖析。

    静态方法是线程安全的

    先看一个类

    public class  Test{

    public static  String hello(String str){

    Stringtmp="";

    tmp  =  tmp+str;

    return tmp;

    }

    }

    hello方法会不会有多线程安全问题呢?没有!!

    静态方法如果没有使用静态变量,则没有线程安全问题。

    为什么呢?因为静态方法内声明的变量,每个线程调用时,都会新创建一份,而不会共用一个存储单元。比如这里的tmp,每个线程都会创建自己的一份,因此不会有线程安全问题

    注意,静态变量,由于是在类加载时占用一个存储区,每个线程都是共用这个存储区的,所以如果在静态方法里使用了静态变量,这就会有线程安全问题!

    总结:只要方法内含有静态变量,就是非线程安全的

    展开全文
  • Java多线程安全问题

    2021-02-28 14:02:05
    上一次我们说到在卖票问题中如果不将总票数设置为static静态变量,就会出现错票,即同样一张票会出售多次。在今天的问题中,我们继续通过卖票问题来进行研究。我们在每一个线程进行判断条件后让线程睡眠一段时间(让...

    上一次我们说到在卖票问题中如果不将总票数设置为static静态变量,就会出现错票,

    即同样一张票会出售多次。

    在今天的问题中,我们继续通过卖票问题来进行研究。

    我们在每一个线程进行判断条件后让线程睡眠一段时间(让判断条件与数据操作之间相隔一段时间),看看会有什么效果?

    48b08b6a11637fbe955c427ff39151c3.png

    运行结果:

    ba62a4c22218aafa21008c922b147a03.png

    通过运行结果我们可以发现有出售第0张票和第-1张票的情况 ,这是为什么呢?

    是因为当有多个线程对一个共享数据进行操作时,可能会出现多线程的安全问题

    这也是我们今天研究的主题。

    当遇到这样的多线程安全问题时,我们可以通过synchronized关键字将它解决。

    synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

    1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;

    2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

    3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

    4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

    我们用synchronized关键字修饰一个代码块,将if判断语句“包”起来--->放入代码块中,再次运行

    67e83663fa0e53d8423c349620183f30.png

    结果如下:

    2574c0e0a399f2641877ad8e17a9ca6e.png

    我们可以看出,没有再发生出售第0,-1张票的情况。

    这是因为我们使用了synchronized关键字,使得一个线程在调用此同步锁的时候,将对象锁锁住(拥有对象锁)

    注意,这里锁的是作为对象锁的对象,并不是代码块,切记切记......

    线程拥有对象锁即只有该线程在执行完代码块中的内容后,将对象锁释放,其它线程才能拥有此对象锁,执行代码快中的内容,

    若该线程没有将对象锁释放,则其它线程便只能等待,等到该线程执行完代码块中内容后将锁释放,然后其它线程拥有此对象锁,进而执行代码快中的内容。

    若没有synchronized关键字,则假设此时总票数ticket为1,A线程此时获得CPU执行权,通过判断ticket=1 > 0,进入if语句,然后睡眠。(此时ticket没有减1,仍为1)

    A线程进入睡眠后,B线程获得CPU执行权,通过判断ticket=1 > 0,进入if语句,然后睡眠。

    A线程苏醒后,进行ticket--操作,此时ticke输出t为1,之后ticket=0,通过判断ticket=0 > 0失败,不执行if语句,进而退出while循环,执行完毕。

    B线程苏醒后,此时ticket为不为1,为0,则ticket进行--操作后,输出为0,之后ticket=-1,通过判断ticket= -1 > 0失败,不执行if语句,进而退出while循环,执行完毕。

    若程序主线程中还定义了第三个线程,则如线程B,输出为-1 ----->这就是出现出售第0,-1张票情况的原因。

    若加了synchronized代码块,则,A线程在执行完代码块中的内容后(进行ticket-1操作后),

    其它线程才能根据ticket进行判断,若不满足条件,就会结束循环,执行完毕。就不会发生上述出售第0,-1张票的情况

    从而解决多个线程对一个共享数据进行操作时出现的安全问题......

    接下来我们使用两个线程分别调用同步代码块和同步方法

    811585b8872c6afba1ef5b4d36c6712e.png

    4e0757ce962feb20c38ea416319fb7b0.png

    68b05150b8733edd5636ec707eb4811c.png

    运行结果仍会有出售第0张票的情况,,小伙伴们可以自己试一下。

    这是因为两个线程的对象锁是不同的,A线程的对像锁是obj,而B线程的对像锁是this,

    (这里需要注意的是,B线程调用了同步方法【同步方法的对像锁是this】

    而【静态同步方法的对象锁是当前类的字节码文件对象】,定义方法--->【类.class | 对象.getClass()】)

    即A线程虽然将它的对象锁(obj)锁住,但并不影响B线程拥有它自己的对像锁(this),

    它们的对像锁是不同的,结果很明显--->这样并不能解决多线程安全问题

    当我们将同步代码块的对像锁设置为this时,两个线程的对像锁相同,便能解决多线程安全问题

    使用静态同步方法与同步方法类似,将同步代码块的对像锁设置为  类.class  或者  对象.getClass(),也能解决多线程安全问题。

    最后再说两个相关知识点:

    同步方法与普通方法可以同时调用

    synchronized获得的锁是可重入的

    一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁.

    【具体理解为:在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时,是可以再次得到该对象的锁的。

    也就是说在一个synchronized方法或块的内部调用本类的其他synchronized方法或块时,是永远可以得到锁的。】

    这里需要注意的是,子类的同步方法中也可以调用父类的同步方法(通过super关键字)。

    展开全文
  • 线程安全问题和解决方案什么是线程安全解决方案:线程同步方式一、同步代码块方式二、同步方法方式三:显式Lock锁 什么是线程安全 线程安全:如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次...
  • 众所周知,多线程会造成线程安全问题,那么多线程为什么会导致线程安全问题呢? 一:首先了解jvm内存的运行时数据区 1.堆区:存储对象实例(和实例变量),数组等 2.java虚拟机栈(方法·栈),存放方法声明,...
  • LinkedList线程安全问题

    2021-02-12 22:51:40
    Java中LinkedList是线程安全的,那么如果在多线程程序中有多个线程访问LinkedList的话会出现什么问题呢?抛出ConcurrentModificationExceptionJDK代码里,ListItr的add(), next(), previous(), remove(), set()...
  • 常用的集合类型如ArrayList,HashMap,HashSet等,在并发环境下修改操作都是线程安全的,会抛出java.util.ConcurrentModificationException异常,这节主要记录如何在并发环境下安全地修改集合数据。List举个...
  • Java多线程之线程安全问题1、线程安全1.1、什么是线程安全?1.2、什么情况下会出现线程安全问题,怎么避免?1.3、一共有哪几类线程安全问题?1.3.1、运行结果错误1.3.2、活跃性问题1.3.3、对象发布和初始化的时候的...
  • Java并发编程-集合类的线程安全问题

    千次阅读 多人点赞 2021-01-15 16:59:33
    1.List集合的线程安全问题 1.ArrayList线程安全问题。 ArrayList是线程安全的吗,我们不妨运行以下的程序 public static void main(String[] args) { // TODO Auto-generated method stub List<String> ...
  • 一、线程安全问题产生的原因线程安全问题都是由全局变量及静态变量引起的二、线程安全问题SimpleDateFormate sdf = new SimpleDateFormat();使用sdf.parse(dateStr);sdf.format(date);在sdf内有一个对Caleadar对象的...
  • 因为静态的,那么这个静态内部类是所有的外部类实例共用一个呢(这样会有线程安全问题),还是每个实例有自己对应的静态内部类实例?(这样不会有线程安全问题)根据测试结果,静态内部成员类,没有出现线程安全问题....
  • parallelStream线程安全问题的处理 1.1、实例(传统方法) package com.asia.tip; import java.util.ArrayList; import java.util.List; import org.junit.Test; import org.springframework.boot.test.context....
  • 简单讨论一下在一个类中使用静态字段(static field)和静态方法(static method)是否会有线程安全问题。我们在知道, 静态字段(static field)和静态方法(static method)的调用是通过类来调用。静态方法不对特...
  • 解决线程安全问题的几种方式 解决线程安全问题之同步代码块 解决线程安全问题之同步方法@静态同步方法 解决线程安全问题之Lock锁
  • 在多个线程同时操作改集合对象时,会出现哪些问题呢?在传统的集合包内的集合类到底为什么线程安全呢?在新的JUC包类又有什么可以替代呢?介绍①为什么ArrayList 是线性不安全的?②替代措施及解决方案?ArrayList ...
  • HashMap的线程不安全主要体现在下面两...常被问到的HashMap和Hashtable的区别1、线程安全两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全。Hashtable的实现方法里面都添加了synchronized关键字来确...
  • 多线程调用static方法线程安全问题

    千次阅读 2021-11-10 15:44:42
    最近在工作中遇到了线程安全问题,是在一个方法中调用了静态方法解析Date的字符串。 因为 SimpleDateFormat这个类是线程不安全的,所以不能在静态方法中定义全局的成员变量。 @Test void contextLoads() { ...
  • 因为这些信息不仅会被一个线程访问到,还有可能被多个线程同时访问,那么就有可能在并发读写的情况下发生线程安全问题。比如多线程同时 i++ 的例子: /** *描述:共享的变量或资源带来的线程安全问题 */ ...
  • 线程情况下,mysql_init会设置线程的私有数据,如果不注意,则会在mysql_real_connect时出现段错误。错误如下:void* func(void* arg){MYSQL* mysql = (MYSQL *)arg;mysql_real_connect(mysql, “127.0.0.1″, ...
  • // 是否安全? Map<String,Object> map = new HashMap<>(); // 是否安全? String S1 = "..."; // 是否安全? final String S2 = "..."; // 是否安全? Date D1 = new Date(); // 是否安全
  • Redis--线程安全问题

    2021-02-28 02:27:16
    Redis线程安全问题 我们之前说Redis是一个高并发高性能的内存数据库 那么Redis是否存在线程安全问题呢? 答案是不存在! 因为Redis6.0之前都是单线程的!但是利用的IO多路复用技术 + 底层是C语言实现的, 所以数据还是...
  • 这个问题的答案是静态变量和全局变量都可能引起线程安全问题。这两种变量引起线程安全问题的原因和区别如下:1、静态变量静态变量即静态成员变量。只要有修改变量值的操作,无论是在单例或者非单例都是线程不安全的...
  • 但其实,Spring并没有保证这些对象的线程安全,需要由开发者自己编写解决线程安全问题的代码。 Spring对每个bean提供了一个scope属性来表示该bean的作用域。它是bean的生命周期。例如,一个scope为singleton的bean...
  • 1、在@Controller/@Service等容器中,默认情况下,scope值是单例-singleton的,也是...4、一定要定义变量的话,用ThreadLocal来封装,这个是线程安全的 5.如果单例Bean,是一个无状态Bean,也就是线程中的操作不会对Be
  • 简单讨论一下在一个类中使用静态字段(static field)和静态方法(static method)是否会有线程安全问题。我们在知道, 静态字段(static field)和静态方法(static method)的调用是通过类来调用。静态方法不对特...
  • public class App { public static void main(String[]... } } 并行Stream因为考虑效率问题,所以没有在意线程安全 我们可以使用线程安全的数据流处理使数据同步 原文:https://www.cnblogs.com/freeht/p/13080566.html
  • 实例变量,静态变量,局部变量谁不会出现线程安全问题? 1.从JVM的角度,在栈中的变量不会出现线程安全问题,为什么?因为一个线程一个栈,栈不共享,不共享就不会出现线程安全问题,那么这三个只有局部变量不会...
  • 产生线程安全问题的原因:静态变量即类变量,只初始化一次,位于方法区,为所有对象共享,共享一份内存,一旦静态变量被修改,其他对象均对修改可见,故线程非安全。 静态变量多线程操作示例: 根据上图代码可知,...
  • 如果考虑到多线程访问,当一个线程正在迭代某个集合,而另一个线程修改了集合的内容时, 设计方向: 1. 直接抛异常,ConcurrentModificationException; 2. 可正常迭代: a. 不能保证数据一致性;如...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 973,129
精华内容 389,251
关键字:

线程安全问题