1. synchronized概述
synchronized是Java的一个关键字,修饰符。是Java多线程加锁机制的一种,一种隐式内置锁/监听器锁(对比显式Lock锁)。它也是一种互斥锁,保证了被修饰的块每次只能有一条线程访问。
1.1 细分
- 对象锁:synchronize修饰的是实例方法、synchronized语句块参数使用的是实例对象
- 类锁: synchronized修饰的是类方法、synchronized语句块参数是类类型
在进入方法或者语句块的时候获取到相关的锁,方法结束或者语句块结束自动释放锁。不同Lock的手动操作。
1.2 用处
- 原子性: 保证线程原子性,如上所述,修饰的块或者方法每次只能有一条线程访问。
- 可见性: 保证了修饰块方法修改的变量对其他线程是可见的。
1.3 案例
1.3.1 多线程问题
package xyz.cglzwz.thread_concurrency.other._synchronized;
/**
* count++三步,并发条件不同步会有问题
* synchronized(new Object())也是不行的,锁不一致
*
* @author chgl16
* @date 2019-04-05
*/
public class ConcurrentProblem {
static int count = 0;
static Object o = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable r = () -> {
for (int i = 0; i < 10000; ++i) {
count++;
}
};
Thread t1 = new Thread(r, "t1");
Thread t2 = new Thread(r, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
输出(每次都会不一样)
12223
如果不并发结果显然就是20000,但是在这里多线程的情况下,输出一般都是10000-15000左右。因为count++ 实际上是三条操作
- 先从内存(方法区)读取count到当前线程(栈帧)
- 当前线程值的count+1
- 写回内存
因此会出现很多情况:t1获取到count=5的时候(第一步),t2也获取到count=5(第一步),然后大家都写入了,写完后内存中的count是6,而不是7,因此少了1。
如果如下加锁,那就是保证了结果必为20000
Object o = new Object();
Runnable r = () -> {
for (int i = 0; i < 10000; ++i) {
synchronized (o) {
count++;
}
}
};
这里使用的是对象锁,每次只能是t1、t2其中的一个持有这个锁,保证了count++细分的三步是原子性的。
如果如下加锁,结果有也不会是20000,但是比不加的大
Runnable r = () -> {
for (int i = 0; i < 10000; ++i) {
synchronized (new Object()) {
count++;
}
}
};
因为每次都会创建一把新锁,所以其实t1和t2访问count++语句块是不会互斥的。
1.3.2 死锁问题
package xyz.cglzwz.thread_concurrency.other._synchronized;
/**
* 死锁
* @author chgl16
* @date 2019-04-05
*/
public class Deadlock {
private static Object resourceA = new Object();
private static Object resourceB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
synchronized(resourceA) {
System.out.println(Thread.currentThread().getName() + " 获取到了 resourceA");
try {
// 休眠一秒,以保证线程B获取到了resourceB
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(resourceB) {
System.out.println(Thread.currentThread().getName() + " 获取到了 resourceB");
}
}
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
synchronized(resourceB) {
System.out.println(Thread.currentThread().getName() + " 获取到了 resourceB");
try {
// 休眠一秒,以保证线程A获取到了resourceA
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(resourceA) {
System.out.println(Thread.currentThread().getName() + " 获取到了 resourceA");
}
}
}
};
t1.start();
t2.start();
}
}
输出
t1 获取到了 resourceA
t2 获取到了 resourceB
- 这里的t1和t2在持有自己的资源的情况下(语句块未结束,未释放锁,这里把锁当做资源),而又相互请求对方的资源,明显死锁。两个都获取不到。
- 解决方法就是打破死锁的必要条件,这里可以把每个run方法的synchronized语句块嵌套改为并列,这样大家都分别释放了资源再请求新资源就不会互斥等待了。
2. synchronized性质
2.1 可重入性
待续
2.2 不可中断性
一旦这个锁已经被别的线程获取了,==当前线程还想获取的话,只能选择等待或者阻塞,直到别的线程释放了这个锁。==如果别的线程不释放,当前线程就会永久等待,这就是不可中断。(不可以中断别的线程获取到锁的状态)
相比之下,Lock类这种显式锁是拥有中断的能力的。如果觉得当前线程等待太久,是有权利去中断获取到该锁的线程的执行的。此外如果觉得等待太久不想等了,也是可以退出的。
3. 常见问题
- 两个线程同时访问一个对象的同步方法:
package xyz.cglzwz.thread_concurrency.other._synchronized;
public class SynchronizedDemo1 implements Runnable {
static SynchronizedDemo1 instance = new SynchronizedDemo1();
@Override
public void run() {
method1();
}
public synchronized void method1() {
System.out.println(Thread.currentThread().getName() + "开始访问");
try {
// 休眠下,更好看结果
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "结束访问");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance, "t1");
Thread t2 = new Thread(instance, "t2");
t1.start();
t2.start();
}
}
结果串行,因为是同一个对象锁
t1开始访问
t1结束访问
t2开始访问
t2结束访问
- 两个线程访问的是两个对象的同步方法
结果是并行的,因为是两个不同的对象锁 - 两个线程访问的是synchronized的静态方法
结果是串行的,因为是同一把类锁 - 同时访问同步方法与非同步方法
package xyz.cglzwz.thread_concurrency.other._synchronized;
public class SynchronizedDemo4 implements Runnable {
static SynchronizedDemo4 instance = new SynchronizedDemo4();
@Override
public void run() {
if (Thread.currentThread().getName().equals("t1"))
method1();
else
method2();
}
public synchronized void method1() {
System.out.println(Thread.currentThread().getName() + "开始访问1");
try {
// 休眠下,更好看结果
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "结束访问1");
}
public void method2() {
System.out.println(Thread.currentThread().getName() + "开始访问2");
try {
// 休眠下,更好看结果
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + "结束访问2");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance, "t1");
Thread t2 = new Thread(instance, "t2");
t1.start();
t2.start();
}
}
t1开始访问1
t2开始访问2
t1结束访问1
t2结束访问2
结果是并行的,一个同步只是作用在自己范围内
5. 访问同一个对象的不同的普通同步方法:
因为是通一个对象锁,所以是串行的
6. 同时访问静态synchronized和非静态synchronized方法:
因为是一个是类锁,一个是实例锁,所以是并行的。
7. 方法抛出异常后,会释放锁
3.1 核心思想
3个核心思想;
- 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1、5种情况)
- 每个实例都对应有自己的一把锁,不同实例之前互不影响;
例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象共用同一把类锁(对应2、3、4、6种情况) - 无论是方法正常执行完毕或者方法抛出异常,都会释放锁(对应第7种情况)
4. synchronized原理
待续