下面有两段代码:
public class test {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++;
}
});
t1.start();
t1.join(); //程序这样写,相当于线程顺序执行
t2.start();
t2.join();
System.out.println(count);
}
}
上面代码中每个线程先start(),再join(),这样就相当于线程顺序执行了。不会发生线程不安全。
public class test {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
显然,第二个代码的打印结果出出现了线程不安全。一个原因是因为count++这个操作不是原子的。
我们首先来看count++这个操作在CPU上是怎样执行的?
count++在CPU上执行大致分为三步:
那么当多个线程执行count++操作时,由于线程的调度顺序是随机的,就会如下图所示,假设执行顺序从上到下。
上述组合情况只列了四个,还是在两个线程的情况下,如果线程更多,那排列组合的情况也会更多。那么这种线程安全问题如何解决呢?为此我们引入加锁这种方式来解决。
解决办法: 加锁 synchronized
// 加锁 针对同一个变量进行修改时存在的线程问题的 例如下面的 count++
public class Demo10 {
private 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 < 10000; i++) {
synchronized (locker) { //加锁方式 ()中需要表示一个用来加锁的对象 synchronized 还可以修饰方法
count++;
}
}
});
Thread t2 = new Thread( () -> {
for (int i = 0; i < 10000; i++) {
synchronized (locker) { //加锁方式 ()中需要表示一个用来加锁的对象
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
可以看到,加锁之后,符合预期结果了。
产生线程安全问题的原因 (面试题)
synchronized修饰的两种方法
1.修饰静态方法,实则修饰类对象 synchronized (类名.class) { } 类对象在一个java进程中是唯一的。
public static int count = 0;
//修饰静态方法简化
synchronized public static void increase() { //修饰静态方法 实则修饰类对象 加锁
count++;
}
//修饰静态方法完整
public static void increase() {
synchronized (Counter.class) { //类对象
count++;
}
}
2.修饰非静态普通方法,实则修饰this synchronized (this) { }
public static int count = 0;
//修饰非静态方法简化 实则修饰this
synchronized public void increase() {
count++;
}
//修饰非静态方法完整
public void increase() {
synchronized (this) { //this
count++;
}
}
synchronized的特性
可重入锁 可重入锁是如何实现的?
死锁
public static void main(String[] args) {
//创建两个锁对象
Object locker1 = new Object();
Object locker2 = new Object();
//创建两个线程
Thread t1 = new Thread(() -> {
synchronized (locker1) {
try {
//此处的sleep很重要,确保t1和t2都拿到一把锁之后,再往后执行
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
synchronized (locker2) {
System.out.println(Thread.currentThread().getName());
}
}
},"t1");
Thread t2 = new Thread(() -> {
synchronized (locker2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
synchronized (locker1) {
System.out.println(Thread.currentThread().getName());
}
}
},"t2");
t1.start();
t2.start();
}
运行代码,并用jconsole观察:
不难发现,两个线程都处于BLOCKED阻塞状态,想要获取的锁都被对方拥有。
死锁的成因及如何解决??
成因有四个必要条件:
解决死锁的方法
只要破坏上述必要条件中的任意一条就行,但前两条本身就是锁的特性,破坏不了,故破坏后两条任意一条就行。
对于第三条:调整代码逻辑,避免"锁嵌套"逻辑。也得看实际需求,可能实际需求就是会有"锁嵌套"这种逻辑。所以第四条的破坏就尤为重要。
public class bbbbbbbb {
public static void main(String[] args) {
//创建两个锁对象
Object locker1 = new Object();
Object locker2 = new Object();
//创建两个线程
Thread t1 = new Thread(() -> {
synchronized (locker1) {
try {
//此处的sleep很重要,确保t1和t2都拿到一把锁之后,再往后执行
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//挪出来
synchronized (locker2) {
System.out.println("t1 加锁成功!");
}
},"t1");
Thread t2 = new Thread(() -> {
synchronized (locker2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (locker1) {
System.out.println("t2 加锁成功!");
}
},"t2");
t1.start();
t2.start();
}
}
对于第四条;可以约定加锁的顺序,就可以避免循环等待。即针对锁进行编号。比如约定在再多把锁的时候,先加编号小的锁,再加编号大的锁。规定所有线程都要遵守这个规则。
面试题: 你是否了解死锁,谈谈对死锁的理解???
volatile 关键字作用及使用
1).保证内存可见性
由于计算机运行的程序/代码,经常要访问数据,这些数据被存储在内存中。CPU在使用这个变量的时候,就会把内存中的数据先读出来,再放到CPU的寄存器中,再参与运算。(load 在上文count++的三步时讲过)。
寄存器:我们通常都知道,读内存的速度远远大于读外存的速度,而读寄存器的速度却又远远快于内存。CPU在进行大部分操作时都很快,但一旦操作到读/写内存时速度就变慢了。
为了解决这个变慢问题,提高效率,此时编译器就可能会对代码做出优化,即把一些本来要度内存的操作,优化成读取寄存器了,这样就会减少读内存的操作,也就会提高整体程序的效率了。但这种优化操作并不一定是我们想要的。于是也要去解决防止编译器自动优化。方法如下。
示例多线程代码:
预期目标是改变isQuit的值,然后打印 t1 线程执行结束!
import java.util.Scanner;
public class test {
//两个线程
//预期目标是改变isQuit的值,然后打印 t1 线程执行结束!
public static int isQuit = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(isQuit == 0) {
}
System.out.println("t1 线程执行结束");
});
Thread t2 = new Thread(() -> {
Scanner sc = new Scanner(System.in);
System.out.print("请输入 isQuit=");
isQuit = sc.nextInt();
});
t1.start();
t2.start();
}
}
结果是程序并没有停止运行。此时就是发生内存可见性问题,即编译器通过优化读取操作,把读内存优化成都寄存器了,而改变后的值在却内存中,虽然是读速度变快了,但是有可能也就不准了,这样的优化操作也无疑会使预期结果出错。
如何解决?即对该变量加上修饰符 volatile。就会禁止编译器做这种优化操作。
import java.util.Scanner;
public class test {
//两个线程
//预期目标是改变isQuit的值,然后打印 t1 线程执行结束!
public volatile static int isQuit = 0; //加 volatile
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(isQuit == 0) {
}
System.out.println("t1 线程执行结束");
});
Thread t2 = new Thread(() -> {
Scanner sc = new Scanner(System.in);
System.out.print("请输入 isQuit=");
isQuit = sc.nextInt();
});
t1.start();
t2.start();
}
}
或者,不加 volatile ,给循环里加个 sleep 也行,原因是加了sleep之后,while循环的执行速度变慢了,这样load操作的开销就不大了。因此优化也就没必要进行了。故没有触发load的优化,也就没内存可见性问题了。但总的来说,编译器优不优化,拿捏不准,还是最好使用volatile更靠谱。
不过这种优化前提是在一个线程中使用,另一个线程中修改isQuit的值,编译器以为没人修改isQuit,就做出了优化。如果都是在一个线程中使用并修改,就不会有BUG了。
import java.util.Scanner;
public class aaaaaaaa {
//两个线程
//预期目标是改变isQuit的值,然后打印 t1 线程执行结束!
public volatile static int isQuit = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(isQuit == 0) {
try {
Thread.sleep(1000); //加sleep
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1 线程执行结束");
});
Thread t2 = new Thread(() -> {
Scanner sc = new Scanner(System.in);
System.out.print("请输入 isQuit=");
isQuit = sc.nextInt();
});
t1.start();
t2.start();
}
}
"内存可见性"问题
若对于什么是原子操作,内存可见性,指令重排序,类对象不懂的等可看气的下一篇博客。