当多个线程同时对共享数据
进行操作时,可能会引发线程安全问题。解决多线程的并发安全问题,主要方法是加锁。在java中,加锁有两个方法,即synchronized
和Lock
.
synchronized方法
synchronized是java的关键字,该关键字可以作用于同步代码块
和同步方法
,被synchronized修饰具有原子性
和可见性
。
- synchronized作用于同步代码块
synchronized ( 同步监视器 ) {
//操作共享数据的代码
}
同步监视器
:即锁,任何一个对象都可以充当锁。要求:多个线程必须共用同一把锁。
- synchronized作用于同步方法
如果操作共享数据的代码恰好在一个方法中,可以把方法声明为同步的。
public synchronized void max( int a, int b ){
//操作共享数据的代码
}
注意
- 同步方法仍然涉及同步监视器,只是不需要显示声明。
- 非静态的同步方法,同步监视器是this(即当前对象)
- 静态的同步方法,同步监视器是当前类本身。
Lock对象
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。其具体实现使用ReentrantLock。
使用Lock接口对象实现同步
- 实例化ReentrantLock
- 调用lock()方法
- 调用解锁unlock()方法->需要手动解锁
public static void main(String[] args) {
//Lock对象,多个线程竞争一把锁,即一个Lock对象
Lock lock = new ReentrantLock();
//线程t1
Thread t1 = new Thread(){
@Override
public void run() {
try{
System.out.println( "t1线程试图占有对象:lock" );
lock.lock();
System.out.println( "t1线程占有对象:lock" );
Thread.sleep( 1000 );
}
catch( InterruptedException e ){
e.printStackTrace();
}
finally{
System.out.println( "t1线程释放对象:lock" );
lock.unlock();
}
System.out.println( "t1线程结束");
}
};
t1.start();
//线程t2
Thread t2 = new Thread(){
@Override
public void run() {
try{
System.out.println( "t2线程试图占有对象:lock" );
lock.lock();
System.out.println( "t2线程占有对象:lock" );
Thread.sleep( 1000 );
}
catch( InterruptedException e ){
e.printStackTrace();
}
finally{
System.out.println( "t2线程释放对象:lock" );
lock.unlock();
}
System.out.println( "t2线程结束");
}
};
t2.start();
}
运行结果
t1线程试图占有对象:lock
t2线程试图占有对象:lock
t2线程占有对象:lock
t2线程释放对象:lock
t2线程结束
t1线程占有对象:lock
t1线程释放对象:lock
t1线程结束
注意
- lock()与unlock()方法之间的代码为需要同步的代码;
- Lock对象需要手动解锁,因此可以使用try-catch-finally结构,在finally块中手动解锁
- Lock对象同时提供了一个tryLock()方法,如果在指定时间内无法获取锁,放弃获取。由于该特性,Lock对象可以避免死锁。
线程交互
- 使用
synchronized
方式进行线程交互,用到的是同步监视器的wait()
,notify()
和notifyAll()
方法。由于synchronized方式的同步监视器可以是任意对象,因此这三个方法是Object
类的方法,并且由final
关键字修饰,不能重写。同时,这三个方法只能在同步代码块或者同步方法中调用。 - 使用
Lock
对象进行线程交互,首先需要通过lock获取一个Condition
对象,然后分别调用这个Condition对象的await()
,signal()
和signalAll()
方法。同样的,这三个方法的调用必须在同步代码块中。