同步(一)
一、同步的基本概念
1、同步的场景
线程获取同步锁,获取失败则阻塞等待,获取成功则执行任务,执行完毕后释放锁。
2、线程安全问题
(1)内存读取
- cpu在内存读取数据时,顺序优先级:寄存器-高速缓存-内存
- 计算完成后缓存数据写回内存中
(2)可见性
- 每个线程都有独立的工作内存,并对其他线程是不可见的。线程执行如用到某变量,将变量从主内存复制到工作内存,对变量操作完成后再将变量写回主内存。
- 可见性就是指线程A对某变量操作后,其他线程能够看到被修改的值,可以通过volidate等方式实现可见性。
(3)原子性
对于基本类型的赋值操作是原子操作,即这些操作是不可被中断的,要么执行,要么不执行。
x = 10; //语句1(原子性操作)
y = x; //语句2
x++; //语句3
- 语句1直接将10赋值给x,会直接将10写入到工作内存
- 语句2需要先读x,然后将x写入工作内存,然后赋值,不是原子性操作
- 语句3需要先读x,然后+1,然后赋值。
(4)有序性
- Java内存模型中允许编译器和处理器对指令进行重排序,虽然重排序过程不会影响到单线程执行的正确性,但是会影响到多线程并发执行的正确性。
- 可以通过volidate来保证有序性,synchronized和Lock保证每个时刻只有一个线程执行同步代码,这相当于是让线程顺序执行同步代码,从而保证了有序性。
(5)线程安全问题的原因
线程A和线程B需要将共享变量拷贝到本地内存中,并对各自的共享变量副本进行操作,操作完成后同步到主内存中,可能导致共享内存数据错乱的问题。
二、synchronized
1、基本特性:
- synchronized可以用于修饰类的实例方法、静态方法和代码块
- 可重入性:对同一个执行线程,它在获得了锁之后,在调用其他需要同样锁的代码时,可以直接调用。
- 内存可见性:在释放锁时,所有写入都会写回内存,而获得锁后,都会从内存中读最新数据。
- 当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
2、类锁和对象锁
public class SyncTest {
private static int num;
public void setClassText() {
//类锁
synchronized (SyncTest.class) {
System.out.println("类锁开始执行");
++num;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("类锁执行完成");
}
}
public static synchronized void setObject1Text() {
//类锁
System.out.println("类锁2开始执行");
++num;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("类锁2执行完成");
}
public synchronized void setObject1Text() {
//对象锁
System.out.println("对象锁1开始执行");
++num;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("对象锁1执行完成");
}
public void setObject2Text() {
//对象锁
synchronized(this) {
System.out.println("对象锁2开始执行");
++num;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("对象锁2执行完成");
}
}
}
注:
- 线程A持有对象锁,不会影响到线程B去持有类锁
- 线程A和B操作同一个对象锁,线程A持有对象锁,线程B只能等到该对象锁释放
- synchronized如果修饰static方法(类锁),不会影响到非static(对象锁)修饰的方法的调用
3、wait、notify
- void notifyAl1( )
解除那些在该对象上调用 wait 方法的线程的阻塞状态。该方法只能在同步方法或同步块内部调用。 如果当前线程不是对象锁的持有者,该方法拋出一个IllegalMonitorStateException异常。 - void notify()
随机选择一个在该对象上调用 wait 方法的线程,解除其阻塞状态。 该方法只能在一个同步方法或同步块中调用。 如果当前线程不是对象锁的持有者, 该方法抛出一个 IllegalMonitorStateException 异常。 - void wait(long mi11is)
导致线程进人等待状态直到它被通知。该方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者,该方法拋出一个 IllegalMonitorStateException 异常。 - void wait(long mi11Is, int nanos)
导致线程进入等待状态直到它被通知或者经过指定的时间。 这些方法只能在一个同步方法中调用。如果当前线程不是对象锁的持有者该方法拋出一个 IllegalMonitorStateException 异常。
注意:
- 调用notify会把在条件队列中等待的线程唤醒并从队列中移除,但它不会释放对象锁,只有在包含notify的synchronzied代码块执行完后,等待的线程才会从wait调用中返回。
- 调用wait把当前线程放入条件等待队列,释放对象锁。等待时间到或被其他线程调用notify/notifyAll从条件队列中移除,此时要重新竞争对象锁。
三、锁对象
锁可以理解为进入某个门的钥匙,两个人都想进入门内,A持有钥匙可进入,B只能等待。A放下了钥匙,B才有机会获取到钥匙进入。
- 锁用于保护代码片段,只有一个线程执行被保护的代码
- 需要保证是同一个锁对象
- 需要通过调用unlock去释放锁
- 锁可以拥有一个或多个相关的条件对象
1、Lock
public interface Lock {
/**
* 获取锁,如果锁被另一线程拥有则发生阻塞
*/
void lock();
/**
* 尝试获取锁,立即返回不阻塞
* @return 获取成功返回true
*/
boolean tryLock();
/**
* 尝试获取锁,如果成功立即返回,否则阻塞等待,阻塞时间不会超 * 过给定的值
* @return 获取成功返回true
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 释放锁
*/
void unlock();
/**
* 获取一个与该锁相关的条件
* @return
*/
Condition newCondition();
......
}
synchronized和Lock对比:
- 锁可以以非阻塞方式获取锁、可以响应中断、可以限时
- synchronized会自动释放锁,而Lock一定要求程序员手工释放
2、ReentrantLock:
(1)构造:
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 构建一个公平策略的锁,公平锁偏爱等待时候最长的线程,会降低性能
* @param fair
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
(2)条件对象
public Condition newCondition() {
......
}
一个锁对象可以有一个或多个条件对象,通过newCondition获取一个条件对象
(3)await、signalAll
- void await( )
将该线程放到条件的等待集中。 - void signalAll( )
解除该条件的等待集中的所有线程的阻塞状态。 - void signal()
从该条件的等待集中随机地选择一个线程, 解除其阻塞状态。
注意:
- await()对应于Object的wait(),signal()对应于notify,signalAll()对应于notifyAll()
- 调用await、signalAll等方法前需要先获取锁,如果没有锁,会抛出异常IllegalMonitorStateException
3、读写锁:
特性:
只要没有任何线程写入变量,并发读取可变变量通常是安全的。所以读锁可以同时被多个线程持有,只要没有线程持有写锁。这样可以提升性能和吞吐量,因为读取比写入更加频繁。
使用场景:多线程操作同一文件,读操作比较多,写操作比较少的情况。
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
//获取读、写锁
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
//对读的操作使用读锁
readLock.lock();
try {
......
} finally {
readLock.unlock();
}
//对写的操作使用写锁
writeLock.lock();
try {
......
} finally {
writeLock.unlock();
}
四、Volidate域
1、应用场景:
- 如果声明一个域为 volatile, 它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
- 对变量的写操作不会依赖于当前值。(不保证原子性)
- 该变量没有包含在具有其他变量的不变式中
特性:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
- Volatile变量不能提供原子性
例如
public class VolatileTest {
volatile boolean flag;
public void finish() {
flag = true;
}
public void begin() {
flag =false;
}
}
通过volatile修饰保证标识位在多线程中的可见性
2、原理:
- 它确保指令重排序时不会把其后面的指令排到被volidate修饰的变量之前,也不会把前面的指令排到该变量的后面;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
3、synchronized和volatile比较:
- volatile是线程同步的轻量级实现,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。
- 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞
- volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性
五、死锁
有a, b两个线程,a持有锁A,在等待锁B,而b持有锁B,在等待锁A,a,b陷入了互相等待,最后谁都执行不下去。
public class DeadLockDemo {
private static Object lockA = new Object();
private static Object lockB = new Object();
private static void startThreadA() {
Thread aThread = new Thread() {
@Override
public void run() {
synchronized (lockA) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (lockB) {
}
}
}
};
aThread.start();
}
private static void startThreadB() {
Thread bThread = new Thread() {
@Override
public void run() {
synchronized (lockB) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
synchronized (lockA) {
}
}
}
};
bThread.start();
}
public static void main(String[] args) {
startThreadA();
startThreadB();
}
}
解决方式:
- 应该尽量避免在持有一个锁的同时去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同的顺序去申请锁
- 显式锁接口Lock,它支持尝试获取锁(tryLock)和带时间限制的获取锁方法,可以在获取不到锁的时候释放已经持有的锁,然后再次尝试获取锁或干脆放弃,以避免死锁。