线程不安全的原因
- 线程之间是抢占式执行的【根本原因】
- 多个线程修改同一个变量
为了规则这类线程安全问题,可以尝试变换代码的组织形式,达到一个线程只改一个变量 - 原子性指一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。
- 内存可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到(和编译器优化相关)
- 指令重排序(和编译器优化相关)
如何解决线程不安全的问题
以原子性为切入点,使用synchronized关键字
synchronized加锁
先来看一个两个线程同时改变一个变量的例子
static class Count{
public int count=0;
public void increase(){
count++;
}
}
public static void main(String[] args) {
Count count = new Count();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
count.increase();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
count.increase();
}
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count.count);
}
运行之后,我们发现得到的结果并不是我们所期望的100000;所以我们通过synchronized关键字来进行加锁操作。
以下是加锁的两种方式,都能够保证线程安全。
synchronized如果是修饰代码块的时候,需要显示的在()中指定一个要加锁的对象。
- 如果是synchronized直接修饰的非静态方法,相当于加锁的对象就是this。
- 修饰代码块中加锁对象是类对象的话
public void increase(){
synchronized (this){
count++;
}
}
synchronized可以直接用于修饰普通方法或者静态方法。修饰静态方式的时候,相当于针对类对象进行加锁。【由于类对象是单例的,两个线程并发调用该方法,一定会触发锁竞争】
synchronized public void increase(){
count++;
}
锁的由来:能够修饰一个方法/代码块。进入代码块,就加锁,出了代码块就解锁。
synchronized保证了代码块在任意时刻最多只有一个线程能执行(互斥性)。
synchronized的功能本质就是把“并发”变成“串行”
synchronized需要指定一个具体的加锁对象。
如果是修饰一个非静态方法,加锁对象就是this.
如果是修饰一个静态方法,加锁对象就是当前的类的对象。
如果修饰一个代码块,加锁对象通过()来指定。
两个线程竞争同一把锁,才可能出现阻塞。
当线程1获取到锁之后,线程2也尝试获取锁,就会出现阻塞等待的请款。直到线程1释放了锁,线程2才有可能获取到锁。
synchronized还能保证内存可见性。
在synchronized内部如果要访问变量,就会保证一定能操作内存(禁止了优化,例如保证每一次CPU都从内存中读取最新的数据,修改之后,每一次都将数据写回到内存,不允许省略操作)
synchronized实现了可重入性
synchronized public void increase(){
synchronized (this){
count++;
}
}
synchronized内部记录当前这个锁是哪个线程持有的。
Java标准库(集合类)中的线程安全类
Java集合类中很多都是线程不安全的,这些类可能会涉及到多线程修改共享数据,又没有任何加锁措施。
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
有一些是线程安全的,使用了一些锁机制来控制
- Vertor
- HashTable
- ConcurrentHashMap
- StringBuffer
还有虽然没有加锁,但是不涉及“修改”,仍然是线程安全的。
- String
以内存可见性为切入点,使用volatile关键字,但是不能保证原子性(与synchronized的不同)
因此,volatile关键字的目的是告诉虚拟机:
- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻回写到主内存。
先来看一个例子
public static int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (count==0){
}
System.out.println("循环结束");
}
});
t1.start();
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个整数");
count = sc.nextInt();
}
我们输入1之后,线程并没有结束。错误的原因是快速的循环,频繁的从内存中读取数据,编译器进行了优化->并不是每一次都从内存(主内存)中读取数据,而是从工作内存中读取。
加入volatile之后,禁止了编译器优化,保证每次读取的数据都是从主内存中读取。
volatile public static int count = 0;