一、什么是线程安全
想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的
🌰栗子
package Thread; public class demo77 { private static int count; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { count++; } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { count++; } }); t1.start(); // 两个线程在创建好了后,线程所对应的PCB加入到系统链表,参与系统调度 t2.start(); // 让主线程main等t1、t2执行完了再接着往下走 t1.join(); t2.join(); System.out.println(count); } }
想这样,在多线程情况下,程序的运行结果不符合我们的预期,这被称为线程不安全
二、造成线程不安全的原因
根本原因:操作系统的随机调度执行,抢占式执行
还有:我们可以看到我们的count是一个全局变量,我们的线程t1、线程t2对count变量同时都进行了修改——++操作(为什么说是同时呢,因为我们的t1线程、t2线程在创建完了后就参与到系统调度,由系统随机分配线程的执行,可能是t2线程先执行10个指令然后t1再执行10个指令,相当于是同时)
那么我们就改一下代码让t1、t2分批次对count修改不就行了吗?
package Thread; public class demo77 { private static int count; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { count++; } }); Thread t2 = new Thread(() -> { try { t1.join();// 等t1线程执行完了,t2线程再执行 } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 0; i < 5000; i++) { count++; } }); t1.start(); // 两个线程在创建好了后,线程所对应的PCB加入到系统链表,参与系统调度 t2.start(); t2.join(); // 等t2线程执行完了,主线程main再接着执行,执行顺序:t1->t2->打印count System.out.println(count); } }
大家有没有想过为什么多个线程同时执行count++的时候就会出现BUG呢?
这是因为我们多个线程同时对同一变量修改的所造成的BUG往往和我们操作的原子性有关!!!这时候的操作往往不是一个整体,多个线程并发执行这些操作就可能出现一些问题
如果我们在变量修改过程中,操作是原子的——只是对应一个机器指令,那么即使是多个线程同时对同一个变量修改也不一定会造成BUG,但也可能造成BUG——要看具体的业务场景
总之我们要避免多个线程同时对同一个变量来操作
🍑 对原子性在多线程并发执行中出现问题的分析
🔔🔔注意:
当我们执行t1.start()、t2.start()后,t1线程和t2线程就在操作系统内核中创建出来了,t1、t2线程就参与到了系统调度当中
而调度是随机的——他可能先让t1执行几个指令,然后t2再执行几个指令、最后再把CPU的控制权交给t1。
于是因为系统的调度是随机的(这是罪魁祸首,但我们无法改变),当我们多个线程同时执行一些不是整体的操作的时候(++或--)由于并发就会产生一些问题:
🌰栗子一
🌰栗子二
🌰栗子二
为什么会产生上面的BUG呢?
就是因为我们的++操作不是一个整体,是一个由多个指令所组成的操作
解决方案:也是加锁:“synchronized”,意味着把这三条指令打包成了一组指令,然后把这一组指令看出成一条指令了,类似于数学里的“整体代换”思想。
首先我们要明白加锁操作都是针对某一个对象来进行的(加锁本质就是给对象头里设置个标记),加锁有以下几种形式
形式一、
形式二、
package Thread; class Counter { public static int count; // public synchronized void increase() { // ++count; 这两种写法视为是等价的 // } public void increase() { synchronized (this) { // 这里this可以是任意对象,this可以有多个Counter counter1 = new Counter(), Counter counter2 = new Counter(); ++count; } } } public class demo777 { public static void main(String[] args) throws InterruptedException { Counter counter1 = new Counter(); Counter counter2 = new Counter(); Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) { counter1.increase(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) { counter1.increase(); } // 多个线程去调用这个increase方法,其实就是针对这个Counter对象counter1来进行加锁 // 如果一个线程t1获取到了该对象counter1的锁,那么另一个线程t2就要等到counter1对应的锁开了后(t1线程执行完该锁里的内容——++操作)t2才能执行++操作 // 此时++操作相当于是成为了一个整体(相当于一个指令,当一个线程再执行这个加锁的整体的指令的时候,另一个线程只能阻塞等待) }); t1.start(); t2.start(); t1.join(); t2.join(); // 确保线程t1和线程t2都执行完了,main主线程再接着执行——输出count System.out.println(Counter.count); // 输出10000 } }
形式三、
当我们给不同的对象上锁后,如果用住房来比喻
不同的房间相当于是不同的对象,不同的线程相当于是不同的客人
如果房间1住了客人A,那么房间1就上了锁,客人B就需要等客人A不再住房间1(开了锁)然后客人B才能住房间1;或者客人B住其他的房间(其他的对象,没上锁的)
package Thread; // 测试线程竞争,对锁的竞争 public class demo7777 { public static Object object1 = new Object(); public static Object object2 = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { // 针对object1对象进行加锁,加锁操作是针对某一个对象来进行的 synchronized (object1) { System.out.println("t1线程start"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1线程finish"); } }); t1.start(); Thread t2 = new Thread(() -> { synchronized (object1) { // 针对object1对象来进行加锁操作 System.out.println("t2线程start"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2线程finish"); } }); t2.start(); } }
我们上面就是两个线程t1和t2同时对object1这个对象进行了加锁,然后t1与t2直接就产生了竞争。从上述代码的实现过程中我们也可以看到,等到t1线程执行完了后,t2线程才开始执行。
但如果是两个线程对不同的对象进行加锁,则没有竞争(就像两个客人(两个线程)住不同的房间(不同的对象)当然不会发生竞争。
package Thread; // 测试线程竞争,对锁的竞争 public class demo7777 { public static Object object1 = new Object(); public static Object object2 = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (object1) { // 针对object1对象来进行加锁,加锁操作是针对一个对象来进行的 System.out.println("t1线程start"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1线程finish"); } }); t1.start(); Thread t2 = new Thread(() -> { synchronized (object2) { // 针对object2对象进行加锁 System.out.println("t2线程start"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2线程finish"); } }); t2.start(); } }
对上面补充一下: