synchronized的互斥和同步
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
线程不安全的解决方案
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
解决i++线程不安全场景
public class Test {
static int counter = 0;
static final Object object = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (object) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (object) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter);
}
}
当第一个线程要进入临界区,要首先获取object实例的锁,第二个线程要进入临界区发现了没有该object实例的锁,不能进入,要等线程1操作完临界区代码(写回内存)后才能操作。
锁对象是不同实例的思考
public class Test {
static int counter = 0;
static final Object object1 = new Object();
static final Object object2 = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (object1) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (object2) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter);
}
}
显而易见结果是错误的,相当于线程1要进入临界区之前,获取了object1实例的锁,线程2也要进入临界区,发现object2的锁没有被其他线程所占用,它也获得了该锁,也能进入临界区操作,这样就导致了线程之间没有互斥或同步。
如果有一个线程不加synchronized也是不行的。该线程是不能被阻塞住。
面向对象的编程重构
public class Room {
private int counter = 0;
public void dec() {
synchronized (this) { // 当前实例对象的锁
counter--;
}
}
public void inc() {
synchronized (this) {
counter++;
}
}
public int getCounter() {
synchronized (this) {
return counter;
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.inc();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.dec();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(room.getCounter());
}
}
执行自增和自减方法的都是同一个实例对象room,两个线程进入临界区要互斥。
public void inc() {
synchronized (this) {
counter++;
}
}
// 等价于下面,都是要求获取当前运行实例的锁才能访问,也就是this对象
public synchronized void inc() {
counter++;
}
public static void test() {
synchronized (Room.class) { // 类锁,类锁.class和实例锁this是两把不一样的锁
}
}
// 等价于下面
public static synchronized void test() {
}