@TOC
一:什么是线程不安全??
先看一段代码:
public class Demo1 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2=new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(" count = "+count);}
}
上面的代码可能我们以为最终的count=100000;
但最终小于100000,这就是线程安全在搞鬼.
因为线程是随机调度 的,抢占式执行,就会使代码执行结果不可控,结果也就是随机的.
我们往往希望结果使我们所预期的,而这种不可控,随机的执行结果就称为bug.
多线程代码,引起了bug,这样的问题就是"线程安全问题",存在线程安全问题的代码,就称为"线程不安全".
二:为什么代码执行结果不是100000
count++操作其实是三个指令:
(1)把内存中的count中的数值,读到CPU寄存器中.(load)
(2)把寄存器中的值+1,(add).
(3)把寄存器上述计算后的值,写回到内存的count里.(save)
但因为不止一个线程,某个线程在执行指令的过程中,当它执行到任何一个指令的时候,由于随机调度,抢占式执行,就有可能被其他线程把它的CPU给抢占走.(操作系统把前一个线程调度走,后一个线程执行).
下面列举两个线程并发执行的时候,可能的执行指令的顺序,(不同的执行顺序,得到的结果可能存在差异)
我们知道count数据是存在内存中的,而同一个进程,不同的线程是共用一块内存资源的,也就是t1,t2线程操作的是同一块内存地址.
以下图为例:
1: t1线程 load操作拿到count的值,拿到了0,放到了A寄存器中;
2:t2线程 load 操作拿到count的值,拿到了0,放到了B寄存器中;
3:t1线程执行add操作,将A寄存器中的值+1,
4:t2线程执行add操作,将B寄存器中的值+1,
5:t1线程执行save 操作,将A寄存器中的值写回到内存中,内存中的值由0变为1.
6:t2线程执行save 操作,将B寄存器中的值写回到内存中,内存中的值由1变为1.
三:出现线程不安全的原因:
1:线程在系统中是随机调度,抢占式执行的,
2:当前代码中,多个线程同时修改同一个变量.
如果只有一个线程,那么修改操作没问题.
如果多个线程是读取操作,那么也没事.
如果多个线程修改不同的变量,也没事.
3:线程针对变量的修改操作不是"原子"的,而是三条指令.
但有的修改操作,是原子的,比如直接对变量进行赋值操作(CPU中就一个move指令).
4:内存可见性问题,引起的线程不安全
5:指令重排序,引起的线程不安全
四:解决线程不安全问题
线程不安全原因1:我们不能修改,因为硬件方面就是这样设计的(内核就是这样设计的 ).
原因2:不能修改,因为此时场景就是多线修改同一个变量.
原因3:可以通过"加锁"操作,将多个指令打包成一个整体.
通过"加锁",就达到了"互斥"的效果
互斥:t1线程在执行的时候,t2线程不能执行(阻塞等待),t2线程在执行的时候,t1线程不能执行(阻塞等待),互斥也称为锁竞争,锁冲突.
1:锁的操作:
(1)加锁:t1加上锁之后,t2也尝试加锁,就会阻塞等待(都是系统内核控制)(在Java中可以看到BLOCKED状态)
(2)解锁:直到t1解锁了之后,t2才有可能拿到锁(加锁成功).
2:编写代码
首先创建一个对象,使用这个对象作为锁:
在Java中可以使用任何对象作为加锁对象.
创建锁对象的意义:
锁对象的用途:有且只有一个,那就是用来区分,多个线程是否针对同一个对象(count)加锁,如果是,就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待;如果不是,就不会出现锁竞争,也就不会阻塞等待.
通过设定不同的锁对象,来确定竞争关系.
public class Demo2 {public static int count=0;public static void main(String[] args) throws InterruptedException {//首先创建一个对象,使用这个对象作为锁Object locker = new Object();Thread t1=new Thread(()->{for (int i = 0; i < 5000; i++) {synchronized (locker){count++;}}});Thread t2=new Thread(()->{for (int i = 0; i < 5000; i++) {synchronized (locker){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " +count);}
}
2.1 synchronized (){}
()里面写"锁对象"
{}:当进入代码块,就相当于对锁对象进行了加锁操作.
当出了代码块,就相当于对锁对象进行了解锁操作.
public class Demo2 {public static int count=0;public static void main(String[] args) throws InterruptedException {//首先创建一个对象,使用这个对象作为锁Object locker = new Object();Thread t1=new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (locker){count++;}}});Thread t2=new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (locker){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " +count);}
}
上述代码就相当于两个线程,针对同一个锁对象加锁,就会产生互斥.
t1,t2做的事情就是:判断循环条件,加锁,load ,add, save ,解锁,i++;
假设:由于是线程是并发执行的,这里假设t1线程先执行到了加锁操作,并且t1还没解锁,那么t2线程就不能获得锁对象,就不能进行加锁操作,
假设t1刚解锁,t2 就加锁了,但t1线程并不是什么代码也不执行,而是继续执行i++,循环判断条件,当t1执行到了synchronized,发现不能获得锁对象,那么t1线程只好阻塞等待了.
因此:在t1,t2两个线程中,每次count++是存在锁竞争的,会变成"串行"执行,但是执行for 循环中的条件以及i++仍然是并发执行的.
public class Demo2 {public static int count=0;public static void main(String[] args) throws InterruptedException {//首先创建一个对象,使用这个对象作为锁Object locker = new Object();Object locker2=new Object();Thread t1=new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (locker){count++;}}});Thread t2=new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (locker2){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " +count);}
}
上述代码就是两个线程针对不同对象加锁,就不会产生互斥,也就不会发生阻塞等待现象了.
操作系统中的加锁,解锁功能,核心还是CPU提供的指令(硬件提供了这样的能力,软件上才有对应的功能)
3:加锁的变种写法:
3.1:创建一个自定义类,并把这个类的引用作为加锁对象
class Count {public int count;public void add(){count++;}public int get(){return count;}
}
public class Demo1 {public static void main(String[] args) throws InterruptedException {Count count=new Count();Thread t1=new Thread(()->{for (int i = 0; i <50000 ; i++) {synchronized (count){count.add();}}});Thread t2=new Thread(()->{for (int i = 0; i <50000 ; i++) {synchronized (count){count.add();}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count= " +count.get());}
}
3.2: synchronized (this){}
class Count1 {public int count;public void add(){synchronized (this){count++;}}public int get(){return count;}}
public class Demo2 {public static void main(String[] args) throws InterruptedException {Count1 count1=new Count1();Thread t1=new Thread(()->{for (int i = 0; i <50000 ; i++) {count1.add();}});Thread t2=new Thread(()->{for (int i = 0; i <50000 ; i++) {count1.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = "+count1.get());}
}
3.3: synchronized public void add(){}
class Count1 {public int count;synchronized public void add(){count++;}public int get(){return count;}}
public class Demo2 {public static void main(String[] args) throws InterruptedException {Count1 count1=new Count1();Thread t1=new Thread(()->{for (int i = 0; i <50000 ; i++) {count1.add();}});Thread t2=new Thread(()->{for (int i = 0; i <50000 ; i++) {count1.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = "+count1.get());}
}
3.4:针对类对象加锁
class Count1 {public static int count;static void func(){synchronized (Count1.class){count++;}}public int get(){return count;}public class Demo2 {}public static void main(String[] args) throws InterruptedException {Count1 count1=new Count1();Count1 count2=new Count1();Thread t1=new Thread(()->{for (int i = 0; i <50000 ; i++) {count1.func();}});Thread t2=new Thread(()->{for (int i = 0; i <50000 ; i++) {count2.func();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = "+count1.get());}
}