文章目录
- 1.共享会出现什么问题
- 2. 什么是临界区
- 3. synchronized解决方案
- 4 线程八锁
1.共享会出现什么问题
首先,我们先了解对某一资源共享会出现什么问题,然后怎么解决这个问题。
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
public class Main {
public static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter);
}
}
结果分析
我们多次运行上面代码,会发现结果有0,有负数也有正数。这是为什么呢,应该不都是0才对吗,为啥会出现正、负数呢,而且0出现的次数很小,大多都是正、负数。
这是因为 Java 中对静态变量的自增、自减并不是原子操作,要彻底理解,必须从字节码来进行分析。
例如对于i++
而言( i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而对于i--
也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
而Java的内存模型如下,完成静态变量的自增、自减操作需要在主存和工作内存中进行数据交换:
如果在单线程的情况下上面代码顺序执行是没有问题的:
出现负数的情况:
线程2将i
减1在最后一步写入静态变量时,发生线程切换,线程1将i
加1写入后,再次切换到线程2,线程2在写入,这样就产生并发带来的线程不安全问题。
出现正数的情况:
2. 什么是临界区
首先,我们要知道一个很虚运行多个线程本身是没有问题的,问题就出现多个线程访问共享资源:
- 多个线程读共享资源也没有我呢提
- 但多个线程对共享资源进行读写操作时就会发生指令交错,就会出现问题
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
例如:
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
3. synchronized解决方案
synchronized
:俗称对象锁,它采用互斥的方式让同一时刻至多只有一个线程能拥有对象锁,其他线程在想获取这个对象锁的线程就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换带来的线程安全问题。
我们先来了解几个问题,不清楚这几个问题在后续的学习过程中会很难进展:
- 什么是“锁”?
- 锁的的是线程代码还是什么?
我们初学多线程时都可能会被带偏,觉得synchronized
就是锁,其实对象才是锁,synchronized
其实是给对象加锁,所以我们一直都是在给对象加锁,严格意义上并不是在给某个代码块加锁。
-
synchronized
作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象 -
synchronized
作用于静态方法时,锁住的是Class实例 -
synchronized
作用于一个代码块时,锁住的是所有代码块中配置的对象
synchronized 语法
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}
线程1、线程2都去给某个对象加锁,但只有一个线程能够加锁成功。
解决上诉问题代码
public class Main {
public static final Object room = new Object();
public static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter);
}
}
我们可以做这样的类比
synchronized(对象)
中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人 进行计算,线程 t1,t2 想象成两个人- 当线程 t1 执行到
synchronized(room)
时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码 - 这时候如果 t2 也运行到了
synchronized(room)
时,它发现门被锁住了,只能在门外等待,发生了上下文切 换,阻塞住了 - 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦), 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才 能开门进入
- 当 t1 执行完
synchronized{}
块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥 匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的count--
代码
synchronized 实际上是用 对象锁 保证了 临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断
为了加深理解,请思考下面的问题:
- 如果把
synchronized (obj)
放在 for 循环的外面,如何理解?
这样的话就不会出现两个线程交替执行的情况,只有当一个线程执行完后另一个线程才会执行。
- 如果 t1
synchronized(obj1)
而 t2synchronized(obj2)
会怎样运作?
synchronized是给对象加锁,两个线程用不同的对象加锁那么就不会出现两个线程竞争锁对象的情况,相当于两个线程都没有加锁,那么就会出现线程安全问题。
- 如果 t1
synchronized(obj)
而 t2 没有加会怎么样?如何理解?-- 锁对象
线程1加了锁,线程2不加,那就相当于两个线程都没加,还是会出现线程安全问题。
面向对象改进
public class Main {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
room.increment();
}
}
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
room.decrement();
}
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(room.get());
}
}
class Room {
int value = 0;
public void increment() {
synchronized (this) {
value++;
}
}
public void decrement() {
synchronized (this) {
value--;
}
}
public int get() {
synchronized (this) {
return value;
}
}
}
这里的锁对象是this
也就是room这个对象实例。
4 线程八锁
我们前面说过
-
synchronized
作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象 -
synchronized
作用于静态方法时,锁住的是Class实例 -
synchronized
作用于一个代码块时,锁住的是所有代码块中配置的对象
所以
class Test {
public synchronized void test() {
}
}
、、 等价于
class Test {
public void test() {
synchronized (this) {
}
}
}
// 这两个锁的都是对象的实例,即this对象
class Test{
public synchronized static void test() {
}
}
// 等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
// 锁的是类实例
所谓的“线程八锁”其实就是考察synchronized
锁住的是哪个对象
情况1
class Number {
public synchronized void a() {
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
}
public class Test {
public static void main(String[] args) {
Number number = new Number();
new Thread(() -> {number.b();}).start();
new Thread(() -> {number.a();}).start();
}
}
答案是
1 2
或2 1
这里方法a和方法b锁的都是 number这个对象,所以两个线程之间是竞争同一把锁。
情况2
class Number {
public synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
}
public class Test {
public static void main(String[] args) {
Number number = new Number();
new Thread(() -> {number.a();}).start();
new Thread(() -> {number.b();}).start();
}
}
答案是
1s后1 2
或2 1s后1
这里两个线程仍然竞争的是number这同一把锁,线程1拿到锁之后,执行的是sleep方法,虽然放弃了cpu的使用,但并没有放弃锁对象,所以线程2还是获取不到锁,所以必须是一个线程执行完后一个线程才能获取到锁。
情况3
class Number {
public synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
public void c()
{
System.out.println("3");
}
}
public class Test {
public static void main(String[] args) {
Number number = new Number();
new Thread(() -> {number.a();}).start();
new Thread(() -> {number.b();}).start();
new Thread(() -> {number.c();}).start();
}
}
答案:
3 1s后1 2
或2 3 1s后1
或者3 2 1s后1
线程3不去竞争锁对象。当线程3先执行时,不去竞争锁,直接执行。线程1和线程去竞争同一把锁。
情况4
class Number {
public synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
}
public class Test {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n2.b();}).start();
}
}
答案:
2 1s后1
这里线程1竞争的是n1
,线程2竞争的是n2
,所以两个线程之间竞争的不是同一把锁,所以它俩之间不存在互斥。
情况5
class Number {
public static synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
}
public class Test {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n1.b();}).start();
}
}
答案:
2 1s后1
线程1竞争的是Number
实例,而线程2竞争的是n1
,所以它俩竞争的不是同一把锁。
情况6
class Number {
public static synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1");
}
public static synchronized void b() {
System.out.println("2");
}
}
public class Test {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n1.b();}).start();
}
}
答案:
1s后1 2
或2 1s后1
这里线程1和线程2竞争的都是 Number
情况7
class Number {
public static synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1");
}
public synchronized void b() {
System.out.println("2");
}
}
public class Test {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n2.b();}).start();
}
}
答案:
2 1s后1
这里线程1竞争的是Number
实例,线程2竞争的是n2
,它俩竞争的不是同一个对象锁。
情况8
class Number {
public static synchronized void a() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("1");
}
public static synchronized void b() {
System.out.println("2");
}
}
public class Test {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n2.b();}).start();
}
}
答案:
1s后1 2
或2 1s后1
这里线程1和线程2竞争的都是 Number
好了,线程八锁的问题就说到这了,后续还会继续更新!