为了提高系统的资源利用率,促使了进程,线程的出现。进程和线程提高了系统CPU利用率的同时,又引出了一些其他的问题。
这里仅讨论线程安全性的问题,因为多个线程中操作执行顺序是不可预测的,甚至会产生一些奇怪的结果。
多线程造成的安全性问题
下面通过几个简单的示例来看一下,在没有锁的情况下会存在什么问题。
1、错误的单例模式
public class SingleInstance {
// 实例对象
private static Object instance;
/**
* 获取该对象的实例
*/
public Object getInstance() {
if (instance == null) {
instance = new Object();
}
return instance;
}
}
该类提供的方法本意是,多次请求getInstance()方法,instance对象只初始化一次。在单线程的情况下貌似没什么问题,但是在多线程的情况下,就会存在问题了。
假定线程A和线程B同时调用getInstance方法,A线程判断instance对象当前为null,实例化了一个对象。在A线程没有实例化完之前,执行了线程调度,B线程也访问当了if的判断条件中,发现instance也为null,也实例化了一个对象并赋值给了instance。最终结果就是A,B线程拿到了不同的对象实例。
2、共享变量被访问
public class Counter{
private integer count = 0;
public static Integer addCount() {
return count++;
}
}
上述是一个技术器类,它提供了统计每个线程访问服务器次数的作用。但是当多个线程同时并发访问,它的统计出来的数据就会存在误差。为什么会有这个问题呢?
count++并不是一个原子操作,它主要包含这几步,读取count变量在内存中的值,将count值加一,得到的结果再赋值给count变量,这是一个 读取 -> 修改 -> 写入。假设现在有两个线程A和B,当前count变量的值为1,A线程对count变量进行了读取,修改的操作,在没有执行写入操作之前发生了系统调度将线程A挂起,线程B开始执行自己的操作,将B线程读取了count变量的值(count = 1),并进行了后续的操作,将计算的count = 2的结果赋值给了count,执行完毕。线程A继续执行,此时同样将count = 2 写入到count中。这样得到最后的结果count就会少记了一次线程的访问。
如何保证线程安全性
先来看一下线程安全是如何定义的?
在线程安全性的定义中要求,多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏。
Java中提供了锁来保证线程安全性,通过在指定的代码块中添加synchronized或ReentrantLock关键字来保证操作的原子性。
当线程要访问一个被加锁的对象、方法或者代码块时,会自动获得锁,如果当前的锁被其他线程持有,则会将线程先挂起,等持有锁的线程将锁被释放后会发起信号唤醒阻塞中的线程,去抢占该锁。
synchronized的用法
synchronized是Java提供的一种内置锁,synchronized的用法可以分为三种
1. 修饰类
2. 修饰方法(静态方法|非静态方法)
3. 修饰代码块(变量)
- 修饰类
public class SynchronizedClass{
public static Object instance = null;
public Object getInstance() {
synchronized(SynchronizedClass.class) {
if (null == instance) {
return new Instance();
}
return instance;
}
}
}
当synchronized修饰类时,不管是访问SynchronizedClass类的哪个实例对象,所有线程都会竞争同一把锁。
- 修饰静态方法
public class LockStaticMethod{
public static Object instance = null;
public synchronized static Object getInstance() {
if (null == instance) {
return new Instance();
}
return instance;
}
}
当synchronized修饰静态方法时,不管是访问LockStaticMethod类的哪个实例对象中的getInstance方法,所有线程都会竞争同一把锁。
- 修饰非静态方法
public class LockMethod{
public static Object instance = null;
public synchronized Object getInstance() {
if (null == instance) {
return new Instance();
}
return instance;
}
}
当synchronized修饰非静态方法时,不同实例对象持有的锁是相互不影响的。
- 修饰代码块(变量)
public class LockVariable{
public static Object instance = null;
public synchronized Object getInstance() {
synchronized(instance) {
if (null == instance) {
return new Instance();
}
return instance;
}
}
}
synchronized修饰变量同样也分静态变量和非静态变量,他们的含义同静态方法和非静态方法类似。
下面看一下,当线程访问一个被加锁的方法时,执行过程是怎样的。
ReentrantLock的用法
ReentrantLock类是Java 5.0才出现的新的加锁机制,ReentrantLock相比于synchronized提供的加锁机制更加灵活,并且提供了一些synchronized不具备的功能。例如:可中断的锁获取操作、公平队列、非块结构的锁,可定时的锁,可轮训的锁。下面我们来看一下这些特性的使用示例。
- 可中断的锁
可中断的锁提供了对可取消任务加锁的需求,具体使用方式如下所示
private ReentrantLock lock = new ReentrantLock();
public void interruptedLock() throws InterruptedException {
lock.lockInterruptibly();
try {
throw new InterruptedException();
}finally {
lock.unlock();
}
}
- 公平队列
ReentrantLock提供了公平锁,线程按照它们发出请求的顺序来获得锁。ReentrantLock类提供了ReentrantLock(boolean fair)构造函数来初始化公平锁。使用时只要将参数设置为true即可。
ReentrantLock fairLock = new ReentrantLock(true);
- 非块结构的锁
- 可定时的锁
ReentrantLock提供了定时的方法tryLock(long timeout, TimeUnit unit),他能根据剩余时间来提供一个时限,如果操作不能在指定的时间内给出结果,那么就会使程序提前结束,示例如下:
public class TimeTryLock {
public static void main(String[] args) throws Exception {
ReentrantLock reentrantLock = new ReentrantLock();
if (reentrantLock.tryLock(100L, TimeUnit.SECONDS)) {
System.out.println("success");
}
}
}
- 可轮训的锁
tryLock方法实现了有条件的获取锁的模式,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。可轮训锁提供了另一种选择:避免发生死锁。
先来看一下在synchronized模式下死锁的情况
public class SyncDeadLock {
public void deadLock(Integer v1, Integer v2) {
synchronized (v1) {
System.out.println(Thread.currentThread() + "get object lock " + v1);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (v2) {
System.out.println(Thread.currentThread() + "get object lock " + v2);
}
}
}
public static void main(String[] args) {
Integer v1 = 0;
Integer v2 = 2;
SyncDeadLock syncDeadLock = new SyncDeadLock();
new Thread(new Runnable() {
@Override
public void run() {
syncDeadLock.deadLock(v1, v2);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
syncDeadLock.deadLock(v2, v1);
}
}).start();
}
}
有了tryLock之后,通过tryLock就可以有效的避免死锁,tryLock可以尝试获取锁,如果没有获取到锁,可以主动将当前持有的锁释放掉,这样可以避免死锁发生的条件,看下面的例子:
public class ReentrantLockDeadLock {
private static Integer count = 10;
public boolean tryLock(ReentrantLock lock1, ReentrantLock lock2) {
while (true) {
if (lock1.tryLock()) {
try {
System.out.println(Thread.currentThread() + " get " + lock1);
if (lock2.tryLock()) {
try {
System.out.println(Thread.currentThread() + " get " + lock2);
} finally {
System.out.println(Thread.currentThread() + " unlock " + lock2);
lock2.unlock();
System.out.println("count = " + --count);
if (count < 0) {
return true;
}
}
}
} finally {
System.out.println(Thread.currentThread() + " unlock " + lock1);
lock1.unlock();
}
}
}
}
public static void main(String[] args) {
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
ReentrantLockDeadLock mainLock = new ReentrantLockDeadLock();
new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.MICROSECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
mainLock.tryLock(lock1, lock2);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
mainLock.tryLock(lock2, lock1);
}
}).start();
}
}