一 对某个对象加锁
// 锁的是对象,不是代码
public class T {
private int count = 10;
private Object o = new Object();
public void m() {
// 任何线程要执行下面的代码,必须先拿到 o 的锁,只有拿到 o 这把锁,才能执行下面这段代码。
// 一旦 A 线程拿到这把锁,B 线程只有等 A 线程执行完,释放了 o 这把锁,B 再获取这把锁,B 线程才能执行。
synchronized (o) {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
二 用当前对象当锁
public class T {
private int count = 10;
public void m() {
// 锁的是 this
synchronized (this) { // 任何线程要执行下面的代码,必须先拿到 this 的锁,用当前对象当锁
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
}
三 在方法前加 synchronized
public class T {
private int count = 10;
public synchronized void m() { // 等同于在方法代码执行时加 synchronized(this)
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
}
四 静态方法前加 synchronized
public class T {
private static int count = 10;
// 锁的是 XX.class
public synchronized static void m() { // 这里等同于 synchronized(T.class)
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void mm() {
synchronized (T.class) { // 这里不能写成 synchronized(this)
count--;
}
}
}
五 synchronized 的小测试
1 代码
public class T implements Runnable {
private /*volatile*/ int count = 100;
public synchronized void run() { // 必须加 synchronized ,否则两个不同的线程可能会输出相同的值
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
T t = new T();
for (int i = 0; i < 100; i++) {
new Thread(t, "THREAD" + i).start();
}
}
}
2 测试
......
THREAD32 count = 9
THREAD68 count = 8
THREAD69 count = 7
THREAD82 count = 6
THREAD55 count = 5
THREAD59 count = 4
THREAD66 count = 3
THREAD62 count = 2
THREAD63 count = 1
THREAD47 count = 0
六 各个线程对应的锁对象必须相同,否则是锁不住的
1 代码
public class T implements Runnable {
private int count = 10;
public synchronized void run() {
count--;
System.out.println(Thread.currentThread().getName() + " count = " + count);
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
T t = new T(); // 注意:这里5个线程对应T对象是不同的,所以并不能锁住
new Thread(t, "THREAD" + i).start();
}
}
}
2 测试
THREAD1 count = 9
THREAD2 count = 9
THREAD0 count = 9
THREAD4 count = 9
THREAD3 count = 9
七 同步方法和非同步方法同时执行
1 代码
// 同步方法和非同步方法可以同时调用,是锁不住的
public class T {
// 同步方法
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + " m1 start...");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m1 end");
}
// 非同步方法
public void m2() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m2 ");
}
public static void main(String[] args) {
T t = new T();
new Thread(t::m1, "t1").start();
new Thread(t::m2, "t2").start();
/*
lambda 表达式写法
new Thread(()->t.m1(), "t1").start();
new Thread(()->t.m2(), "t2").start();
*/
/*
//1.8 之前的写法
new Thread(new Runnable() {
@Override
public void run() {
t.m1();
}
});
*/
}
}
2 测试
t1 m1 start...
t2 m2
t1 m1 end
八 写方法加锁读方法不加锁问题
1 代码
/**
* @className: Account
* @description: 面试题:模拟银行账户
* 对业务写方法加锁
* 对业务读方法不加锁
* 这样实现容易产生脏读问题
* @date: 2021/6/20
*/
import java.util.concurrent.TimeUnit;
public class Account {
String name;
double balance;
// 对业务写方法加锁
public synchronized void set(String name, double balance) {
this.name = name;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
// 对业务读方法不加锁
public /*synchronized*/ double getBalance(String name) { // 只有在这里加锁,才不会产生脏读问题
return this.balance;
}
public static void main(String[] args) {
Account a = new Account();
new Thread(() -> a.set("zhangsan", 100.0)).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(a.getBalance("zhangsan"));
}
}
2 测试
0.0
100.0
九 可重入锁
1 代码
/**
* @className: T
* @description: 可重入锁
* 一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁
* 也就是说 synchronized 获得的锁是可重入的
* @date: 2021/6/20
*/
import java.util.concurrent.TimeUnit;
public class T {
synchronized void m1() {
System.out.println("m1 start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
m2();
System.out.println("m1 end");
}
synchronized void m2() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m2");
}
public static void main(String[] args) {
new T().m1();
}
}
2 测试
m1 start
m2
m1 end
十 父子类可重入锁,它们锁的都是 this 对象
1 代码
/**
* @className: T
* @description: 父子类相同方法是可重入加锁
* @date: 2021/6/20
*/
import java.util.concurrent.TimeUnit;
public class T {
// 父类 m 方法是对 TT 对象加锁,是可重入锁
synchronized void m() {
System.out.println("m start");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m end");
}
public static void main(String[] args) {
new TT().m();
}
}
class TT extends T {
// 子类 m 方法是对 TT 对象加锁
@Override
synchronized void m() {
System.out.println("child m start");
super.m();
System.out.println("child m end");
}
}
2 测试
child m start
m start
m end
child m end
十一 锁和异常
1 代码
/**
* 程序在执行过程中,如果出现异常,默认情况锁会被释放
* 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。
* 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,
* 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。
* 因此要非常小心的处理同步业务逻辑中的异常
*/
import java.util.concurrent.TimeUnit;
public class T {
int count = 0;
synchronized void m() {
System.out.println(Thread.currentThread().getName() + " start");
while (true) {
count++;
System.out.println(Thread.currentThread().getName() + " count = " + count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
int i = 1 / 0; // 此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
System.out.println(i);
}
}
}
public static void main(String[] args) {
T t = new T();
Runnable r = new Runnable() {
@Override
public void run() {
t.m();
}
};
new Thread(r, "t1").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r, "t2").start();
}
}
2 测试
t1 start
t1 count = 1
t1 count = 2
t1 count = 3
t1 count = 4
t1 count = 5
t2 start
t2 count = 6
Exception in thread "t1" java.lang.ArithmeticException: / by zero
at com.mashibing.juc.c_011.T.m(T.java:27)
at com.mashibing.juc.c_011.T$1.run(T.java:38)
at java.lang.Thread.run(Thread.java:748)
t2 count = 7
t2 count = 8
t2 count = 9
t2 count = 10
十二 小结
1 JDK 早期的是重量级的,它要到OS底层申请锁。
2 锁升级
当 sync (Object)
第1步:markword 在 Object 记录这个线程 ID ,这个机制是偏向锁。
第2步:如果发生线程争用:升级为自旋锁。
第3步:旋10次以后,升级为重量级锁,到 OS 申请锁。
3 锁的使用场景
执行时间短(加锁代码),线程数少,用自旋锁(占CPU,不占用OS)。
执行时间长,线程数多,用重量级锁。(不占CPU,在等待队列中)
锁只能升级,不能降级。
4 synchronized(Object)
不能用 String 常量 Integer Long