上一篇博客主要介绍了Thread类的各种方法,并且阐述了线程不安全的缘由,这篇博客就来讨论一下如何解决线程不安全的问题
线程不安全的原因
抢占式执行
为了程序执行的效率,我们的线程采用抢占式执行,也就是谁抢到资源谁就可以完成任务,因此多个线程执行任务时线程的调度是随机的,因此我们很难找到规律
多个线程修改同一个变量
这个原因我们在上一篇博客讲过了。需要注意的是,一个线程修改一个变量,多个线程修改不同的变量,多个线程读不同的变量都是不存在线程安全问题的,只有多个变量修改同一个变量才会出现问题
修改不是原子的
上一条出现问题的原因就是因为修改并不是原子的——即有可能在修改的读操作时另一个线程就把资源占用了,因此,我们把修改操作锁起来,使得修改时别的线程无法对这个变量进行操作,就能够解决线程安全问题。
需要注意的是,我们不能看代码是一行,修改操作就是原子性的,而是应该看代码背后的cpu操作逻辑
内存可见性问题
由于机器会优化我们的代码,即如果进行100次a++操作,本身应该cpu从内存读取a变量的值,++后再放回内存,这个操作进行100次
但是由于代码自动的优化,就有可能变成了cpu从内存读取a变量的值,++100次后再放回内存。
因此,虽然代码跑的效率更高了,但是在多线程操作时可能产生意想不到的后果
指令重排序
也是代码的优化,代码将指令的顺序重新排序,使得执行的逻辑不变,效率提升,但是在多线程操作下可能产生问题
线程不安全解决方案
我们通过将修改操作变成原子的来解决线程不安全问题
加锁
当一个线程访问变量时,先对变量加锁,完成任务后再对变量进行解锁,当别的线程访问这个已经被加锁的变量,那么就会触发阻塞等待的状态
synchronized
我们的java使用synchronized关键字来加锁,在一个方法前用这个关键字来修饰,那么就可以使这个方法变成原子性的
public synchronized void 方法名(){
}
需要注意的是,由于加锁使得原来多线程的并发执行,变成了串行执行,因此效率会下降,因此我们在确保必要的情况下再进行加锁操作
synchronized还可以修饰代码块
synchronized (对象) {
}
我们要对哪个对象加锁,就在括号中填哪个对象,如果填的是this,那么谁调用这个代码块外面的方法,那么谁就是this。
- synchronized直接修饰方法,就相当于锁的对象是this
- 两个线程在锁同一个对象时会触发锁的阻塞等待,在执行不同对象时就不存在竞争
因此通过加锁操作,我们可以让之前两个线程同时对一个变量++操作的代码进行优化
public class sumByThread {
final static int SUM = 100;
static long a = 0;
static Count c = new Count();
static class Count {
public synchronized void count(){
a++;
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < SUM; i++) {
c.count();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < SUM; i++) {
c.count();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(a);
}
}
下面演示一下不同情况下的加锁状态
demo1
public class demo1 {
static int count = 0;
public static class Counter{
public void increase(){
synchronized (this){
count++;
}
}
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increase();
}
});
}
}
我们的synchronized的加锁对象是this,也就是谁调用increase方法,谁就加锁。我们的t1和t2都是通过counter来调用increase方法的,因此会产生锁冲突
demo2
public class demo2 {
static int count = 0;
public static class Counter{
public void increase(){
synchronized (this){
count++;
}
}
}
public static void main(String[] args) {
Counter counter1 = new Counter();
Counter counter2 = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter1.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter2.increase();
}
});
}
}
上面的代码同样是对this进行加锁,但是t1和t2是通过不同的对象调用increase方法,因此两个线程不会出现锁竞争
demo3
public class demo3 {
static int count = 0;
public static class Counter{
public Object locker = new Object();
public void increase(){
synchronized (locker){
count++;
}
}
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increase();
}
});
}
}
这次我们在Counter类中加入了一个专门用来加锁的对象locker,我们synchronized修饰了这个对象,那么如果我们访问的是同一个locker对象,就会发生锁竞争,由于t1和t2访问的是同一个counter对象,而同一个counter对象中的locker对象就是相同的,因此会发生竞争。
和demo1不同的是,我们专门创建了一个locker对象来对increase方法进行加锁,以后如果还有increase2方法的话,我们可以创建locker2对象来对increase2方法进行加锁,从而使这两个方法互相没有影响
demo4
public class demo4 {
static int count = 0;
public static class Counter{
public Object locker = new Object();
public void increase(){
synchronized (locker){
count++;
}
}
}
public static void main(String[] args) {
Counter counter1 = new Counter();
Counter counter2 = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter1.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter2.increase();
}
});
}
}
和demo3不同的是,demo4中t1和t2访问的是不同的counter对象,因此其内部的locker对象也是不同的,因此不构成锁冲突
demo5
public class demo5 {
static int count = 0;
public static class Counter{
public static Object locker = new Object();
public void increase(){
synchronized (locker){
count++;
}
}
}
public static void main(String[] args) {
Counter counter1 = new Counter();
Counter counter2 = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter1.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter2.increase();
}
});
}
}
和demo4不同的是,demo5中的locker对象是static修饰的,也就是说其是一个静态成员,也就是类属性,而一个进程中类对象只有一个,类属性也只有一个,因此t1和t2虽然是通过不同的counter对象调用increase方法,但是这两个实例中的locker对象是同一个,因此还是会发生锁冲突
demo6
public class demo6 {
static int count = 0;
public static class Counter{
public static Object locker = new Object();
public void increase(){
synchronized (locker){
count++;
}
}
public void increase2(){
synchronized (this){
count++;
}
}
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increase2();
}
});
}
}
demo6中increase1针对locker加锁,而increase2针对调用该方法的当前对象加锁,因此t1针对静态的locker加锁,而t2针对counter对象加锁,二者并不是访问同一个对象,因此不构成锁冲突
demo7
public class demo7 {
static int count = 0;
public static class Counter{
public void increase(){
synchronized (Counter.class){
count++;
}
}
}
public static void main(String[] args) {
Counter counter1 = new Counter();
Counter counter2 = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter1.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter2.increase();
}
});
}
}
demo7中我们针对类对象进行加锁。类对象在之前的反射中讲过,来自于.class文件,在jvm进程中只有一个,因此多个进程针对类对象加锁会产生锁竞争