1. 共享问题
先看一段代码
@Slf4j(topic = "c.yuanzixing")
public class yuanzixing {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", counter);
}
}
结果有可能是0
有可能是负数
还有可能正数
这是因为i++和i--并不是原子操作
i++实际会产生如下的 JVM 字节码指令:
单线程下这八条指令是顺序执行(不会交错)没有问题。
但多线程下这 8 行代码可能交错运行
出现负数的情况
出现正数的情况
一个程序运行多个线程本身是没有问题的,问题出在多个线程访问共享资源,多个线程读共享资源其实也没有问题,但在多个线程对共享资源读写操作时发生指令交错,就会出现问题。
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区。
2. synchronized 解决方案
语法
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}
例如
@Slf4j(topic = "c.Test19")
public class Test17 {
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", counter);
}
}
想象 synchronized(对象)
中的对象是一个房间(room),这个房间有唯一的入口(门),门只能一次进入一人。
- 线程 t1 和 t2 就像两个想进入房间的人。
线程 t1 执行到
synchronized(room)
时,相当于 t1 进入了房间,并锁上了门,拿走了钥匙,在房间里执行count++
代码。线程 t2 如果也运行到了
synchronized(room)
,它会发现门被锁住了,只能在门外等待。此时,线程 t2 会被阻塞,发生上下文切换,等待 t1 完成工作。即使 t1 的 CPU 时间片用完 被踢出了门外(注意,锁住对象并不能让 t1 一直执行),门仍然锁住,t1 依然拿着钥匙。此时,t2 线程仍在阻塞状态,无法进入房间。
当 t1 执行完
synchronized{}
块内的代码后,t1 才会离开房间并解开门上的锁,把钥匙还给 t2 线程。此时 t2 线程才可以进入房间,锁住门,拿上钥匙,执行它的count--
代码。
如图
如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性
此时也不会出错,就是锁住了整个for循环近20000次操作。
如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象
锁不是同一把对象跟没上一样。会出错
如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象
t2没加锁那么则发生上下文切换后轮到t2它因为没锁所以不会被阻塞住,那么他就会执行代码,于是又发生交错执行。会出错。
基于面向对象的设计思想代码可以改进,把需要保护的共享变量放入一个类,
class Room {
int value = 0;
public void increment() {
synchronized (this) {
value++;
}
}
public void decrement() {
synchronized (this) {
value--;
}
}
public int get() {
synchronized (this) {
return value;
}
}
}
注意get也要加锁,因为不加锁可能读到中间变量。
3. 方法上的 synchronized
class Test {
public synchronized void test() {
}
}
等价于
class Test {
public void test() {
synchronized (this) {
}
}
}
class Test {
public synchronized static void test() {
}
}
等价于
class Test {
public static void test() {
synchronized (Test.class) {
}
}
}
下面看六种情况
case1
@Slf4j(topic = "c.Number")
class Number {
public synchronized void a() { log.debug("1"); }
public synchronized void b() { log.debug("2"); }
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> { n1.a(); }).start();
new Thread(() -> { n1.b(); }).start();
}
case2
class Number {
public synchronized void a() { sleep(1); log.debug("1");
}
public synchronized void b() { log.debug("2"); }
public void c() { log.debug("3"); }
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> { n1.a(); }).start();
new Thread(() -> { n1.b(); }).start();
new Thread(() -> { n1.c(); }).start();
}
case1 12或21 case 2: 3 1s 12或 23 1s 1 或 32 1s 1
case3
@Slf4j(topic = "c.Number")
class Number {
public static synchronized void a() { sleep(1); log.debug("1");
}
public synchronized void b() { log.debug("2"); }
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> { n1.a(); }).start();
new Thread(() -> { n1.b(); }).start();
}
case4
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {sleep(1);log.debug("1");}
public static synchronized void b() {log.debug("2");}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
case5
@Slf4j(topic = "c.Number")
class Number {
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n2.b();
}).start();
}
case6
class Number {
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {
n1.a();
}).start();
new Thread(() -> {
n2.b();
}).start();
}
case3 a方法加了static所以所住的是类,b方法所住的是this对象,所以不是同一把锁 2 1s 1
case4: 1s 后12, 或 2 1s后 1 case5:2 1s 1 case 6 :n1 n2虽然是不同的对象,但ab方法都加了static,所以锁住的都是number类,因此是同一把锁,1s 后12, 或 2 1s后 1。