class Counter {
public int count = 0;
public void increase() {
count++;
}
}
public class Java3_9_4 {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
}
};
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter.count);
}
}
这段代码理论上能自增10w次,但是最终结果确实不确定
由于多线程并发执行,导致了代码中出现了BUG,这种情况称为"线程不安全"
线程之间是抢占式执行的(根本原因,线程不安全的万恶之源)
抢占式执行,导致两个线程里面的操作先后顺序无法确定 这种随机性是导致线程不安全的根本原因 (无力改变,操作系统的内核实现)
多个线程修改同一个变量
原子性
像 ++ 这样的操作,本质上是三个步骤(LOAD,ADD,SAVE),是一个"非原子" 的操作,像 = 操作,本质上就是一个步骤,认为是一个"原子" 操作 (可通过加锁方式解决,变成原子的)
内存可见性(与编译器优化有关)
一个内存修改,一个内存读取
由于编译器的优化,可能把中间环节的 SAVE 和 LOAD 操作去掉了
此时读取的线程可能是未修改的结果
(可以用volatile 解决)
指令重排序(也与编译器优化有关)
编译器会自动调整执行指令的顺序,以达到提高执行效率的效果,前提是需要保证最终效果不变,但是在多线程下,会影响结果
最朴实的方法,从原子性入手 加锁 !!!
synchronized
关键字 一定要会拼,会写
synchronized public void increase() {
count++;
}
英文原意为 同步 存在歧义 理解成互斥更合适 如果两个线程同时并发的尝试调用这个synchronized 修饰方法 此时一个线程会先执行这个方法,另一个线程会等待,等到第一个线程执行完之后,第二个线程才会继续执行.
这就相当于 "加锁" 和 "解锁" 进入 synchronized 修饰的方法,就相当于加锁 处理 synchronized 修饰的方法,就相当于解锁
如果当前是已经加锁了的状态,其他线程就无法执行这里的逻辑,就只能阻塞等待
synchronized 还可以修饰代码块
修饰代码块的时候, ( ) 中要你指定一个加锁的对象,如果修饰的是非静态方法,相当于加锁的对象是this
public void increase() {
synchronized (this) {
count++;
}
}
synchronized 不光能起互斥的效果,还能够刷新内存 (解决内存可见性问题)
会让程序跑的慢,但是算的准,用了之后可能就与"高性能" 无关了
同一个线程连续针对同一个同一个锁进行加锁,不会死锁
synchronized 允许可重入
synchronized 允许一个线程针对一把锁,连续锁两次
因为synchronized 内部记录了当前这个锁是哪个线程持有的
synchronized 修饰普通方法的话,相当是针对 this 进行加锁
如果两个线程并发的调用了这个方法,此时是否会触发锁竞争,就看实际的锁对象是否是同一个 synchronized
修饰的是静态方法的话,相当于针对 类对象 进行加锁
由于类对象是单例,两个线程并发调用该方法,一定会触发锁竞争
(可变的,容易改变的)
功能是保证内存可见性,但是不能保证原子性
volatile 的用法比较单一,只能修饰一个具体属性 ,此时代码中针对这个属性的读写操作就一定是内存操作了
public class java3_9_5 {
// 一旦给这个 flag 加上 volatile 后,此时后序针对 flag 的读写操作,都能保证一定是内存操作了
public static volatile int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
while (flag == 0) {
}
System.out.println("线程结束了");
}
};
Thread t2 = new Thread() {
@Override
public void run() {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入");
flag = scanner.nextInt();
}
};
t1.start();
t2.start();
}
}
但是 volatile 不能保证原子性
volatile 是和优化密切相关的 东西
一般来说一个某个变量,在一个线程中读,一个线程中写,此时大概率要用 volatile
集合,大部分是线程不安全的,ArrayList,LinkedList,…都是线程不安全的
线程安全的 :
Vector (不建议使用)也是一个顺序表,能自动扩容什么的,使用了很多 synchronized 来保证线程安全,但是给了很多方法都加上了 synchronized 修饰,在大多数情况下并不需要在多线程下使用 Vector,而我们加太多的 synchronized就会对单线程环境下的操作效率造成负面影响
Stack 继承自 Vector ,所以 Stack 是线程安全的
HashTable (同理不建议使用)
ConcurrentHashMap
StringBuffer(核心方法都带有synchronized)
有的虽然没有加锁,但是不涉及"修改",仍然是线程安全的 : String
String 是不可变对象,不可能存在两个线程并发的修改同一个 String