本篇文章给大家带来了关于java的相关知识,其中主要介绍了关于多线程的相关问题,包括了线程安装、线程加锁与线程不安全的原因、线程安全的标准类等等内容,希望对大家有帮助。
|
本篇文章给大家带来了关于java的相关知识,其中主要介绍了关于多线程的相关问题,包括了线程安装、线程加锁与线程不安全的原因、线程安全的标准类等等内容,希望对大家有帮助。
推荐学习:《java视频教程》 本篇文章介绍的内容为Java多线程中的线程安全问题,此处的安全问题并不是指的像黑客入侵造成的安全问题,线程安全问题是指因多线程抢占式执行而导致程序出现bug的问题。 1.线程安全概述1.1什么是线程安全问题首先我们需要明白操作系统中线程的调度是抢占式执行的,或者说是随机的,这就造成线程调度执行时线程的执行顺序是不确定的,有一些代码执行顺序不同不影响程序运行的结果,但也有一些代码执行顺序发生改变了重写的运行结果会受影响,这就造成程序会出现bug,对于多线程并发时会使程序出现bug的代码称作线程不安全的代码,这就是线程安全问题。 下面,将介绍一种典型的线程安全问题实例,整数自增问题。 1.2一个存在线程安全问题的程序有一天,老师布置了这样一个问题:使用两个线程将变量 class Counter {
private int count;
public void increase() {
++this.count;
}
public int getCount() {
return this.count;
}}public class Main11 {
private static final int CNT = 50000;
private static final Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < CNT; i++) {
counter.increase();
}
});
Thread thread2 = new Thread(() -> {
for (int j = 0; j < CNT; j++) {
counter.increase();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counter.getCount());
}}按理来说,结果应该是 2.线程加锁与线程不安全的原因2.1案例分析上面我们使用多线程运行了一个程序,将一个变量值为0的变量自增10万次,但是最终实际结果比我们预期结果要小,原因就是线程调度的顺序是随机的,造成线程间自增的指令集交叉,导致运行时出现两次自增但值只自增一次的情况,所以得到的结果会偏小。 我们知道一次自增操作可以包含以下几条指令:
我们来画一条时间轴,来总结一下常见的几种情况: 情况1: 线程间指令集,无交叉,运行结果与预期相同,图中寄存器A表示线程1所用的寄存器,寄存器B表示线程2所用的寄存器,后续情况同理。 那如何解决上述线程不安全的问题呢?当然有,那就是对对象加锁。 2.2线程加锁2.2.1什么是加锁为了解决由于“抢占式执行”所导致的线程安全问题,我们可以对操作的对象进行加锁,当一个线程拿到该对象的锁后,会将该对象锁起来,其他线程如果需要执行该对象的任务时,需要等待该线程运行完该对象的任务后才能执行。 举个例子,假设要你去银行的ATM机存钱或者取款,每台ATM机一般都在一间单独的小房子里面,这个小房子有一扇门一把锁,你进去使用ATM机时,门会自动的锁上,这个时候如果有人要来取款,那它得等你使用完并出来它才能进去使用ATM,那么这里的“你”相当于线程,ATM相当于一个对象,小房子相当于一把锁,其他的人相当于其他的线程。 2.2.2如何加锁synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待。 java中的加锁操作可以使用 方式1: 使用 class Counter {
private int count;
synchronized public void increase() {
++this.count;
}
public int getCount() {
return this.count;
}}多线程自增的main方法如下,后面会以相同的栗子介绍 public class Main11 {
private static final int CNT = 50000;
private static final Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < CNT; i++) {
counter.increase();
}
});
Thread thread2 = new Thread(() -> {
for (int j = 0; j < CNT; j++) {
counter.increase();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counter.getCount());
}}看看运行结果: class Counter {
private int count;
public void increase() {
synchronized (this){
++this.count;
}
}
public int getCount() {
return this.count;
}}运行结果: class Counter {
private static int count;
synchronized public static void increase() {
++count;
}
public int getCount() {
return this.count;
}}运行结果: synchronized 的工作过程:
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题,即死锁问题,关于死锁后续文章再做介绍。 综上,synchronized关键字加锁有如下性质:互斥性,刷新内存性,可重入性。 synchronized关键字也相当于一把监视器锁monitor lock,如果不加锁,直接使用 2.2.3再析案例对自增那个代码上锁后,我们再来分析一下为什么加上了所就线程安全了,先列代码: class Counter {
private int count;
synchronized public void increase() {
++this.count;
}
public int getCount() {
return this.count;
}}public class Main11 {
private static final int CNT = 50000;
private static final Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < CNT; i++) {
counter.increase();
}
});
Thread thread2 = new Thread(() -> {
for (int j = 0; j < CNT; j++) {
counter.increase();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counter.getCount());
}}多线程并发执行时,上一次就分析过没有指令集交叉就不会出现问题,因此这里我们只讨论指令交叉后,加锁操作是如何保证线程安全的,不妨记加锁为 2.3线程不安全的原因首先,线程不安全根源是线程间的调度充满随机性,导致原有的逻辑被改变,造成线程不安全,这个问题无法解决,无可奈何。 多个线程针对同一资源进行写(修改)操作,并且针对资源的修改操作不是原子性的,可能会导致线程不安全问题,类似于数据库的事务。 由于编译器的优化,内存可见性无法保证,就是当线程频繁地对同一个变量进行读操作时,会直接从寄存器上读值,不会从内存上读值,这样内存的值修改时,线程就感知不到该变量已经修改,会导致线程安全问题(这是编译器优化的结果,现代的编译器都有类似的优化不止于Java),因为相比于寄存器,从内容中读取数据的效率要小的多,所以编译器会尽可能地在逻辑不变的情况下对代码进行优化,单线程情况下是不会翻车的,但是多线程就不一定了,比如下面一段代码: import java.util.Scanner;public class Main12 {
private static int isQuit;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (isQuit == 0) {
}
System.out.println("线程thread执行完毕!");
});
thread.start();
Scanner sc = new Scanner(System.in);
System.out.println("请输入isQuit的值,不为0线程thread停止执行!");
isQuit = sc.nextInt();
System.out.println("main线程执行完毕!");
}}运行结果: import java.util.Scanner;public class Main12 {
volatile private static int isQuit;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while (isQuit == 0) {
}
System.out.println("线程thread执行完毕!");
});
thread.start();
Scanner sc = new Scanner(System.in);
System.out.println("请输入isQuit的值,不为0线程thread停止执行!");
isQuit = sc.nextInt();
System.out.println("main线程执行完毕!");
}}运行结果: synchronized与volatile关键字的区别: import java.util.Scanner;public class Main12 {
private static int isQuit;
//锁对象
private static final Object lock = new Object();
public static void main(String[] args) {
Thread thread = new Thread(() -> {
synchronized (lock) {
while (isQuit == 0) {
}
System.out.println("线程thread执行完毕!");
}
});
thread.start();
Scanner sc = new Scanner(System.in);
System.out.println("请输入isQuit的值,不为0线程thread停止执行!");
isQuit = sc.nextInt();
System.out.println("main线程执行完毕!");
}}运行结果: 编译器优化除了导致内存可见性感知不到的问题,还有指令重排序也会导致线程安全问题,指令重排序也是编译器优化之一,就是编译器会智能地(保证原有逻辑不变的情况下)调整代码执行顺序,从而提高程序运行的效率,单线程没问题,但是多线程可能会翻车,这个原因了解即可。 3.线程安全的标准类Java 标准库中很多都是线程不安全的。这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施。例如,ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder。 在线程安全问题中可能你还会遇到JMM模型,在这里补充一下,JMM其实就是把操作系统中的寄存器,缓存和内存重新封装了一下,其中在JMM中寄存器和缓存称为工作内存,内存称为主内存。 4.Object类提供的线程等待方法除了Thread类中的能够实现线程等待的方法,如
上面介绍 public class TestDemo12 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行完毕!");
});
thread.start();
System.out.println("wait前");
thread.wait();
System.out.println("wait后");
}}看看运行结果:
现在有两个任务由两个线程执行,假设线程2比线程1先执行,请写出一个多线程程序使任务1在任务2前面完成,其中线程1执行任务1,线程2执行任务2。 class Task{
public void task(int i) {
System.out.println("任务" + i + "完成!");
}}public class WiteNotify {
//锁对象
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
Task task1 = new Task();
task1.task(1);
//通知线程2线程1的任务完成
System.out.println("notify前");
lock.notify();
System.out.println("notify后");
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
Task task2 = new Task();
//等待线程1的任务1执行完毕
System.out.println("wait前");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
task2.task(2);
System.out.println("wait后");
}
});
thread2.start();
Thread.sleep(10);
thread1.start();
}}运行结果: 推荐学习:《java视频教程》 以上就是一起聊聊Java多线程之线程安全问题的详细内容,更多请关注模板之家(www.mb5.com.cn)其它相关文章! |
