系列文章目录
Java多线程【1】synchronized对象锁、内置锁使用
Java多线程【2】Java wait/notify的使用于同步模式保护性暂停
Java多线程【3】同步模式之保护性暂停案例 相亲问题
Java多线程【4】interrupt线程的打断机制、两阶段终止模式
Java多线程【5】异步模式之生产者消费者
Java多线程【6】LockSupport park/unpark原理和使用以及于wait/notify的区别
文章目录
前言
竞态条件
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
临界区
发生竞态条件的代码块就称为临界区。
为什么需要使用synchronized
在多核线程操作共有资源的时会发生状态的安全问题。需要synchronized将临界区中的代码进行同步操作。
一、synchronized能保证什么?
要想保证线程本身操作共同状态数据安全,必须保证临界区种的代码操作原子性,synchronized(对象锁、内置锁)可以保证代码的原子性。
//线程共有状态
static int count = 1000;
public void increment(){
for (int i = 0; i < 4000; i++) {
//临界区
{count--; }
}
}
count- -其实是非原子性的操作,如果多个线程同时执行increment方法就会导致count最后的值出现问题,最简单的解决办法如下
static final Object lock = new Object();
//线程共有状态
static int count = 1000;
public void increment(){
for (int i = 0; i < 4000; i++) {
synchronized (lock){
//临界区
{count--; }
}
}
}
二、synchronized的使用
1、synchronized加在方法上
1.1、普通方法
static Integer count = 1000;
public synchronized void increment(){
for (int i = 0; i < 4000; i++) {
//临界区
{count--; }
}
}
1.1.1 操作同一个对象
Test4 obj = new Test4(o);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
obj.increment();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
obj.increment();
}
});
等同于synchronized(this){临界区}
,属于是锁this对象,这个时候如果线程操作的是同一个对象则不会发生安全问题。
1.1.2 操作多个对象(存在线程安全问题)
如果Test4
是new的不同对象,那么this锁就无效,会发生安全问题
原因:每个对象的都有自己的this对象锁。在申请monitor管程的时候不同this锁的mark word指向的是不同的管程,那么也就意味着存在多个管程和多个管程中的entryList,这些entryList被CUP调度的可能只会跟自身的锁有关,会导致类的静态变量同时被线程修改发生安全性问题。
Test4 obj = new Test4(o);
Test4 obj2 = new Test4(o);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
obj.increment();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
obj2.increment();
}
});
1.2、静态方法
在类加载时,静态方法、静态变量等内容会存储到元空间数据区中,直接提供给类使用。那么在静态方法上加锁,其实就是锁住了类对象。
public static synchronized void increment(){
for (int i = 0; i < 4000; i++) {
//临界区
{count--; }
}
}
这个时候如果Test4
是new的不同对象,也能保证临界区的原子性操作。
2、synchronized加在代码块中
2.1、this锁
- 等同于普通方法上加锁
public void increment(){
for (int i = 0; i < 4000; i++) {
//临界区
synchronized(this) {
{count--; }
}
}
}
2.2、类锁
- 等同于synchronized修饰静态方法。
synchronized(clazz){}
,锁类。
类锁在多个线程操作一个类的同一个对象时能保证安全性,一个类的多个对象就也可以保证安全性。因为不同对象的类在元空间只保存一份,属于同一个锁。
public void increment(){
for (int i = 0; i < 4000; i++) {
//临界区
synchronized(Test4.class) {
{count--; }
}
}
}
2.3、对象锁
synchronized(object){}
。
对象锁相较于前两种锁更加的灵活,可以在创建对象的时候由线程传入,动态给分临界区的分配锁。
static Object lock;
public Test4(Object lock){
this.lock = lock;
}
public void increment(){
for (int i = 0; i < 4000; i++) {
//临界区
synchronized(lock){
{count--; }
}
}
}
三、synchronized的缺点和解决办法
1、synchronized锁的缺点
1.1、上下文切换效率
1.1、分布式系统失效
- 频繁的加锁解锁,造成线程间的上下文切换,有时候反而会导致系统的资源利用率降低。
- 分布式的情况synchronized有可能无效。
2、解决办法
2.1、上下文切换问题
- java在synchronized内部其实隐式的实现了轻量锁(CAS)、偏向锁等一下不需要切换上下文切换的锁。
- 同步的颗粒度越小越好,同步的次数越少越好,
- 尽量使用代码块加锁不要使用方法上加锁,
- 类锁少使用多使用对象锁。
- 实际生产问题上我们得考虑synchronized的使用场景,根据业务灵活多变。
2.2、分布式系统失效问题
- Redis分布式锁。
- Redisson分布式增强锁(开箱即用,实现了锁的及时续费)推荐
- ZK分布式锁 推荐
- MySQL实现分布式锁
但不管如何,合理的使用Java内置锁,可以有效的解决大部分的线程安全问题。