多进程是为了解决并发编程,更加充分利用多核CPU资源。但是进程的创建和销毁开销大,就引入了线程。一个进程包含至少一个线程,创建第一个线程的时候会分配好资源,后续在创建线程就直接共享前面的资源,节省了分配资源的开销。
在多线程环境下代码执行出现bug的情况被称为“线程不安全”。
可能的原因如下
String是不可变对象,因为把set系列方法不对外公布,这就让String变成线程安全的,因为多个线程无法修改同一个变量
咱们上面说了,解决线程不安全最常用的方法就是加锁,让操作变成原子的。加锁就类似于多个男孩子追同一个女孩子,如果男生A追到了,就会在朋友圈官宣类似于加锁,此时其他男孩子就要阻塞等待,直到这个男生分手(释放锁)。其他男生要阻塞等待是因为他们追的是同一个女生(锁发生了竞争)。当然如果是两个男生分别追两个女孩子,男生A和男生B不存在竞争,就不用阻塞等待(不存在锁竞争)。
以加锁count++为例
public class Test5 {
private static int count = 0;
public synchronized static void increase() {
count++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
上面的加锁操作会让并发变成串行,降低了执行效率。但是很多人有误区,以为变串行还有必要用多线程吗?上面的加锁操作只是让increse()变串行了,但是两个for循环还是并发执行,效率还是比单线程高的。
加锁过程:
加锁并不代表CPU一次性完全执行忘,中间也是会发生调度切换。但是即使t1切换走了,t2仍然是BLOCKED状态,无法在CPU上运行。这就类似于图书馆占座,人去上个厕所(调度走了)其他人想坐这个座位(有锁竞争,要阻塞等待)也没办法坐
public class Test5 {
private static int count = 0;
public synchronized static void increase() {
count++;
}
public static void increase2() {
count++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
increase2();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
t1线程调用increase()确实是加锁了,但是t2线程没调用increase2()没有加锁。只有一个线程加锁,没有锁竞争,也就不会有阻塞等待,也就不会让并发修改变成串行修改。就类似A追到女生(加锁了),但是B不讲武德,在守门员面前进球(不加锁,没阻塞等待)。
public class Test6 {
private static int count = 0;
static class Increase {
public void add() {
//代码块加锁
synchronized (this) {//谁调用add方法,谁就是this
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Increase increase = new Increase();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
increase.add();//this就是increase对象,针对increase对象加锁
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
increase.add();//针对increase对象加锁产生锁竞争
//如果这里是 increase2.add()那这里的this就是increase2对象,此时针对increase2加锁,两个线程针对不同对象加锁,不会产生锁竞争,锁竞争是为了其中一个线程阻塞等待,才能保证线程安全。
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
public class Test6 {
private static int count = 0;
static class Increase {
public Object locker = new Object();
public void add() {
//针对locker对象加锁,locker是Increase的一个普通变量,每个Increase实例都有自己的locker实例
synchronized (locker) {
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Increase increase = new Increase();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
increase.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
increase.add();//此时的increase对象是同一个,对应的increase中的locker就是同一个对象,此时两个线程针对同一个对象加锁,仍然会存在锁竞争,就会阻塞等待。
//如果这里是increase2对象,那么increase对象中有一个locker,increase2对象中有一个locker,两个locker不同,即两个线程针对两个对象加锁,不会发生锁竞争,线程不安全
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
public class Test7 {
public static int count = 0;
//静态内部类
static class Increase {
//静态成员
private static Object locker = new Object();
public void add() {
//对类对象加锁
synchronized (locker) {
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Increase increase1 = new Increase();
Increase increase2 = new Increase();
Thread t1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
increase1.add();
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
increase2.add();
//increase1和increase2虽然是两个不同的对象,但是这两个对象中的locker是类属性(唯一的),即对同一个locker加锁,产生锁竞争,会阻塞等待,线程安全
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
public class Test8 {
public static int count = 0;
static class Increase{
public void add() {
//针对类对象加锁
synchronized (Increase.class) {//反射
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Increase increase1 = new Increase();
Increase increase2 = new Increase();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
increase1.add();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
increase2.add();//虽然是两个不同的对象,但是对同一个类对象Increase.class加锁,存在锁竞争,会阻塞等待,线程安全
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
总结:任意对象都可以在synchronized里面作为锁对象,在多线程代码中,我们不关心锁对象是谁,只关心,两个线程是否锁同一个对象,锁同一个对象才有锁竞争,才会阻塞等待;锁不同对象就没有锁竞争
class Counter{
public static int count = 0;
}
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (Counter.count == 0) {
}
System.out.println("执行不到");
});
t1.start();
Thread t2 = new Thread(() -> {
System.out.println("请输入一个数");
Scanner scanner = new Scanner(System.in);
Counter.count = scanner.nextInt();
});
t2.start();
t1.join();
t2.join();
}
}
我们发现t2线程修改了count值,却没有输出“执行不到”,这主要怪编译器优化。
线程t1的代码中while (Counter.count == 0) 会频繁的进行load和cmp操作,其中load操作要不断的从内存中读取数据,而cmp只是在寄存器上不断进行比较,load消耗的时间长,比cmp慢了3-4个数量级。编译器就会帮你优化,既然你频繁的load,而且load结果还一样,那就只执行一次Load操作,后续进行cmp不用再重新读取内存了。在单线程环境下,这个优化是正确的,但是在多线程环境下一个线程把内存改了,但是另外一个线程感知不到,这就是内存可见性问题
解决方案:告诉编译器这个地方不要优化,即使用volatile修饰变量
他的全称是Java Memory Model,我们刚才说过volatile禁止了编译器优化,避免直接读取CPU寄存器中缓存的数据,而是每次都重新读取内存。Java设计时就是让程序员不关注硬件设备,为此Java自己搞了个JMM模型,即把CPU寄存器+缓存统称为工作内存,把物理内存称为主内存。
在JMM角度看volatile:正常程序执行的过程中,会把主内存的数据,先加载到工作内存中,再进行计算处理。编译器优化可能导致不是每次都读取主内存,而是直接读取工作内存的缓存数据,这就可能导致内存可见性问题。volatile起到的效果就是保证每次读取内存时都是真的从主内存中重新读取。
public Instance getInstance() {
if(instance == null) {
sychronized(this) {
if(instance == null) {
instance = new Instance();
}
}
}
return instance;
}
以new对象为例,new对象的可大致分为3个步骤:1. 申请内存,得到内存首地址;2. 调用构造方法,来初始化实例; 3. 把内存首地址赋值给instance引用。这个场景可能会导致”指令重排序“,在单线程角度下,2和3是可以调换的,执行效果一样。但是在多线程环境下如果按照1,3,2顺序执行,有可能t1线程执行了1和3之后,还没执行2之前,t2线程调用getInstance(),此时就会认为instance不为null,直接返回Instance,如果后续对Instance进行解引用就可能导致空指针异常。
为了避免指令重排序,我们可以使用volatile修饰变量,即使用volatile修饰instance