一、什么是线程共享模型?
在前面的章节中,我们介绍了计算机的共享模型,和java的线程共享模型:
1)计算机共享模型
image.png2)java线程共享模型
image.png如上所示,无论是哪种模型,都有线程或cpu自己的运行时缓存或内存,同时都有主内存。
二、线程共享模型存在什么问题?
首先看下面的代码,两个线程,每个线程分别对i进行++操作,加100000次,结果会得到200000吗:
/**
* @description: 线程共享模型问题
* @author:weirx
* @date:2021/11/25 9:48
* @version:3.0
*/
public class ThreadSharedModelProblems {
static int i = 0;
/**
* 两个长度的门闩
*/
static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
i++;
}
// 减少门闩数
countDownLatch.countDown();
});
t1.start();
Thread t2 = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
i++;
}
// 减少门闩数
countDownLatch.countDown();
});
t2.start();
//阻塞等待门闩数降为0
countDownLatch.await();
System.out.println("i = " + i);
}
结果:
i = 143188
产生的原因呢?主要是因为i++并不是一个原子性操作。i++操作的JVM字节码如下:
getstatic #2 // 获取静态变量i
iconst_1 // 定义局部变量1
iadd // 执行自加1操作
putstatic #2 // 将自加1后的值赋给静态变量i
return
那么结合上面的例子和线程共享模型就会是如下模式:
线程共享模型.png线程t1和t2同时去主内存获取获取i的值,并进行自加1的操作,然后再将值赋回给主线程,因为这两个线程之间是没有顺序的,且没有任何的关联,势必会造成线程t1,刚写入主内存的值,被t2覆盖,而t1再次取值,就不是上次的值了。
以上呢就是共享资源所导致的问题。
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
三、解决方案
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
下文重点讲解使用synchronized解决上面的问题。
3.1 synchronized对象锁
对象锁:它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程再想获取这个对象锁时就会阻塞住。
可以理解这个对象为一个房间,这个房间一次只能有一个人进入,代码如下:
public class ThreadSharedModelProblems {
static int i = 0;
/**
* 两个长度的门闩
*/
static CountDownLatch countDownLatch = new CountDownLatch(2);
/**
* 定义一个不可变的对象,此处可以理解成一个房间
*/
static final Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
// 争夺进入房间的机会
synchronized (obj){
i++;
}
}
// 减少门闩数
countDownLatch.countDown();
});
t1.start();
Thread t2 = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
// 争夺进入房间的机会
synchronized (obj){
i++;
}
}
// 减少门闩数
countDownLatch.countDown();
});
t2.start();
//阻塞等待门闩数降为0
countDownLatch.await();
System.out.println("i = " + i);
}
}
synchronized 实际是用对象锁保证了临界区内代码的原子性。临界区内的代码对外是不可分割的,不会被线程切换所打断。
如何理解上面这句话的后半句?cpu在运行时,会发生线程上下文的切换,假设t1正持有对象,及在房间内进行++操作,如果此时cpu时间片用完了,这个t1就会释放占用的cpu资源,但是对象锁仍然被其持有,t2仍然不能获得对象锁。只有当cpu在给t1分配时间片,并完成此次循环操作后,t2才有机会去获得对象锁。
3.2 对象锁的优化
java是一门面向对象的语言,所以像上一章节的对象锁不是好的实现方式,我们应该将其放在对象当中。
写一个Room对象,将++操作和对象锁放在其中,代码如下所示:
Room:
public class Room {
int i = 0;
public int getI() {
synchronized (this) {
return i;
}
}
public void add() {
synchronized (this) {
i++;
}
}
}
main方法:
/**
* @description: 线程共享模型问题
* @author:weirx
* @date:2021/11/25 9:48
* @version:3.0
*/
public class ThreadSharedModelProblems {
/**
* 两个长度的门闩
*/
static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
room.add();
}
// 减少门闩数
countDownLatch.countDown();
});
t1.start();
Thread t2 = new Thread(() -> {
for (int j = 0; j < 100000; j++) {
room.add();
}
// 减少门闩数
countDownLatch.countDown();
});
t2.start();
//阻塞等待门闩数降为0
countDownLatch.await();
System.out.println("i = " + room.getI());
}
}
synchronized (this)当中的this是什么呢?其实就是Room这个对象本身,如下所示:
image.png3.3 方法上的synchronized
1)普通方法上的synchronized,等同于加在当前对象上,如下面代码,test1等同于test2
2)静态方法上的synchronized,等同于加在类上,如下面代码,test3等同于test4
public class MethodSynchronized {
public synchronized void test1() {
System.out.println("this is test1");
}
public void test2() {
synchronized (this) {
System.out.println("this is test2");
}
}
public static synchronized void test3() {
System.out.println("this is test3");
}
public void test4() {
synchronized (MethodSynchronized.class) {
System.out.println("this is test4");
}
}
}
3.4 何谓“线程八锁”?
其实就是考察 synchronized 锁住的是哪个对象,我们主要要记住以下两点:
- 普通方法锁住的是this(当前对象),而静态方法锁住的是类(class)
- 同一时刻,只有一个线程能够持有锁
所谓线程八锁,就是八种不同锁的情况,下面我就不举例了,但是要能够分析,基本在以下几种类型中:
- 同一个对象,内部无论几个非静态方法有锁,都是互斥的
- 同一个类的不同对象,锁不互斥
- 对象锁,即this,与类锁(class),是不互斥的
- 同一个类的内部两个静态方法的锁,是互斥的
四、变量的安全分析
-
成员变量与静态变量是线程安全的吗?
如果它们没有共享,则线程安全。
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况:
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
-
局部变量是线程安全的吗?
局部变量是线程安全的。但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全。(比如由于内部类重写方法,该方法使用了修改了局部变量,且该方法被共享了,则会导致该变量的不安全,可以对这种方法时使用final,或设置为pravite)。
五、常见的线程安全类
常见的线程安全类其实也分为两个方面:
-
使用锁(synchronized,Lock,CAS)
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类需要注意的是,上面举例的类,他们的方法都是原子性的,但是组合使用后并不能保证原子性,需要我们自己进行控制。
-
不可变类(final)
String
IntegerString、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
关于线程共享模型以及synchronized的简单使用就介绍到这里了,有帮助的话点个赞吧。。
网友评论